Desarrollo multiplataforma

Versión para imprimir.

por Gilberto Pacheco Gallegos

1. Conocimientos requeridos

2. Instrucciones de navegación

  • Los siguientes controles te permitirán navegar por todo el contenido del sitio.

    Oculta el menú de navegación.

    Muestra el menú de navegación.

    skip_previous
    arrow_back (Tecla flecha a la izquierda)
    Swipe hacia la derecha

    Regresa a la página anterior.

    skip_next
    arrow_forward (Tecla flecha a la derecha)
    Swipe hacia la izquierda

    Avanza a la página siguiente.

3. Software a instalar

Para la parte web

Microsoft Edge Chromium
Mozilla Firefox
Google Chrome
PHP y Xdebug
Para Linux y apple

Se puede instalar desde las tiendas de software.

Para Windows
  1. Video de Instalación..

  2. Descarga la versión Thread Safe que corresponda a la arquitectura de tu computadora (x64 o x86, la maypría son x64) desde https://windows.php.net/download#php-8.4.

  3. Crea una carpeta con nombre php en el disco C: y descompacta ahí el php

  4. Añade la ruta C:\php a la variable de ambiente PATH.

    1. Haz clic en el logo de Windows.

    2. Haz clic en Configuración.

    3. Haz clic en Sistema.

    4. Haz clic en Acerca de.

    5. Haz clic en Configuración avanzada del sistema. Te pide que autorices. Autoriza.

    6. Haz clic en Variables de entorno.

    7. En Variables del sistema selecciona Path y haz clic en Editar...

    8. Haz clic en Nuevo. Se abre una entrada y en ella teclea C:\php. Haz clic en Aceptar.

    9. Haz clic en Aceptar para cerrar las variables de entorno.

    10. Haz clic en Aceptar para cerrar las propiedades del sistema.

  5. Sigue las instrucciones de https://xdebug.org/wizard:

    • En el botón de Windows haz clic derecho y selecciona Terminal.

    • En la terminal teclea el comando
      php -i output

    • Copia toda la salida, que es bastante larga, y pégala en la página de Xdebug y cliquea Analyse my phpinfo() output y sigue las instrucciones.

  6. Crea el archivo C:\php\php.ini con el siguiente contenido

    extension = php_mbstring.dll
    extension=php_pdo_sqlite.dll
    extension=php_pdo_mysql.dll
    extension=php_openssl.dll
    extension=php_sockets.dll
    extension=php_fileinfo.dll
    extension=php_curl.dll
    display_errors = Off
    zend_extension = xdebug
    xdebug.mode = debug
    xdebug.start_with_request = yes
    
Git
Visual Studio Code

Video de Instalación..

Usa la URL https://code.visualstudio.com/.

Instala las extensiones:

  • es6-string-html de Tobermory
  • anuragsingk-PHP Server de Anuragsingk
  • PHP Debug de Xdebug
  • PHP Extension Pack de Xdebug
  • PHP Intelephsense de Ben Mewburn
  • PHP IntelliSense de Damjan Cvetko
  • Live Server de Ritwick Dey
  • SQLite Viewer de Florian Klampfer
  • Zip Tools de Adam Raichu
  • SFTP de Natizyskunk

Haz clic en el engrane de abajo a la izquierda y seleciona Settings. En Extensions > PHP Server configuration configura lo siguiente

Phpserver Ip
localhost
Phpserver port
80

Para ESP32 (Solo IOT)

CP210x USB to UART Bridge VCP Drivers
Arduino IDE
Arduino core for ESP8266 WiFi chip
Arduino core for ESP32 WiFi chip
EspMQTTClient
  • Video de Instalación de Arduino IDE + ESP8266 + ESP32 + ArduinoWebSockets.

    En el video no se muestra la instalación de la libresría EspMQTTClient, pero se instala como una librería normal. Si te solicita instalar otras librerías, instálalas también.

  • Puedes descargar Arduino IDE desde https://www.arduino.cc/.

  • Las instrucciones de instalación son los siguientes:

    1. Selecciona la pestaña SOFTWARE y busca el DOWNLOAD para tu sistema operativo.

    2. Una vez instalado Arduino IDE, entra a esta aplicación, selecciona el menú Archivo > Preferencias. Se abre un cuadro de diálogo y en el campo Gestor de URLs Adicionales de Tarjetas introduce el valor

      https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json,https://arduino.esp8266.com/stable/package_esp8266com_index.json
    3. Selecciona el menú Herramientas > Placa:xxx > Gestor de tarjetas…. En ocasiones el gestor de tarjetas tarda un poco en habilitarse, por lo que tendrás que cerrar el menú y volver a abrirlo posiblemente unas cuantas veces.

    4. Instala las tarjetas esp32 y esp8266.

    5. Selecciona el menú Herramientas > Administrar bibliotecas…. En ocasiones el gestor de tarjetas tarda un poco en habilitarse, por lo que tendrás que cerrar el menú y volver a abrirlo posiblemente unas cuantas veces.

    6. Instala la libresría EspMQTTClient. Se instala como una librería normal. Si te solicita instalar otras librerías, instálalas también.

4. Introducción a las PWA

Versión para imprimir.

A. Introduccion

  • En esta lección se introduce el concepto de PWA.

B. PWA

  • Aplicación que se entrega a través de la web, creada utilizando tecnologías web comunes como HTML, CSS y JavaScript.

  • Se conoce com PWA por sus siglas en inglés, que significan Progressive Web App.

  • Está destinado a funcionar en cualquier plataforma que use un navegador compatible con los estándares.

  • La funcionalidad incluye:

    • trabajar sin conexión,

    • notificaciones push y

    • acceso al hardware del dispositivo.

    Esto permite crear experiencias de usuario similares a las aplicaciones nativas en dispositivos móviles y de escritorio.

  • Dado que una aplicación web progresiva es un tipo de aplicación web, no hay ningún requisito para que los desarrolladores o usuarios instalen las aplicaciones web a través de sistemas de distribución digital como Apple App Store o Google Play.

  • Es posible subirlas a Google Play y a Microsoft Store.

  • Fuente: https://es.wikipedia.org/wiki/Aplicaci%C3%B3n_web_progresiva

C. Requerimientos de instalación y uso

  • Un archivo de manifiesto web, con los campos correctos completados.

  • Un servidor web que utilice un dominio seguro (HTTPS).

  • Un conjunto de iconos para representar la aplicación en el dispositivo.

  • Un conjunto de capturas de pantallas con otrientaciones verticales y horizontales para mostrarlas al instalas la aplicación.

  • Un archivo service worker registrado para permitir que la aplicación funcione sin conexión.

  • Fuente: https://developer.mozilla.org/es/docs/Web/Progressive_web_apps/Installable_PWAs

D. Navegadores compatibles

  • Google Chrome.

  • Microsoft Edge (basado en Chromium).

  • Apple Safari (versión actualizada).

  • Firefox (no usa el archivo de manifiesto y no permite instalar las app).

E. Herramientas de desarrollo

  • Cualquier herramienta que permita editar HTML, CSS y JavaScript.

  • Un servidor web que use https.

  • Un editor de imágenes.

  • Generador de íconos enmascarables https://maskable.app/.

F. Resumen

  • En esta lección se presentan los siguientes temas:

    • Definición de PWA.

    • Requerimientos de instalación y uso de una PWA.

    • Navegadores compatibles con PWA.

    • Herramientas de desarrollo.

5. PWA básica

Versión para imprimir.

A. Introduccion

B. Hazlo funcionar (con videos)

  1. Prueba e instala, de preferencia con Chrome, el sitio https://gilpgpwa.github.io/.

  2. Descarga el archivo /src/pwab.zip y descompáctalo.

  3. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  4. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  5. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  6. Importa el proyecto de GitHub a Visual Studio Code

  7. Edita los archivos que desees.

  8. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  9. Crea los íconos del proyecto con https://www.photopea.com/.

  10. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  11. Coloca el archivo favicon.ico en la raíz del proyecto.

  12. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  13. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  14. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  15. Prueba tu PWA.

  16. Publica tu PWA.

  17. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  18. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  19. Instala y usa tu PWA en iOS (iPhone y iPad).

C. Hazlo funcionar (texto)

  1. Prueba e instala, de preferencia con Chrome, el sitio https://gilpgpwa.github.io/.

  2. Descarga el archivo /src/pwab.zip y descompáctalo.

  3. Crea tu proyecto en GitHub pages:

    1. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

      • Mantén la selección Public.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

    5. Entra al repositorio y selecciona ⚙️ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto.Si haces cambios, clic en Save.

    6. En la misma pestaña de ⚙️ Settings selecciona ◯ Actions, luego selecciona General y en Actions permissions selecciona el rediobotón Allow xxxx, and select non-xxxx, actions and reusable workflows y abajo la casilla de verificación Allow actions created by GitHub y cliquea el botón Save.

  4. Importa el proyecto de GitHub a Visual Studio Code:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

  5. Edita los archivos que desees.

  6. Crea los íconos del proyecto con https://www.photopea.com/.

  7. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  8. Coloca el archivo favicon.ico en la raíz del proyecto.

  9. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  10. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  11. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  12. Haz clic derecho en index.html, selecciona Open with Live Server y se abre el navegador para que puedas probar localmente el ejemplo.

  13. Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.

  14. Coloca las capturas en la carpeta img y asegúrate de que estén declaradas en el archivo site.webmanifest.

  15. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en vez de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

    5. En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error.

    6. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.

  16. Cuando usas GitHub pages, antes de subir los archivos, no debes modificar el valor de VERSION en el archivo sw.js.

  17. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push
  18. Si usas GitHub pages:

    1. Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.

    2. Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.

    3. A partir de este momento, espera al menos 11 minutos para modificar el valor de VERSION en el archivo sw.js y volver a subir el proyecto.

  19. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  20. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  21. Instala y usa tu PWA en iOS (iPhone y iPad).

D. Archivos

E. index.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>PWA Básica</title>
9
10
 <!-- Resumen para los motores de búsqueda. -->
11
 <meta name="description" content="Ejemplo de PWA">
12
13
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
14
15
 <meta name="viewport" content="width=device-width">
16
17
 <!-- Color de la barra de navegación de Chrome en dispositivos móviles. -->
18
 <meta name="theme-color" content="#cbc693">
19
20
 <!-- Ícono para la página web, que normalmente se pone en la raíz del sitio.
21
  Puede ser diferente para cada página. -->
22
 <link rel="icon" sizes="32x32" href="favicon.ico">
23
24
 <!-- Configuración de la PWA para Chrome, Edge y Safari.
25
  Debe ponerse en todas las páginas. -->
26
 <link rel="manifest" href="site.webmanifest">
27
28
 <link rel="stylesheet" href="css/estilos.css">
29
30
</head>
31
32
<body>
33
34
 <h1>PWA Básica</h1>
35
36
 <p>Hola mundo.</p>
37
38
</body>
39
40
</html>

F. Carpeta « css »

A. css / estilos.css

1
html {
2
 color-scheme: light dark;
3
 font-family: sans-serif;
4
}

G. Carpeta « js »

A. Carpeta « js / lib »

1. js / lib / registraServiceWorker.js

1
const nombreDeServiceWorker = "sw.js"
2
3
try {
4
 navigator.serviceWorker.register(nombreDeServiceWorker)
5
  .then(registro => {
6
   console.log(nombreDeServiceWorker, "registrado.")
7
   console.log(registro)
8
  })
9
  .catch(error => console.log(error))
10
} catch (error) {
11
 console.log(error)
12
}

H. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

short_name

Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.

name

Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.

id

Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.

start_url

Ruta del archivo inicial de la app.

display

Forma de mostrar la app. El término standalone significa que no se muestra la barra de navegación del navegador web.

theme_color

Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.

background_color

Color de fondo de la pantalla desplah en dispositivos móviles.

description

Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

screenshots

Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo que muestra el navegador al instalar la app. Debes incluir al menos una con "form_factor": "wide" y otra con "form_factor": "narrow".

icons

Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.

src

Url de la imagen dentro de la app.

sizes

Dimensiones en pixeles de la imagen, anchoxalto.

type

Tipo mime de la imagen.

purpose

Forma en que se usa la imagen.

maskable

La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.

any

No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.

Normalmente debe proporcionarse un juego de íconos con purpose any y otro juego de íconos con purpose maskable.

form_factor

Orientación de una screenshot.

wide

La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.

narrow

La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.

Debes incluir al menos una screenshot con "form_factor": "wide" y otra con "form_factor": "narrow".

label

Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

1
{
2
 "short_name": "PWA",
3
 "name": "Ejemplo de PWA",
4
 "id": "/index.html",
5
 "start_url": "/index.html",
6
 "display": "standalone",
7
 "theme_color": "#cbc693",
8
 "background_color": "#ffffff",
9
 "description": "Ejemplos básico de PWA.",
10
 "screenshots": [
11
  {
12
   "src": "/img/screenshot_horizontal.png",
13
   "sizes": "1507x777",
14
   "type": "image/png",
15
   "form_factor": "wide",
16
   "label": "PWA Básica"
17
  },
18
  {
19
   "src": "/img/screenshot_vertical.png",
20
   "sizes": "591x980",
21
   "type": "image/png",
22
   "form_factor": "narrow",
23
   "label": "PWA Básica (2)"
24
  }
25
 ],
26
 "icons": [
27
  {
28
   "src": "/img/maskable_icon_x48.png",
29
   "sizes": "48x48",
30
   "type": "image/png",
31
   "purpose": "any"
32
  },
33
  {
34
   "src": "/img/maskable_icon_x72.png",
35
   "sizes": "72x72",
36
   "type": "image/png",
37
   "purpose": "any"
38
  },
39
  {
40
   "src": "/img/maskable_icon_x96.png",
41
   "sizes": "96x96",
42
   "type": "image/png",
43
   "purpose": "any"
44
  },
45
  {
46
   "src": "/img/maskable_icon_x128.png",
47
   "sizes": "128x128",
48
   "type": "image/png",
49
   "purpose": "any"
50
  },
51
  {
52
   "src": "/img/maskable_icon_x192.png",
53
   "sizes": "192x192",
54
   "type": "image/png",
55
   "purpose": "any"
56
  },
57
  {
58
   "src": "/img/maskable_icon_x384.png",
59
   "sizes": "384x384",
60
   "type": "image/png",
61
   "purpose": "any"
62
  },
63
  {
64
   "src": "/img/maskable_icon_x512.png",
65
   "sizes": "512x512",
66
   "type": "image/png",
67
   "purpose": "any"
68
  },
69
  {
70
   "src": "/img/maskable_icon.png",
71
   "sizes": "2730x2730",
72
   "type": "image/png",
73
   "purpose": "any"
74
  },
75
  {
76
   "src": "/img/icono2048.png",
77
   "sizes": "2048x2048",
78
   "type": "image/png",
79
   "purpose": "any"
80
  },
81
  {
82
   "src": "/img/maskable_icon_x48.png",
83
   "sizes": "48x48",
84
   "type": "image/png",
85
   "purpose": "maskable"
86
  },
87
  {
88
   "src": "/img/maskable_icon_x72.png",
89
   "sizes": "72x72",
90
   "type": "image/png",
91
   "purpose": "maskable"
92
  },
93
  {
94
   "src": "/img/maskable_icon_x96.png",
95
   "sizes": "96x96",
96
   "type": "image/png",
97
   "purpose": "maskable"
98
  },
99
  {
100
   "src": "/img/maskable_icon_x128.png",
101
   "sizes": "128x128",
102
   "type": "image/png",
103
   "purpose": "maskable"
104
  },
105
  {
106
   "src": "/img/maskable_icon_x192.png",
107
   "sizes": "192x192",
108
   "type": "image/png",
109
   "purpose": "maskable"
110
  },
111
  {
112
   "src": "/img/maskable_icon_x384.png",
113
   "sizes": "384x384",
114
   "type": "image/png",
115
   "purpose": "maskable"
116
  },
117
  {
118
   "src": "/img/maskable_icon_x512.png",
119
   "sizes": "512x512",
120
   "type": "image/png",
121
   "purpose": "maskable"
122
  },
123
  {
124
   "src": "/img/maskable_icon.png",
125
   "sizes": "2730x2730",
126
   "type": "image/png",
127
   "purpose": "maskable"
128
  }
129
 ]
130
}

I. instruccionesListadoSw.txt

1
Generar el listado de archivos del sw.js desde Visual Studio Code.
2
1. Abrir una terminal desde el menú 
3
    Terminal > New Terminal
4
5
2. Desde la terminal introducir la orden:
6
    Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
8
3. Abrir el archivo generado, que se llama
9
    archivos.txt
10
   y sobre este, realizar los pasos que siguen:
11
12
4. Quita del archivo archivos.txt:
13
    * el encabezado,
14
    * todas las carpetas,
15
    * el archivo .vscode/settings.json,
16
    * el archivo .htaccess,
17
    * el archivo archivos.txt,
18
    * este archivo (instruccionesListadoSw.txt),
19
    * el archivo jsconfig.json,
20
    * el archivo sw.js,
21
    * el archivo de la base de datos, que termina en ".bd" y
22
      está en la carpeta srv,
23
    * todos los archivos de php y
24
    * las líneas en blanco del final
25
26
5. Cambia los \ por / desde Visual Studio Code con las siguientes
27
   combinaciones de teclas:
28
29
    Ctrl+H En el diálogo que aparece introduce lo siguiente:
30
    Find:\
31
    Replace:/
32
33
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
34
 
35
6. Coloca las comillas y coma del final de cada línea desde Visual
36
   Studio Code con las siguientes combinaciones de teclas:
37
38
    Ctrl+H En el diálogo que aparece, selecciona el botón
39
            ".*"
40
           e introduce lo siguiente:
41
    Find:\s*$
42
    Replace:",
43
44
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
45
46
7. Marca la carpeta inicial, presiona la combinación de teclas:
47
48
    Shift+Ctrl+L
49
50
    borra la selección, teclea " y luego ESC
51
52
8. Cambia las secuencias de espacios por / con las siguientes
53
   combinaciones de teclas:
54
55
    Ctrl+H En el diálogo que aparece, selecciona el botón
56
            ".*"
57
           e introduce lo siguiente:
58
    Find:\s+
59
    Replace:/
60
61
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
62
63
9. Cambia las "/ por " con las siguientes combinaciones de teclas:
64
65
    Ctrl+H En el diálogo que aparece, quita la selección del botón
66
            ".*"
67
           e introduce lo siguiente:
68
    Find:"/
69
    Replace:"
70
71
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
72
73
10. Copia el texto al archivo
74
     sw.js
75
    en el contenido del arreglo llamado ARCHIVOS, pero recuerda
76
    mantener el último elemento, que dice:
77
     "/"

J. archivos.txt

1
"favicon.ico",
2
"index.html",
3
"site.webmanifest",
4
"css/estilos.css",
5
"img/icono2048.png",
6
"img/maskable_icon.png",
7
"img/maskable_icon_x128.png",
8
"img/maskable_icon_x192.png",
9
"img/maskable_icon_x384.png",
10
"img/maskable_icon_x48.png",
11
"img/maskable_icon_x512.png",
12
"img/maskable_icon_x72.png",
13
"img/maskable_icon_x96.png",
14
"img/screenshot_horizontal.png",
15
"img/screenshot_vertical.png",
16
"js/registraServiceWorker.js",

K. sw.js

1
/* Este archivo debe estar colocado en la carpeta raíz del sitio.
2
 * 
3
 * Cualquier cambio en el contenido de este archivo hace que el service
4
 * worker se reinstale. */
5
6
/**
7
 * Cambia el número de la versión cuando cambia el contenido de los
8
 * archivos.
9
 * 
10
 * El número a la izquierda del punto (.), en este caso <q>1</q>, se
11
 * conoce como número mayor y se cambia cuando se realizan
12
 * modificaciones grandes o importantes.
13
 * 
14
 * El número a la derecha del punto (.), en este caso <q>00</q>, se
15
 * conoce como número menor y se cambia cuando se realizan
16
 * modificaciones menores.
17
 */
18
const VERSION = "1.00"
19
20
/** Nombre del archivo de cache. */
21
const CACHE = "ejemploPWA"
22
23
/**
24
 * Archivos requeridos para que la aplicación funcione fuera de
25
 * línea.
26
 */
27
const ARCHIVOS = [
28
 "favicon.ico",
29
 "index.html",
30
 "site.webmanifest",
31
 "css/estilos.css",
32
 "img/icono2048.png",
33
 "img/maskable_icon.png",
34
 "img/maskable_icon_x128.png",
35
 "img/maskable_icon_x192.png",
36
 "img/maskable_icon_x384.png",
37
 "img/maskable_icon_x48.png",
38
 "img/maskable_icon_x512.png",
39
 "img/maskable_icon_x72.png",
40
 "img/maskable_icon_x96.png",
41
 "img/screenshot_horizontal.png",
42
 "img/screenshot_vertical.png",
43
 "js/registraServiceWorker.js",
44
 "/"
45
]
46
47
// Verifica si el código corre dentro de un service worker.
48
if (self instanceof ServiceWorkerGlobalScope) {
49
 // Evento al empezar a instalar el servide worker,
50
 self.addEventListener("install",
51
  (/** @type {ExtendableEvent} */ evt) => {
52
   console.log("El service worker se está instalando.")
53
   evt.waitUntil(llenaElCache())
54
  })
55
56
 // Evento al solicitar información a la red.
57
 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
58
  if (evt.request.method === "GET") {
59
   evt.respondWith(buscaLaRespuestaEnElCache(evt))
60
  }
61
 })
62
63
 // Evento cuando el service worker se vuelve activo.
64
 self.addEventListener("activate",
65
  () => console.log("El service worker está activo."))
66
}
67
68
async function llenaElCache() {
69
 console.log("Intentando cargar caché:", CACHE)
70
 // Borra todos los cachés.
71
 const keys = await caches.keys()
72
 for (const key of keys) {
73
  await caches.delete(key)
74
 }
75
 // Abre el caché de este service worker.
76
 const cache = await caches.open(CACHE)
77
 // Carga el listado de ARCHIVOS.
78
 await cache.addAll(ARCHIVOS)
79
 console.log("Cache cargado:", CACHE)
80
 console.log("Versión:", VERSION)
81
}
82
83
/** @param {FetchEvent} evt */
84
async function buscaLaRespuestaEnElCache(evt) {
85
 // Abre el caché.
86
 const cache = await caches.open(CACHE)
87
 const request = evt.request
88
 /* Busca la respuesta a la solicitud en el contenido del caché, sin
89
  * tomar en cuenta la parte después del símbolo "?" en la URL. */
90
 const response = await cache.match(request, { ignoreSearch: true })
91
 if (response === undefined) {
92
  /* Si no la encuentra, empieza a descargar de la red y devuelve
93
   * la promesa. */
94
  return fetch(request)
95
 } else {
96
  // Si la encuentra, devuelve la respuesta encontrada en el caché.
97
  return response
98
 }
99
}

L. favicon.ico

favicon.ico

M. Carpeta « img »

A. img / icono2048.png

icono2048.png

B. img / maskable_icon.png

maskable_icon.png

C. img / maskable_icon_x128.png

maskable_icon_x128.png

D. img / maskable_icon_x192.png

maskable_icon_x192.png

E. img / maskable_icon_x384.png

maskable_icon_x384.png

F. img / maskable_icon_x48.png

maskable_icon_x48.png

G. img / maskable_icon_x512.png

maskable_icon_x512.png

H. img / maskable_icon_x72.png

maskable_icon_x72.png

I. img / maskable_icon_x96.png

maskable_icon_x96.png

J. img / screenshot_horizontal.png

screenshot_horizontal.png

K. img / screenshot_vertical.png

screenshot_vertical.png

N. .htaccess

  • Este archivo configura las respuestas del servidor..

  • Lo utilizan principalmente servidores como Apache o Nginx.

  • Configura el mime type para el archivo de manifiesto y sedhabilita el uso de la cache general de http..

1
AddType application/manifest+json .webmanifest
2
3
ExpiresActive On
4
5
Header set Cache-Control "max-age=1, must-revalidate"
6

O. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

P. Resumen

  • En esta lección se presentó la estructura básica de una PWA.

6. PWA con listas y Material Design 3 Expressive

Versión para imprimir.

A. Introduccion

  • En esta lección se presenta una PWA que despliega listas y usa Material Design 3 Expressive.

  • Puedes probar el ejemplo en https://gilpgpwal.github.io/.

B. Referencias

Sitios de Material Design
Material.io

Material Design es una guía de diseño para aplicaciones multiplataforma. La encuentras en https://m3.material.io/.

Herramienta para selección de colores
Material Theme Builder

https://material-foundation.github.io/material-theme-builder/

Adaptación multiplataforma

La forma de adaptar Material Design en distintas plataformas está en https://material.io/design/platform-guidance/cross-platform-adaptation.html

C. Hazlo funcionar (con videos)

  1. Prueba e instala, de preferencia con Chrome, el sitio https://gilpgpwal.github.io/.

  2. Descarga el archivo /src/pwalista.zip y descompáctalo.

  3. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  4. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  5. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  6. Importa el proyecto de GitHub a Visual Studio Code

  7. Edita los archivos que desees.

  8. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  9. Crea los íconos del proyecto con https://www.photopea.com/.

  10. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  11. Coloca el archivo favicon.ico en la raíz del proyecto.

  12. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  13. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  14. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  15. Prueba tu PWA.

  16. Publica tu PWA.

  17. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  18. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  19. Instala y usa tu PWA en iOS (iPhone y iPad).

D. Hazlo funcionar (texto)

  1. Prueba e instala, de preferencia con Chrome, el sitio https://gilpgpwal.github.io/.

  2. Descarga el archivo /src/pwalista.zip y descompáctalo.

  3. Crea tu proyecto en GitHub pages:

    1. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

      • Mantén la selección Public.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

    5. Entra al repositorio y selecciona ⚙️ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto. Si haces cambios, clic en Save.

    6. En la misma pestaña de ⚙️ Settings selecciona ◯ Actions, luego selecciona General y en Actions permissions selecciona el rediobotón Allow xxxx, and select non-xxxx, actions and reusable workflows y abajo la casilla de verificación Allow actions created by GitHub y cliquea el botón Save.

  4. Importa el proyecto de GitHub a Visual Studio Code:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo, excepto el archivo .htaccess.

  5. Edita los archivos que desees.

  6. Crea los íconos del proyecto con https://www.photopea.com/.

  7. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  8. Coloca el archivo favicon.ico en la raíz del proyecto.

  9. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  10. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  11. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  12. Haz clic derecho en index.html, selecciona Open with Live Server y se abre el navegador para que puedas probar localmente el ejemplo.

  13. Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.

  14. Coloca las capturas en la carpeta img y asegúrate de que estén declaradas en el archivo site.webmanifest.

  15. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en vez de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

    5. En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error.

    6. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.

  16. Cuando usas GitHub pages, antes de subir los archivos, no debes modificar el valor de VERSION en el archivo sw.js.

  17. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push
  18. Si usas GitHub pages:

    1. Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.

    2. Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.

    3. A partir de este momento, espera al menos 11 minutos para modificar el valor de VERSION en el archivo sw.js y volver a subir el proyecto.

  19. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  20. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  21. Instala y usa tu PWA en iOS (iPhone y iPad).

E. Archivos

F. index.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>PWA con Lista</title>
9
10
 <!-- Resumen para los motores de búsqueda. -->
11
 <meta name="description" content="Ejemplo de PWA con Material Design">
12
13
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
14
15
 <meta name="viewport" content="width=device-width">
16
17
 <!-- Color de la barra de navegación de Chrome en dispositivos móviles. -->
18
 <meta name="theme-color" content="#fffbfe">
19
20
 <!-- Ícono para la página web, que normalmente se pone en la raíz del sitio.
21
  Puede ser diferente para cada página. -->
22
 <link rel="icon" sizes="32x32" href="favicon.ico">
23
24
 <!-- Configuración de la PWA para Chrome, Edge y Safari.
25
  Debe ponerse en todas las páginas. -->
26
 <link rel="manifest" href="site.webmanifest">
27
28
 <!-- Permite a los navegadores, que como Safari no soportan el estándar
29
  completo de custom elementsm, lo cumplan totalmente. Debe ponerse en todas
30
  las páginas. -->
31
 <script src="ungap/custom-elements.js"></script>
32
33
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
34
 <script type="module" src="js/nav-tab-fixed.js"></script>
35
36
 <link rel="stylesheet" href="css/estilos.css">
37
 <link rel="stylesheet" href="css/transicion_pestanas.css">
38
 <link rel="stylesheet" href="css/md-tab.css">
39
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
40
 <link rel="stylesheet" href="css/md-list.css">
41
42
</head>
43
44
<body>
45
46
 <md-app-bar class="centered" adicional="tab">
47
48
  <h1>PWA con Lista</h1>
49
50
 </md-app-bar>
51
52
 <nav-tab-fixed id="tab"></nav-tab-fixed>
53
54
 <section>
55
56
  <ul class="md-list">
57
58
   <li class="md-one-line">
59
    <span class="headline">
60
     Ciudad de México
61
    </span>
62
   </li>
63
64
   <li class="md-one-line">
65
    <span class="headline">
66
     Lorem ipsum dolor sit amet consectetur adipisicing elit.
67
     Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
68
     illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
69
     alias!
70
    </span>
71
   </li>
72
73
   <li class="md-one-line icon">
74
    <span class="material-symbols-outlined">account_balance</span>
75
    <span class="headline">
76
     Atenas
77
    </span>
78
   </li>
79
80
   <li class="md-one-line avatar">
81
    <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg">
82
    <span class="headline">
83
     Ana
84
    </span>
85
   </li>
86
87
   <li class="md-one-line image">
88
    <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
89
    <span class="headline">
90
     Neza
91
    </span>
92
   </li>
93
94
   <li class="md-one-line video">
95
    <img alt="Ciudad de San Francisco"
96
      src="img/pexels-craig-dennis-3701822.jpg">
97
    <span class="headline">
98
     San Francisco
99
    </span>
100
   </li>
101
102
   <li class="md-one-line video">
103
    <img alt="Ciudad de San Francisco"
104
      src="img/pexels-craig-dennis-3701822.jpg">
105
    <span class="headline">
106
     Lorem ipsum dolor sit amet consectetur adipisicing elit.
107
     Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
108
     illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
109
    </span>
110
   </li>
111
112
   <li>
113
    <a class="md-one-line image" target="_blank" rel="noopener noreferrer"
114
      href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/">
115
     <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
116
     <span class="headline">
117
      Neza Link
118
     </span>
119
    </a>
120
   </li>
121
122
  </ul>
123
124
  <footer>
125
   <ul>
126
    <li>
127
     <p>
128
      <small>
129
       <a target="_blank" rel="noopener noreferrer"
130
         href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/">
131
        La foto de la niña es de Moises Patrício, publicada en el sitio Pexels.
132
        Haz clic en este hipervínculo para más información.
133
       </a>
134
      </small>
135
     </p>
136
    </li>
137
    <li>
138
     <p>
139
      <small>
140
       <a target="_blank" rel="noopener noreferrer"
141
         href="https://www.pinterest.com.mx/ludresi/">
142
        La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio
143
        Pinterest. Haz clic en este hipervínculo para más información.
144
       </a>
145
      </small>
146
     </p>
147
    </li>
148
    <li>
149
     <p>
150
      <small>
151
       <a target="_blank" rel="noopener noreferrer"
152
         href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/">
153
        La foto del puente de San Francisco es de Craig Dennis, publicada en el
154
        sitio Pexels. Haz clic en este hipervínculo para más información.
155
       </a>
156
      </small>
157
     </p>
158
    </li>
159
   </ul>
160
  </footer>
161
162
 </section>
163
164
</body>
165
166
</html>

G. ayuda.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>Ayuda - PWA con Lista</title>
9
10
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
11
12
 <meta name="viewport" content="width=device-width">
13
 <meta name="theme-color" content="#fffbfe">
14
 <link rel="icon" sizes="32x32" href="favicon.ico">
15
 <link rel="manifest" href="site.webmanifest">
16
 <script src="ungap/custom-elements.js"></script>
17
18
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
19
 <script type="module" src="js/nav-tab-fixed.js"></script>
20
21
 <link rel="stylesheet" href="css/estilos.css">
22
 <link rel="stylesheet" href="css/transicion_pestanas.css">
23
 <link rel="stylesheet" href="css/md-headline.css">
24
 <link rel="stylesheet" href="css/md-tab.css">
25
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
26
 <link rel="stylesheet" href="css/md-list.css">
27
28
</head>
29
30
<body>
31
32
 <md-app-bar adicional="tab">
33
34
  <h1>Ayuda</h1>
35
36
 </md-app-bar>
37
38
 <nav-tab-fixed id="tab"></nav-tab-fixed>
39
40
 <section>
41
42
  <ul class="md-list">
43
   <li class="md-two-line">
44
    <span class="headline">
45
     Título
46
    </span>
47
    <span class="supporting">
48
     PWA con Material Design
49
    </span>
50
   </li>
51
   <li class="md-two-line">
52
    <span class="headline">
53
     Descripción
54
    </span>
55
    <span class="supporting">
56
     Ejemplos de vistas móviles.
57
    </span>
58
   </li>
59
   <li class="md-two-line">
60
    <span class="headline">
61
     Autor
62
    </span>
63
    <span class="supporting">
64
     Gilberto Pacheco Gallegos
65
    </span>
66
   </li>
67
   <li class="md-two-line">
68
    <span class="headline">
69
     Derechos de autor
70
    </span>
71
    <span class="supporting">
72
     © 2025 Gilberto Pacheco Gallegos
73
    </span>
74
   </li>
75
   <li class="md-three-line">
76
    <span class="headline">
77
     Este software usa la librería para PWA
78
    </span>
79
    <span class="supporting">
80
     Esta obra de Gilberto Pacheco Gallegos está bajo una
81
     <a target="_blank" rel="license noreferrer"
82
       href="http://creativecommons.org/licenses/by/4.0/">
83
      Licencia Creative Commons Atribución 4.0 Internacional</a></span>
84
   </li>
85
   <li>
86
    <a class="md-three-line" target="_blank" rel=”noreferrer”
87
      href="https://fonts.google.com/icons">
88
     <span class="headline">
89
      También usa Material Symbols
90
     </span>
91
     <span class="supporting">
92
      Desarrollada por Google bajo licencia Apache 2.0
93
     </span>
94
    </a>
95
   </li>
96
  </ul>
97
98
 </section>
99
100
 <nav-drw></nav-drw>
101
102
</body>
103
104
</html>

H. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

short_name

Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.

name

Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.

id

Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.

start_url

Ruta del archivo inicial de la app.

display

Forma de mostrar la app. El término standalone significa que no se muestra la barra de navegación del navegador web.

theme_color

Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.

background_color

Color de fondo de la pantalla desplah en dispositivos móviles.

description

Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

screenshots

Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo que muestra el navegador al instalar la app. Debes incluir al menos una con "form_factor": "wide" y otra con "form_factor": "narrow".

icons

Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.

src

Url de la imagen dentro de la app.

sizes

Dimensiones en pixeles de la imagen, anchoxalto.

type

Tipo mime de la imagen.

purpose

Forma en que se usa la imagen.

maskable

La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.

any

No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.

Normalmente debe proporcionarse un juego de íconos con purpose any y otro juego de íconos con purpose maskable.

form_factor

Orientación de una screenshot.

wide

La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.

narrow

La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.

Debes incluir al menos una screenshot con "form_factor": "wide" y otra con "form_factor": "narrow".

label

Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

1
{
2
 "short_name": "Lista PWA",
3
 "name": "PWA con Lista",
4
 "id": "/index.html",
5
 "start_url": "/index.html",
6
 "display": "standalone",
7
 "theme_color": "#fffbfe",
8
 "background_color": "#fffbfe",
9
 "display_override": [
10
  "window-controls-overlay"
11
 ],
12
 "description": "Ejemplos de una lista con Material Design 3.",
13
 "screenshots": [
14
  {
15
   "src": "/img/screenshot_horizontal.png",
16
   "sizes": "699x389",
17
   "type": "image/png",
18
   "form_factor": "wide",
19
   "label": "Lista con Material Design 3."
20
  },
21
  {
22
   "src": "/img/screenshot_vertical.png",
23
   "sizes": "502x636",
24
   "type": "image/png",
25
   "form_factor": "narrow",
26
   "label": "Lista con Material Design 3. (2)"
27
  }
28
 ],
29
 "icons": [
30
  {
31
   "purpose": "maskable",
32
   "sizes": "2730x2730",
33
   "src": "img/maskable_icon.png",
34
   "type": "image/png"
35
  },
36
  {
37
   "purpose": "maskable",
38
   "sizes": "48x48",
39
   "src": "img/maskable_icon_x48.png",
40
   "type": "image/png"
41
  },
42
  {
43
   "purpose": "maskable",
44
   "sizes": "72x72",
45
   "src": "img/maskable_icon_x72.png",
46
   "type": "image/png"
47
  },
48
  {
49
   "purpose": "maskable",
50
   "sizes": "96x96",
51
   "src": "img/maskable_icon_x96.png",
52
   "type": "image/png"
53
  },
54
  {
55
   "purpose": "maskable",
56
   "sizes": "128x128",
57
   "src": "img/maskable_icon_x128.png",
58
   "type": "image/png"
59
  },
60
  {
61
   "purpose": "maskable",
62
   "sizes": "192x192",
63
   "src": "img/maskable_icon_x192.png",
64
   "type": "image/png"
65
  },
66
  {
67
   "purpose": "maskable",
68
   "sizes": "384x384",
69
   "src": "img/maskable_icon_x384.png",
70
   "type": "image/png"
71
  },
72
  {
73
   "purpose": "maskable",
74
   "sizes": "512x512",
75
   "src": "img/maskable_icon_x512.png",
76
   "type": "image/png"
77
  },
78
  {
79
   "purpose": "any",
80
   "sizes": "2730x2730",
81
   "src": "img/maskable_icon.png",
82
   "type": "image/png"
83
  },
84
  {
85
   "purpose": "any",
86
   "sizes": "48x48",
87
   "src": "img/maskable_icon_x48.png",
88
   "type": "image/png"
89
  },
90
  {
91
   "purpose": "any",
92
   "sizes": "72x72",
93
   "src": "img/maskable_icon_x72.png",
94
   "type": "image/png"
95
  },
96
  {
97
   "purpose": "any",
98
   "sizes": "96x96",
99
   "src": "img/maskable_icon_x96.png",
100
   "type": "image/png"
101
  },
102
  {
103
   "purpose": "any",
104
   "sizes": "128x128",
105
   "src": "img/maskable_icon_x128.png",
106
   "type": "image/png"
107
  },
108
  {
109
   "purpose": "any",
110
   "sizes": "192x192",
111
   "src": "img/maskable_icon_x192.png",
112
   "type": "image/png"
113
  },
114
  {
115
   "purpose": "any",
116
   "sizes": "384x384",
117
   "src": "img/maskable_icon_x384.png",
118
   "type": "image/png"
119
  },
120
  {
121
   "purpose": "any",
122
   "sizes": "512x512",
123
   "src": "img/maskable_icon_x512.png",
124
   "type": "image/png"
125
  },
126
  {
127
   "purpose": "any",
128
   "sizes": "2048x2048",
129
   "src": "img/icono2048.png",
130
   "type": "image/png"
131
  }
132
 ]
133
}

I. sw.js

1
/* Este archivo debe estar colocado en la carpeta raíz del sitio.
2
 * 
3
 * Cualquier cambio en el contenido de este archivo hace que el service
4
 * worker se reinstale. */
5
6
/**
7
 * Cambia el número de la versión cuando cambia el contenido de los
8
 * archivos.
9
 * 
10
 * El número a la izquierda del punto (.), en este caso <q>1</q>, se
11
 * conoce como número mayor y se cambia cuando se realizan
12
 * modificaciones grandes o importantes.
13
 * 
14
 * El número a la derecha del punto (.), en este caso <q>00</q>, se
15
 * conoce como número menor y se cambia cuando se realizan
16
 * modificaciones menores.
17
 */
18
const VERSION = "1.00"
19
20
/**
21
 * Nombre de la carpeta de caché.
22
 */
23
const CACHE = "pwamd"
24
25
/**
26
 * Archivos requeridos para que la aplicación funcione fuera de
27
 * línea.
28
 */
29
const ARCHIVOS = [
30
 "ayuda.html",
31
 "favicon.ico",
32
 "index.html",
33
 "site.webmanifest",
34
 "css/baseline.css",
35
 "css/colors.css",
36
 "css/elevation.css",
37
 "css/estilos.css",
38
 "css/material-symbols-outlined.css",
39
 "css/md-headline.css",
40
 "css/md-list.css",
41
 "css/md-tab.css",
42
 "css/motion.css",
43
 "css/palette.css",
44
 "css/roboto.css",
45
 "css/shape.css",
46
 "css/state.css",
47
 "css/transicion_pestanas.css",
48
 "css/typography.css",
49
 "css/theme/dark.css",
50
 "css/theme/light.css",
51
 "fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
52
 "fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
53
 "fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
54
 "fonts/roboto-v32-latin-regular.woff2",
55
 "img/Escultura_de_coyote.jpeg",
56
 "img/icono2048.png",
57
 "img/maskable_icon.png",
58
 "img/maskable_icon_x128.png",
59
 "img/maskable_icon_x192.png",
60
 "img/maskable_icon_x384.png",
61
 "img/maskable_icon_x48.png",
62
 "img/maskable_icon_x512.png",
63
 "img/maskable_icon_x72.png",
64
 "img/maskable_icon_x96.png",
65
 "img/pexels-craig-dennis-3701822.jpg",
66
 "img/pexels-moises-patrício-10961948.jpg",
67
 "img/screenshot_horizontal.png",
68
 "img/screenshot_vertical.png",
69
 "js/nav-tab-fixed.js",
70
 "js/lib/ES_APPLE.js",
71
 "js/lib/getAttribute.js",
72
 "js/lib/querySelector.js",
73
 "js/lib/registraServiceWorker.js",
74
 "js/lib/resaltaSiEstasEn.js",
75
 "js/lib/custom/md-app-bar.js",
76
 "ungap/custom-elements.js",
77
 "/"
78
]
79
80
// Verifica si el código corre dentro de un service worker.
81
if (self instanceof ServiceWorkerGlobalScope) {
82
 // Evento al empezar a instalar el servide worker,
83
 self.addEventListener("install",
84
  (/** @type {ExtendableEvent} */ evt) => {
85
   console.log("El service worker se está instalando.")
86
   evt.waitUntil(llenaElCache())
87
  })
88
89
 // Evento al solicitar información a la red.
90
 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
91
  if (evt.request.method === "GET") {
92
   evt.respondWith(buscaLaRespuestaEnElCache(evt))
93
  }
94
 })
95
96
 // Evento cuando el service worker se vuelve activo.
97
 self.addEventListener("activate",
98
  () => console.log("El service worker está activo."))
99
}
100
101
async function llenaElCache() {
102
 console.log("Intentando cargar caché:", CACHE)
103
 // Borra todos los cachés.
104
 const keys = await caches.keys()
105
 for (const key of keys) {
106
  await caches.delete(key)
107
 }
108
 // Abre el caché de este service worker.
109
 const cache = await caches.open(CACHE)
110
 // Carga el listado de ARCHIVOS.
111
 await cache.addAll(ARCHIVOS)
112
 console.log("Cache cargado:", CACHE)
113
 console.log("Versión:", VERSION)
114
}
115
116
/** @param {FetchEvent} evt */
117
async function buscaLaRespuestaEnElCache(evt) {
118
 // Abre el caché.
119
 const cache = await caches.open(CACHE)
120
 const request = evt.request
121
 /* Busca la respuesta a la solicitud en el contenido del caché, sin
122
  * tomar en cuenta la parte después del símbolo "?" en la URL. */
123
 const response = await cache.match(request, { ignoreSearch: true })
124
 if (response === undefined) {
125
  /* Si no la encuentra, empieza a descargar de la red y devuelve
126
   * la promesa. */
127
  return fetch(request)
128
 } else {
129
  // Si la encuentra, devuelve la respuesta encontrada en el caché.
130
  return response
131
 }
132
}

J. Carpeta « css »

A. css / baseline.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
@import url(palette.css);
18
@import url(typography.css);
19
@import url(colors.css);
20
@import url(shape.css);
21
@import url(motion.css);
22
@import url(state.css);
23
@import url(elevation.css);
24
@import url(theme/light.css) screen and (prefers-color-scheme: light);
25
@import url(theme/dark.css) screen and (prefers-color-scheme: dark);
26

B. css / colors.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
.primary {
18
  color: var(--md-sys-color-on-primary);
19
  background-color: var(--md-sys-color-primary);
20
}
21
.on-primary {
22
  color: var(--md-sys-color-primary);
23
  background-color: var(--md-sys-color-on-primary);
24
}
25
.primary-container {
26
  color: var(--md-sys-color-on-primary-container);
27
  background-color: var(--md-sys-color-primary-container);
28
}
29
.on-primary-container {
30
  color: var(--md-sys-color-primary-container);
31
  background-color: var(--md-sys-color-on-primary-container);
32
}
33
.secondary {
34
  color: var(--md-sys-color-on-secondary);
35
  background-color: var(--md-sys-color-secondary);
36
}
37
.on-secondary {
38
  color: var(--md-sys-color-secondary);
39
  background-color: var(--md-sys-color-on-secondary);
40
}
41
.secondary-container {
42
  color: var(--md-sys-color-on-secondary-container);
43
  background-color: var(--md-sys-color-secondary-container);
44
}
45
.on-secondary-container {
46
  color: var(--md-sys-color-secondary-container);
47
  background-color: var(--md-sys-color-on-secondary-container);
48
}
49
.tertiary {
50
  color: var(--md-sys-color-on-tertiary);
51
  background-color: var(--md-sys-color-tertiary);
52
}
53
.on-tertiary {
54
  color: var(--md-sys-color-tertiary);
55
  background-color: var(--md-sys-color-on-tertiary);
56
}
57
.tertiary-container {
58
  color: var(--md-sys-color-on-tertiary-container);
59
  background-color: var(--md-sys-color-tertiary-container);
60
}
61
.on-tertiary-container {
62
  color: var(--md-sys-color-tertiary-container);
63
  background-color: var(--md-sys-color-on-tertiary-container);
64
}
65
.background {
66
  color: var(--md-sys-color-on-background);
67
  background-color: var(--md-sys-color-background);
68
}
69
.surface {
70
  color: var(--md-sys-color-on-surface);
71
  background-color: var(--md-sys-color-surface);
72
}
73
.surface-variant {
74
  color: var(--md-sys-color-on-surface-variant);
75
  background-color: var(--md-sys-color-surface-variant);
76
}
77
.on-surface-variant {
78
  color: var(--md-sys-color-surface-variant);
79
  background-color: var(--md-sys-color-on-surface-variant);
80
}
81
.outline {
82
  border: 1px solid var(--md-sys-color-outline);
83
}
84
.inverse-surface {
85
  color: var(--md-sys-color-on-inverse-surface);
86
  background-color: var(--md-sys-color-inverse-surface);
87
}
88
.on-inverse-surface {
89
  color: var(--md-sys-color-inverse-surface);
90
  background-color: var(--md-sys-color-on-inverse-surface);
91
}
92
.inverse-primary {
93
  color: var(--md-sys-color-on-inverse-primary);
94
  background-color: var(--md-sys-color-inverse-primary);
95
}
96
.on-inverse-primary {
97
  color: var(--md-sys-color-inverse-primary);
98
  background-color: var(--md-sys-color-on-inverse-primary);
99
}
100
.surface-tint {
101
  background-color: var(--md-sys-color-on-surface-tint);
102
}
103
.error {
104
  color: var(--md-sys-color-on-error);
105
  background-color: var(--md-sys-color-error);
106
}
107
.on-error {
108
  color: var(--md-sys-color-error);
109
  background-color: var(--md-sys-color-on-error);
110
}
111
.error-container {
112
  color: var(--md-sys-color-on-error-container);
113
  background-color: var(--md-sys-color-error-container);
114
}
115
.on-error-container {
116
  color: var(--md-sys-color-error-container);
117
  background-color: var(--md-sys-color-on-error-container);
118
}
119
.black {
120
  background-color: var(--md-ref-palette-black);
121
}
122
.black-text {
123
  color: var(--md-ref-palette-black);
124
}
125
.white {
126
  background-color: var(--md-ref-palette-white);
127
}
128
.white-text {
129
  color: var(--md-ref-palette-white);
130
}
131

C. css / elevation.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Surface tint color */
19
  --md-sys-elevation-surface-tint-color: var(--md-sys-color-primary);
20
  /* +5 */
21
  --md-sys-elevation-level5-value: 12px;
22
  --md-sys-elevation-level5-unit: 1px;
23
  --md-sys-elevation-level5: 12px;
24
  /* +4 */
25
  --md-sys-elevation-level4-value: 8px;
26
  --md-sys-elevation-level4-unit: 1px;
27
  --md-sys-elevation-level4: 8px;
28
  /* +3 */
29
  --md-sys-elevation-level3-value: 6px;
30
  --md-sys-elevation-level3-unit: 1px;
31
  --md-sys-elevation-level3: 6px;
32
  /* +2 */
33
  --md-sys-elevation-level2-value: 3px;
34
  --md-sys-elevation-level2-unit: 1px;
35
  --md-sys-elevation-level2: 3px;
36
  /* +1 */
37
  --md-sys-elevation-level1-value: 1px;
38
  --md-sys-elevation-level1-unit: 1px;
39
  --md-sys-elevation-level1: 1px;
40
  /* 0 */
41
  --md-sys-elevation-level0-value: 0px;
42
  --md-sys-elevation-level0-unit: 1px;
43
  --md-sys-elevation-level0: 0px;
44
}
45
.elevation-0 {
46
  box-shadow: var(--md-sys-elevation-level0);
47
}
48
.elevation-1 {
49
  box-shadow: var(--md-sys-elevation-level1);
50
}
51
.elevation-2 {
52
  box-shadow: var(--md-sys-elevation-level2);
53
}
54
.elevation-3 {
55
  box-shadow: var(--md-sys-elevation-level3);
56
}
57
.elevation-4 {
58
  box-shadow: var(--md-sys-elevation-level4);
59
}
60
.elevation-5 {
61
  box-shadow: var(--md-sys-elevation-level5);
62
}
63

D. css / estilos.css

1
/* Definiciones para Material Design 3  */
2
@import url(baseline.css);
3
/* Fonts utilizados */
4
@import url(roboto.css);
5
6
html {
7
 /* Indica los temas del sistema operativo que son soportados. */
8
 color-scheme: light dark;
9
 --tabWidth: 3.75rem;
10
 --anchoNav: 22.5rem;
11
}
12
13
body>section,
14
form>section {
15
 max-width: 600px;
16
 margin-left: auto;
17
 margin-right: auto;
18
}
19
20
/* Quita un borde rojo que coloca Firefox. */
21
:-moz-ui-invalid {
22
 box-shadow: none;
23
}
24
25
body {
26
 margin: 0;
27
 font-family: var(--md-sys-typescale-body-large-font);
28
 font-weight: var(--md-sys-typescale-body-large-weight);
29
 font-size: var(--md-sys-typescale-body-large-size);
30
 font-style: var(--md-sys-typescale-body-large-font-style);
31
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
32
 line-height: var(--md-sys-typescale-body-large-line-height);
33
 text-transform: var(--md-sys-typescale-body-large-text-transform);
34
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
35
 color: var(--md-sys-color-on-background);
36
 background-color: var(--md-sys-color-background);
37
 /* Las siguientes líneas Evita los cambios de apariencia al cargar estilos y
38
 + custom elements, que son conocidos como Flash Of Unstyled Content (fouc). */
39
 opacity: 0;
40
 animation-name: fouc;
41
 animation-fill-mode: forwards;
42
 animation-duration: 1.5s;
43
}
44
45
@keyframes fouc {
46
 to {
47
  opacity: 1;
48
 }
49
}
50
51
html {
52
 --Font: -apple-system, BlinkMacSystemFont, roboto, sans-serif;
53
 --colIntIos: white;
54
 --colIntIosOnBk: #2acc2a;
55
 --colIntIosOnBkFc: #1bbb1b;
56
 --colIntIosOffBk: #dbdbdb;
57
 --colIntIosOffBkFc: #BDBDBD;
58
 /* Plain typeface */
59
 --md-ref-typeface-plain: var(--Font);
60
 /* Brand typeface */
61
 --md-ref-typeface-brand: var(--Font);
62
 --md-sys-typescale-label-large-weight-prominent:
63
  var(--md-ref-typeface-weight-bold);
64
 --md-box_shadow_level4:
65
  0 var(--md-sys-elevation-level4) var(--md-sys-elevation-level4) var(--md-sys-color-shadow);
66
 --md-box_shadow_level3:
67
  0 var(--md-sys-elevation-level3) var(--md-sys-elevation-level3) var(--md-sys-color-shadow);
68
 --md-box_shadow_level2:
69
  0 var(--md-sys-elevation-level2) var(--md-sys-elevation-level2) var(--md-sys-color-shadow);
70
 --md-box_shadow_level1:
71
  0 var(--md-sys-elevation-level1) var(--md-sys-elevation-level1) var(--md-sys-color-shadow);
72
 --md-box_shadow_level0: none;
73
 --iconSize: 1.5rem;
74
 --avatarSize: 2.5rem;
75
 --imageSize: 3.5rem;
76
 --videoWidth: 7.125rem;
77
 --videoHeight: 4rem;
78
 --scroll-headline-duracion: 2s;
79
 --md-sys-state-focus-indicator-outer-offset: 0.125rem;
80
 --md-sys-state-focus-indicator-thickness: 0.1875rem;
81
 /* Pressed state layer opacity */
82
 --state-pressed-transparency-percentage: 84%;
83
 /* Focus state layer opacity */
84
 --state-focus-transparency-percentage: 88%;
85
 /* Hover state layer opacity */
86
 --state-hover-transparency-percentage: 92%;
87
}
88
89
p {
90
 margin: 1rem;
91
}
92
93
a {
94
 color: var(--md-sys-color-on-background);
95
}
96
97
@media (prefers-color-scheme: light) {
98
 html {
99
  --md-riple-color: #00000020;
100
 }
101
}
102
103
@media (prefers-color-scheme: dark) {
104
 html {
105
  --md-riple-color: #ffffff40;
106
 }
107
}
108
109
@keyframes md-ripple {
110
111
 from {
112
  background-size: 100%;
113
 }
114
115
 to {
116
  background-size: 15000%;
117
 }
118
119
}
120
121
@keyframes salePorLaIzquierda {
122
 to {
123
  translate: -100vw 0;
124
 }
125
}
126
127
@keyframes entraPorLaDerecha {
128
 from {
129
  translate: 100vw 0;
130
 }
131
}
132
133
@keyframes aparece {
134
 from {
135
  opacity: 0;
136
 }
137
}
138
139
@keyframes desvanece {
140
 to {
141
  opacity: 0;
142
 }
143
}

E. css / material-symbols-outlined.css

1
@font-face {
2
 font-family: 'Material Symbols Outlined';
3
 font-style: normal;
4
 src:
5
  url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].woff2) format('woff2'),
6
  url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].ttf) format('truetype');
7
}
8
9
.material-symbols-outlined {
10
 font-family: 'Material Symbols Outlined';
11
 font-weight: normal;
12
 font-style: normal;
13
 font-size: 1.5rem;
14
 width: 1.5rem;
15
 height: 1.5rem;
16
 /* Preferred icon size */
17
 display: inline-block;
18
 line-height: 1;
19
 text-transform: none;
20
 letter-spacing: normal;
21
 word-wrap: normal;
22
 white-space: nowrap;
23
 direction: ltr;
24
}

F. css / md-headline.css

1
.md-headline {
2
 box-sizing: border-box;
3
 margin: 0;
4
 color: var(--md-sys-color-on-surface);
5
 background-color: var(--md-sys-color-surface);
6
 transition-property: color;
7
 transition-duration: var(--md-sys-motion-duration-1000);
8
 transition-timing-function: ease-in;
9
}
10
11
.md-headline.scroll-adicional {
12
 color: var(--md-sys-color-surface-container-low);
13
 background-color: var(--md-sys-color-surface-container-low);
14
}
15
16
.md-headline.scroll {
17
 color: var(--md-sys-color-surface);
18
}
19
20
.md-headline.headline-small {
21
 padding: 0 1rem 1.5rem 1rem;
22
}
23
24
.md-headline.headline-medium {
25
 padding: 0 1rem 1.75rem 1rem;
26
}
27
28
md-app-bar.centered~.md-headline {
29
 text-align: center;
30
}
31
32
md-app-bar[headline] h1 {
33
 opacity: 0;
34
 transition-property: opacity;
35
 transition-duration: var(--md-sys-motion-duration-1000);
36
}
37
38
md-app-bar[headline].scroll h1 {
39
 opacity: 1;
40
 transition-timing-function: ease-in;
41
}

G. css / md-list.css

1
.md-list {
2
 margin: 0.5rem 0;
3
 padding: 0;
4
 list-style-type: none;
5
}
6
7
.md-list .md-one-line,
8
.md-list .md-two-line,
9
.md-list .md-three-line {
10
 position: relative;
11
 display: flex;
12
 box-sizing: border-box;
13
}
14
15
/* container */
16
.md-list .md-one-line::before,
17
.md-list .md-two-line::before,
18
.md-list .md-three-line::before {
19
 content: "";
20
 position: absolute;
21
 z-index: -2;
22
 top: 0;
23
 right: 0;
24
 left: 0;
25
 bottom: 0;
26
 background-color: var(--md-sys-color-surface);
27
}
28
29
/* state layer */
30
.md-list .md-one-line::after,
31
.md-list .md-two-line::after,
32
.md-list .md-three-line::after {
33
 content: "";
34
 position: absolute;
35
 z-index: -1;
36
 top: 0;
37
 right: 0;
38
 left: 0;
39
 bottom: 0;
40
 background-color: transparent;
41
}
42
43
.md-list .md-one-line {
44
 align-items: center;
45
 gap: 1rem;
46
 min-height: 3.5rem;
47
 padding: 0.5rem 1.5rem 0.5rem 1rem;
48
}
49
50
.md-list .md-one-line.video,
51
.md-list .md-two-line.video {
52
 padding: 0.75rem 1.5rem 0.75rem 0;
53
}
54
55
.md-list .md-two-line,
56
.md-list .md-three-line {
57
 flex-flow: column;
58
}
59
60
.md-list .md-two-line {
61
 justify-content: center;
62
 min-height: 4.5rem;
63
 padding: 0.5rem 1.5rem 0.5rem 1rem;
64
}
65
66
.md-list .md-two-line.icon,
67
.md-list .md-two-line.avatar,
68
.md-list .md-two-line.image,
69
.md-list .md-two-line.video,
70
.md-list .md-three-line.icon,
71
.md-list .md-three-line.avatar,
72
.md-list .md-three-line.image,
73
.md-list .md-three-line.video {
74
 display: grid;
75
 column-gap: 1rem;
76
 row-gap: 0;
77
 grid-template-areas:
78
  "img headline"
79
  "img supporting";
80
}
81
82
.md-list .md-two-line.icon,
83
.md-list .md-two-line.avatar,
84
.md-list .md-two-line.image,
85
.md-list .md-two-line.video {
86
 align-content: center;
87
 grid-template-rows: 1fr 1fr;
88
}
89
90
.md-list .md-two-line.icon,
91
.md-list .md-three-line.icon {
92
 grid-template-columns: var(--iconSize) 1fr;
93
}
94
95
.md-list .md-two-line.avatar,
96
.md-list .md-three-line.avatar {
97
 grid-template-columns: var(--avatarSize) 1fr;
98
}
99
100
.md-list .md-two-line.image,
101
.md-list .md-three-line.image {
102
 grid-template-columns: var(--imageSize) 1fr;
103
}
104
105
.md-list .md-two-line.video,
106
.md-list .md-three-line.video {
107
 grid-template-columns: var(--videoWidth) 1fr;
108
}
109
110
.md-list .md-three-line {
111
 align-content: flex-start;
112
 min-height: 5.5rem;
113
 padding: 0.75rem 1.5rem 0.75rem 1rem;
114
}
115
116
.md-list .md-three-line.video {
117
 padding: 0.75rem 1.5rem 0.75rem 0;
118
}
119
120
.md-list .md-three-line.icon,
121
.md-list .md-three-line.avatar,
122
.md-list .md-three-line.image,
123
.md-list .md-three-line.video {
124
 align-content: start;
125
 grid-template-rows: var(--md-sys-typescale-label-large-line-height) 1fr;
126
}
127
128
/* state layer */
129
.md-list .md-one-line:hover::after,
130
.md-list .md-two-line:hover::after,
131
.md-list .md-three-line:hover::after {
132
 background-color: var(--md-sys-color-on-surface);
133
 opacity: var(--md-sys-state-hover-state-layer-opacity);
134
}
135
136
/* state layer */
137
.md-list a.md-one-line:focus::after,
138
.md-list a.md-two-line:focus::after,
139
.md-list a.md-three-line:focus::after,
140
.md-list a.md-one-line:focus-visible::after,
141
.md-list a.md-two-line:focus-visible::after,
142
.md-list a.md-three-line:focus-visible::after {
143
 background-color: var(--md-sys-color-on-surface);
144
 opacity: var(--md-sys-state-focus-state-layer-opacity);
145
}
146
147
.md-list a:focus,
148
.md-list a:focus-visible {
149
 outline: none;
150
}
151
152
.md-list a:active {
153
 background-position: center;
154
 background-image:
155
  radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
156
 background-size: 100%;
157
 animation-name: md-ripple;
158
 animation-duration: var(--md-sys-motion-duration-500);
159
 box-shadow: var(--md-box_shadow_level0) !important;
160
}
161
162
/* state layer */
163
.md-list a.md-one-line:active::after,
164
.md-list a.md-two-line:active::after,
165
.md-list a.md-three-line:active::after {
166
 background-color: var(--md-sys-color-on-surface);
167
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
168
}
169
170
.md-list a.md-two-line,
171
.md-list a.md-three-line {
172
 text-decoration: none;
173
}
174
175
.md-list a.md-two-line .headline,
176
.md-list a.md-three-line .headline {
177
 text-decoration: underline;
178
}
179
180
.md-list .headline {
181
 grid-area: headline;
182
 display: block;
183
 box-sizing: border-box;
184
 color: var(--md-sys-color-on-surface);
185
 font-family: var(--md-sys-typescale-body-large-font);
186
 font-weight: var(--md-sys-typescale-body-large-weight);
187
 font-size: var(--md-sys-typescale-body-large-size);
188
 font-style: var(--md-sys-typescale-body-large-font-style);
189
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
190
 line-height: var(--md-sys-typescale-body-large-line-height);
191
 text-transform: var(--md-sys-typescale-body-large-text-transform);
192
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
193
 max-height: var(--md-sys-typescale-body-large-line-height);
194
 white-space: nowrap;
195
 text-overflow: ellipsis;
196
 overflow: hidden;
197
}
198
199
.md-list .md-two-line.icon .headline,
200
.md-list .md-two-line.avatar .headline,
201
.md-list .md-two-line.image .headline,
202
.md-list .md-two-line.video .headline,
203
.md-list .md-three-line.icon .headline,
204
.md-list .md-three-line.avatar .headline,
205
.md-list .md-three-line.image .headline,
206
.md-list .md-three-line.video .headline {
207
 align-self: end;
208
}
209
210
.md-list .supporting {
211
 grid-area: supporting;
212
 display: -webkit-box;
213
 -webkit-box-orient: vertical;
214
 overflow: hidden;
215
 box-sizing: border-box;
216
 align-self: start;
217
 font-family: var(--md-sys-typescale-body-medium-font);
218
 font-weight: var(--md-sys-typescale-body-medium-weight);
219
 font-size: var(--md-sys-typescale-body-medium-size);
220
 font-style: var(--md-sys-typescale-body-medium-font-style);
221
 letter-spacing: var(--md-sys-typescale-body-medium-tracking);
222
 line-height: var(--md-sys-typescale-body-medium-line-height);
223
 text-transform: var(--md-sys-typescale-body-medium-text-transform);
224
 text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
225
}
226
227
.md-list .md-two-line .supporting {
228
 max-height: var(--md-sys-typescale-body-medium-line-height);
229
 line-clamp: 1;
230
 -webkit-line-clamp: 1;
231
}
232
233
.md-list .md-three-line .supporting {
234
 max-height: calc(2 * var(--md-sys-typescale-body-medium-line-height));
235
 line-clamp: 2;
236
 -webkit-line-clamp: 2;
237
}
238
239
.md-list .avatar img,
240
.md-list .avatar label,
241
.md-list .avatar .material-symbols-outlined:first-child {
242
 flex-shrink: 0;
243
 background-color: var(--md-sys-color-primary-container);
244
 color: var(--md-sys-color-on-primary-container);
245
 border-radius: 50%;
246
 width: var(--avatarSize);
247
 height: var(--avatarSize);
248
}
249
250
.md-list .avatar label {
251
 display: inline-block;
252
 font-family: var(--md-sys-typescale-title-medium-font);
253
 font-weight: var(--md-sys-typescale-title-medium-weight);
254
 font-size: var(--md-sys-typescale-title-medium-size);
255
 font-style: var(--md-sys-typescale-title-medium-font-style);
256
 letter-spacing: var(--md-sys-typescale-title-medium-tracking);
257
 line-height: var(--md-sys-typescale-title-medium-line-height);
258
 text-transform: var(--md-sys-typescale-title-medium-text-transform);
259
 text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
260
 overflow: hidden;
261
}
262
263
.md-list .avatar .material-symbols-outlined:first-child {
264
 font-size: var(--avatarSize);
265
}
266
267
.md-list .avatar.md-two-line img,
268
.md-list .avatar.md-two-line label,
269
.md-list .avatar.md-two-line .material-symbols-outlined:first-child {
270
 grid-area: img;
271
 align-self: center;
272
}
273
274
.md-list .avatar.md-three-line img,
275
.md-list .avatar.md-three-line label,
276
.md-list .avatar.md-three-line .material-symbols-outlined:first-child {
277
 grid-area: img;
278
 align-self: start;
279
}
280
281
.md-list .icon img,
282
.md-list .icon .material-symbols-outlined:first-child {
283
 flex-shrink: 0;
284
 color: var(--md-sys-color-on-surface-variant);
285
 width: var(--iconSize);
286
 height: var(--iconSize);
287
}
288
289
.md-list .icon .material-symbols-outlined:first-child {
290
 font-size: var(--iconSize);
291
}
292
293
.md-list .icon.md-two-line img,
294
.md-list .icon.md-two-line .material-symbols-outlined:first-child {
295
 grid-area: img;
296
 align-self: center;
297
}
298
299
.md-list .icon.md-three-line img,
300
.md-list .icon.md-three-line .material-symbols-outlined:first-child {
301
 grid-area: img;
302
 align-self: start;
303
}
304
305
.md-list .video img {
306
 flex-shrink: 0;
307
 color: var(--md-sys-color-on-surface-variant);
308
 width: var(--videoWidth);
309
 height: var(--videoHeight);
310
}
311
312
.md-list .video.md-two-line img {
313
 grid-area: img;
314
 align-self: center;
315
}
316
317
.md-list .video.md-three-line img {
318
 grid-area: img;
319
 align-self: start;
320
}
321
322
.md-list .image img,
323
.md-list .image .material-symbols-outlined:first-child {
324
 flex-shrink: 0;
325
 color: var(--md-sys-color-on-surface-variant);
326
 width: var(--imageSize);
327
 height: var(--imageSize);
328
}
329
330
.md-list .image .material-symbols-outlined:first-child {
331
 font-size: var(--imageSize);
332
}
333
334
.md-list .image.md-two-line img,
335
.md-list .image.md-two-line .material-symbols-outlined:first-child {
336
 grid-area: img;
337
 align-self: center;
338
}
339
340
.md-list .image.md-three-line img,
341
.md-list .image.md-three-line .material-symbols-outlined:first-child {
342
 grid-area: img;
343
 align-self: start;
344
}

H. css / md-tab.css

1
.md-tab {
2
 display: flex;
3
 background-color: transparent;
4
 align-items: stretch;
5
 flex-wrap: nowrap;
6
 overflow-x: auto;
7
 position: sticky;
8
 z-index: 1;
9
}
10
11
.md-tab.fixed {
12
 justify-content: center;
13
}
14
15
.md-tab.scrollable {
16
 padding-left: 2rem;
17
 gap: 1rem;
18
}
19
20
.md-tab.scroll {
21
 background-color: var(--md-sys-color-surface-container-low);
22
}
23
24
.md-tab a {
25
 position: relative;
26
 display: flex;
27
 flex-direction: column;
28
 justify-content: start;
29
 align-items: center;
30
 color: var(--md-sys-color-on-surface-variant);
31
 font-family: var(--md-sys-typescale-title-small-font);
32
 font-weight: var(--md-sys-typescale-title-small-weight);
33
 font-size: var(--md-sys-typescale-title-small-size);
34
 font-style: var(--md-sys-typescale-title-small-font-style);
35
 letter-spacing: var(--md-sys-typescale-title-small-tracking);
36
 line-height: var(--md-sys-typescale-title-small-line-height);
37
 text-transform: var(--md-sys-typescale-title-small-text-transform);
38
 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
39
 text-align: center;
40
 box-sizing: border-box;
41
 border-bottom: 0.1875rem solid var(--md-sys-color-surface);
42
}
43
44
.md-tab.fixed a {
45
 flex: 0 0 var(--tabWidth);
46
}
47
48
.md-tab.scrollable a {
49
 flex: 0 0 auto;
50
}
51
52
.md-tab a.active {
53
 border-bottom-color: var(--md-sys-color-primary);
54
}
55
56
/* state layer */
57
.md-tab a::after {
58
 content: "";
59
 position: absolute;
60
 z-index: -1;
61
 top: 0;
62
 right: 0;
63
 left: 0;
64
 bottom: 0;
65
 background-color: transparent;
66
}
67
68
.md-tab span {
69
 font-size: var(--iconSize);
70
 height: var(--iconSize);
71
 width: var(--iconSize);
72
 color: var(--md-sys-color-on-surface-variant);
73
}
74
75
.md-tab .active span {
76
 color: var(--md-sys-color-primary);
77
}
78
79
.md-tab a:hover {
80
 color: var(--md-sys-color-on-surface);
81
}
82
83
/* state layer */
84
.md-tab a:hover::after {
85
 background-color: var(--md-sys-color-on-surface);
86
 opacity: var(--md-sys-state-hover-state-layer-opacity);
87
}
88
89
.md-tab a.active:hover {
90
 color: var(--md-sys-color-primary);
91
}
92
93
/* state layer */
94
.md-tab a.active:hover::after {
95
 background-color: var(--md-sys-color-primary);
96
 opacity: var(--md-sys-state-hover-state-layer-opacity);
97
}
98
99
.md-tab a:hover span {
100
 color: var(--md-sys-color-on-surface);
101
}
102
103
.md-tab a.active:hover span {
104
 color: var(--md-sys-color-primary);
105
}
106
107
.md-tab a:focus {
108
 outline: none;
109
}
110
111
/* state layer */
112
.md-tab a:focus::after {
113
 background-color: var(--md-sys-color-on-surface);
114
 opacity: var(--md-sys-state-focus-state-layer-opacity);
115
}
116
117
/* state layer */
118
.md-tab a.active:focus::after {
119
 background-color: var(--md-sys-color-primary);
120
 opacity: var(--md-sys-state-hover-state-layer-opacity);
121
}
122
123
.md-tab a:active {
124
 background-position: center;
125
 background-image:
126
   radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
127
 background-size: 100%;
128
 animation-name: md-ripple;
129
 animation-duration: var(--md-sys-motion-duration-500);
130
}
131
132
/* state layer */
133
.md-tab a:active::after {
134
 background-color: var(--md-sys-color-on-surface);
135
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
136
}
137
138
/* state layer */
139
.md-tab a.active:active::after {
140
 background-color: var(--md-sys-color-primary);
141
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
142
}

I. css / motion.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Emphasized decelerate easing (out) */
19
  --md-sys-motion-easing-emphasized-decelerate-x0: 0.05000000074505806;
20
  --md-sys-motion-easing-emphasized-decelerate-y0: 0.699999988079071;
21
  --md-sys-motion-easing-emphasized-decelerate-x1: 0.10000000149011612;
22
  --md-sys-motion-easing-emphasized-decelerate-y1: 1;
23
  /* Emphasized accelerate easing (in) */
24
  --md-sys-motion-easing-emphasized-accelerate-x0: 0.30000001192092896;
25
  --md-sys-motion-easing-emphasized-accelerate-y0: 0;
26
  --md-sys-motion-easing-emphasized-accelerate-x1: 0.800000011920929;
27
  --md-sys-motion-easing-emphasized-accelerate-y1: 0.15000000596046448;
28
  /* Standard decelerate easing (out) */
29
  --md-sys-motion-easing-standard-decelerate-x0: 0;
30
  --md-sys-motion-easing-standard-decelerate-y0: 0;
31
  --md-sys-motion-easing-standard-decelerate-x1: 0;
32
  --md-sys-motion-easing-standard-decelerate-y1: 1;
33
  /* Standard accelerate easing (in) */
34
  --md-sys-motion-easing-standard-accelerate-x0: 0.30000001192092896;
35
  --md-sys-motion-easing-standard-accelerate-y0: 0;
36
  --md-sys-motion-easing-standard-accelerate-x1: 1;
37
  --md-sys-motion-easing-standard-accelerate-y1: 1;
38
  /* Duration 1000ms */
39
  --md-sys-motion-duration-1000: 1000ms;
40
  /* Duration 900ms */
41
  --md-sys-motion-duration-900: 900ms;
42
  /* Duration 800ms */
43
  --md-sys-motion-duration-800: 800ms;
44
  /* Duration 700ms */
45
  --md-sys-motion-duration-700: 700ms;
46
  /* Duration 600ms */
47
  --md-sys-motion-duration-600: 600ms;
48
  /* Duration 550ms */
49
  --md-sys-motion-duration-550: 550ms;
50
  /* Duration 500ms */
51
  --md-sys-motion-duration-500: 500ms;
52
  /* Duration 450ms */
53
  --md-sys-motion-duration-450: 450ms;
54
  /* Duration 400ms */
55
  --md-sys-motion-duration-400: 400ms;
56
  /* Duration 350ms */
57
  --md-sys-motion-duration-350: 350ms;
58
  /* Duration 300ms */
59
  --md-sys-motion-duration-300: 300ms;
60
  /* Duration 250ms */
61
  --md-sys-motion-duration-250: 250ms;
62
  /* Duration 200ms */
63
  --md-sys-motion-duration-200: 200ms;
64
  /* Duration 150ms */
65
  --md-sys-motion-duration-150: 150ms;
66
  /* Duration 100ms */
67
  --md-sys-motion-duration-100: 100ms;
68
  /* Duration 50ms */
69
  --md-sys-motion-duration-50: 50ms;
70
  /* Standard easing (in and out) */
71
  --md-sys-motion-easing-standard-x0: 0.20000000298023224;
72
  --md-sys-motion-easing-standard-y0: 0;
73
  --md-sys-motion-easing-standard-x1: 0;
74
  --md-sys-motion-easing-standard-y1: 1;
75
  /* Linear easing */
76
  --md-sys-motion-easing-linear-x0: 0;
77
  --md-sys-motion-easing-linear-y0: 0;
78
  --md-sys-motion-easing-linear-x1: 1;
79
  --md-sys-motion-easing-linear-y1: 1;
80
  /* Emphasized */
81
  --md-sys-motion-easing-emphasized-x0: 0.20000000298023224;
82
  --md-sys-motion-easing-emphasized-y0: 0;
83
  --md-sys-motion-easing-emphasized-x1: 0;
84
  --md-sys-motion-easing-emphasized-y1: 1;
85
  /* Motion path */
86
  --md-sys-motion-path-standard-path: 1;
87
}
88
.duration-50 {
89
  transition-duration: var(--md-sys-motion-duration-50);
90
}
91
.duration-100 {
92
  transition-duration: var(--md-sys-motion-duration-100);
93
}
94
.duration-150 {
95
  transition-duration: var(--md-sys-motion-duration-150);
96
}
97
.duration-200 {
98
  transition-duration: var(--md-sys-motion-duration-200);
99
}
100
.duration-250 {
101
  transition-duration: var(--md-sys-motion-duration-250);
102
}
103
.duration-300 {
104
  transition-duration: var(--md-sys-motion-duration-300);
105
}
106
.duration-350 {
107
  transition-duration: var(--md-sys-motion-duration-350);
108
}
109
.duration-400 {
110
  transition-duration: var(--md-sys-motion-duration-400);
111
}
112
.duration-450 {
113
  transition-duration: var(--md-sys-motion-duration-450);
114
}
115
.duration-500 {
116
  transition-duration: var(--md-sys-motion-duration-500);
117
}
118
.duration-550 {
119
  transition-duration: var(--md-sys-motion-duration-550);
120
}
121
.duration-600 {
122
  transition-duration: var(--md-sys-motion-duration-600);
123
}
124
.duration-700 {
125
  transition-duration: var(--md-sys-motion-duration-700);
126
}
127
.duration-800 {
128
  transition-duration: var(--md-sys-motion-duration-800);
129
}
130
.duration-900 {
131
  transition-duration: var(--md-sys-motion-duration-900);
132
}
133
.duration-1000 {
134
  transition-duration: var(--md-sys-motion-duration-1000);
135
}
136
.easing-standard {
137
  transition-timing-function: cubic-bezier(
138
    var(--md-sys-motion-easing-standard-x0),
139
    var(--md-sys-motion-easing-standard-y0),
140
    var(--md-sys-motion-easing-standard-x1),
141
    var(--md-sys-motion-easing-standard-y1)
142
  );
143
}
144
.easing-linear {
145
  transition-timing-function: cubic-bezier(
146
    var(--md-sys-motion-easing-linear-x0),
147
    var(--md-sys-motion-easing-linear-y0),
148
    var(--md-sys-motion-easing-linear-x1),
149
    var(--md-sys-motion-easing-linear-y1)
150
  );
151
}
152
.easing-standard-accelerate {
153
  transition-timing-function: cubic-bezier(
154
    var(--md-sys-motion-easing-standard-accelerate-x0),
155
    var(--md-sys-motion-easing-standard-accelerate-y0),
156
    var(--md-sys-motion-easing-standard-accelerate-x1),
157
    var(--md-sys-motion-easing-standard-accelerate-y1)
158
  );
159
}
160
.easing-standard-decelerate {
161
  transition-timing-function: cubic-bezier(
162
    var(--md-sys-motion-easing-standard-decelerate-x0),
163
    var(--md-sys-motion-easing-standard-decelerate-y0),
164
    var(--md-sys-motion-easing-standard-decelerate-x1),
165
    var(--md-sys-motion-easing-standard-decelerate-y1)
166
  );
167
}
168
.easing-emphasized {
169
  transition-timing-function: cubic-bezier(
170
    var(--md-sys-motion-easing-emphasized-x0),
171
    var(--md-sys-motion-easing-emphasized-y0),
172
    var(--md-sys-motion-easing-emphasized-x1),
173
    var(--md-sys-motion-easing-emphasized-y1)
174
  );
175
}
176

J. css / palette.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Error 0 */
19
  --md-ref-palette-error0: #000000ff;
20
  /* Error 10 */
21
  --md-ref-palette-error10: #410e0bff;
22
  /* Error 20 */
23
  --md-ref-palette-error20: #601410ff;
24
  /* Error 30 */
25
  --md-ref-palette-error30: #8c1d18ff;
26
  /* Error 40 */
27
  --md-ref-palette-error40: #b3261eff;
28
  /* Error 50 */
29
  --md-ref-palette-error50: #dc362eff;
30
  /* Error 60 */
31
  --md-ref-palette-error60: #e46962ff;
32
  /* Error 70 */
33
  --md-ref-palette-error70: #ec928eff;
34
  /* Error 80 */
35
  --md-ref-palette-error80: #f2b8b5ff;
36
  /* Error 90 */
37
  --md-ref-palette-error90: #f9dedcff;
38
  /* Error 95 */
39
  --md-ref-palette-error95: #fceeeeff;
40
  /* Error 99 */
41
  --md-ref-palette-error99: #fffbf9ff;
42
  /* Error 100 */
43
  --md-ref-palette-error100: #ffffffff;
44
  /* Tertiary 0 */
45
  --md-ref-palette-tertiary0: #000000ff;
46
  /* Tertiary 10 */
47
  --md-ref-palette-tertiary10: #31111dff;
48
  /* Tertiary 20 */
49
  --md-ref-palette-tertiary20: #492532ff;
50
  /* Tertiary 30 */
51
  --md-ref-palette-tertiary30: #633b48ff;
52
  /* Tertiary 40 */
53
  --md-ref-palette-tertiary40: #7d5260ff;
54
  /* Tertiary 50 */
55
  --md-ref-palette-tertiary50: #986977ff;
56
  /* Tertiary 60 */
57
  --md-ref-palette-tertiary60: #b58392ff;
58
  /* Tertiary 70 */
59
  --md-ref-palette-tertiary70: #d29dacff;
60
  /* Tertiary 80 */
61
  --md-ref-palette-tertiary80: #efb8c8ff;
62
  /* Tertiary 90 */
63
  --md-ref-palette-tertiary90: #ffd8e4ff;
64
  /* Tertiary 95 */
65
  --md-ref-palette-tertiary95: #ffecf1ff;
66
  /* Tertiary 99 */
67
  --md-ref-palette-tertiary99: #fffbfaff;
68
  /* Tertiary 100 */
69
  --md-ref-palette-tertiary100: #ffffffff;
70
  /* Secondary 0 */
71
  --md-ref-palette-secondary0: #000000ff;
72
  /* Secondary 10 */
73
  --md-ref-palette-secondary10: #1d192bff;
74
  /* Secondary 20 */
75
  --md-ref-palette-secondary20: #332d41ff;
76
  /* Secondary 30 */
77
  --md-ref-palette-secondary30: #4a4458ff;
78
  /* Secondary 40 */
79
  --md-ref-palette-secondary40: #625b71ff;
80
  /* Secondary 50 */
81
  --md-ref-palette-secondary50: #7a7289ff;
82
  /* Secondary 60 */
83
  --md-ref-palette-secondary60: #958da5ff;
84
  /* Secondary 70 */
85
  --md-ref-palette-secondary70: #b0a7c0ff;
86
  /* Secondary 80 */
87
  --md-ref-palette-secondary80: #ccc2dcff;
88
  /* Secondary 90 */
89
  --md-ref-palette-secondary90: #e8def8ff;
90
  /* Secondary 95 */
91
  --md-ref-palette-secondary95: #f6edffff;
92
  /* Secondary 99 */
93
  --md-ref-palette-secondary99: #fffbfeff;
94
  /* Secondary 100 */
95
  --md-ref-palette-secondary100: #ffffffff;
96
  /* Primary 0 */
97
  --md-ref-palette-primary0: #000000ff;
98
  /* Primary 10 */
99
  --md-ref-palette-primary10: #21005dff;
100
  /* Primary 20 */
101
  --md-ref-palette-primary20: #381e72ff;
102
  /* Primary 30 */
103
  --md-ref-palette-primary30: #4f378bff;
104
  /* Primary 40 */
105
  --md-ref-palette-primary40: #6750a4ff;
106
  /* Primary 50 */
107
  --md-ref-palette-primary50: #7f67beff;
108
  /* Primary 60 */
109
  --md-ref-palette-primary60: #9a82dbff;
110
  /* Primary 70 */
111
  --md-ref-palette-primary70: #b69df8ff;
112
  /* Primary 80 */
113
  --md-ref-palette-primary80: #d0bcffff;
114
  /* Primary 90 */
115
  --md-ref-palette-primary90: #eaddffff;
116
  /* Primary 95 */
117
  --md-ref-palette-primary95: #f6edffff;
118
  /* Primary 99 */
119
  --md-ref-palette-primary99: #fffbfeff;
120
  /* Primary 100 */
121
  --md-ref-palette-primary100: #ffffffff;
122
  /* Neutral Variant 0 */
123
  --md-ref-palette-neutral-variant0: #000000ff;
124
  /* Neutral Variant 10 */
125
  --md-ref-palette-neutral-variant10: #1d1a22ff;
126
  /* Neutral Variant 20 */
127
  --md-ref-palette-neutral-variant20: #322f37ff;
128
  /* Neutral Variant 30 */
129
  --md-ref-palette-neutral-variant30: #49454fff;
130
  /* Neutral Variant 40 */
131
  --md-ref-palette-neutral-variant40: #605d66ff;
132
  /* Neutral Variant 50 */
133
  --md-ref-palette-neutral-variant50: #79747eff;
134
  /* Neutral Variant 60 */
135
  --md-ref-palette-neutral-variant60: #938f99ff;
136
  /* Neutral Variant 70 */
137
  --md-ref-palette-neutral-variant70: #aea9b4ff;
138
  /* Neutral Variant 80 */
139
  --md-ref-palette-neutral-variant80: #cac4d0ff;
140
  /* Neutral Variant 90 */
141
  --md-ref-palette-neutral-variant90: #e7e0ecff;
142
  /* Neutral Variant 95 */
143
  --md-ref-palette-neutral-variant95: #f5eefaff;
144
  /* Neutral Variant 99 */
145
  --md-ref-palette-neutral-variant99: #fffbfeff;
146
  /* Neutral Variant 100 */
147
  --md-ref-palette-neutral-variant100: #ffffffff;
148
  /* Neutral 0 */
149
  --md-ref-palette-neutral0: #000000ff;
150
  /* Neutral 10 */
151
  --md-ref-palette-neutral10: #1c1b1fff;
152
  /* Neutral 20 */
153
  --md-ref-palette-neutral20: #313033ff;
154
  /* Neutral 30 */
155
  --md-ref-palette-neutral30: #484649ff;
156
  /* Neutral 40 */
157
  --md-ref-palette-neutral40: #605d62ff;
158
  /* Neutral 50 */
159
  --md-ref-palette-neutral50: #787579ff;
160
  /* Neutral 60 */
161
  --md-ref-palette-neutral60: #939094ff;
162
  /* Neutral 70 */
163
  --md-ref-palette-neutral70: #aeaaaeff;
164
  /* Neutral 80 */
165
  --md-ref-palette-neutral80: #c9c5caff;
166
  /* Neutral 90 */
167
  --md-ref-palette-neutral90: #e6e1e5ff;
168
  /* Neutral 95 */
169
  --md-ref-palette-neutral95: #f4eff4ff;
170
  /* Neutral 99 */
171
  --md-ref-palette-neutral99: #fffbfeff;
172
  /* Neutral 100 */
173
  --md-ref-palette-neutral100: #ffffffff;
174
  /* Black */
175
  --md-ref-palette-black: #000000ff;
176
  /* White */
177
  --md-ref-palette-white: #ffffffff;
178
}
179

K. css / roboto.css

1
/* roboto-regular - latin */
2
@font-face {
3
 /* Revisa
4
  * https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
5
  * para otras opciones. */
6
 font-display: swap;
7
 font-family: 'Roboto';
8
 font-style: normal;
9
 font-weight: 400;
10
 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
11
 src: url('../fonts/roboto-v32-latin-regular.woff2') format('woff2');
12
}

L. css / shape.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Fully rounded */
19
  --md-sys-shape-corner-full-family: 3px;
20
  /* Extra large top rounding */
21
  --md-sys-shape-corner-extra-large-top-family: 1px;
22
  --md-sys-shape-corner-extra-large-top-default-size: 0px;
23
  --md-sys-shape-corner-extra-large-top-top-left: 28px;
24
  --md-sys-shape-corner-extra-large-top-top-right-unit: 1px;
25
  --md-sys-shape-corner-extra-large-top-top-right: 28px;
26
  /* Extra large rounding */
27
  --md-sys-shape-corner-extra-large-family: 1px;
28
  --md-sys-shape-corner-extra-large-default-size-unit: 1px;
29
  --md-sys-shape-corner-extra-large-default-size: 28px;
30
  /* Large top rounding */
31
  --md-sys-shape-corner-large-top-family: 1px;
32
  --md-sys-shape-corner-large-top-default-size-unit: 1px;
33
  --md-sys-shape-corner-large-top-default-size: 0px;
34
  --md-sys-shape-corner-large-top-top-left-unit: 1px;
35
  --md-sys-shape-corner-large-top-top-left: 16px;
36
  --md-sys-shape-corner-large-top-top-right-unit: 1px;
37
  --md-sys-shape-corner-large-top-top-right: 16px;
38
  /* Large end rounding */
39
  --md-sys-shape-corner-large-end-family: 1px;
40
  --md-sys-shape-corner-large-end-default-size-unit: 1px;
41
  --md-sys-shape-corner-large-end-default-size: 0px;
42
  --md-sys-shape-corner-large-end-top-right-unit: 1px;
43
  --md-sys-shape-corner-large-end-top-right: 16px;
44
  --md-sys-shape-corner-large-end-bottom-right-unit: 1px;
45
  --md-sys-shape-corner-large-end-bottom-right: 16px;
46
  /* Large rounding */
47
  --md-sys-shape-corner-large-family: 1px;
48
  --md-sys-shape-corner-large-default-size-unit: 1px;
49
  --md-sys-shape-corner-large-default-size: 16px;
50
  /* Medium rounding */
51
  --md-sys-shape-corner-medium-family: 1px;
52
  --md-sys-shape-corner-medium-default-size-unit: 1px;
53
  --md-sys-shape-corner-medium-default-size: 12px;
54
  /* Small rounding */
55
  --md-sys-shape-corner-small-family: 1px;
56
  --md-sys-shape-corner-small-default-size-unit: 1px;
57
  --md-sys-shape-corner-small-default-size: 8px;
58
  /* Extra small top rounding */
59
  --md-sys-shape-corner-extra-small-top-family: 1px;
60
  --md-sys-shape-corner-extra-small-top-default-size-unit: 1px;
61
  --md-sys-shape-corner-extra-small-top-default-size: 0px;
62
  --md-sys-shape-corner-extra-small-top-top-left-unit: 1px;
63
  --md-sys-shape-corner-extra-small-top-top-left: 4px;
64
  --md-sys-shape-corner-extra-small-top-top-right-unit: 1px;
65
  --md-sys-shape-corner-extra-small-top-top-right: 4px;
66
  /* Extra small rounding */
67
  --md-sys-shape-corner-extra-small-family: 1px;
68
  --md-sys-shape-corner-extra-small-default-size-unit: 1px;
69
  --md-sys-shape-corner-extra-small-default-size: 4px;
70
  /* No rounding */
71
  --md-sys-shape-corner-none-family: 1px;
72
  --md-sys-shape-corner-none-default-size-unit: 1px;
73
  --md-sys-shape-corner-none-default-size: 0px;
74
75
  --md-sys-shape-small: var(--md-sys-shape-corner-small-default-size);
76
  --md-sys-shape-medium: var(--md-sys-shape-corner-medium-default-size);
77
  --md-sys-shape-large: var(--md-sys-shape-corner-large-default-size);
78
}
79
80
.shape-none {
81
  border-radius: var(--md-sys-shape-corner-none-default-size);
82
}
83
.shape-extra-small {
84
  border-radius: var(--md-sys-shape-corner-extra-small-default-size);
85
}
86
.shape-small {
87
  border-radius: var(--md-sys-shape-corner-small-default-size);
88
}
89
.shape-medium {
90
  border-radius: var(--md-sys-shape-corner-medium-default-size);
91
}
92
.shape-large {
93
  border-radius: var(--md-sys-shape-corner-large-default-size);
94
}
95
.shape-extra-large {
96
  border-radius: var(--md-sys-shape-corner-extra-large-default-size);
97
}
98
.extra-small-top {
99
  border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
100
  border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
101
}
102
.large-end {
103
  border-top-right-radius: var(--md-sys-shape-corner-large-end-top-right);
104
  border-bottom-right-radius: var(--md-sys-shape-corner-large-end-bottom-right);
105
}
106
.large-top {
107
  border-top-left-radius: var(--md-sys-shape-corner-large-top-top-left);
108
  border-top-right-radius: var(--md-sys-shape-corner-large-top-top-right);
109
}
110
.extra-large-top {
111
  border-top-left-radius: var(--md-sys-shape-corner-extra-large-top-top-left);
112
  border-top-right-radius: var(--md-sys-shape-corner-extra-large-top-top-right);
113
}
114

M. css / state.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Dragged state layer opacity */
19
  --md-sys-state-dragged-state-layer-opacity: 0.1599999964237213;
20
  /* Pressed state layer opacity */
21
  --md-sys-state-pressed-state-layer-opacity: 0.11999999731779099;
22
  /* Focus state layer opacity */
23
  --md-sys-state-focus-state-layer-opacity: 0.11999999731779099;
24
  /* Hover state layer opacity */
25
  --md-sys-state-hover-state-layer-opacity: 0.07999999821186066;
26
}
27
.hover-state-layer {
28
  opacity: var(--md-sys-state-hover-state-layer-opacity);
29
}
30
.pressed-state-layer {
31
  opacity: var(--md-sys-state-pressed-state-layer-opacity);
32
}
33
.dragged-state-layer {
34
  opacity: var(--md-sys-state-dragged-state-layer-opacity);
35
}
36
.focus-state-layer {
37
  opacity: var(--md-sys-state-focus-state-layer-opacity);
38
}
39

N. css / transicion_pestanas.css

1
@view-transition {
2
 navigation: auto;
3
}
4
5
#headline {
6
 view-transition-name: encabezado;
7
}
8
9
main {
10
 view-transition-name: contenido;
11
}
12
13
::view-transition-group(encabezado) {
14
 animation-duration: var(--md-sys-motion-duration-1000);
15
}
16
17
::view-transition-group(contenido) {
18
 animation-duration: var(--md-sys-motion-duration-1000);
19
}
20
21
html::view-transition-old(encabezado) {
22
 animation-name: salePorLaIzquierda;
23
}
24
25
html::view-transition-new(encabezado) {
26
 animation-name: entraPorLaDerecha;
27
}
28
29
html::view-transition-old(contenido) {
30
 animation-name: salePorLaIzquierda;
31
}
32
33
html::view-transition-new(contenido) {
34
 animation-duration: var(--md-sys-motion-duration-700);
35
 animation-name: entraPorLaDerecha;
36
}

O. css / typography.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
/* This file is generated */
18
19
/* DO NOT EDIT */
20
21
:root {
22
  /* Label Small */
23
  --md-sys-typescale-label-small-text-transform: unset;
24
  --md-sys-typescale-label-small-axis-value: unset;
25
  --md-sys-typescale-label-small-font-style: unset;
26
  --md-sys-typescale-label-small-text-decoration: unset;
27
  /* Label Small line height */
28
  --md-sys-typescale-label-small-line-height-value: 16px;
29
  --md-sys-typescale-label-small-line-height-unit: 2px;
30
  --md-sys-typescale-label-small-line-height: 16px;
31
  /* Label Small font tracking */
32
  --md-sys-typescale-label-small-tracking-value: 0.5px;
33
  --md-sys-typescale-label-small-tracking-unit: 2px;
34
  --md-sys-typescale-label-small-tracking: 0.5px;
35
  /* Label Small font size */
36
  --md-sys-typescale-label-small-size-value: 11px;
37
  --md-sys-typescale-label-small-size-unit: 2px;
38
  --md-sys-typescale-label-small-size: 11px;
39
  /* Label Small font weight */
40
  --md-sys-typescale-label-small-weight: var(--md-ref-typeface-weight-medium);
41
  /* Label Small font name */
42
  --md-sys-typescale-label-small-font: var(--md-ref-typeface-plain);
43
  /* Label Medium */
44
  --md-sys-typescale-label-medium-axis-value: unset;
45
  --md-sys-typescale-label-medium-font-style: unset;
46
  --md-sys-typescale-label-medium-text-decoration: unset;
47
  /* Label Medium text transform */
48
  --md-sys-typescale-label-medium-text-transform: 1;
49
  /* Label Medium line height */
50
  --md-sys-typescale-label-medium-line-height-value: 16px;
51
  --md-sys-typescale-label-medium-line-height-unit: 2px;
52
  --md-sys-typescale-label-medium-line-height: 16px;
53
  /* Label Medium font tracking */
54
  --md-sys-typescale-label-medium-tracking-value: 0.5px;
55
  --md-sys-typescale-label-medium-tracking-unit: 2px;
56
  --md-sys-typescale-label-medium-tracking: 0.5px;
57
  /* Label Medium font size */
58
  --md-sys-typescale-label-medium-size-value: 12px;
59
  --md-sys-typescale-label-medium-size-unit: 2px;
60
  --md-sys-typescale-label-medium-size: 12px;
61
  /* Label Medium font weight */
62
  --md-sys-typescale-label-medium-weight: var(--md-ref-typeface-weight-medium);
63
  /* Label Medium font name */
64
  --md-sys-typescale-label-medium-font: var(--md-ref-typeface-plain);
65
  /* Label Large */
66
  --md-sys-typescale-label-large-text-transform: unset;
67
  --md-sys-typescale-label-large-axis-value: unset;
68
  --md-sys-typescale-label-large-font-style: unset;
69
  --md-sys-typescale-label-large-text-decoration: unset;
70
  /* Label Large line height */
71
  --md-sys-typescale-label-large-line-height-value: 20px;
72
  --md-sys-typescale-label-large-line-height-unit: 2px;
73
  --md-sys-typescale-label-large-line-height: 20px;
74
  /* Label Large font tracking */
75
  --md-sys-typescale-label-large-tracking-value: 0.10000000149011612px;
76
  --md-sys-typescale-label-large-tracking-unit: 2px;
77
  --md-sys-typescale-label-large-tracking: 0.10000000149011612px;
78
  /* Label Large font size */
79
  --md-sys-typescale-label-large-size-value: 14px;
80
  --md-sys-typescale-label-large-size-unit: 2px;
81
  --md-sys-typescale-label-large-size: 14px;
82
  /* Label Large font weight */
83
  --md-sys-typescale-label-large-weight: var(--md-ref-typeface-weight-medium);
84
  /* Label Large font name */
85
  --md-sys-typescale-label-large-font: var(--md-ref-typeface-plain);
86
  /* Body Small */
87
  --md-sys-typescale-body-small-text-transform: unset;
88
  --md-sys-typescale-body-small-axis-value: unset;
89
  --md-sys-typescale-body-small-font-style: unset;
90
  --md-sys-typescale-body-small-text-decoration: unset;
91
  /* Body Small line height */
92
  --md-sys-typescale-body-small-line-height-value: 16px;
93
  --md-sys-typescale-body-small-line-height-unit: 2px;
94
  --md-sys-typescale-body-small-line-height: 16px;
95
  /* Body Small font tracking */
96
  --md-sys-typescale-body-small-tracking-value: 0.4000000059604645px;
97
  --md-sys-typescale-body-small-tracking-unit: 2px;
98
  --md-sys-typescale-body-small-tracking: 0.4000000059604645px;
99
  /* Body Small font size */
100
  --md-sys-typescale-body-small-size-value: 12px;
101
  --md-sys-typescale-body-small-size-unit: 2px;
102
  --md-sys-typescale-body-small-size: 12px;
103
  /* Body Small font weight */
104
  --md-sys-typescale-body-small-weight: var(--md-ref-typeface-weight-regular);
105
  /* Body Small font name */
106
  --md-sys-typescale-body-small-font: var(--md-ref-typeface-plain);
107
  /* Body Medium */
108
  --md-sys-typescale-body-medium-text-transform: unset;
109
  --md-sys-typescale-body-medium-axis-value: unset;
110
  --md-sys-typescale-body-medium-font-style: unset;
111
  --md-sys-typescale-body-medium-text-decoration: unset;
112
  /* Body Medium line height */
113
  --md-sys-typescale-body-medium-line-height-value: 20px;
114
  --md-sys-typescale-body-medium-line-height-unit: 2px;
115
  --md-sys-typescale-body-medium-line-height: 20px;
116
  /* Body Medium font tracking */
117
  --md-sys-typescale-body-medium-tracking-value: 0.25px;
118
  --md-sys-typescale-body-medium-tracking-unit: 2px;
119
  --md-sys-typescale-body-medium-tracking: 0.25px;
120
  /* Body Medium font size */
121
  --md-sys-typescale-body-medium-size-value: 14px;
122
  --md-sys-typescale-body-medium-size-unit: 2px;
123
  --md-sys-typescale-body-medium-size: 14px;
124
  /* Body Medium font weight */
125
  --md-sys-typescale-body-medium-weight: var(--md-ref-typeface-weight-regular);
126
  /* Body Medium font name */
127
  --md-sys-typescale-body-medium-font: var(--md-ref-typeface-plain);
128
  /* Body Large */
129
  --md-sys-typescale-body-large-text-transform: unset;
130
  --md-sys-typescale-body-large-axis-value: unset;
131
  --md-sys-typescale-body-large-font-style: unset;
132
  --md-sys-typescale-body-large-text-decoration: unset;
133
  /* Body Large line height */
134
  --md-sys-typescale-body-large-line-height-value: 24px;
135
  --md-sys-typescale-body-large-line-height-unit: 2px;
136
  --md-sys-typescale-body-large-line-height: 24px;
137
  /* Body Large font tracking */
138
  --md-sys-typescale-body-large-tracking-value: 0.5px;
139
  --md-sys-typescale-body-large-tracking-unit: 2px;
140
  --md-sys-typescale-body-large-tracking: 0.5px;
141
  /* Body Large font size */
142
  --md-sys-typescale-body-large-size-value: 16px;
143
  --md-sys-typescale-body-large-size-unit: 2px;
144
  --md-sys-typescale-body-large-size: 16px;
145
  /* Body Large font weight */
146
  --md-sys-typescale-body-large-weight: var(--md-ref-typeface-weight-regular);
147
  /* Body Large font name */
148
  --md-sys-typescale-body-large-font: var(--md-ref-typeface-plain);
149
  /* Title Small */
150
  --md-sys-typescale-title-small-text-transform: unset;
151
  --md-sys-typescale-title-small-axis-value: unset;
152
  --md-sys-typescale-title-small-font-style: unset;
153
  --md-sys-typescale-title-small-text-decoration: unset;
154
  /* Title Small line height */
155
  --md-sys-typescale-title-small-line-height-value: 20px;
156
  --md-sys-typescale-title-small-line-height-unit: 2px;
157
  --md-sys-typescale-title-small-line-height: 20px;
158
  /* Title Small font tracking */
159
  --md-sys-typescale-title-small-tracking-value: 0.10000000149011612px;
160
  --md-sys-typescale-title-small-tracking-unit: 2px;
161
  --md-sys-typescale-title-small-tracking: 0.10000000149011612px;
162
  /* Title Small font size */
163
  --md-sys-typescale-title-small-size-value: 14px;
164
  --md-sys-typescale-title-small-size-unit: 2px;
165
  --md-sys-typescale-title-small-size: 14px;
166
  /* Title Small font weight */
167
  --md-sys-typescale-title-small-weight: var(--md-ref-typeface-weight-medium);
168
  /* Title Small font name */
169
  --md-sys-typescale-title-small-font: var(--md-ref-typeface-plain);
170
  /* Title Medium */
171
  --md-sys-typescale-title-medium-text-transform: unset;
172
  --md-sys-typescale-title-medium-axis-value: unset;
173
  --md-sys-typescale-title-medium-font-style: unset;
174
  --md-sys-typescale-title-medium-text-decoration: unset;
175
  /* Title Medium line height */
176
  --md-sys-typescale-title-medium-line-height-value: 24px;
177
  --md-sys-typescale-title-medium-line-height-unit: 2px;
178
  --md-sys-typescale-title-medium-line-height: 24px;
179
  /* Title Medium font tracking */
180
  --md-sys-typescale-title-medium-tracking-value: 0.15000000596046448px;
181
  --md-sys-typescale-title-medium-tracking-unit: 2px;
182
  --md-sys-typescale-title-medium-tracking: 0.15000000596046448px;
183
  /* Title Medium font size */
184
  --md-sys-typescale-title-medium-size-value: 16px;
185
  --md-sys-typescale-title-medium-size-unit: 2px;
186
  --md-sys-typescale-title-medium-size: 16px;
187
  /* Title Medium font weight */
188
  --md-sys-typescale-title-medium-weight: var(--md-ref-typeface-weight-medium);
189
  /* Title Medium font name */
190
  --md-sys-typescale-title-medium-font: var(--md-ref-typeface-plain);
191
  /* Title Large */
192
  --md-sys-typescale-title-large-text-transform: unset;
193
  --md-sys-typescale-title-large-axis-value: unset;
194
  --md-sys-typescale-title-large-font-style: unset;
195
  --md-sys-typescale-title-large-text-decoration: unset;
196
  /* Title Large line height */
197
  --md-sys-typescale-title-large-line-height-value: 28px;
198
  --md-sys-typescale-title-large-line-height-unit: 2px;
199
  --md-sys-typescale-title-large-line-height: 28px;
200
  /* Title Large font tracking */
201
  --md-sys-typescale-title-large-tracking-value: 0px;
202
  --md-sys-typescale-title-large-tracking-unit: 2px;
203
  --md-sys-typescale-title-large-tracking: 0px;
204
  /* Title Large font size */
205
  --md-sys-typescale-title-large-size-value: 22px;
206
  --md-sys-typescale-title-large-size-unit: 2px;
207
  --md-sys-typescale-title-large-size: 22px;
208
  /* Title Large font weight */
209
  --md-sys-typescale-title-large-weight: var(--md-ref-typeface-weight-regular);
210
  /* Title Large font name */
211
  --md-sys-typescale-title-large-font: var(--md-ref-typeface-brand);
212
  /* Headline Small */
213
  --md-sys-typescale-headline-small-text-transform: unset;
214
  --md-sys-typescale-headline-small-axis-value: unset;
215
  --md-sys-typescale-headline-small-font-style: unset;
216
  --md-sys-typescale-headline-small-text-decoration: unset;
217
  /* Headline Small line height */
218
  --md-sys-typescale-headline-small-line-height-value: 32px;
219
  --md-sys-typescale-headline-small-line-height-unit: 2px;
220
  --md-sys-typescale-headline-small-line-height: 32px;
221
  /* Headline Small font tracking */
222
  --md-sys-typescale-headline-small-tracking-value: 0px;
223
  --md-sys-typescale-headline-small-tracking-unit: 2px;
224
  --md-sys-typescale-headline-small-tracking: 0px;
225
  /* Headline Small font size */
226
  --md-sys-typescale-headline-small-size-value: 24px;
227
  --md-sys-typescale-headline-small-size-unit: 2px;
228
  --md-sys-typescale-headline-small-size: 24px;
229
  /* Headline Small font weight */
230
  --md-sys-typescale-headline-small-weight: var(
231
    --md-ref-typeface-weight-regular
232
  );
233
  /* Headline Small font name */
234
  --md-sys-typescale-headline-small-font: var(--md-ref-typeface-brand);
235
  /* Headline Medium */
236
  --md-sys-typescale-headline-medium-text-transform: unset;
237
  --md-sys-typescale-headline-medium-axis-value: unset;
238
  --md-sys-typescale-headline-medium-font-style: unset;
239
  --md-sys-typescale-headline-medium-text-decoration: unset;
240
  /* Headline Medium line height */
241
  --md-sys-typescale-headline-medium-line-height-value: 36px;
242
  --md-sys-typescale-headline-medium-line-height-unit: 2px;
243
  --md-sys-typescale-headline-medium-line-height: 36px;
244
  /* Headline Medium font tracking */
245
  --md-sys-typescale-headline-medium-tracking-value: 0px;
246
  --md-sys-typescale-headline-medium-tracking-unit: 2px;
247
  --md-sys-typescale-headline-medium-tracking: 0px;
248
  /* Headline Medium font size */
249
  --md-sys-typescale-headline-medium-size-value: 28px;
250
  --md-sys-typescale-headline-medium-size-unit: 2px;
251
  --md-sys-typescale-headline-medium-size: 28px;
252
  /* Headline Medium font weight */
253
  --md-sys-typescale-headline-medium-weight: var(
254
    --md-ref-typeface-weight-regular
255
  );
256
  /* Headline Medium font name */
257
  --md-sys-typescale-headline-medium-font: var(--md-ref-typeface-brand);
258
  /* Headline Large */
259
  --md-sys-typescale-headline-large-text-transform: unset;
260
  --md-sys-typescale-headline-large-axis-value: unset;
261
  --md-sys-typescale-headline-large-font-style: unset;
262
  --md-sys-typescale-headline-large-text-decoration: unset;
263
  /* Headline Large line height */
264
  --md-sys-typescale-headline-large-line-height-value: 40px;
265
  --md-sys-typescale-headline-large-line-height-unit: 2px;
266
  --md-sys-typescale-headline-large-line-height: 40px;
267
  /* Headline Large font tracking */
268
  --md-sys-typescale-headline-large-tracking-value: 0px;
269
  --md-sys-typescale-headline-large-tracking-unit: 2px;
270
  --md-sys-typescale-headline-large-tracking: 0px;
271
  /* Headline Large font size */
272
  --md-sys-typescale-headline-large-size-value: 32px;
273
  --md-sys-typescale-headline-large-size-unit: 2px;
274
  --md-sys-typescale-headline-large-size: 32px;
275
  /* Headline Large font name */
276
  --md-sys-typescale-headline-large-font: var(--md-ref-typeface-brand);
277
  /* Headline Large font weight */
278
  --md-sys-typescale-headline-large-weight: var(
279
    --md-ref-typeface-weight-regular
280
  );
281
  /* Display Small */
282
  --md-sys-typescale-display-small-text-transform: unset;
283
  --md-sys-typescale-display-small-axis-value: unset;
284
  --md-sys-typescale-display-small-font-style: unset;
285
  --md-sys-typescale-display-small-text-decoration: unset;
286
  /* Display Small line height */
287
  --md-sys-typescale-display-small-line-height-value: 44px;
288
  --md-sys-typescale-display-small-line-height-unit: 2px;
289
  --md-sys-typescale-display-small-line-height: 44px;
290
  /* Display Small font tracking */
291
  --md-sys-typescale-display-small-tracking-value: 0px;
292
  --md-sys-typescale-display-small-tracking-unit: 2px;
293
  --md-sys-typescale-display-small-tracking: 0px;
294
  /* Display Small font size */
295
  --md-sys-typescale-display-small-size-value: 36px;
296
  --md-sys-typescale-display-small-size-unit: 2px;
297
  --md-sys-typescale-display-small-size: 36px;
298
  /* Display Small font weight */
299
  --md-sys-typescale-display-small-weight: var(
300
    --md-ref-typeface-weight-regular
301
  );
302
  /* Display Small font name */
303
  --md-sys-typescale-display-small-font: var(--md-ref-typeface-brand);
304
  /* Display Medium */
305
  --md-sys-typescale-display-medium-text-transform: unset;
306
  --md-sys-typescale-display-medium-axis-value: unset;
307
  --md-sys-typescale-display-medium-font-style: unset;
308
  --md-sys-typescale-display-medium-text-decoration: unset;
309
  /* Display Medium line height */
310
  --md-sys-typescale-display-medium-line-height-value: 52px;
311
  --md-sys-typescale-display-medium-line-height-unit: 2px;
312
  --md-sys-typescale-display-medium-line-height: 52px;
313
  /* Display Medium font tracking */
314
  --md-sys-typescale-display-medium-tracking-value: 0px;
315
  --md-sys-typescale-display-medium-tracking-unit: 2px;
316
  --md-sys-typescale-display-medium-tracking: 0px;
317
  /* Display Medium font size */
318
  --md-sys-typescale-display-medium-size-value: 45px;
319
  --md-sys-typescale-display-medium-size-unit: 2px;
320
  --md-sys-typescale-display-medium-size: 45px;
321
  /* Display Medium font weight */
322
  --md-sys-typescale-display-medium-weight: var(
323
    --md-ref-typeface-weight-regular
324
  );
325
  /* Display Medium font name */
326
  --md-sys-typescale-display-medium-font: var(--md-ref-typeface-brand);
327
  /* Display Large */
328
  --md-sys-typescale-display-large-text-transform: unset;
329
  --md-sys-typescale-display-large-axis-value: unset;
330
  --md-sys-typescale-display-large-font-style: unset;
331
  --md-sys-typescale-display-large-text-decoration: unset;
332
  /* Display Large line height */
333
  --md-sys-typescale-display-large-line-height-value: 64px;
334
  --md-sys-typescale-display-large-line-height-unit: 2px;
335
  --md-sys-typescale-display-large-line-height: 64px;
336
  /* Display Large font tracking */
337
  --md-sys-typescale-display-large-tracking-value: -0.25px;
338
  --md-sys-typescale-display-large-tracking-unit: 2px;
339
  --md-sys-typescale-display-large-tracking: -0.25px;
340
  /* Display Large font size */
341
  --md-sys-typescale-display-large-size-value: 57px;
342
  --md-sys-typescale-display-large-size-unit: 2px;
343
  --md-sys-typescale-display-large-size: 57px;
344
  /* Display Large font weight */
345
  --md-sys-typescale-display-large-weight: var(
346
    --md-ref-typeface-weight-regular
347
  );
348
  /* Display Large font name */
349
  --md-sys-typescale-display-large-font: var(--md-ref-typeface-brand);
350
  /* Plain typeface */
351
  --md-ref-typeface-plain: Roboto;
352
  /* Brand typeface */
353
  --md-ref-typeface-brand: Roboto;
354
  /* Bold weight */
355
  --md-ref-typeface-weight-bold: 700;
356
  /* Medium weight */
357
  --md-ref-typeface-weight-medium: 500;
358
  /* Regular weight */
359
  --md-ref-typeface-weight-regular: 400;
360
}
361
362
/* Label Small */
363
.label-small {
364
  font-family: var(--md-sys-typescale-label-small-font);
365
  font-weight: var(--md-sys-typescale-label-small-weight);
366
  font-size: var(--md-sys-typescale-label-small-size);
367
  font-style: var(--md-sys-typescale-label-small-font-style);
368
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
369
  line-height: var(--md-sys-typescale-label-small-line-height);
370
  text-transform: var(--md-sys-typescale-label-small-text-transform);
371
  text-decoration: var(--md-sys-typescale-label-small-text-decoration);
372
}
373
/* Label Medium */
374
.label-medium {
375
  font-family: var(--md-sys-typescale-label-medium-font);
376
  font-weight: var(--md-sys-typescale-label-medium-weight);
377
  font-size: var(--md-sys-typescale-label-medium-size);
378
  font-style: var(--md-sys-typescale-label-medium-font-style);
379
  letter-spacing: var(--md-sys-typescale-label-medium-tracking);
380
  line-height: var(--md-sys-typescale-label-medium-line-height);
381
  text-transform: var(--md-sys-typescale-label-medium-text-transform);
382
  text-decoration: var(--md-sys-typescale-label-medium-text-decoration);
383
}
384
/* Label Large */
385
.label-large {
386
  font-family: var(--md-sys-typescale-label-large-font);
387
  font-weight: var(--md-sys-typescale-label-large-weight);
388
  font-size: var(--md-sys-typescale-label-large-size);
389
  font-style: var(--md-sys-typescale-label-large-font-style);
390
  letter-spacing: var(--md-sys-typescale-label-large-tracking);
391
  line-height: var(--md-sys-typescale-label-large-line-height);
392
  text-transform: var(--md-sys-typescale-label-large-text-transform);
393
  text-decoration: var(--md-sys-typescale-label-large-text-decoration);
394
}
395
/* Body Small */
396
.body-small {
397
  font-family: var(--md-sys-typescale-body-small-font);
398
  font-weight: var(--md-sys-typescale-body-small-weight);
399
  font-size: var(--md-sys-typescale-body-small-size);
400
  font-style: var(--md-sys-typescale-body-small-font-style);
401
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
402
  line-height: var(--md-sys-typescale-body-small-line-height);
403
  text-transform: var(--md-sys-typescale-body-small-text-transform);
404
  text-decoration: var(--md-sys-typescale-body-small-text-decoration);
405
}
406
/* Body Medium */
407
.body-medium {
408
  font-family: var(--md-sys-typescale-body-medium-font);
409
  font-weight: var(--md-sys-typescale-body-medium-weight);
410
  font-size: var(--md-sys-typescale-body-medium-size);
411
  font-style: var(--md-sys-typescale-body-medium-font-style);
412
  letter-spacing: var(--md-sys-typescale-body-medium-tracking);
413
  line-height: var(--md-sys-typescale-body-medium-line-height);
414
  text-transform: var(--md-sys-typescale-body-medium-text-transform);
415
  text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
416
}
417
/* Body Large */
418
.body-large {
419
  font-family: var(--md-sys-typescale-body-large-font);
420
  font-weight: var(--md-sys-typescale-body-large-weight);
421
  font-size: var(--md-sys-typescale-body-large-size);
422
  font-style: var(--md-sys-typescale-body-large-font-style);
423
  letter-spacing: var(--md-sys-typescale-body-large-tracking);
424
  line-height: var(--md-sys-typescale-body-large-line-height);
425
  text-transform: var(--md-sys-typescale-body-large-text-transform);
426
  text-decoration: var(--md-sys-typescale-body-large-text-decoration);
427
}
428
/* Title Small */
429
.title-small {
430
  font-family: var(--md-sys-typescale-title-small-font);
431
  font-weight: var(--md-sys-typescale-title-small-weight);
432
  font-size: var(--md-sys-typescale-title-small-size);
433
  font-style: var(--md-sys-typescale-title-small-font-style);
434
  letter-spacing: var(--md-sys-typescale-title-small-tracking);
435
  line-height: var(--md-sys-typescale-title-small-line-height);
436
  text-transform: var(--md-sys-typescale-title-small-text-transform);
437
  text-decoration: var(--md-sys-typescale-title-small-text-decoration);
438
}
439
/* Title Medium */
440
.title-medium {
441
  font-family: var(--md-sys-typescale-title-medium-font);
442
  font-weight: var(--md-sys-typescale-title-medium-weight);
443
  font-size: var(--md-sys-typescale-title-medium-size);
444
  font-style: var(--md-sys-typescale-title-medium-font-style);
445
  letter-spacing: var(--md-sys-typescale-title-medium-tracking);
446
  line-height: var(--md-sys-typescale-title-medium-line-height);
447
  text-transform: var(--md-sys-typescale-title-medium-text-transform);
448
  text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
449
}
450
/* Title Large */
451
.title-large {
452
  font-family: var(--md-sys-typescale-title-large-font);
453
  font-weight: var(--md-sys-typescale-title-large-weight);
454
  font-size: var(--md-sys-typescale-title-large-size);
455
  font-style: var(--md-sys-typescale-title-large-font-style);
456
  letter-spacing: var(--md-sys-typescale-title-large-tracking);
457
  line-height: var(--md-sys-typescale-title-large-line-height);
458
  text-transform: var(--md-sys-typescale-title-large-text-transform);
459
  text-decoration: var(--md-sys-typescale-title-large-text-decoration);
460
}
461
/* Headline Small */
462
.headline-small {
463
  font-family: var(--md-sys-typescale-headline-small-font);
464
  font-weight: var(--md-sys-typescale-headline-small-weight);
465
  font-size: var(--md-sys-typescale-headline-small-size);
466
  font-style: var(--md-sys-typescale-headline-small-font-style);
467
  letter-spacing: var(--md-sys-typescale-headline-small-tracking);
468
  line-height: var(--md-sys-typescale-headline-small-line-height);
469
  text-transform: var(--md-sys-typescale-headline-small-text-transform);
470
  text-decoration: var(--md-sys-typescale-headline-small-text-decoration);
471
}
472
/* Headline Medium */
473
.headline-medium {
474
  font-family: var(--md-sys-typescale-headline-medium-font);
475
  font-weight: var(--md-sys-typescale-headline-medium-weight);
476
  font-size: var(--md-sys-typescale-headline-medium-size);
477
  font-style: var(--md-sys-typescale-headline-medium-font-style);
478
  letter-spacing: var(--md-sys-typescale-headline-medium-tracking);
479
  line-height: var(--md-sys-typescale-headline-medium-line-height);
480
  text-transform: var(--md-sys-typescale-headline-medium-text-transform);
481
  text-decoration: var(--md-sys-typescale-headline-medium-text-decoration);
482
}
483
/* Headline Large */
484
.headline-large {
485
  font-family: var(--md-sys-typescale-headline-large-font);
486
  font-weight: var(--md-sys-typescale-headline-large-weight);
487
  font-size: var(--md-sys-typescale-headline-large-size);
488
  font-style: var(--md-sys-typescale-headline-large-font-style);
489
  letter-spacing: var(--md-sys-typescale-headline-large-tracking);
490
  line-height: var(--md-sys-typescale-headline-large-line-height);
491
  text-transform: var(--md-sys-typescale-headline-large-text-transform);
492
  text-decoration: var(--md-sys-typescale-headline-large-text-decoration);
493
}
494
/* Display Small */
495
.display-small {
496
  font-family: var(--md-sys-typescale-display-small-font);
497
  font-weight: var(--md-sys-typescale-display-small-weight);
498
  font-size: var(--md-sys-typescale-display-small-size);
499
  font-style: var(--md-sys-typescale-display-small-font-style);
500
  letter-spacing: var(--md-sys-typescale-display-small-tracking);
501
  line-height: var(--md-sys-typescale-display-small-line-height);
502
  text-transform: var(--md-sys-typescale-display-small-text-transform);
503
  text-decoration: var(--md-sys-typescale-display-small-text-decoration);
504
}
505
/* Display Medium */
506
.display-medium {
507
  font-family: var(--md-sys-typescale-display-medium-font);
508
  font-weight: var(--md-sys-typescale-display-medium-weight);
509
  font-size: var(--md-sys-typescale-display-medium-size);
510
  font-style: var(--md-sys-typescale-display-medium-font-style);
511
  letter-spacing: var(--md-sys-typescale-display-medium-tracking);
512
  line-height: var(--md-sys-typescale-display-medium-line-height);
513
  text-transform: var(--md-sys-typescale-display-medium-text-transform);
514
  text-decoration: var(--md-sys-typescale-display-medium-text-decoration);
515
}
516
/* Display Large */
517
.display-large {
518
  font-family: var(--md-sys-typescale-display-large-font);
519
  font-weight: var(--md-sys-typescale-display-large-weight);
520
  font-size: var(--md-sys-typescale-display-large-size);
521
  font-style: var(--md-sys-typescale-display-large-font-style);
522
  letter-spacing: var(--md-sys-typescale-display-large-tracking);
523
  line-height: var(--md-sys-typescale-display-large-line-height);
524
  text-transform: var(--md-sys-typescale-display-large-text-transform);
525
  text-decoration: var(--md-sys-typescale-display-large-text-decoration);
526
}
527

P. Carpeta « css / theme »

1. css / theme / dark.css

1
.dark {
2
  --md-sys-color-primary: rgb(170 199 255);
3
  --md-sys-color-surface-tint: rgb(170 199 255);
4
  --md-sys-color-on-primary: rgb(10 48 95);
5
  --md-sys-color-primary-container: rgb(40 71 119);
6
  --md-sys-color-on-primary-container: rgb(214 227 255);
7
  --md-sys-color-secondary: rgb(190 198 220);
8
  --md-sys-color-on-secondary: rgb(40 49 65);
9
  --md-sys-color-secondary-container: rgb(62 71 89);
10
  --md-sys-color-on-secondary-container: rgb(218 226 249);
11
  --md-sys-color-tertiary: rgb(221 188 224);
12
  --md-sys-color-on-tertiary: rgb(63 40 68);
13
  --md-sys-color-tertiary-container: rgb(87 62 92);
14
  --md-sys-color-on-tertiary-container: rgb(250 216 253);
15
  --md-sys-color-error: rgb(255 180 171);
16
  --md-sys-color-on-error: rgb(105 0 5);
17
  --md-sys-color-error-container: rgb(147 0 10);
18
  --md-sys-color-on-error-container: rgb(255 218 214);
19
  --md-sys-color-background: rgb(17 19 24);
20
  --md-sys-color-on-background: rgb(226 226 233);
21
  --md-sys-color-surface: rgb(17 19 24);
22
  --md-sys-color-on-surface: rgb(226 226 233);
23
  --md-sys-color-surface-variant: rgb(68 71 78);
24
  --md-sys-color-on-surface-variant: rgb(196 198 208);
25
  --md-sys-color-outline: rgb(142 144 153);
26
  --md-sys-color-outline-variant: rgb(68 71 78);
27
  --md-sys-color-shadow: rgb(0 0 0);
28
  --md-sys-color-scrim: rgb(0 0 0);
29
  --md-sys-color-inverse-surface: rgb(226 226 233);
30
  --md-sys-color-inverse-on-surface: rgb(46 48 54);
31
  --md-sys-color-inverse-primary: rgb(65 95 145);
32
  --md-sys-color-primary-fixed: rgb(214 227 255);
33
  --md-sys-color-on-primary-fixed: rgb(0 27 62);
34
  --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35
  --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36
  --md-sys-color-secondary-fixed: rgb(218 226 249);
37
  --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38
  --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39
  --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40
  --md-sys-color-tertiary-fixed: rgb(250 216 253);
41
  --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42
  --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43
  --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44
  --md-sys-color-surface-dim: rgb(17 19 24);
45
  --md-sys-color-surface-bright: rgb(55 57 62);
46
  --md-sys-color-surface-container-lowest: rgb(12 14 19);
47
  --md-sys-color-surface-container-low: rgb(25 28 32);
48
  --md-sys-color-surface-container: rgb(29 32 36);
49
  --md-sys-color-surface-container-high: rgb(40 42 47);
50
  --md-sys-color-surface-container-highest: rgb(51 53 58);
51
}
52

2. css / theme / light.css

1
.light {
2
  --md-sys-color-primary: rgb(65 95 145);
3
  --md-sys-color-surface-tint: rgb(65 95 145);
4
  --md-sys-color-on-primary: rgb(255 255 255);
5
  --md-sys-color-primary-container: rgb(214 227 255);
6
  --md-sys-color-on-primary-container: rgb(40 71 119);
7
  --md-sys-color-secondary: rgb(86 95 113);
8
  --md-sys-color-on-secondary: rgb(255 255 255);
9
  --md-sys-color-secondary-container: rgb(218 226 249);
10
  --md-sys-color-on-secondary-container: rgb(62 71 89);
11
  --md-sys-color-tertiary: rgb(112 85 117);
12
  --md-sys-color-on-tertiary: rgb(255 255 255);
13
  --md-sys-color-tertiary-container: rgb(250 216 253);
14
  --md-sys-color-on-tertiary-container: rgb(87 62 92);
15
  --md-sys-color-error: rgb(186 26 26);
16
  --md-sys-color-on-error: rgb(255 255 255);
17
  --md-sys-color-error-container: rgb(255 218 214);
18
  --md-sys-color-on-error-container: rgb(147 0 10);
19
  --md-sys-color-background: rgb(249 249 255);
20
  --md-sys-color-on-background: rgb(25 28 32);
21
  --md-sys-color-surface: rgb(249 249 255);
22
  --md-sys-color-on-surface: rgb(25 28 32);
23
  --md-sys-color-surface-variant: rgb(224 226 236);
24
  --md-sys-color-on-surface-variant: rgb(68 71 78);
25
  --md-sys-color-outline: rgb(116 119 127);
26
  --md-sys-color-outline-variant: rgb(196 198 208);
27
  --md-sys-color-shadow: rgb(0 0 0);
28
  --md-sys-color-scrim: rgb(0 0 0);
29
  --md-sys-color-inverse-surface: rgb(46 48 54);
30
  --md-sys-color-inverse-on-surface: rgb(240 240 247);
31
  --md-sys-color-inverse-primary: rgb(170 199 255);
32
  --md-sys-color-primary-fixed: rgb(214 227 255);
33
  --md-sys-color-on-primary-fixed: rgb(0 27 62);
34
  --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35
  --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36
  --md-sys-color-secondary-fixed: rgb(218 226 249);
37
  --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38
  --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39
  --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40
  --md-sys-color-tertiary-fixed: rgb(250 216 253);
41
  --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42
  --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43
  --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44
  --md-sys-color-surface-dim: rgb(217 217 224);
45
  --md-sys-color-surface-bright: rgb(249 249 255);
46
  --md-sys-color-surface-container-lowest: rgb(255 255 255);
47
  --md-sys-color-surface-container-low: rgb(243 243 250);
48
  --md-sys-color-surface-container: rgb(237 237 244);
49
  --md-sys-color-surface-container-high: rgb(231 232 238);
50
  --md-sys-color-surface-container-highest: rgb(226 226 233);
51
}
52

K. Carpeta « fonts »

A. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints

B. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf

C. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2

D. fonts / roboto-v32-latin-regular.woff2

L. Carpeta « img »

A. img / Escultura_de_coyote.jpeg

icono2048.png

B. img / icono2048.png

icono2048.png

C. img / maskable_icon.png

maskable_icon.png

D. img / maskable_icon_x128.png

maskable_icon_x128.png

E. img / maskable_icon_x192.png

maskable_icon_x192.png

F. img / maskable_icon_x384.png

maskable_icon_x384.png

G. img / maskable_icon_x48.png

maskable_icon_x48.png

H. img / maskable_icon_x512.png

maskable_icon_x512.png

I. img / maskable_icon_x72.png

maskable_icon_x72.png

J. img / maskable_icon_x96.png

maskable_icon_x96.png

K. img / pexels-craig-dennis-3701822.jpg

icono2048.png

L. img / pexels-moises-patrício-10961948.jpg

icono2048.png

M. img / screenshot_horizontal.png

screenshot_horizontal.png

N. img / screenshot_vertical.png

screenshot_vertical.png

M. Carpeta « js »

A. js / nav-tab-fixed.js

1
import { resaltaSiEstasEn } from "./lib/resaltaSiEstasEn.js"
2
3
export class NavTabFixed extends HTMLElement {
4
5
 constructor() {
6
  super()
7
  this.creado = false
8
 }
9
10
 connectedCallback() {
11
  this.classList.add("md-tab", "fixed")
12
13
  if (!this.creado) {
14
15
   this.innerHTML = /* HTML */`
16
    <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html">
17
     <span class="material-symbols-outlined">home</span>
18
     Inicio
19
    </a>
20
 
21
    <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html">
22
     <span class="material-symbols-outlined">help</span>
23
     Ayuda
24
    </a>`
25
26
   this.creado = true
27
28
  }
29
30
 }
31
32
}
33
34
customElements.define("nav-tab-fixed", NavTabFixed)

B. Carpeta « js / lib »

1. js / lib / ES_APPLE.js

1
export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent)

2. js / lib / getAttribute.js

1
/**
2
 * @param {HTMLElement} elementoHtml
3
 * @param {string} nombre
4
 * @returns {string}
5
 */
6
export function getAttribute(elementoHtml, nombre) {
7
 const valor = elementoHtml.getAttribute(nombre)
8
 return valor === null ? "" : valor
9
}

3. js / lib / querySelector.js

1
/**
2
 * @template { HTMLElement } T
3
 * @param { Document | Element | ShadowRoot } raiz
4
 * @param { string } query
5
 * @returns { T }
6
 */
7
export function querySelector(raiz, query) {
8
 /** @type { T | null } */
9
 const resutado = raiz.querySelector(query)
10
 if (resutado === null)
11
  throw new Error(`No se encuentra ${query}.`)
12
 return resutado
13
}

4. js / lib / registraServiceWorker.js

1
const nombreDeServiceWorker = "sw.js"
2
3
try {
4
 navigator.serviceWorker.register(nombreDeServiceWorker)
5
  .then(registro => {
6
   console.log(nombreDeServiceWorker, "registrado.")
7
   console.log(registro)
8
  })
9
  .catch(error => console.log(error))
10
} catch (error) {
11
 console.log(error)
12
}

5. js / lib / resaltaSiEstasEn.js

1
/**
2
 * @param {string[]} paginas
3
 */
4
export function resaltaSiEstasEn(paginas) {
5
6
 const pathname = location.pathname
7
8
 for (const pagina of paginas) {
9
10
  if (pathname === pagina) {
11
   queueMicrotask(() => {
12
    const tab = document.querySelector(".active")
13
    if (tab !== null && tab.closest(".scrollable") !== null) {
14
     tab.scrollIntoView({ inline: "center", block: "end" })
15
    }
16
   })
17
   return `class="active"`
18
  }
19
20
 }
21
22
 return ""
23
24
}

6. Carpeta « js / lib / custom »

A. js / lib / custom / md-app-bar.js

1
import { ES_APPLE } from "../ES_APPLE.js"
2
import { getAttribute } from "../getAttribute.js"
3
import { querySelector } from "../querySelector.js"
4
5
class MdAppBar extends HTMLElement {
6
7
 getContent() {
8
  return /* HTML */`
9
   <style>
10
11
    :host {
12
     display: flex;
13
     box-sizing: border-box;
14
     align-items: center;
15
     padding: 0 0.25rem;
16
     background-color: var(--md-sys-color-surface);
17
     position: sticky;
18
     z-index: 1;
19
     left: env(titlebar-area-x, 0);
20
     top: env(titlebar-area-y, 0);
21
     height: env(titlebar-area-height, 4rem);
22
     width: env(titlebar-area-width, 100%);
23
    }
24
    
25
    :host(.apple) {
26
     height: env(titlebar-area-height, 3rem);
27
    }
28
29
    :host(.scroll) {
30
     background-color: var(--md-sys-color-surface-container-low);
31
    }
32
33
    #navigation {
34
     flex: 0 0 auto;
35
     overflow: hidden
36
    }
37
38
    #navigation ::slotted(*) {
39
     color: var(--md-sys-color-on-surface);
40
    }
41
42
    #acciones {
43
     margin-left: auto;
44
     flex: 0 0 auto;
45
     overflow: hidden
46
    }
47
48
    :host(.centered) #navigation,
49
    :host(.centered) #acciones {
50
     flex: 0 0 6rem;
51
     overflow: hidden
52
    }
53
54
    #headline::slotted(*) {
55
     -webkit-app-region: drag;
56
     flex: 1 1 auto;
57
     white-space: nowrap;
58
     text-overflow: ellipsis;
59
     overflow: hidden;
60
     font-family: var(--md-sys-typescale-title-large-font);
61
     font-weight: var(--md-sys-typescale-title-large-weight);
62
     font-size: var(--md-sys-typescale-title-large-size);
63
     font-style: var(--md-sys-typescale-title-large-font-style);
64
     letter-spacing: var(--md-sys-typescale-title-large-tracking);
65
     line-height: var(--md-sys-typescale-title-large-line-height);
66
     text-transform: var(--md-sys-typescale-title-large-text-transform);
67
     text-decoration: var(--md-sys-typescale-title-large-text-decoration);
68
     color: var(--md-sys-color-on-surface);
69
    }
70
71
    :host(.centered) #headline::slotted(*) {
72
     flex: 1 1 auto;
73
     text-align: center
74
    }
75
76
   </style>
77
78
   <span id="navigation">
79
    <slot name="navigation"></slot>
80
   </span>
81
   <slot id="headline"></slot>
82
   <span id="acciones">
83
    <slot name="action"></slot>
84
   </span>`
85
 }
86
87
 constructor() {
88
  super()
89
  if (ES_APPLE) {
90
   document.body.classList.add("apple")
91
   document.body.classList.remove("material")
92
  } else {
93
   document.body.classList.add("material")
94
   document.body.classList.remove("apple")
95
  }
96
97
  /**
98
   * @private
99
   * @readonly
100
   */
101
  const shadow = this.attachShadow({ mode: "open" })
102
  shadow.innerHTML = this.getContent()
103
  this._configuraAction = this._configuraAction.bind(this)
104
  /**
105
   * @private
106
   * @type {number}
107
   */
108
  this._posY = 0
109
  /**
110
   * @private
111
   * @type {boolean}
112
   */
113
  this._scrolling = false
114
  /**
115
    * @private
116
    * @type { HTMLSlotElement }
117
    */
118
  this._navigation = querySelector(shadow, '[name="navigation"]')
119
  /**
120
    * @private
121
    * @type { HTMLSlotElement }
122
    */
123
  this._action = querySelector(shadow, '[name="action"]')
124
  /**
125
    * @private
126
    * @type { HTMLHeadingElement | null }
127
    */
128
  this._headline = null
129
  /**
130
    * @private
131
    * @type { HTMLElement | null }
132
    */
133
  this._adicional = null
134
  this._action.addEventListener("slotchange", this._configuraAction)
135
  addEventListener("scroll", () => this._onScroll())
136
  addEventListener("load", () => this.configurOtros())
137
 }
138
139
 connectedCallback() {
140
  this.role = "toolbar"
141
  this._configuraAction()
142
 }
143
144
 configurOtros() {
145
  const idHeadline = getAttribute(this, "headline")
146
  if (idHeadline !== "") {
147
   const headline = document.getElementById(idHeadline)
148
   if (headline instanceof HTMLHeadingElement) {
149
    this._headline = headline
150
    if (this.classList.contains("apple") || this.classList.contains("medium")) {
151
     headline.classList.add("md-headline", "headline-small")
152
    } else {
153
     headline.classList.add("md-headline", "headline-medium")
154
    }
155
   }
156
  }
157
  const idAdicional = getAttribute(this, "adicional")
158
  if (idAdicional !== "") {
159
   this._adicional = document.getElementById(idAdicional)
160
   if (this._adicional !== null) {
161
    if (this.classList.contains("apple")) {
162
     this._adicional.style.top = "env(titlebar-area-height, 3rem)"
163
    } else {
164
     this._adicional.style.top = "env(titlebar-area-height, 4rem)"
165
    }
166
   }
167
  }
168
 }
169
170
 _configuraAction() {
171
  const assignedElements = this._action.assignedElements()
172
  if (this.isConnected) {
173
   if (ES_APPLE) {
174
    this.classList.add("apple")
175
    this.classList.remove("material")
176
   } else {
177
    this.classList.add("material")
178
    this.classList.remove("apple")
179
   }
180
   if (this.classList.contains("centered")) {
181
    this.classList.remove("centrado")
182
    this.classList.remove("justificado")
183
   } else {
184
    if (ES_APPLE && assignedElements.length <= 1) {
185
     this.classList.add("centrado")
186
     this.classList.remove("justificado")
187
    } else {
188
     this.classList.add("justificado")
189
     this.classList.remove("centrado")
190
    }
191
   }
192
  }
193
 }
194
195
 /** @private */
196
 _onScroll() {
197
  this._posY = scrollY
198
  if (!this._scrolling) {
199
   requestAnimationFrame(() => this._avanza())
200
  }
201
  this._scrolling = true
202
 }
203
204
 /** @private */
205
 _avanza() {
206
  if (this._posY === 0) {
207
   this.classList.remove("scroll")
208
   if (this._headline !== null) {
209
    if (this._adicional === null) {
210
     this._headline.classList.remove("scroll")
211
    } else {
212
     this._headline.classList.remove("scroll-adicional")
213
    }
214
   }
215
   if (this._adicional !== null) {
216
    this._adicional.classList.remove("scroll")
217
   }
218
  } else {
219
   this.classList.add("scroll")
220
   if (this._headline !== null) {
221
    if (this._adicional === null) {
222
     this._headline.classList.add("scroll")
223
    } else {
224
     this._headline.classList.add("scroll-adicional")
225
    }
226
   }
227
   if (this._adicional !== null) {
228
    this._adicional.classList.add("scroll")
229
   }
230
  }
231
  this._scrolling = false
232
 }
233
234
}
235
236
customElements.define("md-app-bar", MdAppBar)

N. favicon.ico

favicon.ico

O. .htaccess

  • Este archivo configura las respuestas del servidor..

  • Lo utilizan principalmente servidores como Apache o Nginx.

  • Configura el mime type para el archivo de manifiesto y sedhabilita el uso de la cache general de http..

1
AddType application/manifest+json .webmanifest
2
3
ExpiresActive On
4
5
Header set Cache-Control "max-age=1, must-revalidate"
6

P. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

Q. instruccionesListadoSw.txt

1
Generar el listado de archivos del sw.js desde Visual Studio Code.
2
1. Abrir una terminal desde el menú 
3
    Terminal > New Terminal
4
5
2. Desde la terminal introducir la orden:
6
    Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
8
3. Abrir el archivo generado, que se llama
9
    archivos.txt
10
   y sobre este, realizar los pasos que siguen:
11
12
4. Quita del archivo archivos.txt:
13
    * el encabezado,
14
    * todas las carpetas,
15
    * todos los archivos dentro de .vscode como:
16
       * el archivo .vscode/settings.json,
17
       * el archivo .vscode/launch.json,
18
    * el archivo .htaccess,
19
    * el archivo archivos.txt,
20
    * este archivo (instruccionesListadoSw.txt),
21
    * el archivo jsconfig.json,
22
    * el archivo sw.js,
23
    * el archivo de la base de datos, que termina en ".db" y
24
      está en la carpeta php,
25
    * todos los archivos de php y
26
    * las líneas en blanco del final
27
28
5. Cambia los \ por / desde Visual Studio Code con las siguientes
29
   combinaciones de teclas:
30
31
    Ctrl+H En el diálogo que aparece introduce lo siguiente:
32
    Find:\
33
    Replace:/
34
35
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
36
 
37
6. Coloca las comillas y coma del final de cada línea desde Visual
38
   Studio Code con las siguientes combinaciones de teclas:
39
40
    Ctrl+H En el diálogo que aparece, selecciona el botón
41
            ".*"
42
           e introduce lo siguiente:
43
    Find:\s*$
44
    Replace:",
45
46
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
47
48
7. Marca la carpeta inicial, presiona la combinación de teclas:
49
50
    Shift+Ctrl+L
51
52
    borra la selección, teclea " y luego ESC
53
54
8. Cambia las secuencias de espacios por / con las siguientes
55
   combinaciones de teclas:
56
57
    Ctrl+H En el diálogo que aparece, selecciona el botón
58
            ".*"
59
           e introduce lo siguiente:
60
    Find:\s+
61
    Replace:/
62
63
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
64
65
9. Cambia las "/ por " con las siguientes combinaciones de teclas:
66
67
    Ctrl+H En el diálogo que aparece, quita la selección del botón
68
            ".*"
69
           e introduce lo siguiente:
70
    Find:"/
71
    Replace:"
72
73
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
74
75
10. Copia el texto al archivo
76
     sw.js
77
    en el contenido del arreglo llamado ARCHIVOS, pero recuerda
78
    mantener el último elemento, que dice:
79
     "/"

R. archivos.txt

1
"ayuda.html",
2
"favicon.ico",
3
"index.html",
4
"site.webmanifest",
5
"css/baseline.css",
6
"css/colors.css",
7
"css/elevation.css",
8
"css/estilos.css",
9
"css/material-symbols-outlined.css",
10
"css/md-headline.css",
11
"css/md-list.css",
12
"css/md-tab.css",
13
"css/motion.css",
14
"css/palette.css",
15
"css/roboto.css",
16
"css/shape.css",
17
"css/state.css",
18
"css/transicion_pestanas.css",
19
"css/typography.css",
20
"css/theme/dark.css",
21
"css/theme/light.css",
22
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
23
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
24
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
25
"fonts/roboto-v32-latin-regular.woff2",
26
"img/Escultura_de_coyote.jpeg",
27
"img/icono2048.png",
28
"img/maskable_icon.png",
29
"img/maskable_icon_x128.png",
30
"img/maskable_icon_x192.png",
31
"img/maskable_icon_x384.png",
32
"img/maskable_icon_x48.png",
33
"img/maskable_icon_x512.png",
34
"img/maskable_icon_x72.png",
35
"img/maskable_icon_x96.png",
36
"img/pexels-craig-dennis-3701822.jpg",
37
"img/pexels-moises-patrício-10961948.jpg",
38
"img/screenshot_horizontal.png",
39
"img/screenshot_vertical.png",
40
"js/nav-tab-fixed.js",
41
"js/lib/ES_APPLE.js",
42
"js/lib/getAttribute.js",
43
"js/lib/querySelector.js",
44
"js/lib/registraServiceWorker.js",
45
"js/lib/resaltaSiEstasEn.js",
46
"js/lib/custom/md-app-bar.js",
47
"ungap/custom-elements.js",

S. Carpeta « ungap »

A. ungap / custom-elements.js

T. Resumen

  • En esta lección se presentó la estructura básica de una PWA con Material Design 3 Expressive.

7. PWA con formulario y Material Design 3 Expressive

Versión para imprimir.

A. Introduccion

  • En esta lección se presenta una PWA que despliega un formulario y usa Material Design 3 Expressive.

  • Puedes probar el ejemplo en https://pwaform.rf.gd/.

B. Referencias

Sitios de Material Design
Material.io

Material Design es una guía de diseño para aplicaciones multiplataforma. La encuentras en https://m3.material.io/.

Herramienta para selección de colores
Material Theme Builder

https://material-foundation.github.io/material-theme-builder/

Adaptación multiplataforma

La forma de adaptar Material Design en distintas plataformas está en https://material.io/design/platform-guidance/cross-platform-adaptation.html

C. Hazlo funcionar (con videos)

  1. Prueba e instala, de preferencia con Chrome, el sitio https://pwaform.rf.gd/.

  2. Descarga el archivo /src/pwaform.zip y descompáctalo.

  3. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  4. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  5. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  6. Importa el proyecto de GitHub a Visual Studio Code

  7. Edita los archivos que desees.

  8. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  9. Crea los íconos del proyecto con https://www.photopea.com/.

  10. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  11. Coloca el archivo favicon.ico en la raíz del proyecto.

  12. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  13. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  14. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  15. Prueba tu PWA.

  16. Publica tu PWA.

  17. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  18. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  19. Instala y usa tu PWA en iOS (iPhone y iPad).

D. Hazlo funcionar (texto)

  1. Prueba e instala, de preferencia con Chrome, el sitio pwaform.rf.gd/.

  2. Descarga el archivo /src/pwaform.zip y descompáctalo.

  3. Crea tu proyecto en GitHub pages:

    1. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

      • Mantén la selección Public.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

    5. Entra al repositorio y selecciona ⚙️ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto. Si haces cambios, clic en Save.

    6. En la misma pestaña de ⚙️ Settings selecciona ◯ Actions, luego selecciona General y en Actions permissions selecciona el rediobotón Allow xxxx, and select non-xxxx, actions and reusable workflows y abajo la casilla de verificación Allow actions created by GitHub y cliquea el botón Save.

  4. Importa el proyecto de GitHub a Visual Studio Code:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo, excepto el archivo .htaccess.

  5. Edita los archivos que desees.

  6. Crea los íconos del proyecto con https://www.photopea.com/.

  7. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  8. Coloca el archivo favicon.ico en la raíz del proyecto.

  9. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  10. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  11. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  12. Haz clic derecho en index.html, selecciona Open with Live Server y se abre el navegador para que puedas probar localmente el ejemplo.

  13. Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.

  14. Coloca las capturas en la carpeta img y asegúrate de que estén declaradas en el archivo site.webmanifest.

  15. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en vez de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

    5. En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error.

    6. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.

  16. Cuando usas GitHub pages, antes de subir los archivos, no debes modificar el valor de VERSION en el archivo sw.js.

  17. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push
  18. Si usas GitHub pages:

    1. Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.

    2. Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.

    3. A partir de este momento, espera al menos 11 minutos para modificar el valor de VERSION en el archivo sw.js y volver a subir el proyecto.

  19. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  20. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  21. Instala y usa tu PWA en iOS (iPhone y iPad).

E. Archivos

F. index.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>PWA con Formulario</title>
9
10
 <meta name="description" content="Ejemplo de PWA con Formulario">
11
12
 <!-- <script type="module" src="js/lib/registraServiceWorker.js"></script> -->
13
 <script type="module" src="js/lib/manejaErrores.js"></script>
14
15
 <meta name="viewport" content="width=device-width">
16
 <meta name="theme-color" content="#fffbfe">
17
 <link rel="icon" sizes="32x32" href="favicon.ico">
18
 <link rel="manifest" href="site.webmanifest">
19
 <script src="ungap/custom-elements.js"></script>
20
21
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
22
 <script type="module" src="js/nav-tab-fixed.js"></script>
23
24
 <link rel="stylesheet" href="css/estilos.css">
25
 <link rel="stylesheet" href="css/transicion_pestanas.css">
26
 <link rel="stylesheet" href="css/md-tab.css">
27
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
28
 <link rel="stylesheet" href="css/md-filled-text-field.css">
29
 <link rel="stylesheet" href="css/md-filled-button.css">
30
 <link rel="stylesheet" href="css/md-outline-button.css">
31
32
</head>
33
34
<body>
35
36
 <form id="formulario" novalidate>
37
38
  <md-app-bar class="centered" adicional="tab">
39
40
   <h1>PWA con Formulario</h1>
41
42
  </md-app-bar>
43
44
  <nav-tab-fixed id="tab"></nav-tab-fixed>
45
46
  <section>
47
48
   <!-- Usa
49
    class="float"
50
   cuando quieras que la etiqueta esté arriba todo el tiempo. -->
51
   <p>
52
    <label class="md-filled-text-field float">
53
     <output name="respuesta">Todavía no hay respuesta</output>
54
     <span>Respuesta</span>
55
    </label>
56
   </p>
57
58
   <p>
59
    <label class="md-filled-text-field">
60
     <input name="nombre" required placeholder="Nombre*">
61
     <span>Nombre *</span>
62
     <small id="supportingNombre">Obligatorio</small>
63
    </label>
64
   </p>
65
66
   <p>
67
    <label class="md-filled-text-field">
68
     <input name="email" type="email" placeholder="Email">
69
     <span accesskey="M">Email</span>
70
     <small id="supportingEmail">Texto con formato de email</small>
71
    </label>
72
   </p>
73
74
   <p>
75
    <label class="md-filled-text-field float">
76
     <input name="fecha" type="date" placeholder="Fecha">
77
     <span>Fecha de nacimiento</span>
78
    </label>
79
   </p>
80
81
   <p>
82
    <label class="md-filled-text-field">
83
     <textarea name="dieccion" rows="3" placeholder="Dirección"></textarea>
84
     <span>Dirección</span>
85
    </label>
86
   </p>
87
88
   <p>
89
    <button id="botonSaludo" class="md-filled-button">Saludar</button>
90
    <button id="botonDatos" class="md-outline-button">Datos</button>
91
   </p>
92
93
  </section>
94
95
 </form>
96
97
 <script type="module">
98
99
  import { muestraTextoDeAyuda } from "./js/lib/muestraTextoDeAyuda.js"
100
101
  formulario.nombre.addEventListener("input", copiaMensajes)
102
  formulario.email.addEventListener("input", copiaMensajes)
103
  formulario.addEventListener("submit", procesa)
104
105
  function copiaMensajes() {
106
   muestraTextoDeAyuda(formulario.nombre, supportingNombre, "Obligatorio")
107
   muestraTextoDeAyuda(
108
    formulario.email, supportingEmail, "Texto con formato de email"
109
   )
110
  }
111
112
  /**
113
   * @param {SubmitEvent} evt
114
   */
115
  function procesa(evt) {
116
   evt.preventDefault()
117
   copiaMensajes()
118
   if (
119
    formulario.nombre.validity.valid
120
    && formulario.email.validity.valid
121
    && formulario.fecha.validity.valid
122
    && formulario.dieccion.validity.valid
123
   ) {
124
    const botonSeleccionado = evt.submitter
125
    if (botonSeleccionado === botonSaludo) {
126
     saluda()
127
    } else if (botonSeleccionado === botonDatos) {
128
     datos()
129
    }
130
   }
131
  }
132
133
  function saluda() {
134
   formulario.respuesta.value = `Hola ${formulario.nombre.value}`
135
  }
136
137
  function datos() {
138
   formulario.respuesta.value =
139
    `Nombre: ${formulario.nombre.value} Email: ${formulario.email.value}`
140
  }
141
142
 </script>
143
144
</body>
145
146
</html>

G. ayuda.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>Ayuda - PWA con Formulario</title>
9
10
 <!-- <script type="module" src="js/lib/registraServiceWorker.js"></script> -->
11
12
 <meta name="viewport" content="width=device-width">
13
 <meta name="theme-color" content="#fffbfe">
14
 <link rel="icon" sizes="32x32" href="favicon.ico">
15
 <link rel="manifest" href="site.webmanifest">
16
 <script src="ungap/custom-elements.js"></script>
17
18
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
19
 <script type="module" src="js/nav-tab-fixed.js"></script>
20
21
 <link rel="stylesheet" href="css/estilos.css">
22
 <link rel="stylesheet" href="css/transicion_pestanas.css">
23
 <link rel="stylesheet" href="css/md-headline.css">
24
 <link rel="stylesheet" href="css/md-tab.css">
25
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
26
 <link rel="stylesheet" href="css/md-list.css">
27
28
</head>
29
30
<body>
31
32
 <md-app-bar adicional="tab">
33
34
  <h1>Ayuda</h1>
35
36
 </md-app-bar>
37
38
 <nav-tab-fixed id="tab"></nav-tab-fixed>
39
40
 <section>
41
42
  <ul class="md-list">
43
   <li class="md-two-line">
44
    <span class="headline">
45
     Título
46
    </span>
47
    <span class="supporting">
48
     PWA con Material Design
49
    </span>
50
   </li>
51
   <li class="md-two-line">
52
    <span class="headline">
53
     Descripción
54
    </span>
55
    <span class="supporting">
56
     Ejemplos de vistas móviles.
57
    </span>
58
   </li>
59
   <li class="md-two-line">
60
    <span class="headline">
61
     Autor
62
    </span>
63
    <span class="supporting">
64
     Gilberto Pacheco Gallegos
65
    </span>
66
   </li>
67
   <li class="md-two-line">
68
    <span class="headline">
69
     Derechos de autor
70
    </span>
71
    <span class="supporting">
72
     © 2025 Gilberto Pacheco Gallegos
73
    </span>
74
   </li>
75
   <li class="md-three-line">
76
    <span class="headline">
77
     Este software usa la librería para PWA
78
    </span>
79
    <span class="supporting">
80
     Esta obra de Gilberto Pacheco Gallegos está bajo una
81
     <a target="_blank" rel="license noreferrer"
82
       href="http://creativecommons.org/licenses/by/4.0/">
83
      Licencia Creative Commons Atribución 4.0 Internacional</a></span>
84
   </li>
85
   <li>
86
    <a class="md-three-line" target="_blank" rel=”noreferrer”
87
      href="https://fonts.google.com/icons">
88
     <span class="headline">
89
      También usa Material Symbols
90
     </span>
91
     <span class="supporting">
92
      Desarrollada por Google bajo licencia Apache 2.0
93
     </span>
94
    </a>
95
   </li>
96
  </ul>
97
98
 </section>
99
100
 <nav-drw></nav-drw>
101
102
</body>
103
104
</html>

H. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

short_name

Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.

name

Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.

id

Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.

start_url

Ruta del archivo inicial de la app.

display

Forma de mostrar la app. El término standalone significa que no se muestra la barra de navegación del navegador web.

theme_color

Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.

background_color

Color de fondo de la pantalla desplah en dispositivos móviles.

description

Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

screenshots

Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo que muestra el navegador al instalar la app. Debes incluir al menos una con "form_factor": "wide" y otra con "form_factor": "narrow".

icons

Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.

src

Url de la imagen dentro de la app.

sizes

Dimensiones en pixeles de la imagen, anchoxalto.

type

Tipo mime de la imagen.

purpose

Forma en que se usa la imagen.

maskable

La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.

any

No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.

Normalmente debe proporcionarse un juego de íconos con purpose any y otro juego de íconos con purpose maskable.

form_factor

Orientación de una screenshot.

wide

La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.

narrow

La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.

Debes incluir al menos una screenshot con "form_factor": "wide" y otra con "form_factor": "narrow".

label

Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

1
{
2
 "short_name": "Formulario PWA",
3
 "name": "PWA con Formulario",
4
 "id": "/index.html",
5
 "start_url": "/index.html",
6
 "display": "standalone",
7
 "theme_color": "#fffbfe",
8
 "background_color": "#fffbfe",
9
 "display_override": [
10
  "window-controls-overlay"
11
 ],
12
 "description": "PWA con componentes de Material Design.",
13
 "screenshots": [
14
  {
15
   "src": "/img/screenshot_horizontal.png",
16
   "sizes": "760x384",
17
   "type": "image/png",
18
   "form_factor": "wide",
19
   "label": "PWA con Material Design"
20
  },
21
  {
22
   "src": "/img/screenshot_vertical.png",
23
   "sizes": "557x759",
24
   "type": "image/png",
25
   "form_factor": "narrow",
26
   "label": "PWA con Material Design (2)"
27
  }
28
 ],
29
 "icons": [
30
  {
31
    "purpose": "maskable",
32
    "sizes": "2730x2730",
33
    "src": "img/maskable_icon.png",
34
    "type": "image/png"
35
  },
36
  {
37
    "purpose": "maskable",
38
    "sizes": "48x48",
39
    "src": "img/maskable_icon_x48.png",
40
    "type": "image/png"
41
  },
42
  {
43
    "purpose": "maskable",
44
    "sizes": "72x72",
45
    "src": "img/maskable_icon_x72.png",
46
    "type": "image/png"
47
  },
48
  {
49
    "purpose": "maskable",
50
    "sizes": "96x96",
51
    "src": "img/maskable_icon_x96.png",
52
    "type": "image/png"
53
  },
54
  {
55
    "purpose": "maskable",
56
    "sizes": "128x128",
57
    "src": "img/maskable_icon_x128.png",
58
    "type": "image/png"
59
  },
60
  {
61
    "purpose": "maskable",
62
    "sizes": "192x192",
63
    "src": "img/maskable_icon_x192.png",
64
    "type": "image/png"
65
  },
66
  {
67
    "purpose": "maskable",
68
    "sizes": "384x384",
69
    "src": "img/maskable_icon_x384.png",
70
    "type": "image/png"
71
  },
72
  {
73
    "purpose": "maskable",
74
    "sizes": "512x512",
75
    "src": "img/maskable_icon_x512.png",
76
    "type": "image/png"
77
  },
78
  {
79
    "purpose": "any",
80
    "sizes": "2730x2730",
81
    "src": "img/maskable_icon.png",
82
    "type": "image/png"
83
  },
84
  {
85
    "purpose": "any",
86
    "sizes": "48x48",
87
    "src": "img/maskable_icon_x48.png",
88
    "type": "image/png"
89
  },
90
  {
91
    "purpose": "any",
92
    "sizes": "72x72",
93
    "src": "img/maskable_icon_x72.png",
94
    "type": "image/png"
95
  },
96
  {
97
    "purpose": "any",
98
    "sizes": "96x96",
99
    "src": "img/maskable_icon_x96.png",
100
    "type": "image/png"
101
  },
102
  {
103
    "purpose": "any",
104
    "sizes": "128x128",
105
    "src": "img/maskable_icon_x128.png",
106
    "type": "image/png"
107
  },
108
  {
109
    "purpose": "any",
110
    "sizes": "192x192",
111
    "src": "img/maskable_icon_x192.png",
112
    "type": "image/png"
113
  },
114
  {
115
    "purpose": "any",
116
    "sizes": "384x384",
117
    "src": "img/maskable_icon_x384.png",
118
    "type": "image/png"
119
  },
120
  {
121
    "purpose": "any",
122
    "sizes": "512x512",
123
    "src": "img/maskable_icon_x512.png",
124
    "type": "image/png"
125
  },
126
  {
127
   "purpose": "any",
128
   "sizes": "2048x2048",
129
   "src": "img/icono2048.png",
130
   "type": "image/png"
131
  }
132
 ]
133
}

I. sw.js

1
/* Este archivo debe estar colocado en la carpeta raíz del sitio.
2
 * 
3
 * Cualquier cambio en el contenido de este archivo hace que el service
4
 * worker se reinstale. */
5
6
/**
7
 * Cambia el número de la versión cuando cambia el contenido de los
8
 * archivos.
9
 * 
10
 * El número a la izquierda del punto (.), en este caso <q>1</q>, se
11
 * conoce como número mayor y se cambia cuando se realizan
12
 * modificaciones grandes o importantes.
13
 * 
14
 * El número a la derecha del punto (.), en este caso <q>00</q>, se
15
 * conoce como número menor y se cambia cuando se realizan
16
 * modificaciones menores.
17
 */
18
const VERSION = "1.00"
19
20
/**
21
 * Nombre de la carpeta de caché.
22
 */
23
const CACHE = "pwamd"
24
25
/**
26
 * Archivos requeridos para que la aplicación funcione fuera de
27
 * línea.
28
 */
29
const ARCHIVOS = [
30
"ayuda.html",
31
"favicon.ico",
32
"index.html",
33
"site.webmanifest",
34
"css/baseline.css",
35
"css/colors.css",
36
"css/elevation.css",
37
"css/estilos.css",
38
"css/material-symbols-outlined.css",
39
"css/md-filled-button.css",
40
"css/md-filled-text-field.css",
41
"css/md-headline.css",
42
"css/md-list.css",
43
"css/md-outline-button.css",
44
"css/md-tab.css",
45
"css/motion.css",
46
"css/palette.css",
47
"css/roboto.css",
48
"css/shape.css",
49
"css/state.css",
50
"css/transicion_pestanas.css",
51
"css/typography.css",
52
"css/theme/dark.css",
53
"css/theme/light.css",
54
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
55
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
56
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
57
"fonts/roboto-v32-latin-regular.woff2",
58
"img/icono2048.png",
59
"img/maskable_icon.png",
60
"img/maskable_icon_x128.png",
61
"img/maskable_icon_x192.png",
62
"img/maskable_icon_x384.png",
63
"img/maskable_icon_x48.png",
64
"img/maskable_icon_x512.png",
65
"img/maskable_icon_x72.png",
66
"img/maskable_icon_x96.png",
67
"img/screenshot_horizontal.png",
68
"img/screenshot_vertical.png",
69
"js/nav-tab-fixed.js",
70
"js/lib/ES_APPLE.js",
71
"js/lib/getAttribute.js",
72
"js/lib/manejaErrores.js",
73
"js/lib/muestraError.js",
74
"js/lib/muestraTextoDeAyuda.js",
75
"js/lib/ProblemDetailsError.js",
76
"js/lib/querySelector.js",
77
"js/lib/registraServiceWorker.js",
78
"js/lib/resaltaSiEstasEn.js",
79
"js/lib/custom/md-app-bar.js",
80
"ungap/custom-elements.js",
81
 "/"
82
]
83
84
// Verifica si el código corre dentro de un service worker.
85
if (self instanceof ServiceWorkerGlobalScope) {
86
 // Evento al empezar a instalar el servide worker,
87
 self.addEventListener("install",
88
  (/** @type {ExtendableEvent} */ evt) => {
89
   console.log("El service worker se está instalando.")
90
   evt.waitUntil(llenaElCache())
91
  })
92
93
 // Evento al solicitar información a la red.
94
 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
95
  if (evt.request.method === "GET") {
96
   evt.respondWith(buscaLaRespuestaEnElCache(evt))
97
  }
98
 })
99
100
 // Evento cuando el service worker se vuelve activo.
101
 self.addEventListener("activate",
102
  () => console.log("El service worker está activo."))
103
}
104
105
async function llenaElCache() {
106
 console.log("Intentando cargar caché:", CACHE)
107
 // Borra todos los cachés.
108
 const keys = await caches.keys()
109
 for (const key of keys) {
110
  await caches.delete(key)
111
 }
112
 // Abre el caché de este service worker.
113
 const cache = await caches.open(CACHE)
114
 // Carga el listado de ARCHIVOS.
115
 await cache.addAll(ARCHIVOS)
116
 console.log("Cache cargado:", CACHE)
117
 console.log("Versión:", VERSION)
118
}
119
120
/** @param {FetchEvent} evt */
121
async function buscaLaRespuestaEnElCache(evt) {
122
 // Abre el caché.
123
 const cache = await caches.open(CACHE)
124
 const request = evt.request
125
 /* Busca la respuesta a la solicitud en el contenido del caché, sin
126
  * tomar en cuenta la parte después del símbolo "?" en la URL. */
127
 const response = await cache.match(request, { ignoreSearch: true })
128
 if (response === undefined) {
129
  /* Si no la encuentra, empieza a descargar de la red y devuelve
130
   * la promesa. */
131
  return fetch(request)
132
 } else {
133
  // Si la encuentra, devuelve la respuesta encontrada en el caché.
134
  return response
135
 }
136
}

J. Carpeta « css »

A. css / baseline.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
@import url(palette.css);
18
@import url(typography.css);
19
@import url(colors.css);
20
@import url(shape.css);
21
@import url(motion.css);
22
@import url(state.css);
23
@import url(elevation.css);
24
@import url(theme/light.css) screen and (prefers-color-scheme: light);
25
@import url(theme/dark.css) screen and (prefers-color-scheme: dark);
26

B. css / colors.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
.primary {
18
  color: var(--md-sys-color-on-primary);
19
  background-color: var(--md-sys-color-primary);
20
}
21
.on-primary {
22
  color: var(--md-sys-color-primary);
23
  background-color: var(--md-sys-color-on-primary);
24
}
25
.primary-container {
26
  color: var(--md-sys-color-on-primary-container);
27
  background-color: var(--md-sys-color-primary-container);
28
}
29
.on-primary-container {
30
  color: var(--md-sys-color-primary-container);
31
  background-color: var(--md-sys-color-on-primary-container);
32
}
33
.secondary {
34
  color: var(--md-sys-color-on-secondary);
35
  background-color: var(--md-sys-color-secondary);
36
}
37
.on-secondary {
38
  color: var(--md-sys-color-secondary);
39
  background-color: var(--md-sys-color-on-secondary);
40
}
41
.secondary-container {
42
  color: var(--md-sys-color-on-secondary-container);
43
  background-color: var(--md-sys-color-secondary-container);
44
}
45
.on-secondary-container {
46
  color: var(--md-sys-color-secondary-container);
47
  background-color: var(--md-sys-color-on-secondary-container);
48
}
49
.tertiary {
50
  color: var(--md-sys-color-on-tertiary);
51
  background-color: var(--md-sys-color-tertiary);
52
}
53
.on-tertiary {
54
  color: var(--md-sys-color-tertiary);
55
  background-color: var(--md-sys-color-on-tertiary);
56
}
57
.tertiary-container {
58
  color: var(--md-sys-color-on-tertiary-container);
59
  background-color: var(--md-sys-color-tertiary-container);
60
}
61
.on-tertiary-container {
62
  color: var(--md-sys-color-tertiary-container);
63
  background-color: var(--md-sys-color-on-tertiary-container);
64
}
65
.background {
66
  color: var(--md-sys-color-on-background);
67
  background-color: var(--md-sys-color-background);
68
}
69
.surface {
70
  color: var(--md-sys-color-on-surface);
71
  background-color: var(--md-sys-color-surface);
72
}
73
.surface-variant {
74
  color: var(--md-sys-color-on-surface-variant);
75
  background-color: var(--md-sys-color-surface-variant);
76
}
77
.on-surface-variant {
78
  color: var(--md-sys-color-surface-variant);
79
  background-color: var(--md-sys-color-on-surface-variant);
80
}
81
.outline {
82
  border: 1px solid var(--md-sys-color-outline);
83
}
84
.inverse-surface {
85
  color: var(--md-sys-color-on-inverse-surface);
86
  background-color: var(--md-sys-color-inverse-surface);
87
}
88
.on-inverse-surface {
89
  color: var(--md-sys-color-inverse-surface);
90
  background-color: var(--md-sys-color-on-inverse-surface);
91
}
92
.inverse-primary {
93
  color: var(--md-sys-color-on-inverse-primary);
94
  background-color: var(--md-sys-color-inverse-primary);
95
}
96
.on-inverse-primary {
97
  color: var(--md-sys-color-inverse-primary);
98
  background-color: var(--md-sys-color-on-inverse-primary);
99
}
100
.surface-tint {
101
  background-color: var(--md-sys-color-on-surface-tint);
102
}
103
.error {
104
  color: var(--md-sys-color-on-error);
105
  background-color: var(--md-sys-color-error);
106
}
107
.on-error {
108
  color: var(--md-sys-color-error);
109
  background-color: var(--md-sys-color-on-error);
110
}
111
.error-container {
112
  color: var(--md-sys-color-on-error-container);
113
  background-color: var(--md-sys-color-error-container);
114
}
115
.on-error-container {
116
  color: var(--md-sys-color-error-container);
117
  background-color: var(--md-sys-color-on-error-container);
118
}
119
.black {
120
  background-color: var(--md-ref-palette-black);
121
}
122
.black-text {
123
  color: var(--md-ref-palette-black);
124
}
125
.white {
126
  background-color: var(--md-ref-palette-white);
127
}
128
.white-text {
129
  color: var(--md-ref-palette-white);
130
}
131

C. css / elevation.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Surface tint color */
19
  --md-sys-elevation-surface-tint-color: var(--md-sys-color-primary);
20
  /* +5 */
21
  --md-sys-elevation-level5-value: 12px;
22
  --md-sys-elevation-level5-unit: 1px;
23
  --md-sys-elevation-level5: 12px;
24
  /* +4 */
25
  --md-sys-elevation-level4-value: 8px;
26
  --md-sys-elevation-level4-unit: 1px;
27
  --md-sys-elevation-level4: 8px;
28
  /* +3 */
29
  --md-sys-elevation-level3-value: 6px;
30
  --md-sys-elevation-level3-unit: 1px;
31
  --md-sys-elevation-level3: 6px;
32
  /* +2 */
33
  --md-sys-elevation-level2-value: 3px;
34
  --md-sys-elevation-level2-unit: 1px;
35
  --md-sys-elevation-level2: 3px;
36
  /* +1 */
37
  --md-sys-elevation-level1-value: 1px;
38
  --md-sys-elevation-level1-unit: 1px;
39
  --md-sys-elevation-level1: 1px;
40
  /* 0 */
41
  --md-sys-elevation-level0-value: 0px;
42
  --md-sys-elevation-level0-unit: 1px;
43
  --md-sys-elevation-level0: 0px;
44
}
45
.elevation-0 {
46
  box-shadow: var(--md-sys-elevation-level0);
47
}
48
.elevation-1 {
49
  box-shadow: var(--md-sys-elevation-level1);
50
}
51
.elevation-2 {
52
  box-shadow: var(--md-sys-elevation-level2);
53
}
54
.elevation-3 {
55
  box-shadow: var(--md-sys-elevation-level3);
56
}
57
.elevation-4 {
58
  box-shadow: var(--md-sys-elevation-level4);
59
}
60
.elevation-5 {
61
  box-shadow: var(--md-sys-elevation-level5);
62
}
63

D. css / estilos.css

1
/* Definiciones para Material Design 3  */
2
@import url(baseline.css);
3
/* Fonts utilizados */
4
@import url(roboto.css);
5
6
html {
7
 /* Indica los temas del sistema operativo que son soportados. */
8
 color-scheme: light dark;
9
 --tabWidth: 3.75rem;
10
 --anchoNav: 22.5rem;
11
}
12
13
body>section,
14
form>section {
15
 max-width: 600px;
16
 margin-left: auto;
17
 margin-right: auto;
18
}
19
20
/* Quita un borde rojo que coloca Firefox. */
21
:-moz-ui-invalid {
22
 box-shadow: none;
23
}
24
25
body {
26
 margin: 0;
27
 font-family: var(--md-sys-typescale-body-large-font);
28
 font-weight: var(--md-sys-typescale-body-large-weight);
29
 font-size: var(--md-sys-typescale-body-large-size);
30
 font-style: var(--md-sys-typescale-body-large-font-style);
31
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
32
 line-height: var(--md-sys-typescale-body-large-line-height);
33
 text-transform: var(--md-sys-typescale-body-large-text-transform);
34
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
35
 color: var(--md-sys-color-on-background);
36
 background-color: var(--md-sys-color-background);
37
 /* Las siguientes líneas Evita los cambios de apariencia al cargar estilos y
38
 + custom elements, que son conocidos como Flash Of Unstyled Content (fouc). */
39
 opacity: 0;
40
 animation-name: fouc;
41
 animation-fill-mode: forwards;
42
 animation-duration: 1.5s;
43
}
44
45
@keyframes fouc {
46
 to {
47
  opacity: 1;
48
 }
49
}
50
51
html {
52
 --Font: -apple-system, BlinkMacSystemFont, roboto, sans-serif;
53
 --colIntIos: white;
54
 --colIntIosOnBk: #2acc2a;
55
 --colIntIosOnBkFc: #1bbb1b;
56
 --colIntIosOffBk: #dbdbdb;
57
 --colIntIosOffBkFc: #BDBDBD;
58
 /* Plain typeface */
59
 --md-ref-typeface-plain: var(--Font);
60
 /* Brand typeface */
61
 --md-ref-typeface-brand: var(--Font);
62
 --md-sys-typescale-label-large-weight-prominent:
63
  var(--md-ref-typeface-weight-bold);
64
 --md-box_shadow_level4:
65
  0 var(--md-sys-elevation-level4) var(--md-sys-elevation-level4) var(--md-sys-color-shadow);
66
 --md-box_shadow_level3:
67
  0 var(--md-sys-elevation-level3) var(--md-sys-elevation-level3) var(--md-sys-color-shadow);
68
 --md-box_shadow_level2:
69
  0 var(--md-sys-elevation-level2) var(--md-sys-elevation-level2) var(--md-sys-color-shadow);
70
 --md-box_shadow_level1:
71
  0 var(--md-sys-elevation-level1) var(--md-sys-elevation-level1) var(--md-sys-color-shadow);
72
 --md-box_shadow_level0: none;
73
 --iconSize: 1.5rem;
74
 --avatarSize: 2.5rem;
75
 --imageSize: 3.5rem;
76
 --videoWidth: 7.125rem;
77
 --videoHeight: 4rem;
78
 --scroll-headline-duracion: 2s;
79
 --md-sys-state-focus-indicator-outer-offset: 0.125rem;
80
 --md-sys-state-focus-indicator-thickness: 0.1875rem;
81
 /* Pressed state layer opacity */
82
 --state-pressed-transparency-percentage: 84%;
83
 /* Focus state layer opacity */
84
 --state-focus-transparency-percentage: 88%;
85
 /* Hover state layer opacity */
86
 --state-hover-transparency-percentage: 92%;
87
}
88
89
p {
90
 margin: 1rem;
91
}
92
93
a {
94
 color: var(--md-sys-color-on-background);
95
}
96
97
@media (prefers-color-scheme: light) {
98
 html {
99
  --md-riple-color: #00000020;
100
 }
101
}
102
103
@media (prefers-color-scheme: dark) {
104
 html {
105
  --md-riple-color: #ffffff40;
106
 }
107
}
108
109
@keyframes md-ripple {
110
111
 from {
112
  background-size: 100%;
113
 }
114
115
 to {
116
  background-size: 15000%;
117
 }
118
119
}
120
121
@keyframes salePorLaIzquierda {
122
 to {
123
  translate: -100vw 0;
124
 }
125
}
126
127
@keyframes entraPorLaDerecha {
128
 from {
129
  translate: 100vw 0;
130
 }
131
}
132
133
@keyframes aparece {
134
 from {
135
  opacity: 0;
136
 }
137
}
138
139
@keyframes desvanece {
140
 to {
141
  opacity: 0;
142
 }
143
}

E. css / material-symbols-outlined.css

1
@font-face {
2
 font-family: 'Material Symbols Outlined';
3
 font-style: normal;
4
 src:
5
  url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].woff2) format('woff2'),
6
  url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].ttf) format('truetype');
7
}
8
9
.material-symbols-outlined {
10
 font-family: 'Material Symbols Outlined';
11
 font-weight: normal;
12
 font-style: normal;
13
 font-size: 1.5rem;
14
 width: 1.5rem;
15
 height: 1.5rem;
16
 /* Preferred icon size */
17
 display: inline-block;
18
 line-height: 1;
19
 text-transform: none;
20
 letter-spacing: normal;
21
 word-wrap: normal;
22
 white-space: nowrap;
23
 direction: ltr;
24
}

F. css / md-filled-button.css

1
/* container */
2
.md-filled-button::before {
3
 content: "";
4
 position: absolute;
5
 z-index: -2;
6
 top: 0;
7
 right: 0;
8
 left: 0;
9
 bottom: 0;
10
 background-color: var(--md-sys-color-primary);
11
}
12
13
/* state layer */
14
.md-filled-button::after {
15
 content: "";
16
 position: absolute;
17
 z-index: -1;
18
 top: 0;
19
 right: 0;
20
 left: 0;
21
 bottom: 0;
22
 background-color: transparent;
23
}
24
25
/* label, shape */
26
.md-filled-button {
27
 position: relative;
28
 box-sizing: border-box;
29
 border-radius: 1.25rem;
30
 height: 2.5rem;
31
 line-height: 2.5rem;
32
 padding: 0 1.5rem;
33
 border: none;
34
 background-color: transparent;
35
 box-shadow: var(--md-box_shadow_level0);
36
 font-family: var(--md-sys-typescale-label-large-font);
37
 font-weight: var(--md-sys-typescale-label-large-weight);
38
 font-size: var(--md-sys-typescale-label-large-size);
39
 font-style: var(--md-sys-typescale-label-large-font-style);
40
 letter-spacing: var(--md-sys-typescale-label-large-tracking);
41
 text-transform: var(--md-sys-typescale-label-large-text-transform);
42
 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
43
 color: var(--md-sys-color-on-primary);
44
 white-space: nowrap;
45
 text-overflow: ellipsis;
46
 overflow: hidden;
47
}
48
49
/* label, shape */
50
.md-filled-button:hover {
51
 color: var(--md-sys-color-on-primary);
52
 box-shadow: var(--md-box_shadow_level1);
53
}
54
55
/* state layer */
56
.md-filled-button:hover::after {
57
 background-color: var(--md-sys-color-on-primary);
58
 opacity: var(--md-sys-state-hover-state-layer-opacity);
59
}
60
61
/* label, shape */
62
.md-filled-button:focus {
63
 outline: none;
64
 color: var(--md-sys-color-on-primary);
65
 box-shadow: var(--md-box_shadow_level0) !important;
66
}
67
68
/* state layer */
69
.md-filled-button:focus::after {
70
 background-color: var(--md-sys-color-on-primary);
71
 opacity: var(--md-sys-state-focus-state-layer-opacity);
72
}
73
74
/* label, shape */
75
.md-filled-button:active {
76
 color: var(--md-sys-color-on-primary);
77
 background-position: center;
78
 background-image:
79
  radial-gradient(circle, var(--md-sys-color-on-primary-container) 1%, transparent 1%);
80
 background-size: 100%;
81
 animation-name: md-ripple;
82
 animation-duration: var(--md-sys-motion-duration-500);
83
 box-shadow: var(--md-box_shadow_level0) !important;
84
}
85
86
/* state layer */
87
.md-filled-button:active::after {
88
 background-color: var(--md-sys-color-on-primary);
89
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
90
}
91
92
/* label, shape */
93
.md-filled-button:disabled {
94
 background-color: transparent !important;
95
 color: var(--md-sys-color-on-surface) !important;
96
 opacity: 0.38 !important;
97
 box-shadow: var(--md-box_shadow_level0) !important;
98
}
99
100
/* container */
101
.md-filled-button:disabled::before {
102
 background-color: var(--md-sys-color-on-surface) !important;
103
 opacity: 0.12 !important;
104
}
105
106
/* state layer */
107
.md-filled-button:disabled::after {
108
 background-color: transparent !important;
109
 opacity: 1 !important;
110
}

G. css / md-filled-text-field.css

1
.md-filled-text-field {
2
 position: relative;
3
 overflow: hidden;
4
 display: flex;
5
 flex-direction: column;
6
 align-items: stretch;
7
 padding-top: calc(0.5rem + var(--md-sys-typescale-body-small-line-height));
8
 border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
9
 border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
10
 overflow: hidden;
11
}
12
13
/* container */
14
.md-filled-text-field::before {
15
 content: "";
16
 position: absolute;
17
 z-index: -2;
18
 top: 0;
19
 right: 0;
20
 left: 0;
21
 bottom: 0;
22
 background-color: var(--md-sys-color-surface-container-highest);
23
}
24
25
/* state layer */
26
.md-filled-text-field::after {
27
 content: "";
28
 position: absolute;
29
 z-index: -1;
30
 top: 0;
31
 right: 0;
32
 left: 0;
33
 bottom: 0;
34
 background-color: transparent;
35
}
36
37
.md-filled-text-field span,
38
.md-filled-text-field label {
39
 position: absolute;
40
 top: 0.5rem;
41
 left: 1rem;
42
 right: 1rem;
43
 display: block;
44
 transform: translateY(1rem);
45
 transition-property: transform;
46
 transition-duration: var(--md-sys-motion-duration-300);
47
 white-space: nowrap;
48
 text-overflow: ellipsis;
49
 overflow: hidden;
50
 color: var(--md-sys-color-on-surface-variant);
51
 font-family: var(--md-sys-typescale-body-large-font);
52
 font-weight: var(--md-sys-typescale-body-large-weight);
53
 font-size: var(--md-sys-typescale-body-large-size);
54
 font-style: var(--md-sys-typescale-body-large-font-style);
55
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
56
 line-height: var(--md-sys-typescale-body-large-line-height);
57
 text-transform: var(--md-sys-typescale-body-large-text-transform);
58
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
59
}
60
61
.md-filled-text-field :not(:placeholder-shown)+span,
62
.md-filled-text-field :not(:placeholder-shown)+label,
63
.md-filled-text-field .populated+span,
64
.md-filled-text-field .populated+label,
65
.md-filled-text-field:focus-within span,
66
.md-filled-text-field:focus-within label,
67
.md-filled-text-field.float span,
68
.md-filled-text-field.float label {
69
 transform: translateY(0);
70
 font-family: var(--md-sys-typescale-body-small-font);
71
 font-weight: var(--md-sys-typescale-body-small-weight);
72
 font-size: var(--md-sys-typescale-body-small-size);
73
 font-style: var(--md-sys-typescale-body-small-font-style);
74
 letter-spacing: var(--md-sys-typescale-body-small-tracking);
75
 line-height: var(--md-sys-typescale-body-small-line-height);
76
 text-transform: var(--md-sys-typescale-body-small-text-transform);
77
 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
78
}
79
80
.md-filled-text-field :not(label, span, small) {
81
 position: relative;
82
 caret-color: var(--md-sys-color-primary);
83
 min-height: 2rem;
84
 box-sizing: border-box;
85
 padding-left: 1rem;
86
 padding-bottom: 0.5rem;
87
 padding-right: 1rem;
88
 border: none;
89
 resize: none;
90
 color: var(--md-sys-color-on-surface);
91
 font-family: var(--md-sys-typescale-body-large-font);
92
 font-weight: var(--md-sys-typescale-body-large-weight);
93
 font-size: var(--md-sys-typescale-body-large-size);
94
 font-style: var(--md-sys-typescale-body-large-font-style);
95
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
96
 line-height: var(--md-sys-typescale-body-large-line-height);
97
 text-transform: var(--md-sys-typescale-body-large-text-transform);
98
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
99
 background-color: transparent;
100
 outline: none;
101
 border-bottom-width: 0.0625rem;
102
 border-bottom-style: solid;
103
 border-bottom-color: var(--md-sys-color-on-surface-variant);
104
}
105
106
.md-filled-text-field ::placeholder {
107
 color: transparent;
108
}
109
110
.md-filled-text-field small {
111
 display: block;
112
 color: var(--md-sys-color-on-surface-variant);
113
 background-color: var(--md-sys-color-background);
114
 font-family: var(--md-sys-typescale-body-small-font);
115
 font-weight: var(--md-sys-typescale-body-small-weight);
116
 font-size: var(--md-sys-typescale-body-small-size);
117
 font-style: var(--md-sys-typescale-body-small-font-style);
118
 letter-spacing: var(--md-sys-typescale-body-small-tracking);
119
 line-height: var(--md-sys-typescale-body-small-line-height);
120
 text-transform: var(--md-sys-typescale-body-small-text-transform);
121
 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
122
 padding: 0.25rem 1rem 0 1rem;
123
 white-space: nowrap;
124
 text-overflow: ellipsis;
125
 overflow: hidden;
126
}
127
128
.md-filled-text-field:hover span,
129
.md-filled-text-field:hover label {
130
 color: var(--md-sys-color-on-surface-variant);
131
}
132
133
.md-filled-text-field:hover :not(label, span, small) {
134
 padding-bottom: 0.5rem;
135
 border-bottom-width: 0.0625rem;
136
 border-bottom-color: var(--md-sys-color-on-surface);
137
}
138
139
.md-filled-text-field:hover::after {
140
 background-color: var(--md-sys-color-on-surface);
141
 opacity: var(--md-sys-state-hover-state-layer-opacity);
142
}
143
144
.md-filled-text-field:hover small {
145
 color: var(--md-sys-color-on-surface-variant);
146
}
147
148
.md-filled-text-field:focus-within span,
149
.md-filled-text-field:focus-within label {
150
 color: var(--md-sys-color-primary);
151
}
152
153
.md-filled-text-field :focus {
154
 color: var(--md-sys-color-on-surface);
155
 outline: none;
156
 padding-bottom: 0.4375rem;
157
 border-bottom-width: 0.125rem;
158
 border-bottom-color: var(--md-sys-color-primary);
159
}
160
161
.md-filled-text-field:hover :focus {
162
 padding-bottom: 0.4375rem;
163
 border-bottom-width: 0.125rem;
164
}
165
166
.md-filled-text-field:focus-within small {
167
 color: var(--md-sys-color-on-surface-variant);
168
}
169
170
.md-filled-text-field :invalid {
171
 color: var(--md-sys-color-on-surface);
172
 border-bottom-color: var(--md-sys-color-error);
173
}
174
175
.md-filled-text-field :invalid+span,
176
.md-filled-text-field :invalid+label {
177
 color: var(--md-sys-color-error);
178
}
179
180
.md-filled-text-field :invalid~small,
181
.md-filled-text-field:hover :invalid~small,
182
.md-filled-text-field:focus-within .input-text:invalid~small {
183
 color: var(--md-sys-color-error);
184
}
185
186
.md-filled-text-field :invalid:focus {
187
 caret-color: var(--md-sys-color-error);
188
 border-bottom-color: var(--md-sys-color-error);
189
}
190
191
.md-filled-text-field:hover :invalid {
192
 color: var(--md-sys-color-on-surface);
193
 border-bottom-color: var(--md-sys-color-error);
194
}

H. css / md-headline.css

1
.md-headline {
2
 box-sizing: border-box;
3
 margin: 0;
4
 color: var(--md-sys-color-on-surface);
5
 background-color: var(--md-sys-color-surface);
6
 transition-property: color;
7
 transition-duration: var(--md-sys-motion-duration-1000);
8
 transition-timing-function: ease-in;
9
}
10
11
.md-headline.scroll-adicional {
12
 color: var(--md-sys-color-surface-container-low);
13
 background-color: var(--md-sys-color-surface-container-low);
14
}
15
16
.md-headline.scroll {
17
 color: var(--md-sys-color-surface);
18
}
19
20
.md-headline.headline-small {
21
 padding: 0 1rem 1.5rem 1rem;
22
}
23
24
.md-headline.headline-medium {
25
 padding: 0 1rem 1.75rem 1rem;
26
}
27
28
md-app-bar.centered~.md-headline {
29
 text-align: center;
30
}
31
32
md-app-bar[headline] h1 {
33
 opacity: 0;
34
 transition-property: opacity;
35
 transition-duration: var(--md-sys-motion-duration-1000);
36
}
37
38
md-app-bar[headline].scroll h1 {
39
 opacity: 1;
40
 transition-timing-function: ease-in;
41
}

I. css / md-list.css

1
.md-list {
2
 margin: 0.5rem 0;
3
 padding: 0;
4
 list-style-type: none;
5
}
6
7
.md-list .md-one-line,
8
.md-list .md-two-line,
9
.md-list .md-three-line {
10
 position: relative;
11
 display: flex;
12
 box-sizing: border-box;
13
}
14
15
/* container */
16
.md-list .md-one-line::before,
17
.md-list .md-two-line::before,
18
.md-list .md-three-line::before {
19
 content: "";
20
 position: absolute;
21
 z-index: -2;
22
 top: 0;
23
 right: 0;
24
 left: 0;
25
 bottom: 0;
26
 background-color: var(--md-sys-color-surface);
27
}
28
29
/* state layer */
30
.md-list .md-one-line::after,
31
.md-list .md-two-line::after,
32
.md-list .md-three-line::after {
33
 content: "";
34
 position: absolute;
35
 z-index: -1;
36
 top: 0;
37
 right: 0;
38
 left: 0;
39
 bottom: 0;
40
 background-color: transparent;
41
}
42
43
.md-list .md-one-line {
44
 align-items: center;
45
 gap: 1rem;
46
 min-height: 3.5rem;
47
 padding: 0.5rem 1.5rem 0.5rem 1rem;
48
}
49
50
.md-list .md-one-line.video,
51
.md-list .md-two-line.video {
52
 padding: 0.75rem 1.5rem 0.75rem 0;
53
}
54
55
.md-list .md-two-line,
56
.md-list .md-three-line {
57
 flex-flow: column;
58
}
59
60
.md-list .md-two-line {
61
 justify-content: center;
62
 min-height: 4.5rem;
63
 padding: 0.5rem 1.5rem 0.5rem 1rem;
64
}
65
66
.md-list .md-two-line.icon,
67
.md-list .md-two-line.avatar,
68
.md-list .md-two-line.image,
69
.md-list .md-two-line.video,
70
.md-list .md-three-line.icon,
71
.md-list .md-three-line.avatar,
72
.md-list .md-three-line.image,
73
.md-list .md-three-line.video {
74
 display: grid;
75
 column-gap: 1rem;
76
 row-gap: 0;
77
 grid-template-areas:
78
  "img headline"
79
  "img supporting";
80
}
81
82
.md-list .md-two-line.icon,
83
.md-list .md-two-line.avatar,
84
.md-list .md-two-line.image,
85
.md-list .md-two-line.video {
86
 align-content: center;
87
 grid-template-rows: 1fr 1fr;
88
}
89
90
.md-list .md-two-line.icon,
91
.md-list .md-three-line.icon {
92
 grid-template-columns: var(--iconSize) 1fr;
93
}
94
95
.md-list .md-two-line.avatar,
96
.md-list .md-three-line.avatar {
97
 grid-template-columns: var(--avatarSize) 1fr;
98
}
99
100
.md-list .md-two-line.image,
101
.md-list .md-three-line.image {
102
 grid-template-columns: var(--imageSize) 1fr;
103
}
104
105
.md-list .md-two-line.video,
106
.md-list .md-three-line.video {
107
 grid-template-columns: var(--videoWidth) 1fr;
108
}
109
110
.md-list .md-three-line {
111
 align-content: flex-start;
112
 min-height: 5.5rem;
113
 padding: 0.75rem 1.5rem 0.75rem 1rem;
114
}
115
116
.md-list .md-three-line.video {
117
 padding: 0.75rem 1.5rem 0.75rem 0;
118
}
119
120
.md-list .md-three-line.icon,
121
.md-list .md-three-line.avatar,
122
.md-list .md-three-line.image,
123
.md-list .md-three-line.video {
124
 align-content: start;
125
 grid-template-rows: var(--md-sys-typescale-label-large-line-height) 1fr;
126
}
127
128
/* state layer */
129
.md-list .md-one-line:hover::after,
130
.md-list .md-two-line:hover::after,
131
.md-list .md-three-line:hover::after {
132
 background-color: var(--md-sys-color-on-surface);
133
 opacity: var(--md-sys-state-hover-state-layer-opacity);
134
}
135
136
/* state layer */
137
.md-list a.md-one-line:focus::after,
138
.md-list a.md-two-line:focus::after,
139
.md-list a.md-three-line:focus::after,
140
.md-list a.md-one-line:focus-visible::after,
141
.md-list a.md-two-line:focus-visible::after,
142
.md-list a.md-three-line:focus-visible::after {
143
 background-color: var(--md-sys-color-on-surface);
144
 opacity: var(--md-sys-state-focus-state-layer-opacity);
145
}
146
147
.md-list a:focus,
148
.md-list a:focus-visible {
149
 outline: none;
150
}
151
152
.md-list a:active {
153
 background-position: center;
154
 background-image:
155
  radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
156
 background-size: 100%;
157
 animation-name: md-ripple;
158
 animation-duration: var(--md-sys-motion-duration-500);
159
 box-shadow: var(--md-box_shadow_level0) !important;
160
}
161
162
/* state layer */
163
.md-list a.md-one-line:active::after,
164
.md-list a.md-two-line:active::after,
165
.md-list a.md-three-line:active::after {
166
 background-color: var(--md-sys-color-on-surface);
167
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
168
}
169
170
.md-list a.md-two-line,
171
.md-list a.md-three-line {
172
 text-decoration: none;
173
}
174
175
.md-list a.md-two-line .headline,
176
.md-list a.md-three-line .headline {
177
 text-decoration: underline;
178
}
179
180
.md-list .headline {
181
 grid-area: headline;
182
 display: block;
183
 box-sizing: border-box;
184
 color: var(--md-sys-color-on-surface);
185
 font-family: var(--md-sys-typescale-body-large-font);
186
 font-weight: var(--md-sys-typescale-body-large-weight);
187
 font-size: var(--md-sys-typescale-body-large-size);
188
 font-style: var(--md-sys-typescale-body-large-font-style);
189
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
190
 line-height: var(--md-sys-typescale-body-large-line-height);
191
 text-transform: var(--md-sys-typescale-body-large-text-transform);
192
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
193
 max-height: var(--md-sys-typescale-body-large-line-height);
194
 white-space: nowrap;
195
 text-overflow: ellipsis;
196
 overflow: hidden;
197
}
198
199
.md-list .md-two-line.icon .headline,
200
.md-list .md-two-line.avatar .headline,
201
.md-list .md-two-line.image .headline,
202
.md-list .md-two-line.video .headline,
203
.md-list .md-three-line.icon .headline,
204
.md-list .md-three-line.avatar .headline,
205
.md-list .md-three-line.image .headline,
206
.md-list .md-three-line.video .headline {
207
 align-self: end;
208
}
209
210
.md-list .supporting {
211
 grid-area: supporting;
212
 display: -webkit-box;
213
 -webkit-box-orient: vertical;
214
 overflow: hidden;
215
 box-sizing: border-box;
216
 align-self: start;
217
 font-family: var(--md-sys-typescale-body-medium-font);
218
 font-weight: var(--md-sys-typescale-body-medium-weight);
219
 font-size: var(--md-sys-typescale-body-medium-size);
220
 font-style: var(--md-sys-typescale-body-medium-font-style);
221
 letter-spacing: var(--md-sys-typescale-body-medium-tracking);
222
 line-height: var(--md-sys-typescale-body-medium-line-height);
223
 text-transform: var(--md-sys-typescale-body-medium-text-transform);
224
 text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
225
}
226
227
.md-list .md-two-line .supporting {
228
 max-height: var(--md-sys-typescale-body-medium-line-height);
229
 line-clamp: 1;
230
 -webkit-line-clamp: 1;
231
}
232
233
.md-list .md-three-line .supporting {
234
 max-height: calc(2 * var(--md-sys-typescale-body-medium-line-height));
235
 line-clamp: 2;
236
 -webkit-line-clamp: 2;
237
}
238
239
.md-list .avatar img,
240
.md-list .avatar label,
241
.md-list .avatar .material-symbols-outlined:first-child {
242
 flex-shrink: 0;
243
 background-color: var(--md-sys-color-primary-container);
244
 color: var(--md-sys-color-on-primary-container);
245
 border-radius: 50%;
246
 width: var(--avatarSize);
247
 height: var(--avatarSize);
248
}
249
250
.md-list .avatar label {
251
 display: inline-block;
252
 font-family: var(--md-sys-typescale-title-medium-font);
253
 font-weight: var(--md-sys-typescale-title-medium-weight);
254
 font-size: var(--md-sys-typescale-title-medium-size);
255
 font-style: var(--md-sys-typescale-title-medium-font-style);
256
 letter-spacing: var(--md-sys-typescale-title-medium-tracking);
257
 line-height: var(--md-sys-typescale-title-medium-line-height);
258
 text-transform: var(--md-sys-typescale-title-medium-text-transform);
259
 text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
260
 overflow: hidden;
261
}
262
263
.md-list .avatar .material-symbols-outlined:first-child {
264
 font-size: var(--avatarSize);
265
}
266
267
.md-list .avatar.md-two-line img,
268
.md-list .avatar.md-two-line label,
269
.md-list .avatar.md-two-line .material-symbols-outlined:first-child {
270
 grid-area: img;
271
 align-self: center;
272
}
273
274
.md-list .avatar.md-three-line img,
275
.md-list .avatar.md-three-line label,
276
.md-list .avatar.md-three-line .material-symbols-outlined:first-child {
277
 grid-area: img;
278
 align-self: start;
279
}
280
281
.md-list .icon img,
282
.md-list .icon .material-symbols-outlined:first-child {
283
 flex-shrink: 0;
284
 color: var(--md-sys-color-on-surface-variant);
285
 width: var(--iconSize);
286
 height: var(--iconSize);
287
}
288
289
.md-list .icon .material-symbols-outlined:first-child {
290
 font-size: var(--iconSize);
291
}
292
293
.md-list .icon.md-two-line img,
294
.md-list .icon.md-two-line .material-symbols-outlined:first-child {
295
 grid-area: img;
296
 align-self: center;
297
}
298
299
.md-list .icon.md-three-line img,
300
.md-list .icon.md-three-line .material-symbols-outlined:first-child {
301
 grid-area: img;
302
 align-self: start;
303
}
304
305
.md-list .video img {
306
 flex-shrink: 0;
307
 color: var(--md-sys-color-on-surface-variant);
308
 width: var(--videoWidth);
309
 height: var(--videoHeight);
310
}
311
312
.md-list .video.md-two-line img {
313
 grid-area: img;
314
 align-self: center;
315
}
316
317
.md-list .video.md-three-line img {
318
 grid-area: img;
319
 align-self: start;
320
}
321
322
.md-list .image img,
323
.md-list .image .material-symbols-outlined:first-child {
324
 flex-shrink: 0;
325
 color: var(--md-sys-color-on-surface-variant);
326
 width: var(--imageSize);
327
 height: var(--imageSize);
328
}
329
330
.md-list .image .material-symbols-outlined:first-child {
331
 font-size: var(--imageSize);
332
}
333
334
.md-list .image.md-two-line img,
335
.md-list .image.md-two-line .material-symbols-outlined:first-child {
336
 grid-area: img;
337
 align-self: center;
338
}
339
340
.md-list .image.md-three-line img,
341
.md-list .image.md-three-line .material-symbols-outlined:first-child {
342
 grid-area: img;
343
 align-self: start;
344
}

J. css / md-outline-button.css

1
.md-outline-button {
2
 position: relative;
3
 box-sizing: border-box;
4
 border-radius: 1.25rem;
5
 height: 2.5rem;
6
 padding: 0 1.5rem;
7
 border: 0.0625rem solid var(--md-sys-color-outline);
8
 background-color: transparent;
9
 box-shadow: var(--md-box_shadow_level0);
10
 font-family: var(--md-sys-typescale-label-large-font);
11
 font-weight: var(--md-sys-typescale-label-large-weight);
12
 font-size: var(--md-sys-typescale-label-large-size);
13
 font-style: var(--md-sys-typescale-label-large-font-style);
14
 letter-spacing: var(--md-sys-typescale-label-large-tracking);
15
 text-transform: var(--md-sys-typescale-label-large-text-transform);
16
 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
17
 color: var(--md-sys-color-primary);
18
 white-space: nowrap;
19
 text-overflow: ellipsis;
20
 overflow: hidden;
21
}
22
23
/* state layer */
24
.md-outline-button::after {
25
 content: "";
26
 position: absolute;
27
 z-index: -1;
28
 top: 0;
29
 right: 0;
30
 left: 0;
31
 bottom: 0;
32
 background-color: transparent;
33
}
34
35
.md-outline-button:hover {
36
 color: var(--md-sys-color-primary);
37
 border-color: var(--md-sys-color-outline);
38
}
39
40
/* state layer */
41
.md-outline-button:hover::after {
42
 background-color: var(--md-sys-color-primary);
43
 opacity: var(--md-sys-state-hover-state-layer-opacity);
44
}
45
46
.md-outline-button:focus {
47
 outline: none;
48
 color: var(--md-sys-color-primary);
49
 border-color: var(--md-sys-color-outline);
50
}
51
52
/* state layer */
53
.md-outline-button:focus::after {
54
 background-color: var(--md-sys-color-primary);
55
 opacity: var(--md-sys-state-focus-state-layer-opacity);
56
}
57
58
.md-outline-button:active {
59
 color: var(--md-sys-color-primary);
60
 border-color: var(--md-sys-color-outline);
61
 background-position: center;
62
 background-image:
63
  radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
64
 background-size: 100%;
65
 animation-name: md-ripple;
66
 animation-duration: var(--md-sys-motion-duration-500);
67
 box-shadow: var(--md-box_shadow_level0) !important;
68
}
69
70
/* state layer */
71
.md-outline-button:active::after {
72
 background-color: var(--md-sys-color-primary);
73
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
74
}
75
76
.md-outline-button:disabled {
77
 background-color: transparent !important;
78
 border-color: var(--md-sys-color-on-surface) !important;
79
 color: var(--md-sys-color-on-surface) !important;
80
 opacity: 0.38 !important;
81
}
82
83
/* container */
84
.md-outline-button:disabled::after {
85
 background-color: transparent !important;
86
 opacity: 1 !important;
87
}

K. css / md-tab.css

1
.md-tab {
2
 display: flex;
3
 background-color: transparent;
4
 align-items: stretch;
5
 flex-wrap: nowrap;
6
 overflow-x: auto;
7
 position: sticky;
8
 z-index: 1;
9
}
10
11
.md-tab.fixed {
12
 justify-content: center;
13
}
14
15
.md-tab.scrollable {
16
 padding-left: 2rem;
17
 gap: 1rem;
18
}
19
20
.md-tab.scroll {
21
 background-color: var(--md-sys-color-surface-container-low);
22
}
23
24
.md-tab a {
25
 position: relative;
26
 display: flex;
27
 flex-direction: column;
28
 justify-content: start;
29
 align-items: center;
30
 color: var(--md-sys-color-on-surface-variant);
31
 font-family: var(--md-sys-typescale-title-small-font);
32
 font-weight: var(--md-sys-typescale-title-small-weight);
33
 font-size: var(--md-sys-typescale-title-small-size);
34
 font-style: var(--md-sys-typescale-title-small-font-style);
35
 letter-spacing: var(--md-sys-typescale-title-small-tracking);
36
 line-height: var(--md-sys-typescale-title-small-line-height);
37
 text-transform: var(--md-sys-typescale-title-small-text-transform);
38
 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
39
 text-align: center;
40
 box-sizing: border-box;
41
 border-bottom: 0.1875rem solid var(--md-sys-color-surface);
42
}
43
44
.md-tab.fixed a {
45
 flex: 0 0 var(--tabWidth);
46
}
47
48
.md-tab.scrollable a {
49
 flex: 0 0 auto;
50
}
51
52
.md-tab a.active {
53
 border-bottom-color: var(--md-sys-color-primary);
54
}
55
56
/* state layer */
57
.md-tab a::after {
58
 content: "";
59
 position: absolute;
60
 z-index: -1;
61
 top: 0;
62
 right: 0;
63
 left: 0;
64
 bottom: 0;
65
 background-color: transparent;
66
}
67
68
.md-tab span {
69
 font-size: var(--iconSize);
70
 height: var(--iconSize);
71
 width: var(--iconSize);
72
 color: var(--md-sys-color-on-surface-variant);
73
}
74
75
.md-tab .active span {
76
 color: var(--md-sys-color-primary);
77
}
78
79
.md-tab a:hover {
80
 color: var(--md-sys-color-on-surface);
81
}
82
83
/* state layer */
84
.md-tab a:hover::after {
85
 background-color: var(--md-sys-color-on-surface);
86
 opacity: var(--md-sys-state-hover-state-layer-opacity);
87
}
88
89
.md-tab a.active:hover {
90
 color: var(--md-sys-color-primary);
91
}
92
93
/* state layer */
94
.md-tab a.active:hover::after {
95
 background-color: var(--md-sys-color-primary);
96
 opacity: var(--md-sys-state-hover-state-layer-opacity);
97
}
98
99
.md-tab a:hover span {
100
 color: var(--md-sys-color-on-surface);
101
}
102
103
.md-tab a.active:hover span {
104
 color: var(--md-sys-color-primary);
105
}
106
107
.md-tab a:focus {
108
 outline: none;
109
}
110
111
/* state layer */
112
.md-tab a:focus::after {
113
 background-color: var(--md-sys-color-on-surface);
114
 opacity: var(--md-sys-state-focus-state-layer-opacity);
115
}
116
117
/* state layer */
118
.md-tab a.active:focus::after {
119
 background-color: var(--md-sys-color-primary);
120
 opacity: var(--md-sys-state-hover-state-layer-opacity);
121
}
122
123
.md-tab a:active {
124
 background-position: center;
125
 background-image:
126
   radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
127
 background-size: 100%;
128
 animation-name: md-ripple;
129
 animation-duration: var(--md-sys-motion-duration-500);
130
}
131
132
/* state layer */
133
.md-tab a:active::after {
134
 background-color: var(--md-sys-color-on-surface);
135
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
136
}
137
138
/* state layer */
139
.md-tab a.active:active::after {
140
 background-color: var(--md-sys-color-primary);
141
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
142
}

L. css / motion.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Emphasized decelerate easing (out) */
19
  --md-sys-motion-easing-emphasized-decelerate-x0: 0.05000000074505806;
20
  --md-sys-motion-easing-emphasized-decelerate-y0: 0.699999988079071;
21
  --md-sys-motion-easing-emphasized-decelerate-x1: 0.10000000149011612;
22
  --md-sys-motion-easing-emphasized-decelerate-y1: 1;
23
  /* Emphasized accelerate easing (in) */
24
  --md-sys-motion-easing-emphasized-accelerate-x0: 0.30000001192092896;
25
  --md-sys-motion-easing-emphasized-accelerate-y0: 0;
26
  --md-sys-motion-easing-emphasized-accelerate-x1: 0.800000011920929;
27
  --md-sys-motion-easing-emphasized-accelerate-y1: 0.15000000596046448;
28
  /* Standard decelerate easing (out) */
29
  --md-sys-motion-easing-standard-decelerate-x0: 0;
30
  --md-sys-motion-easing-standard-decelerate-y0: 0;
31
  --md-sys-motion-easing-standard-decelerate-x1: 0;
32
  --md-sys-motion-easing-standard-decelerate-y1: 1;
33
  /* Standard accelerate easing (in) */
34
  --md-sys-motion-easing-standard-accelerate-x0: 0.30000001192092896;
35
  --md-sys-motion-easing-standard-accelerate-y0: 0;
36
  --md-sys-motion-easing-standard-accelerate-x1: 1;
37
  --md-sys-motion-easing-standard-accelerate-y1: 1;
38
  /* Duration 1000ms */
39
  --md-sys-motion-duration-1000: 1000ms;
40
  /* Duration 900ms */
41
  --md-sys-motion-duration-900: 900ms;
42
  /* Duration 800ms */
43
  --md-sys-motion-duration-800: 800ms;
44
  /* Duration 700ms */
45
  --md-sys-motion-duration-700: 700ms;
46
  /* Duration 600ms */
47
  --md-sys-motion-duration-600: 600ms;
48
  /* Duration 550ms */
49
  --md-sys-motion-duration-550: 550ms;
50
  /* Duration 500ms */
51
  --md-sys-motion-duration-500: 500ms;
52
  /* Duration 450ms */
53
  --md-sys-motion-duration-450: 450ms;
54
  /* Duration 400ms */
55
  --md-sys-motion-duration-400: 400ms;
56
  /* Duration 350ms */
57
  --md-sys-motion-duration-350: 350ms;
58
  /* Duration 300ms */
59
  --md-sys-motion-duration-300: 300ms;
60
  /* Duration 250ms */
61
  --md-sys-motion-duration-250: 250ms;
62
  /* Duration 200ms */
63
  --md-sys-motion-duration-200: 200ms;
64
  /* Duration 150ms */
65
  --md-sys-motion-duration-150: 150ms;
66
  /* Duration 100ms */
67
  --md-sys-motion-duration-100: 100ms;
68
  /* Duration 50ms */
69
  --md-sys-motion-duration-50: 50ms;
70
  /* Standard easing (in and out) */
71
  --md-sys-motion-easing-standard-x0: 0.20000000298023224;
72
  --md-sys-motion-easing-standard-y0: 0;
73
  --md-sys-motion-easing-standard-x1: 0;
74
  --md-sys-motion-easing-standard-y1: 1;
75
  /* Linear easing */
76
  --md-sys-motion-easing-linear-x0: 0;
77
  --md-sys-motion-easing-linear-y0: 0;
78
  --md-sys-motion-easing-linear-x1: 1;
79
  --md-sys-motion-easing-linear-y1: 1;
80
  /* Emphasized */
81
  --md-sys-motion-easing-emphasized-x0: 0.20000000298023224;
82
  --md-sys-motion-easing-emphasized-y0: 0;
83
  --md-sys-motion-easing-emphasized-x1: 0;
84
  --md-sys-motion-easing-emphasized-y1: 1;
85
  /* Motion path */
86
  --md-sys-motion-path-standard-path: 1;
87
}
88
.duration-50 {
89
  transition-duration: var(--md-sys-motion-duration-50);
90
}
91
.duration-100 {
92
  transition-duration: var(--md-sys-motion-duration-100);
93
}
94
.duration-150 {
95
  transition-duration: var(--md-sys-motion-duration-150);
96
}
97
.duration-200 {
98
  transition-duration: var(--md-sys-motion-duration-200);
99
}
100
.duration-250 {
101
  transition-duration: var(--md-sys-motion-duration-250);
102
}
103
.duration-300 {
104
  transition-duration: var(--md-sys-motion-duration-300);
105
}
106
.duration-350 {
107
  transition-duration: var(--md-sys-motion-duration-350);
108
}
109
.duration-400 {
110
  transition-duration: var(--md-sys-motion-duration-400);
111
}
112
.duration-450 {
113
  transition-duration: var(--md-sys-motion-duration-450);
114
}
115
.duration-500 {
116
  transition-duration: var(--md-sys-motion-duration-500);
117
}
118
.duration-550 {
119
  transition-duration: var(--md-sys-motion-duration-550);
120
}
121
.duration-600 {
122
  transition-duration: var(--md-sys-motion-duration-600);
123
}
124
.duration-700 {
125
  transition-duration: var(--md-sys-motion-duration-700);
126
}
127
.duration-800 {
128
  transition-duration: var(--md-sys-motion-duration-800);
129
}
130
.duration-900 {
131
  transition-duration: var(--md-sys-motion-duration-900);
132
}
133
.duration-1000 {
134
  transition-duration: var(--md-sys-motion-duration-1000);
135
}
136
.easing-standard {
137
  transition-timing-function: cubic-bezier(
138
    var(--md-sys-motion-easing-standard-x0),
139
    var(--md-sys-motion-easing-standard-y0),
140
    var(--md-sys-motion-easing-standard-x1),
141
    var(--md-sys-motion-easing-standard-y1)
142
  );
143
}
144
.easing-linear {
145
  transition-timing-function: cubic-bezier(
146
    var(--md-sys-motion-easing-linear-x0),
147
    var(--md-sys-motion-easing-linear-y0),
148
    var(--md-sys-motion-easing-linear-x1),
149
    var(--md-sys-motion-easing-linear-y1)
150
  );
151
}
152
.easing-standard-accelerate {
153
  transition-timing-function: cubic-bezier(
154
    var(--md-sys-motion-easing-standard-accelerate-x0),
155
    var(--md-sys-motion-easing-standard-accelerate-y0),
156
    var(--md-sys-motion-easing-standard-accelerate-x1),
157
    var(--md-sys-motion-easing-standard-accelerate-y1)
158
  );
159
}
160
.easing-standard-decelerate {
161
  transition-timing-function: cubic-bezier(
162
    var(--md-sys-motion-easing-standard-decelerate-x0),
163
    var(--md-sys-motion-easing-standard-decelerate-y0),
164
    var(--md-sys-motion-easing-standard-decelerate-x1),
165
    var(--md-sys-motion-easing-standard-decelerate-y1)
166
  );
167
}
168
.easing-emphasized {
169
  transition-timing-function: cubic-bezier(
170
    var(--md-sys-motion-easing-emphasized-x0),
171
    var(--md-sys-motion-easing-emphasized-y0),
172
    var(--md-sys-motion-easing-emphasized-x1),
173
    var(--md-sys-motion-easing-emphasized-y1)
174
  );
175
}
176

M. css / palette.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Error 0 */
19
  --md-ref-palette-error0: #000000ff;
20
  /* Error 10 */
21
  --md-ref-palette-error10: #410e0bff;
22
  /* Error 20 */
23
  --md-ref-palette-error20: #601410ff;
24
  /* Error 30 */
25
  --md-ref-palette-error30: #8c1d18ff;
26
  /* Error 40 */
27
  --md-ref-palette-error40: #b3261eff;
28
  /* Error 50 */
29
  --md-ref-palette-error50: #dc362eff;
30
  /* Error 60 */
31
  --md-ref-palette-error60: #e46962ff;
32
  /* Error 70 */
33
  --md-ref-palette-error70: #ec928eff;
34
  /* Error 80 */
35
  --md-ref-palette-error80: #f2b8b5ff;
36
  /* Error 90 */
37
  --md-ref-palette-error90: #f9dedcff;
38
  /* Error 95 */
39
  --md-ref-palette-error95: #fceeeeff;
40
  /* Error 99 */
41
  --md-ref-palette-error99: #fffbf9ff;
42
  /* Error 100 */
43
  --md-ref-palette-error100: #ffffffff;
44
  /* Tertiary 0 */
45
  --md-ref-palette-tertiary0: #000000ff;
46
  /* Tertiary 10 */
47
  --md-ref-palette-tertiary10: #31111dff;
48
  /* Tertiary 20 */
49
  --md-ref-palette-tertiary20: #492532ff;
50
  /* Tertiary 30 */
51
  --md-ref-palette-tertiary30: #633b48ff;
52
  /* Tertiary 40 */
53
  --md-ref-palette-tertiary40: #7d5260ff;
54
  /* Tertiary 50 */
55
  --md-ref-palette-tertiary50: #986977ff;
56
  /* Tertiary 60 */
57
  --md-ref-palette-tertiary60: #b58392ff;
58
  /* Tertiary 70 */
59
  --md-ref-palette-tertiary70: #d29dacff;
60
  /* Tertiary 80 */
61
  --md-ref-palette-tertiary80: #efb8c8ff;
62
  /* Tertiary 90 */
63
  --md-ref-palette-tertiary90: #ffd8e4ff;
64
  /* Tertiary 95 */
65
  --md-ref-palette-tertiary95: #ffecf1ff;
66
  /* Tertiary 99 */
67
  --md-ref-palette-tertiary99: #fffbfaff;
68
  /* Tertiary 100 */
69
  --md-ref-palette-tertiary100: #ffffffff;
70
  /* Secondary 0 */
71
  --md-ref-palette-secondary0: #000000ff;
72
  /* Secondary 10 */
73
  --md-ref-palette-secondary10: #1d192bff;
74
  /* Secondary 20 */
75
  --md-ref-palette-secondary20: #332d41ff;
76
  /* Secondary 30 */
77
  --md-ref-palette-secondary30: #4a4458ff;
78
  /* Secondary 40 */
79
  --md-ref-palette-secondary40: #625b71ff;
80
  /* Secondary 50 */
81
  --md-ref-palette-secondary50: #7a7289ff;
82
  /* Secondary 60 */
83
  --md-ref-palette-secondary60: #958da5ff;
84
  /* Secondary 70 */
85
  --md-ref-palette-secondary70: #b0a7c0ff;
86
  /* Secondary 80 */
87
  --md-ref-palette-secondary80: #ccc2dcff;
88
  /* Secondary 90 */
89
  --md-ref-palette-secondary90: #e8def8ff;
90
  /* Secondary 95 */
91
  --md-ref-palette-secondary95: #f6edffff;
92
  /* Secondary 99 */
93
  --md-ref-palette-secondary99: #fffbfeff;
94
  /* Secondary 100 */
95
  --md-ref-palette-secondary100: #ffffffff;
96
  /* Primary 0 */
97
  --md-ref-palette-primary0: #000000ff;
98
  /* Primary 10 */
99
  --md-ref-palette-primary10: #21005dff;
100
  /* Primary 20 */
101
  --md-ref-palette-primary20: #381e72ff;
102
  /* Primary 30 */
103
  --md-ref-palette-primary30: #4f378bff;
104
  /* Primary 40 */
105
  --md-ref-palette-primary40: #6750a4ff;
106
  /* Primary 50 */
107
  --md-ref-palette-primary50: #7f67beff;
108
  /* Primary 60 */
109
  --md-ref-palette-primary60: #9a82dbff;
110
  /* Primary 70 */
111
  --md-ref-palette-primary70: #b69df8ff;
112
  /* Primary 80 */
113
  --md-ref-palette-primary80: #d0bcffff;
114
  /* Primary 90 */
115
  --md-ref-palette-primary90: #eaddffff;
116
  /* Primary 95 */
117
  --md-ref-palette-primary95: #f6edffff;
118
  /* Primary 99 */
119
  --md-ref-palette-primary99: #fffbfeff;
120
  /* Primary 100 */
121
  --md-ref-palette-primary100: #ffffffff;
122
  /* Neutral Variant 0 */
123
  --md-ref-palette-neutral-variant0: #000000ff;
124
  /* Neutral Variant 10 */
125
  --md-ref-palette-neutral-variant10: #1d1a22ff;
126
  /* Neutral Variant 20 */
127
  --md-ref-palette-neutral-variant20: #322f37ff;
128
  /* Neutral Variant 30 */
129
  --md-ref-palette-neutral-variant30: #49454fff;
130
  /* Neutral Variant 40 */
131
  --md-ref-palette-neutral-variant40: #605d66ff;
132
  /* Neutral Variant 50 */
133
  --md-ref-palette-neutral-variant50: #79747eff;
134
  /* Neutral Variant 60 */
135
  --md-ref-palette-neutral-variant60: #938f99ff;
136
  /* Neutral Variant 70 */
137
  --md-ref-palette-neutral-variant70: #aea9b4ff;
138
  /* Neutral Variant 80 */
139
  --md-ref-palette-neutral-variant80: #cac4d0ff;
140
  /* Neutral Variant 90 */
141
  --md-ref-palette-neutral-variant90: #e7e0ecff;
142
  /* Neutral Variant 95 */
143
  --md-ref-palette-neutral-variant95: #f5eefaff;
144
  /* Neutral Variant 99 */
145
  --md-ref-palette-neutral-variant99: #fffbfeff;
146
  /* Neutral Variant 100 */
147
  --md-ref-palette-neutral-variant100: #ffffffff;
148
  /* Neutral 0 */
149
  --md-ref-palette-neutral0: #000000ff;
150
  /* Neutral 10 */
151
  --md-ref-palette-neutral10: #1c1b1fff;
152
  /* Neutral 20 */
153
  --md-ref-palette-neutral20: #313033ff;
154
  /* Neutral 30 */
155
  --md-ref-palette-neutral30: #484649ff;
156
  /* Neutral 40 */
157
  --md-ref-palette-neutral40: #605d62ff;
158
  /* Neutral 50 */
159
  --md-ref-palette-neutral50: #787579ff;
160
  /* Neutral 60 */
161
  --md-ref-palette-neutral60: #939094ff;
162
  /* Neutral 70 */
163
  --md-ref-palette-neutral70: #aeaaaeff;
164
  /* Neutral 80 */
165
  --md-ref-palette-neutral80: #c9c5caff;
166
  /* Neutral 90 */
167
  --md-ref-palette-neutral90: #e6e1e5ff;
168
  /* Neutral 95 */
169
  --md-ref-palette-neutral95: #f4eff4ff;
170
  /* Neutral 99 */
171
  --md-ref-palette-neutral99: #fffbfeff;
172
  /* Neutral 100 */
173
  --md-ref-palette-neutral100: #ffffffff;
174
  /* Black */
175
  --md-ref-palette-black: #000000ff;
176
  /* White */
177
  --md-ref-palette-white: #ffffffff;
178
}
179

N. css / roboto.css

1
/* roboto-regular - latin */
2
@font-face {
3
 /* Revisa
4
  * https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
5
  * para otras opciones. */
6
 font-display: swap;
7
 font-family: 'Roboto';
8
 font-style: normal;
9
 font-weight: 400;
10
 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
11
 src: url('../fonts/roboto-v32-latin-regular.woff2') format('woff2');
12
}

O. css / shape.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Fully rounded */
19
  --md-sys-shape-corner-full-family: 3px;
20
  /* Extra large top rounding */
21
  --md-sys-shape-corner-extra-large-top-family: 1px;
22
  --md-sys-shape-corner-extra-large-top-default-size: 0px;
23
  --md-sys-shape-corner-extra-large-top-top-left: 28px;
24
  --md-sys-shape-corner-extra-large-top-top-right-unit: 1px;
25
  --md-sys-shape-corner-extra-large-top-top-right: 28px;
26
  /* Extra large rounding */
27
  --md-sys-shape-corner-extra-large-family: 1px;
28
  --md-sys-shape-corner-extra-large-default-size-unit: 1px;
29
  --md-sys-shape-corner-extra-large-default-size: 28px;
30
  /* Large top rounding */
31
  --md-sys-shape-corner-large-top-family: 1px;
32
  --md-sys-shape-corner-large-top-default-size-unit: 1px;
33
  --md-sys-shape-corner-large-top-default-size: 0px;
34
  --md-sys-shape-corner-large-top-top-left-unit: 1px;
35
  --md-sys-shape-corner-large-top-top-left: 16px;
36
  --md-sys-shape-corner-large-top-top-right-unit: 1px;
37
  --md-sys-shape-corner-large-top-top-right: 16px;
38
  /* Large end rounding */
39
  --md-sys-shape-corner-large-end-family: 1px;
40
  --md-sys-shape-corner-large-end-default-size-unit: 1px;
41
  --md-sys-shape-corner-large-end-default-size: 0px;
42
  --md-sys-shape-corner-large-end-top-right-unit: 1px;
43
  --md-sys-shape-corner-large-end-top-right: 16px;
44
  --md-sys-shape-corner-large-end-bottom-right-unit: 1px;
45
  --md-sys-shape-corner-large-end-bottom-right: 16px;
46
  /* Large rounding */
47
  --md-sys-shape-corner-large-family: 1px;
48
  --md-sys-shape-corner-large-default-size-unit: 1px;
49
  --md-sys-shape-corner-large-default-size: 16px;
50
  /* Medium rounding */
51
  --md-sys-shape-corner-medium-family: 1px;
52
  --md-sys-shape-corner-medium-default-size-unit: 1px;
53
  --md-sys-shape-corner-medium-default-size: 12px;
54
  /* Small rounding */
55
  --md-sys-shape-corner-small-family: 1px;
56
  --md-sys-shape-corner-small-default-size-unit: 1px;
57
  --md-sys-shape-corner-small-default-size: 8px;
58
  /* Extra small top rounding */
59
  --md-sys-shape-corner-extra-small-top-family: 1px;
60
  --md-sys-shape-corner-extra-small-top-default-size-unit: 1px;
61
  --md-sys-shape-corner-extra-small-top-default-size: 0px;
62
  --md-sys-shape-corner-extra-small-top-top-left-unit: 1px;
63
  --md-sys-shape-corner-extra-small-top-top-left: 4px;
64
  --md-sys-shape-corner-extra-small-top-top-right-unit: 1px;
65
  --md-sys-shape-corner-extra-small-top-top-right: 4px;
66
  /* Extra small rounding */
67
  --md-sys-shape-corner-extra-small-family: 1px;
68
  --md-sys-shape-corner-extra-small-default-size-unit: 1px;
69
  --md-sys-shape-corner-extra-small-default-size: 4px;
70
  /* No rounding */
71
  --md-sys-shape-corner-none-family: 1px;
72
  --md-sys-shape-corner-none-default-size-unit: 1px;
73
  --md-sys-shape-corner-none-default-size: 0px;
74
75
  --md-sys-shape-small: var(--md-sys-shape-corner-small-default-size);
76
  --md-sys-shape-medium: var(--md-sys-shape-corner-medium-default-size);
77
  --md-sys-shape-large: var(--md-sys-shape-corner-large-default-size);
78
}
79
80
.shape-none {
81
  border-radius: var(--md-sys-shape-corner-none-default-size);
82
}
83
.shape-extra-small {
84
  border-radius: var(--md-sys-shape-corner-extra-small-default-size);
85
}
86
.shape-small {
87
  border-radius: var(--md-sys-shape-corner-small-default-size);
88
}
89
.shape-medium {
90
  border-radius: var(--md-sys-shape-corner-medium-default-size);
91
}
92
.shape-large {
93
  border-radius: var(--md-sys-shape-corner-large-default-size);
94
}
95
.shape-extra-large {
96
  border-radius: var(--md-sys-shape-corner-extra-large-default-size);
97
}
98
.extra-small-top {
99
  border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
100
  border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
101
}
102
.large-end {
103
  border-top-right-radius: var(--md-sys-shape-corner-large-end-top-right);
104
  border-bottom-right-radius: var(--md-sys-shape-corner-large-end-bottom-right);
105
}
106
.large-top {
107
  border-top-left-radius: var(--md-sys-shape-corner-large-top-top-left);
108
  border-top-right-radius: var(--md-sys-shape-corner-large-top-top-right);
109
}
110
.extra-large-top {
111
  border-top-left-radius: var(--md-sys-shape-corner-extra-large-top-top-left);
112
  border-top-right-radius: var(--md-sys-shape-corner-extra-large-top-top-right);
113
}
114

P. css / state.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Dragged state layer opacity */
19
  --md-sys-state-dragged-state-layer-opacity: 0.1599999964237213;
20
  /* Pressed state layer opacity */
21
  --md-sys-state-pressed-state-layer-opacity: 0.11999999731779099;
22
  /* Focus state layer opacity */
23
  --md-sys-state-focus-state-layer-opacity: 0.11999999731779099;
24
  /* Hover state layer opacity */
25
  --md-sys-state-hover-state-layer-opacity: 0.07999999821186066;
26
}
27
.hover-state-layer {
28
  opacity: var(--md-sys-state-hover-state-layer-opacity);
29
}
30
.pressed-state-layer {
31
  opacity: var(--md-sys-state-pressed-state-layer-opacity);
32
}
33
.dragged-state-layer {
34
  opacity: var(--md-sys-state-dragged-state-layer-opacity);
35
}
36
.focus-state-layer {
37
  opacity: var(--md-sys-state-focus-state-layer-opacity);
38
}
39

Q. css / transicion_pestanas.css

1
@view-transition {
2
 navigation: auto;
3
}
4
5
#headline {
6
 view-transition-name: encabezado;
7
}
8
9
main {
10
 view-transition-name: contenido;
11
}
12
13
::view-transition-group(encabezado) {
14
 animation-duration: var(--md-sys-motion-duration-1000);
15
}
16
17
::view-transition-group(contenido) {
18
 animation-duration: var(--md-sys-motion-duration-1000);
19
}
20
21
html::view-transition-old(encabezado) {
22
 animation-name: salePorLaIzquierda;
23
}
24
25
html::view-transition-new(encabezado) {
26
 animation-name: entraPorLaDerecha;
27
}
28
29
html::view-transition-old(contenido) {
30
 animation-name: salePorLaIzquierda;
31
}
32
33
html::view-transition-new(contenido) {
34
 animation-duration: var(--md-sys-motion-duration-700);
35
 animation-name: entraPorLaDerecha;
36
}

R. css / typography.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
/* This file is generated */
18
19
/* DO NOT EDIT */
20
21
:root {
22
  /* Label Small */
23
  --md-sys-typescale-label-small-text-transform: unset;
24
  --md-sys-typescale-label-small-axis-value: unset;
25
  --md-sys-typescale-label-small-font-style: unset;
26
  --md-sys-typescale-label-small-text-decoration: unset;
27
  /* Label Small line height */
28
  --md-sys-typescale-label-small-line-height-value: 16px;
29
  --md-sys-typescale-label-small-line-height-unit: 2px;
30
  --md-sys-typescale-label-small-line-height: 16px;
31
  /* Label Small font tracking */
32
  --md-sys-typescale-label-small-tracking-value: 0.5px;
33
  --md-sys-typescale-label-small-tracking-unit: 2px;
34
  --md-sys-typescale-label-small-tracking: 0.5px;
35
  /* Label Small font size */
36
  --md-sys-typescale-label-small-size-value: 11px;
37
  --md-sys-typescale-label-small-size-unit: 2px;
38
  --md-sys-typescale-label-small-size: 11px;
39
  /* Label Small font weight */
40
  --md-sys-typescale-label-small-weight: var(--md-ref-typeface-weight-medium);
41
  /* Label Small font name */
42
  --md-sys-typescale-label-small-font: var(--md-ref-typeface-plain);
43
  /* Label Medium */
44
  --md-sys-typescale-label-medium-axis-value: unset;
45
  --md-sys-typescale-label-medium-font-style: unset;
46
  --md-sys-typescale-label-medium-text-decoration: unset;
47
  /* Label Medium text transform */
48
  --md-sys-typescale-label-medium-text-transform: 1;
49
  /* Label Medium line height */
50
  --md-sys-typescale-label-medium-line-height-value: 16px;
51
  --md-sys-typescale-label-medium-line-height-unit: 2px;
52
  --md-sys-typescale-label-medium-line-height: 16px;
53
  /* Label Medium font tracking */
54
  --md-sys-typescale-label-medium-tracking-value: 0.5px;
55
  --md-sys-typescale-label-medium-tracking-unit: 2px;
56
  --md-sys-typescale-label-medium-tracking: 0.5px;
57
  /* Label Medium font size */
58
  --md-sys-typescale-label-medium-size-value: 12px;
59
  --md-sys-typescale-label-medium-size-unit: 2px;
60
  --md-sys-typescale-label-medium-size: 12px;
61
  /* Label Medium font weight */
62
  --md-sys-typescale-label-medium-weight: var(--md-ref-typeface-weight-medium);
63
  /* Label Medium font name */
64
  --md-sys-typescale-label-medium-font: var(--md-ref-typeface-plain);
65
  /* Label Large */
66
  --md-sys-typescale-label-large-text-transform: unset;
67
  --md-sys-typescale-label-large-axis-value: unset;
68
  --md-sys-typescale-label-large-font-style: unset;
69
  --md-sys-typescale-label-large-text-decoration: unset;
70
  /* Label Large line height */
71
  --md-sys-typescale-label-large-line-height-value: 20px;
72
  --md-sys-typescale-label-large-line-height-unit: 2px;
73
  --md-sys-typescale-label-large-line-height: 20px;
74
  /* Label Large font tracking */
75
  --md-sys-typescale-label-large-tracking-value: 0.10000000149011612px;
76
  --md-sys-typescale-label-large-tracking-unit: 2px;
77
  --md-sys-typescale-label-large-tracking: 0.10000000149011612px;
78
  /* Label Large font size */
79
  --md-sys-typescale-label-large-size-value: 14px;
80
  --md-sys-typescale-label-large-size-unit: 2px;
81
  --md-sys-typescale-label-large-size: 14px;
82
  /* Label Large font weight */
83
  --md-sys-typescale-label-large-weight: var(--md-ref-typeface-weight-medium);
84
  /* Label Large font name */
85
  --md-sys-typescale-label-large-font: var(--md-ref-typeface-plain);
86
  /* Body Small */
87
  --md-sys-typescale-body-small-text-transform: unset;
88
  --md-sys-typescale-body-small-axis-value: unset;
89
  --md-sys-typescale-body-small-font-style: unset;
90
  --md-sys-typescale-body-small-text-decoration: unset;
91
  /* Body Small line height */
92
  --md-sys-typescale-body-small-line-height-value: 16px;
93
  --md-sys-typescale-body-small-line-height-unit: 2px;
94
  --md-sys-typescale-body-small-line-height: 16px;
95
  /* Body Small font tracking */
96
  --md-sys-typescale-body-small-tracking-value: 0.4000000059604645px;
97
  --md-sys-typescale-body-small-tracking-unit: 2px;
98
  --md-sys-typescale-body-small-tracking: 0.4000000059604645px;
99
  /* Body Small font size */
100
  --md-sys-typescale-body-small-size-value: 12px;
101
  --md-sys-typescale-body-small-size-unit: 2px;
102
  --md-sys-typescale-body-small-size: 12px;
103
  /* Body Small font weight */
104
  --md-sys-typescale-body-small-weight: var(--md-ref-typeface-weight-regular);
105
  /* Body Small font name */
106
  --md-sys-typescale-body-small-font: var(--md-ref-typeface-plain);
107
  /* Body Medium */
108
  --md-sys-typescale-body-medium-text-transform: unset;
109
  --md-sys-typescale-body-medium-axis-value: unset;
110
  --md-sys-typescale-body-medium-font-style: unset;
111
  --md-sys-typescale-body-medium-text-decoration: unset;
112
  /* Body Medium line height */
113
  --md-sys-typescale-body-medium-line-height-value: 20px;
114
  --md-sys-typescale-body-medium-line-height-unit: 2px;
115
  --md-sys-typescale-body-medium-line-height: 20px;
116
  /* Body Medium font tracking */
117
  --md-sys-typescale-body-medium-tracking-value: 0.25px;
118
  --md-sys-typescale-body-medium-tracking-unit: 2px;
119
  --md-sys-typescale-body-medium-tracking: 0.25px;
120
  /* Body Medium font size */
121
  --md-sys-typescale-body-medium-size-value: 14px;
122
  --md-sys-typescale-body-medium-size-unit: 2px;
123
  --md-sys-typescale-body-medium-size: 14px;
124
  /* Body Medium font weight */
125
  --md-sys-typescale-body-medium-weight: var(--md-ref-typeface-weight-regular);
126
  /* Body Medium font name */
127
  --md-sys-typescale-body-medium-font: var(--md-ref-typeface-plain);
128
  /* Body Large */
129
  --md-sys-typescale-body-large-text-transform: unset;
130
  --md-sys-typescale-body-large-axis-value: unset;
131
  --md-sys-typescale-body-large-font-style: unset;
132
  --md-sys-typescale-body-large-text-decoration: unset;
133
  /* Body Large line height */
134
  --md-sys-typescale-body-large-line-height-value: 24px;
135
  --md-sys-typescale-body-large-line-height-unit: 2px;
136
  --md-sys-typescale-body-large-line-height: 24px;
137
  /* Body Large font tracking */
138
  --md-sys-typescale-body-large-tracking-value: 0.5px;
139
  --md-sys-typescale-body-large-tracking-unit: 2px;
140
  --md-sys-typescale-body-large-tracking: 0.5px;
141
  /* Body Large font size */
142
  --md-sys-typescale-body-large-size-value: 16px;
143
  --md-sys-typescale-body-large-size-unit: 2px;
144
  --md-sys-typescale-body-large-size: 16px;
145
  /* Body Large font weight */
146
  --md-sys-typescale-body-large-weight: var(--md-ref-typeface-weight-regular);
147
  /* Body Large font name */
148
  --md-sys-typescale-body-large-font: var(--md-ref-typeface-plain);
149
  /* Title Small */
150
  --md-sys-typescale-title-small-text-transform: unset;
151
  --md-sys-typescale-title-small-axis-value: unset;
152
  --md-sys-typescale-title-small-font-style: unset;
153
  --md-sys-typescale-title-small-text-decoration: unset;
154
  /* Title Small line height */
155
  --md-sys-typescale-title-small-line-height-value: 20px;
156
  --md-sys-typescale-title-small-line-height-unit: 2px;
157
  --md-sys-typescale-title-small-line-height: 20px;
158
  /* Title Small font tracking */
159
  --md-sys-typescale-title-small-tracking-value: 0.10000000149011612px;
160
  --md-sys-typescale-title-small-tracking-unit: 2px;
161
  --md-sys-typescale-title-small-tracking: 0.10000000149011612px;
162
  /* Title Small font size */
163
  --md-sys-typescale-title-small-size-value: 14px;
164
  --md-sys-typescale-title-small-size-unit: 2px;
165
  --md-sys-typescale-title-small-size: 14px;
166
  /* Title Small font weight */
167
  --md-sys-typescale-title-small-weight: var(--md-ref-typeface-weight-medium);
168
  /* Title Small font name */
169
  --md-sys-typescale-title-small-font: var(--md-ref-typeface-plain);
170
  /* Title Medium */
171
  --md-sys-typescale-title-medium-text-transform: unset;
172
  --md-sys-typescale-title-medium-axis-value: unset;
173
  --md-sys-typescale-title-medium-font-style: unset;
174
  --md-sys-typescale-title-medium-text-decoration: unset;
175
  /* Title Medium line height */
176
  --md-sys-typescale-title-medium-line-height-value: 24px;
177
  --md-sys-typescale-title-medium-line-height-unit: 2px;
178
  --md-sys-typescale-title-medium-line-height: 24px;
179
  /* Title Medium font tracking */
180
  --md-sys-typescale-title-medium-tracking-value: 0.15000000596046448px;
181
  --md-sys-typescale-title-medium-tracking-unit: 2px;
182
  --md-sys-typescale-title-medium-tracking: 0.15000000596046448px;
183
  /* Title Medium font size */
184
  --md-sys-typescale-title-medium-size-value: 16px;
185
  --md-sys-typescale-title-medium-size-unit: 2px;
186
  --md-sys-typescale-title-medium-size: 16px;
187
  /* Title Medium font weight */
188
  --md-sys-typescale-title-medium-weight: var(--md-ref-typeface-weight-medium);
189
  /* Title Medium font name */
190
  --md-sys-typescale-title-medium-font: var(--md-ref-typeface-plain);
191
  /* Title Large */
192
  --md-sys-typescale-title-large-text-transform: unset;
193
  --md-sys-typescale-title-large-axis-value: unset;
194
  --md-sys-typescale-title-large-font-style: unset;
195
  --md-sys-typescale-title-large-text-decoration: unset;
196
  /* Title Large line height */
197
  --md-sys-typescale-title-large-line-height-value: 28px;
198
  --md-sys-typescale-title-large-line-height-unit: 2px;
199
  --md-sys-typescale-title-large-line-height: 28px;
200
  /* Title Large font tracking */
201
  --md-sys-typescale-title-large-tracking-value: 0px;
202
  --md-sys-typescale-title-large-tracking-unit: 2px;
203
  --md-sys-typescale-title-large-tracking: 0px;
204
  /* Title Large font size */
205
  --md-sys-typescale-title-large-size-value: 22px;
206
  --md-sys-typescale-title-large-size-unit: 2px;
207
  --md-sys-typescale-title-large-size: 22px;
208
  /* Title Large font weight */
209
  --md-sys-typescale-title-large-weight: var(--md-ref-typeface-weight-regular);
210
  /* Title Large font name */
211
  --md-sys-typescale-title-large-font: var(--md-ref-typeface-brand);
212
  /* Headline Small */
213
  --md-sys-typescale-headline-small-text-transform: unset;
214
  --md-sys-typescale-headline-small-axis-value: unset;
215
  --md-sys-typescale-headline-small-font-style: unset;
216
  --md-sys-typescale-headline-small-text-decoration: unset;
217
  /* Headline Small line height */
218
  --md-sys-typescale-headline-small-line-height-value: 32px;
219
  --md-sys-typescale-headline-small-line-height-unit: 2px;
220
  --md-sys-typescale-headline-small-line-height: 32px;
221
  /* Headline Small font tracking */
222
  --md-sys-typescale-headline-small-tracking-value: 0px;
223
  --md-sys-typescale-headline-small-tracking-unit: 2px;
224
  --md-sys-typescale-headline-small-tracking: 0px;
225
  /* Headline Small font size */
226
  --md-sys-typescale-headline-small-size-value: 24px;
227
  --md-sys-typescale-headline-small-size-unit: 2px;
228
  --md-sys-typescale-headline-small-size: 24px;
229
  /* Headline Small font weight */
230
  --md-sys-typescale-headline-small-weight: var(
231
    --md-ref-typeface-weight-regular
232
  );
233
  /* Headline Small font name */
234
  --md-sys-typescale-headline-small-font: var(--md-ref-typeface-brand);
235
  /* Headline Medium */
236
  --md-sys-typescale-headline-medium-text-transform: unset;
237
  --md-sys-typescale-headline-medium-axis-value: unset;
238
  --md-sys-typescale-headline-medium-font-style: unset;
239
  --md-sys-typescale-headline-medium-text-decoration: unset;
240
  /* Headline Medium line height */
241
  --md-sys-typescale-headline-medium-line-height-value: 36px;
242
  --md-sys-typescale-headline-medium-line-height-unit: 2px;
243
  --md-sys-typescale-headline-medium-line-height: 36px;
244
  /* Headline Medium font tracking */
245
  --md-sys-typescale-headline-medium-tracking-value: 0px;
246
  --md-sys-typescale-headline-medium-tracking-unit: 2px;
247
  --md-sys-typescale-headline-medium-tracking: 0px;
248
  /* Headline Medium font size */
249
  --md-sys-typescale-headline-medium-size-value: 28px;
250
  --md-sys-typescale-headline-medium-size-unit: 2px;
251
  --md-sys-typescale-headline-medium-size: 28px;
252
  /* Headline Medium font weight */
253
  --md-sys-typescale-headline-medium-weight: var(
254
    --md-ref-typeface-weight-regular
255
  );
256
  /* Headline Medium font name */
257
  --md-sys-typescale-headline-medium-font: var(--md-ref-typeface-brand);
258
  /* Headline Large */
259
  --md-sys-typescale-headline-large-text-transform: unset;
260
  --md-sys-typescale-headline-large-axis-value: unset;
261
  --md-sys-typescale-headline-large-font-style: unset;
262
  --md-sys-typescale-headline-large-text-decoration: unset;
263
  /* Headline Large line height */
264
  --md-sys-typescale-headline-large-line-height-value: 40px;
265
  --md-sys-typescale-headline-large-line-height-unit: 2px;
266
  --md-sys-typescale-headline-large-line-height: 40px;
267
  /* Headline Large font tracking */
268
  --md-sys-typescale-headline-large-tracking-value: 0px;
269
  --md-sys-typescale-headline-large-tracking-unit: 2px;
270
  --md-sys-typescale-headline-large-tracking: 0px;
271
  /* Headline Large font size */
272
  --md-sys-typescale-headline-large-size-value: 32px;
273
  --md-sys-typescale-headline-large-size-unit: 2px;
274
  --md-sys-typescale-headline-large-size: 32px;
275
  /* Headline Large font name */
276
  --md-sys-typescale-headline-large-font: var(--md-ref-typeface-brand);
277
  /* Headline Large font weight */
278
  --md-sys-typescale-headline-large-weight: var(
279
    --md-ref-typeface-weight-regular
280
  );
281
  /* Display Small */
282
  --md-sys-typescale-display-small-text-transform: unset;
283
  --md-sys-typescale-display-small-axis-value: unset;
284
  --md-sys-typescale-display-small-font-style: unset;
285
  --md-sys-typescale-display-small-text-decoration: unset;
286
  /* Display Small line height */
287
  --md-sys-typescale-display-small-line-height-value: 44px;
288
  --md-sys-typescale-display-small-line-height-unit: 2px;
289
  --md-sys-typescale-display-small-line-height: 44px;
290
  /* Display Small font tracking */
291
  --md-sys-typescale-display-small-tracking-value: 0px;
292
  --md-sys-typescale-display-small-tracking-unit: 2px;
293
  --md-sys-typescale-display-small-tracking: 0px;
294
  /* Display Small font size */
295
  --md-sys-typescale-display-small-size-value: 36px;
296
  --md-sys-typescale-display-small-size-unit: 2px;
297
  --md-sys-typescale-display-small-size: 36px;
298
  /* Display Small font weight */
299
  --md-sys-typescale-display-small-weight: var(
300
    --md-ref-typeface-weight-regular
301
  );
302
  /* Display Small font name */
303
  --md-sys-typescale-display-small-font: var(--md-ref-typeface-brand);
304
  /* Display Medium */
305
  --md-sys-typescale-display-medium-text-transform: unset;
306
  --md-sys-typescale-display-medium-axis-value: unset;
307
  --md-sys-typescale-display-medium-font-style: unset;
308
  --md-sys-typescale-display-medium-text-decoration: unset;
309
  /* Display Medium line height */
310
  --md-sys-typescale-display-medium-line-height-value: 52px;
311
  --md-sys-typescale-display-medium-line-height-unit: 2px;
312
  --md-sys-typescale-display-medium-line-height: 52px;
313
  /* Display Medium font tracking */
314
  --md-sys-typescale-display-medium-tracking-value: 0px;
315
  --md-sys-typescale-display-medium-tracking-unit: 2px;
316
  --md-sys-typescale-display-medium-tracking: 0px;
317
  /* Display Medium font size */
318
  --md-sys-typescale-display-medium-size-value: 45px;
319
  --md-sys-typescale-display-medium-size-unit: 2px;
320
  --md-sys-typescale-display-medium-size: 45px;
321
  /* Display Medium font weight */
322
  --md-sys-typescale-display-medium-weight: var(
323
    --md-ref-typeface-weight-regular
324
  );
325
  /* Display Medium font name */
326
  --md-sys-typescale-display-medium-font: var(--md-ref-typeface-brand);
327
  /* Display Large */
328
  --md-sys-typescale-display-large-text-transform: unset;
329
  --md-sys-typescale-display-large-axis-value: unset;
330
  --md-sys-typescale-display-large-font-style: unset;
331
  --md-sys-typescale-display-large-text-decoration: unset;
332
  /* Display Large line height */
333
  --md-sys-typescale-display-large-line-height-value: 64px;
334
  --md-sys-typescale-display-large-line-height-unit: 2px;
335
  --md-sys-typescale-display-large-line-height: 64px;
336
  /* Display Large font tracking */
337
  --md-sys-typescale-display-large-tracking-value: -0.25px;
338
  --md-sys-typescale-display-large-tracking-unit: 2px;
339
  --md-sys-typescale-display-large-tracking: -0.25px;
340
  /* Display Large font size */
341
  --md-sys-typescale-display-large-size-value: 57px;
342
  --md-sys-typescale-display-large-size-unit: 2px;
343
  --md-sys-typescale-display-large-size: 57px;
344
  /* Display Large font weight */
345
  --md-sys-typescale-display-large-weight: var(
346
    --md-ref-typeface-weight-regular
347
  );
348
  /* Display Large font name */
349
  --md-sys-typescale-display-large-font: var(--md-ref-typeface-brand);
350
  /* Plain typeface */
351
  --md-ref-typeface-plain: Roboto;
352
  /* Brand typeface */
353
  --md-ref-typeface-brand: Roboto;
354
  /* Bold weight */
355
  --md-ref-typeface-weight-bold: 700;
356
  /* Medium weight */
357
  --md-ref-typeface-weight-medium: 500;
358
  /* Regular weight */
359
  --md-ref-typeface-weight-regular: 400;
360
}
361
362
/* Label Small */
363
.label-small {
364
  font-family: var(--md-sys-typescale-label-small-font);
365
  font-weight: var(--md-sys-typescale-label-small-weight);
366
  font-size: var(--md-sys-typescale-label-small-size);
367
  font-style: var(--md-sys-typescale-label-small-font-style);
368
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
369
  line-height: var(--md-sys-typescale-label-small-line-height);
370
  text-transform: var(--md-sys-typescale-label-small-text-transform);
371
  text-decoration: var(--md-sys-typescale-label-small-text-decoration);
372
}
373
/* Label Medium */
374
.label-medium {
375
  font-family: var(--md-sys-typescale-label-medium-font);
376
  font-weight: var(--md-sys-typescale-label-medium-weight);
377
  font-size: var(--md-sys-typescale-label-medium-size);
378
  font-style: var(--md-sys-typescale-label-medium-font-style);
379
  letter-spacing: var(--md-sys-typescale-label-medium-tracking);
380
  line-height: var(--md-sys-typescale-label-medium-line-height);
381
  text-transform: var(--md-sys-typescale-label-medium-text-transform);
382
  text-decoration: var(--md-sys-typescale-label-medium-text-decoration);
383
}
384
/* Label Large */
385
.label-large {
386
  font-family: var(--md-sys-typescale-label-large-font);
387
  font-weight: var(--md-sys-typescale-label-large-weight);
388
  font-size: var(--md-sys-typescale-label-large-size);
389
  font-style: var(--md-sys-typescale-label-large-font-style);
390
  letter-spacing: var(--md-sys-typescale-label-large-tracking);
391
  line-height: var(--md-sys-typescale-label-large-line-height);
392
  text-transform: var(--md-sys-typescale-label-large-text-transform);
393
  text-decoration: var(--md-sys-typescale-label-large-text-decoration);
394
}
395
/* Body Small */
396
.body-small {
397
  font-family: var(--md-sys-typescale-body-small-font);
398
  font-weight: var(--md-sys-typescale-body-small-weight);
399
  font-size: var(--md-sys-typescale-body-small-size);
400
  font-style: var(--md-sys-typescale-body-small-font-style);
401
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
402
  line-height: var(--md-sys-typescale-body-small-line-height);
403
  text-transform: var(--md-sys-typescale-body-small-text-transform);
404
  text-decoration: var(--md-sys-typescale-body-small-text-decoration);
405
}
406
/* Body Medium */
407
.body-medium {
408
  font-family: var(--md-sys-typescale-body-medium-font);
409
  font-weight: var(--md-sys-typescale-body-medium-weight);
410
  font-size: var(--md-sys-typescale-body-medium-size);
411
  font-style: var(--md-sys-typescale-body-medium-font-style);
412
  letter-spacing: var(--md-sys-typescale-body-medium-tracking);
413
  line-height: var(--md-sys-typescale-body-medium-line-height);
414
  text-transform: var(--md-sys-typescale-body-medium-text-transform);
415
  text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
416
}
417
/* Body Large */
418
.body-large {
419
  font-family: var(--md-sys-typescale-body-large-font);
420
  font-weight: var(--md-sys-typescale-body-large-weight);
421
  font-size: var(--md-sys-typescale-body-large-size);
422
  font-style: var(--md-sys-typescale-body-large-font-style);
423
  letter-spacing: var(--md-sys-typescale-body-large-tracking);
424
  line-height: var(--md-sys-typescale-body-large-line-height);
425
  text-transform: var(--md-sys-typescale-body-large-text-transform);
426
  text-decoration: var(--md-sys-typescale-body-large-text-decoration);
427
}
428
/* Title Small */
429
.title-small {
430
  font-family: var(--md-sys-typescale-title-small-font);
431
  font-weight: var(--md-sys-typescale-title-small-weight);
432
  font-size: var(--md-sys-typescale-title-small-size);
433
  font-style: var(--md-sys-typescale-title-small-font-style);
434
  letter-spacing: var(--md-sys-typescale-title-small-tracking);
435
  line-height: var(--md-sys-typescale-title-small-line-height);
436
  text-transform: var(--md-sys-typescale-title-small-text-transform);
437
  text-decoration: var(--md-sys-typescale-title-small-text-decoration);
438
}
439
/* Title Medium */
440
.title-medium {
441
  font-family: var(--md-sys-typescale-title-medium-font);
442
  font-weight: var(--md-sys-typescale-title-medium-weight);
443
  font-size: var(--md-sys-typescale-title-medium-size);
444
  font-style: var(--md-sys-typescale-title-medium-font-style);
445
  letter-spacing: var(--md-sys-typescale-title-medium-tracking);
446
  line-height: var(--md-sys-typescale-title-medium-line-height);
447
  text-transform: var(--md-sys-typescale-title-medium-text-transform);
448
  text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
449
}
450
/* Title Large */
451
.title-large {
452
  font-family: var(--md-sys-typescale-title-large-font);
453
  font-weight: var(--md-sys-typescale-title-large-weight);
454
  font-size: var(--md-sys-typescale-title-large-size);
455
  font-style: var(--md-sys-typescale-title-large-font-style);
456
  letter-spacing: var(--md-sys-typescale-title-large-tracking);
457
  line-height: var(--md-sys-typescale-title-large-line-height);
458
  text-transform: var(--md-sys-typescale-title-large-text-transform);
459
  text-decoration: var(--md-sys-typescale-title-large-text-decoration);
460
}
461
/* Headline Small */
462
.headline-small {
463
  font-family: var(--md-sys-typescale-headline-small-font);
464
  font-weight: var(--md-sys-typescale-headline-small-weight);
465
  font-size: var(--md-sys-typescale-headline-small-size);
466
  font-style: var(--md-sys-typescale-headline-small-font-style);
467
  letter-spacing: var(--md-sys-typescale-headline-small-tracking);
468
  line-height: var(--md-sys-typescale-headline-small-line-height);
469
  text-transform: var(--md-sys-typescale-headline-small-text-transform);
470
  text-decoration: var(--md-sys-typescale-headline-small-text-decoration);
471
}
472
/* Headline Medium */
473
.headline-medium {
474
  font-family: var(--md-sys-typescale-headline-medium-font);
475
  font-weight: var(--md-sys-typescale-headline-medium-weight);
476
  font-size: var(--md-sys-typescale-headline-medium-size);
477
  font-style: var(--md-sys-typescale-headline-medium-font-style);
478
  letter-spacing: var(--md-sys-typescale-headline-medium-tracking);
479
  line-height: var(--md-sys-typescale-headline-medium-line-height);
480
  text-transform: var(--md-sys-typescale-headline-medium-text-transform);
481
  text-decoration: var(--md-sys-typescale-headline-medium-text-decoration);
482
}
483
/* Headline Large */
484
.headline-large {
485
  font-family: var(--md-sys-typescale-headline-large-font);
486
  font-weight: var(--md-sys-typescale-headline-large-weight);
487
  font-size: var(--md-sys-typescale-headline-large-size);
488
  font-style: var(--md-sys-typescale-headline-large-font-style);
489
  letter-spacing: var(--md-sys-typescale-headline-large-tracking);
490
  line-height: var(--md-sys-typescale-headline-large-line-height);
491
  text-transform: var(--md-sys-typescale-headline-large-text-transform);
492
  text-decoration: var(--md-sys-typescale-headline-large-text-decoration);
493
}
494
/* Display Small */
495
.display-small {
496
  font-family: var(--md-sys-typescale-display-small-font);
497
  font-weight: var(--md-sys-typescale-display-small-weight);
498
  font-size: var(--md-sys-typescale-display-small-size);
499
  font-style: var(--md-sys-typescale-display-small-font-style);
500
  letter-spacing: var(--md-sys-typescale-display-small-tracking);
501
  line-height: var(--md-sys-typescale-display-small-line-height);
502
  text-transform: var(--md-sys-typescale-display-small-text-transform);
503
  text-decoration: var(--md-sys-typescale-display-small-text-decoration);
504
}
505
/* Display Medium */
506
.display-medium {
507
  font-family: var(--md-sys-typescale-display-medium-font);
508
  font-weight: var(--md-sys-typescale-display-medium-weight);
509
  font-size: var(--md-sys-typescale-display-medium-size);
510
  font-style: var(--md-sys-typescale-display-medium-font-style);
511
  letter-spacing: var(--md-sys-typescale-display-medium-tracking);
512
  line-height: var(--md-sys-typescale-display-medium-line-height);
513
  text-transform: var(--md-sys-typescale-display-medium-text-transform);
514
  text-decoration: var(--md-sys-typescale-display-medium-text-decoration);
515
}
516
/* Display Large */
517
.display-large {
518
  font-family: var(--md-sys-typescale-display-large-font);
519
  font-weight: var(--md-sys-typescale-display-large-weight);
520
  font-size: var(--md-sys-typescale-display-large-size);
521
  font-style: var(--md-sys-typescale-display-large-font-style);
522
  letter-spacing: var(--md-sys-typescale-display-large-tracking);
523
  line-height: var(--md-sys-typescale-display-large-line-height);
524
  text-transform: var(--md-sys-typescale-display-large-text-transform);
525
  text-decoration: var(--md-sys-typescale-display-large-text-decoration);
526
}
527

S. Carpeta « css / theme »

1. css / theme / dark.css

1
.dark {
2
  --md-sys-color-primary: rgb(170 199 255);
3
  --md-sys-color-surface-tint: rgb(170 199 255);
4
  --md-sys-color-on-primary: rgb(10 48 95);
5
  --md-sys-color-primary-container: rgb(40 71 119);
6
  --md-sys-color-on-primary-container: rgb(214 227 255);
7
  --md-sys-color-secondary: rgb(190 198 220);
8
  --md-sys-color-on-secondary: rgb(40 49 65);
9
  --md-sys-color-secondary-container: rgb(62 71 89);
10
  --md-sys-color-on-secondary-container: rgb(218 226 249);
11
  --md-sys-color-tertiary: rgb(221 188 224);
12
  --md-sys-color-on-tertiary: rgb(63 40 68);
13
  --md-sys-color-tertiary-container: rgb(87 62 92);
14
  --md-sys-color-on-tertiary-container: rgb(250 216 253);
15
  --md-sys-color-error: rgb(255 180 171);
16
  --md-sys-color-on-error: rgb(105 0 5);
17
  --md-sys-color-error-container: rgb(147 0 10);
18
  --md-sys-color-on-error-container: rgb(255 218 214);
19
  --md-sys-color-background: rgb(17 19 24);
20
  --md-sys-color-on-background: rgb(226 226 233);
21
  --md-sys-color-surface: rgb(17 19 24);
22
  --md-sys-color-on-surface: rgb(226 226 233);
23
  --md-sys-color-surface-variant: rgb(68 71 78);
24
  --md-sys-color-on-surface-variant: rgb(196 198 208);
25
  --md-sys-color-outline: rgb(142 144 153);
26
  --md-sys-color-outline-variant: rgb(68 71 78);
27
  --md-sys-color-shadow: rgb(0 0 0);
28
  --md-sys-color-scrim: rgb(0 0 0);
29
  --md-sys-color-inverse-surface: rgb(226 226 233);
30
  --md-sys-color-inverse-on-surface: rgb(46 48 54);
31
  --md-sys-color-inverse-primary: rgb(65 95 145);
32
  --md-sys-color-primary-fixed: rgb(214 227 255);
33
  --md-sys-color-on-primary-fixed: rgb(0 27 62);
34
  --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35
  --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36
  --md-sys-color-secondary-fixed: rgb(218 226 249);
37
  --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38
  --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39
  --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40
  --md-sys-color-tertiary-fixed: rgb(250 216 253);
41
  --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42
  --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43
  --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44
  --md-sys-color-surface-dim: rgb(17 19 24);
45
  --md-sys-color-surface-bright: rgb(55 57 62);
46
  --md-sys-color-surface-container-lowest: rgb(12 14 19);
47
  --md-sys-color-surface-container-low: rgb(25 28 32);
48
  --md-sys-color-surface-container: rgb(29 32 36);
49
  --md-sys-color-surface-container-high: rgb(40 42 47);
50
  --md-sys-color-surface-container-highest: rgb(51 53 58);
51
}
52

2. css / theme / light.css

1
.light {
2
  --md-sys-color-primary: rgb(65 95 145);
3
  --md-sys-color-surface-tint: rgb(65 95 145);
4
  --md-sys-color-on-primary: rgb(255 255 255);
5
  --md-sys-color-primary-container: rgb(214 227 255);
6
  --md-sys-color-on-primary-container: rgb(40 71 119);
7
  --md-sys-color-secondary: rgb(86 95 113);
8
  --md-sys-color-on-secondary: rgb(255 255 255);
9
  --md-sys-color-secondary-container: rgb(218 226 249);
10
  --md-sys-color-on-secondary-container: rgb(62 71 89);
11
  --md-sys-color-tertiary: rgb(112 85 117);
12
  --md-sys-color-on-tertiary: rgb(255 255 255);
13
  --md-sys-color-tertiary-container: rgb(250 216 253);
14
  --md-sys-color-on-tertiary-container: rgb(87 62 92);
15
  --md-sys-color-error: rgb(186 26 26);
16
  --md-sys-color-on-error: rgb(255 255 255);
17
  --md-sys-color-error-container: rgb(255 218 214);
18
  --md-sys-color-on-error-container: rgb(147 0 10);
19
  --md-sys-color-background: rgb(249 249 255);
20
  --md-sys-color-on-background: rgb(25 28 32);
21
  --md-sys-color-surface: rgb(249 249 255);
22
  --md-sys-color-on-surface: rgb(25 28 32);
23
  --md-sys-color-surface-variant: rgb(224 226 236);
24
  --md-sys-color-on-surface-variant: rgb(68 71 78);
25
  --md-sys-color-outline: rgb(116 119 127);
26
  --md-sys-color-outline-variant: rgb(196 198 208);
27
  --md-sys-color-shadow: rgb(0 0 0);
28
  --md-sys-color-scrim: rgb(0 0 0);
29
  --md-sys-color-inverse-surface: rgb(46 48 54);
30
  --md-sys-color-inverse-on-surface: rgb(240 240 247);
31
  --md-sys-color-inverse-primary: rgb(170 199 255);
32
  --md-sys-color-primary-fixed: rgb(214 227 255);
33
  --md-sys-color-on-primary-fixed: rgb(0 27 62);
34
  --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35
  --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36
  --md-sys-color-secondary-fixed: rgb(218 226 249);
37
  --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38
  --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39
  --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40
  --md-sys-color-tertiary-fixed: rgb(250 216 253);
41
  --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42
  --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43
  --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44
  --md-sys-color-surface-dim: rgb(217 217 224);
45
  --md-sys-color-surface-bright: rgb(249 249 255);
46
  --md-sys-color-surface-container-lowest: rgb(255 255 255);
47
  --md-sys-color-surface-container-low: rgb(243 243 250);
48
  --md-sys-color-surface-container: rgb(237 237 244);
49
  --md-sys-color-surface-container-high: rgb(231 232 238);
50
  --md-sys-color-surface-container-highest: rgb(226 226 233);
51
}
52

K. Carpeta « fonts »

A. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints

B. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf

C. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2

D. fonts / roboto-v32-latin-regular.woff2

L. Carpeta « img »

A. img / icono2048.png

icono2048.png

B. img / maskable_icon.png

maskable_icon.png

C. img / maskable_icon_x128.png

maskable_icon_x128.png

D. img / maskable_icon_x192.png

maskable_icon_x192.png

E. img / maskable_icon_x384.png

maskable_icon_x384.png

F. img / maskable_icon_x48.png

maskable_icon_x48.png

G. img / maskable_icon_x512.png

maskable_icon_x512.png

H. img / maskable_icon_x72.png

maskable_icon_x72.png

I. img / maskable_icon_x96.png

maskable_icon_x96.png

J. img / screenshot_horizontal.png

screenshot_horizontal.png

K. img / screenshot_vertical.png

screenshot_vertical.png

M. Carpeta « js »

A. js / nav-tab-fixed.js

1
import { resaltaSiEstasEn } from "./lib/resaltaSiEstasEn.js"
2
3
export class NavTabFixed extends HTMLElement {
4
5
 constructor() {
6
  super()
7
  this.creado = false
8
 }
9
10
 connectedCallback() {
11
  this.classList.add("md-tab", "fixed")
12
13
  if (!this.creado) {
14
15
   this.innerHTML = /* HTML */`
16
    <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html">
17
     <span class="material-symbols-outlined">home</span>
18
     Inicio
19
    </a>
20
 
21
    <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html">
22
     <span class="material-symbols-outlined">help</span>
23
     Ayuda
24
    </a>`
25
26
   this.creado = true
27
28
  }
29
30
 }
31
32
}
33
34
customElements.define("nav-tab-fixed", NavTabFixed)

B. Carpeta « js / lib »

1. js / lib / ES_APPLE.js

1
export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent)

2. js / lib / getAttribute.js

1
/**
2
 * @param {HTMLElement} elementoHtml
3
 * @param {string} nombre
4
 * @returns {string}
5
 */
6
export function getAttribute(elementoHtml, nombre) {
7
 const valor = elementoHtml.getAttribute(nombre)
8
 return valor === null ? "" : valor
9
}

3. js / lib / manejaErrores.js

1
import { muestraError } from "./muestraError.js"
2
3
/**
4
 * Intercepta Response.prototype.json para capturar errores de parseo
5
 * y asegurar que se reporten correctamente en navegadores Chromium.
6
 */
7
{
8
 const originalJson = Response.prototype.json
9
10
 Response.prototype.json = function () {
11
  // Llamamos al método original usando el contexto (this) de la respuesta
12
  return originalJson.apply(this, arguments)
13
   .catch((/** @type {any} */ error) => {
14
    // Corrige un error de Chrome que evita el manejo correcto de errores.
15
    throw new Error(error)
16
   })
17
 }
18
}
19
20
window.onerror = function (
21
  /** @type {string} */ _message,
22
  /** @type {string} */ _url,
23
  /** @type {number} */ _line,
24
  /** @type {number} */ _column,
25
  /** @type {Error} */ errorObject
26
) {
27
 muestraError(errorObject)
28
 return true
29
}
30
31
window.addEventListener('unhandledrejection', event => {
32
 muestraError(event.reason)
33
 event.preventDefault()
34
})
35

4. js / lib / muestraError.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Muestra los datos de una Error en la consola y en un cuadro de alerta.
5
 * @param { ProblemDetailsError | Error | null } error descripción del error.
6
 */
7
export function muestraError(error) {
8
9
 if (error === null) {
10
11
  console.error("Error")
12
  alert("Error")
13
14
 } else if (error instanceof ProblemDetailsError) {
15
16
  const problemDetails = error.problemDetails
17
18
  let mensaje =
19
   typeof problemDetails["title"] === "string" ? problemDetails["title"] : ""
20
  if (typeof problemDetails["detail"] === "string") {
21
   if (mensaje !== "") {
22
    mensaje += "\n\n"
23
   }
24
   mensaje += problemDetails["detail"]
25
  }
26
  if (mensaje === "") {
27
   mensaje = "Error"
28
  }
29
  console.error(error, problemDetails)
30
  alert(mensaje)
31
32
 } else {
33
34
  console.error(error)
35
  alert(error.message)
36
37
 }
38
39
}

5. js / lib / muestraTextoDeAyuda.js

1
/**
2
 * Si un elemento HTML tiene un mensaje de validación, lo
3
 * muestra en su elemento de ayuda; en caso contrario, muestra
4
 * un mensaje de ayuda. 
5
 * @param { {
6
 *   validity: { valid: boolean };
7
 *   validationMessage: string
8
 *  } } elementoHtml elemento que contiene datos de validación.
9
 * @param { HTMLElement } elementoDeAyuda elemento fonde
10
 * se muestran los elementos de validación para elementoHtml.
11
 * @param { string } mensajeDeAyuda mensaje de ayuda cuando el
12
 *  estado de elementoHtml es válido.
13
 */
14
export function muestraTextoDeAyuda(elementoHtml, elementoDeAyuda,
15
 mensajeDeAyuda) {
16
 if (elementoHtml.validity.valid) {
17
  elementoDeAyuda.textContent = mensajeDeAyuda
18
 } else {
19
  elementoDeAyuda.textContent = elementoHtml.validationMessage
20
 }
21
}

6. js / lib / ProblemDetailsError.js

1
export class ProblemDetailsError extends Error {
2
3
 /**
4
  * Detalle de los errores devueltos por un servicio.
5
  * Crea una instancia de ProblemDetailsError.
6
  * @param {object} problemDetails Objeto con la descripcipon del error.
7
  */
8
 constructor(problemDetails) {
9
10
  super(typeof problemDetails["detail"] === "string"
11
   ? problemDetails["detail"]
12
   : (typeof problemDetails["title"] === "string"
13
    ? problemDetails["title"]
14
    : "Error"))
15
16
  this.problemDetails = problemDetails
17
18
 }
19
20
}

7. js / lib / querySelector.js

1
/**
2
 * @template { HTMLElement } T
3
 * @param { Document | Element | ShadowRoot } raiz
4
 * @param { string } query
5
 * @returns { T }
6
 */
7
export function querySelector(raiz, query) {
8
 /** @type { T | null } */
9
 const resutado = raiz.querySelector(query)
10
 if (resutado === null)
11
  throw new Error(`No se encuentra ${query}.`)
12
 return resutado
13
}

8. js / lib / registraServiceWorker.js

1
const nombreDeServiceWorker = "sw.js"
2
3
try {
4
 navigator.serviceWorker.register(nombreDeServiceWorker)
5
  .then(registro => {
6
   console.log(nombreDeServiceWorker, "registrado.")
7
   console.log(registro)
8
  })
9
  .catch(error => console.log(error))
10
} catch (error) {
11
 console.log(error)
12
}

9. js / lib / resaltaSiEstasEn.js

1
/**
2
 * @param {string[]} paginas
3
 */
4
export function resaltaSiEstasEn(paginas) {
5
6
 const pathname = location.pathname
7
8
 for (const pagina of paginas) {
9
10
  if (pathname === pagina) {
11
   queueMicrotask(() => {
12
    const tab = document.querySelector(".active")
13
    if (tab !== null && tab.closest(".scrollable") !== null) {
14
     tab.scrollIntoView({ inline: "center", block: "end" })
15
    }
16
   })
17
   return `class="active"`
18
  }
19
20
 }
21
22
 return ""
23
24
}

10. Carpeta « js / lib / custom »

A. js / lib / custom / md-app-bar.js

1
import { ES_APPLE } from "../ES_APPLE.js"
2
import { getAttribute } from "../getAttribute.js"
3
import { querySelector } from "../querySelector.js"
4
5
class MdAppBar extends HTMLElement {
6
7
 getContent() {
8
  return /* HTML */`
9
   <style>
10
11
    :host {
12
     display: flex;
13
     box-sizing: border-box;
14
     align-items: center;
15
     padding: 0 0.25rem;
16
     background-color: var(--md-sys-color-surface);
17
     position: sticky;
18
     z-index: 1;
19
     left: env(titlebar-area-x, 0);
20
     top: env(titlebar-area-y, 0);
21
     height: env(titlebar-area-height, 4rem);
22
     width: env(titlebar-area-width, 100%);
23
    }
24
    
25
    :host(.apple) {
26
     height: env(titlebar-area-height, 3rem);
27
    }
28
29
    :host(.scroll) {
30
     background-color: var(--md-sys-color-surface-container-low);
31
    }
32
33
    #navigation {
34
     flex: 0 0 auto;
35
     overflow: hidden
36
    }
37
38
    #navigation ::slotted(*) {
39
     color: var(--md-sys-color-on-surface);
40
    }
41
42
    #acciones {
43
     margin-left: auto;
44
     flex: 0 0 auto;
45
     overflow: hidden
46
    }
47
48
    :host(.centered) #navigation,
49
    :host(.centered) #acciones {
50
     flex: 0 0 6rem;
51
     overflow: hidden
52
    }
53
54
    #headline::slotted(*) {
55
     -webkit-app-region: drag;
56
     flex: 1 1 auto;
57
     white-space: nowrap;
58
     text-overflow: ellipsis;
59
     overflow: hidden;
60
     font-family: var(--md-sys-typescale-title-large-font);
61
     font-weight: var(--md-sys-typescale-title-large-weight);
62
     font-size: var(--md-sys-typescale-title-large-size);
63
     font-style: var(--md-sys-typescale-title-large-font-style);
64
     letter-spacing: var(--md-sys-typescale-title-large-tracking);
65
     line-height: var(--md-sys-typescale-title-large-line-height);
66
     text-transform: var(--md-sys-typescale-title-large-text-transform);
67
     text-decoration: var(--md-sys-typescale-title-large-text-decoration);
68
     color: var(--md-sys-color-on-surface);
69
    }
70
71
    :host(.centered) #headline::slotted(*) {
72
     flex: 1 1 auto;
73
     text-align: center
74
    }
75
76
   </style>
77
78
   <span id="navigation">
79
    <slot name="navigation"></slot>
80
   </span>
81
   <slot id="headline"></slot>
82
   <span id="acciones">
83
    <slot name="action"></slot>
84
   </span>`
85
 }
86
87
 constructor() {
88
  super()
89
  if (ES_APPLE) {
90
   document.body.classList.add("apple")
91
   document.body.classList.remove("material")
92
  } else {
93
   document.body.classList.add("material")
94
   document.body.classList.remove("apple")
95
  }
96
97
  /**
98
   * @private
99
   * @readonly
100
   */
101
  const shadow = this.attachShadow({ mode: "open" })
102
  shadow.innerHTML = this.getContent()
103
  this._configuraAction = this._configuraAction.bind(this)
104
  /**
105
   * @private
106
   * @type {number}
107
   */
108
  this._posY = 0
109
  /**
110
   * @private
111
   * @type {boolean}
112
   */
113
  this._scrolling = false
114
  /**
115
    * @private
116
    * @type { HTMLSlotElement }
117
    */
118
  this._navigation = querySelector(shadow, '[name="navigation"]')
119
  /**
120
    * @private
121
    * @type { HTMLSlotElement }
122
    */
123
  this._action = querySelector(shadow, '[name="action"]')
124
  /**
125
    * @private
126
    * @type { HTMLHeadingElement | null }
127
    */
128
  this._headline = null
129
  /**
130
    * @private
131
    * @type { HTMLElement | null }
132
    */
133
  this._adicional = null
134
  this._action.addEventListener("slotchange", this._configuraAction)
135
  addEventListener("scroll", () => this._onScroll())
136
  addEventListener("load", () => this.configurOtros())
137
 }
138
139
 connectedCallback() {
140
  this.role = "toolbar"
141
  this._configuraAction()
142
 }
143
144
 configurOtros() {
145
  const idHeadline = getAttribute(this, "headline")
146
  if (idHeadline !== "") {
147
   const headline = document.getElementById(idHeadline)
148
   if (headline instanceof HTMLHeadingElement) {
149
    this._headline = headline
150
    if (this.classList.contains("apple") || this.classList.contains("medium")) {
151
     headline.classList.add("md-headline", "headline-small")
152
    } else {
153
     headline.classList.add("md-headline", "headline-medium")
154
    }
155
   }
156
  }
157
  const idAdicional = getAttribute(this, "adicional")
158
  if (idAdicional !== "") {
159
   this._adicional = document.getElementById(idAdicional)
160
   if (this._adicional !== null) {
161
    if (this.classList.contains("apple")) {
162
     this._adicional.style.top = "env(titlebar-area-height, 3rem)"
163
    } else {
164
     this._adicional.style.top = "env(titlebar-area-height, 4rem)"
165
    }
166
   }
167
  }
168
 }
169
170
 _configuraAction() {
171
  const assignedElements = this._action.assignedElements()
172
  if (this.isConnected) {
173
   if (ES_APPLE) {
174
    this.classList.add("apple")
175
    this.classList.remove("material")
176
   } else {
177
    this.classList.add("material")
178
    this.classList.remove("apple")
179
   }
180
   if (this.classList.contains("centered")) {
181
    this.classList.remove("centrado")
182
    this.classList.remove("justificado")
183
   } else {
184
    if (ES_APPLE && assignedElements.length <= 1) {
185
     this.classList.add("centrado")
186
     this.classList.remove("justificado")
187
    } else {
188
     this.classList.add("justificado")
189
     this.classList.remove("centrado")
190
    }
191
   }
192
  }
193
 }
194
195
 /** @private */
196
 _onScroll() {
197
  this._posY = scrollY
198
  if (!this._scrolling) {
199
   requestAnimationFrame(() => this._avanza())
200
  }
201
  this._scrolling = true
202
 }
203
204
 /** @private */
205
 _avanza() {
206
  if (this._posY === 0) {
207
   this.classList.remove("scroll")
208
   if (this._headline !== null) {
209
    if (this._adicional === null) {
210
     this._headline.classList.remove("scroll")
211
    } else {
212
     this._headline.classList.remove("scroll-adicional")
213
    }
214
   }
215
   if (this._adicional !== null) {
216
    this._adicional.classList.remove("scroll")
217
   }
218
  } else {
219
   this.classList.add("scroll")
220
   if (this._headline !== null) {
221
    if (this._adicional === null) {
222
     this._headline.classList.add("scroll")
223
    } else {
224
     this._headline.classList.add("scroll-adicional")
225
    }
226
   }
227
   if (this._adicional !== null) {
228
    this._adicional.classList.add("scroll")
229
   }
230
  }
231
  this._scrolling = false
232
 }
233
234
}
235
236
customElements.define("md-app-bar", MdAppBar)

N. favicon.ico

favicon.ico

O. .htaccess

  • Este archivo configura las respuestas del servidor..

  • Lo utilizan principalmente servidores como Apache o Nginx.

  • Configura el mime type para el archivo de manifiesto y sedhabilita el uso de la cache general de http..

1
AddType application/manifest+json .webmanifest
2
3
ExpiresActive On
4
5
Header set Cache-Control "max-age=1, must-revalidate"
6

P. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

Q. instruccionesListadoSw.txt

1
Generar el listado de archivos del sw.js desde Visual Studio Code.
2
1. Abrir una terminal desde el menú 
3
    Terminal > New Terminal
4
5
2. Desde la terminal introducir la orden:
6
    Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
8
3. Abrir el archivo generado, que se llama
9
    archivos.txt
10
   y sobre este, realizar los pasos que siguen:
11
12
4. Quita del archivo archivos.txt:
13
    * el encabezado,
14
    * todas las carpetas,
15
    * todos los archivos dentro de .vscode como:
16
       * el archivo .vscode/settings.json,
17
       * el archivo .vscode/launch.json,
18
    * el archivo .htaccess,
19
    * el archivo archivos.txt,
20
    * este archivo (instruccionesListadoSw.txt),
21
    * el archivo jsconfig.json,
22
    * el archivo sw.js,
23
    * el archivo de la base de datos, que termina en ".db" y
24
      está en la carpeta php,
25
    * todos los archivos de php y
26
    * las líneas en blanco del final
27
28
5. Cambia los \ por / desde Visual Studio Code con las siguientes
29
   combinaciones de teclas:
30
31
    Ctrl+H En el diálogo que aparece introduce lo siguiente:
32
    Find:\
33
    Replace:/
34
35
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
36
 
37
6. Coloca las comillas y coma del final de cada línea desde Visual
38
   Studio Code con las siguientes combinaciones de teclas:
39
40
    Ctrl+H En el diálogo que aparece, selecciona el botón
41
            ".*"
42
           e introduce lo siguiente:
43
    Find:\s*$
44
    Replace:",
45
46
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
47
48
7. Marca la carpeta inicial, presiona la combinación de teclas:
49
50
    Shift+Ctrl+L
51
52
    borra la selección, teclea " y luego ESC
53
54
8. Cambia las secuencias de espacios por / con las siguientes
55
   combinaciones de teclas:
56
57
    Ctrl+H En el diálogo que aparece, selecciona el botón
58
            ".*"
59
           e introduce lo siguiente:
60
    Find:\s+
61
    Replace:/
62
63
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
64
65
9. Cambia las "/ por " con las siguientes combinaciones de teclas:
66
67
    Ctrl+H En el diálogo que aparece, quita la selección del botón
68
            ".*"
69
           e introduce lo siguiente:
70
    Find:"/
71
    Replace:"
72
73
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
74
75
10. Copia el texto al archivo
76
     sw.js
77
    en el contenido del arreglo llamado ARCHIVOS, pero recuerda
78
    mantener el último elemento, que dice:
79
     "/"

R. archivos.txt

1
"ayuda.html",
2
"favicon.ico",
3
"index.html",
4
"site.webmanifest",
5
"css/baseline.css",
6
"css/colors.css",
7
"css/elevation.css",
8
"css/estilos.css",
9
"css/material-symbols-outlined.css",
10
"css/md-filled-button.css",
11
"css/md-filled-text-field.css",
12
"css/md-headline.css",
13
"css/md-list.css",
14
"css/md-outline-button.css",
15
"css/md-tab.css",
16
"css/motion.css",
17
"css/palette.css",
18
"css/roboto.css",
19
"css/shape.css",
20
"css/state.css",
21
"css/transicion_pestanas.css",
22
"css/typography.css",
23
"css/theme/dark.css",
24
"css/theme/light.css",
25
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
26
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
27
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
28
"fonts/roboto-v32-latin-regular.woff2",
29
"img/icono2048.png",
30
"img/maskable_icon.png",
31
"img/maskable_icon_x128.png",
32
"img/maskable_icon_x192.png",
33
"img/maskable_icon_x384.png",
34
"img/maskable_icon_x48.png",
35
"img/maskable_icon_x512.png",
36
"img/maskable_icon_x72.png",
37
"img/maskable_icon_x96.png",
38
"img/screenshot_horizontal.png",
39
"img/screenshot_vertical.png",
40
"js/nav-tab-fixed.js",
41
"js/lib/ES_APPLE.js",
42
"js/lib/getAttribute.js",
43
"js/lib/manejaErrores.js",
44
"js/lib/muestraError.js",
45
"js/lib/muestraTextoDeAyuda.js",
46
"js/lib/ProblemDetailsError.js",
47
"js/lib/querySelector.js",
48
"js/lib/registraServiceWorker.js",
49
"js/lib/resaltaSiEstasEn.js",
50
"js/lib/custom/md-app-bar.js",
51
"ungap/custom-elements.js",

S. Carpeta « ungap »

A. ungap / custom-elements.js

1
/*! (c) Andrea Giammarchi @webreflection ISC */
2
!function(){"use strict";var e=function(e,t){var n=function(e){for(var t=0,n=e.length;t<n;t++)r(e[t])},r=function(e){var t=e.target,n=e.attributeName,r=e.oldValue;t.attributeChangedCallback(n,r,t.getAttribute(n))};return function(o,a){var l=o.constructor.observedAttributes;return l&&e(a).then((function(){new t(n).observe(o,{attributes:!0,attributeOldValue:!0,attributeFilter:l});for(var e=0,a=l.length;e<a;e++)o.hasAttribute(l[e])&&r({target:o,attributeName:l[e],oldValue:null})})),o}};function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function n(e,n){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,n){if(e){if("string"==typeof e)return t(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?t(e,n):void 0}}(e))||n&&e&&"number"==typeof e.length){r&&(e=r);var o=0,a=function(){};return{s:a,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,i=!0,c=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return i=e.done,e},e:function(e){c=!0,l=e},f:function(){try{i||null==r.return||r.return()}finally{if(c)throw l}}}}
3
/*! (c) Andrea Giammarchi - ISC */var r=!0,o=!1,a="querySelectorAll",l="querySelectorAll",i=self,c=i.document,u=i.Element,s=i.MutationObserver,f=i.Set,d=i.WeakMap,h=function(e){return l in e},v=[].filter,p=function(e){var t=new d,i=function(n,r){var o;if(r)for(var a,l=function(e){return e.matches||e.webkitMatchesSelector||e.msMatchesSelector}(n),i=0,c=y.length;i<c;i++)l.call(n,a=y[i])&&(t.has(n)||t.set(n,new f),(o=t.get(n)).has(a)||(o.add(a),e.handle(n,r,a)));else t.has(n)&&(o=t.get(n),t.delete(n),o.forEach((function(t){e.handle(n,r,t)})))},p=function(e){for(var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],n=0,r=e.length;n<r;n++)i(e[n],t)},y=e.query,g=e.root||c,m=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:document,l=arguments.length>2&&void 0!==arguments[2]?arguments[2]:MutationObserver,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:["*"],c=function t(o,l,i,c,u,s){var f,d=n(o);try{for(d.s();!(f=d.n()).done;){var h=f.value;(s||a in h)&&(u?i.has(h)||(i.add(h),c.delete(h),e(h,u)):c.has(h)||(c.add(h),i.delete(h),e(h,u)),s||t(h[a](l),l,i,c,u,r))}}catch(e){d.e(e)}finally{d.f()}},u=new l((function(e){if(i.length){var t,a=i.join(","),l=new Set,u=new Set,s=n(e);try{for(s.s();!(t=s.n()).done;){var f=t.value,d=f.addedNodes,h=f.removedNodes;c(h,a,l,u,o,o),c(d,a,l,u,r,o)}}catch(e){s.e(e)}finally{s.f()}}})),s=u.observe;return(u.observe=function(e){return s.call(u,e,{subtree:r,childList:r})})(t),u}(i,g,s,y),w=u.prototype.attachShadow;return w&&(u.prototype.attachShadow=function(e){var t=w.call(this,e);return m.observe(t),t}),y.length&&p(g[l](y)),{drop:function(e){for(var n=0,r=e.length;n<r;n++)t.delete(e[n])},flush:function(){for(var e=m.takeRecords(),t=0,n=e.length;t<n;t++)p(v.call(e[t].removedNodes,h),!1),p(v.call(e[t].addedNodes,h),!0)},observer:m,parse:p}},y=self,g=y.document,m=y.Map,w=y.MutationObserver,b=y.Object,E=y.Set,S=y.WeakMap,A=y.Element,M=y.HTMLElement,O=y.Node,N=y.Error,C=y.TypeError,T=y.Reflect,q=b.defineProperty,D=b.keys,I=b.getOwnPropertyNames,P=b.setPrototypeOf,k=!self.customElements,L=function(e){for(var t=D(e),n=[],r=new E,o=t.length,a=0;a<o;a++){n[a]=e[t[a]];try{delete e[t[a]]}catch(e){r.add(a)}}return function(){for(var a=0;a<o;a++)r.has(a)||(e[t[a]]=n[a])}};if(k){var x=function(){var e=this.constructor;if(!$.has(e))throw new C("Illegal constructor");var t=$.get(e);if(W)return F(W,t);var n=H.call(g,t);return F(P(n,e.prototype),t)},H=g.createElement,$=new m,_=new m,j=new m,R=new m,V=[],U=p({query:V,handle:function(e,t,n){var r=j.get(n);if(t&&!r.isPrototypeOf(e)){var o=L(e);W=P(e,r);try{new r.constructor}finally{W=null,o()}}var a="".concat(t?"":"dis","connectedCallback");a in r&&e[a]()}}).parse,W=null,B=function(e){if(!_.has(e)){var t,n=new Promise((function(e){t=e}));_.set(e,{$:n,_:t})}return _.get(e).$},F=e(B,w);self.customElements={define:function(e,t){if(R.has(e))throw new N('the name "'.concat(e,'" has already been used with this registry'));$.set(t,e),j.set(e,t.prototype),R.set(e,t),V.push(e),B(e).then((function(){U(g.querySelectorAll(e))})),_.get(e)._(t)},get:function(e){return R.get(e)},whenDefined:B},q(x.prototype=M.prototype,"constructor",{value:x}),self.HTMLElement=x,g.createElement=function(e,t){var n=t&&t.is,r=n?R.get(n):R.get(e);return r?new r:H.call(g,e)},"isConnected"in O.prototype||q(O.prototype,"isConnected",{configurable:!0,get:function(){return!(this.ownerDocument.compareDocumentPosition(this)&this.DOCUMENT_POSITION_DISCONNECTED)}})}else if(k=!self.customElements.get("extends-br"))try{var z=function e(){return self.Reflect.construct(HTMLBRElement,[],e)};z.prototype=HTMLLIElement.prototype;var G="extends-br";self.customElements.define("extends-br",z,{extends:"br"}),k=g.createElement("br",{is:G}).outerHTML.indexOf(G)<0;var J=self.customElements,K=J.get,Q=J.whenDefined;self.customElements.whenDefined=function(e){var t=this;return Q.call(this,e).then((function(n){return n||K.call(t,e)}))}}catch(e){}if(k){var X=function(e){var t=ae.get(e);ve(t.querySelectorAll(this),e.isConnected)},Y=self.customElements,Z=g.createElement,ee=Y.define,te=Y.get,ne=Y.upgrade,re=T||{construct:function(e){return e.call(this)}},oe=re.construct,ae=new S,le=new E,ie=new m,ce=new m,ue=new m,se=new m,fe=[],de=[],he=function(e){return se.get(e)||te.call(Y,e)},ve=p({query:de,handle:function(e,t,n){var r=ue.get(n);if(t&&!r.isPrototypeOf(e)){var o=L(e);we=P(e,r);try{new r.constructor}finally{we=null,o()}}var a="".concat(t?"":"dis","connectedCallback");a in r&&e[a]()}}).parse,pe=p({query:fe,handle:function(e,t){ae.has(e)&&(t?le.add(e):le.delete(e),de.length&&X.call(de,e))}}).parse,ye=A.prototype.attachShadow;ye&&(A.prototype.attachShadow=function(e){var t=ye.call(this,e);return ae.set(this,t),t});var ge=function(e){if(!ce.has(e)){var t,n=new Promise((function(e){t=e}));ce.set(e,{$:n,_:t})}return ce.get(e).$},me=e(ge,w),we=null;I(self).filter((function(e){return/^HTML.*Element$/.test(e)})).forEach((function(e){var t=self[e];function n(){var e=this.constructor;if(!ie.has(e))throw new C("Illegal constructor");var n=ie.get(e),r=n.is,o=n.tag;if(r){if(we)return me(we,r);var a=Z.call(g,o);return a.setAttribute("is",r),me(P(a,e.prototype),r)}return oe.call(this,t,[],e)}q(n.prototype=t.prototype,"constructor",{value:n}),q(self,e,{value:n})})),g.createElement=function(e,t){var n=t&&t.is;if(n){var r=se.get(n);if(r&&ie.get(r).tag===e)return new r}var o=Z.call(g,e);return n&&o.setAttribute("is",n),o},Y.get=he,Y.whenDefined=ge,Y.upgrade=function(e){var t=e.getAttribute("is");if(t){var n=se.get(t);if(n)return void me(P(e,n.prototype),t)}ne.call(Y,e)},Y.define=function(e,t,n){if(he(e))throw new N("'".concat(e,"' has already been defined as a custom element"));var r,o=n&&n.extends;ie.set(t,o?{is:e,tag:o}:{is:"",tag:e}),o?(r="".concat(o,'[is="').concat(e,'"]'),ue.set(r,t.prototype),se.set(e,t),de.push(r)):(ee.apply(Y,arguments),fe.push(r=e)),ge(e).then((function(){o?(ve(g.querySelectorAll(r)),le.forEach(X,[r])):pe(g.querySelectorAll(r))})),ce.get(e)._(t)}}}();

T. Resumen

  • En esta lección se presentó la estructura básica de una PWA con un formulario y Material Design 3 Expressive.

8. Acceso al dispositivo

Versión para imprimir.

A. Introduccion

  • En esta lección se muestra como aceder directamente a algunas funciones del dispositivo.

B. Geolocalización

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

1
<!DOCTYPE html>
2
<html lang="es">
3

    
4
<head>
5

    
6
 <meta charset="UTF-8">
7
 <meta name="viewport"
8
   content="width=device-width">
9

    
10
 <title>GPS</title>
11

    
12
 <style>
13
  html {
14
   color-scheme: light dark;
15
  }
16
 </style>
17

    
18
</head>
19

    
20
<body>
21

    
22
 <h1>GPS</h1>
23

    
24
 <p>
25
  <label>
26
   Latitud
27
   <output id="latitud"></output>
28
  </label>
29
 </p>
30

    
31
 <p>
32
  <label>
33
   Longitud
34
   <output id="longitud"></output>
35
  </label>
36
 </p>
37

    
38
 <script>
39

    
40
  navigator.geolocation.
41
   watchPosition(p => {
42

    
43
    latitud.value =
44
     p.coords.latitude
45

    
46
    longitud.value =
47
     p.coords.longitude
48

    
49
   });
50

    
51
 </script>
52

    
53
</body>
54

    
55
</html>

C. Archivos y cámara

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

Observa como funciona en Android.

Observa como funciona en iOS.

1
<!DOCTYPE html>
2
<html lang="es">
3

    
4
<head>
5

    
6
 <meta charset="UTF-8">
7
 <meta name="viewport"
8
   content="width=device-width">
9

    
10
 <title>Archivos y Cámara</title>
11

    
12
 <style>
13
  html {
14
   color-scheme: light dark;
15
  }
16
 </style>
17

    
18
</head>
19

    
20
<body>
21

    
22
 <h1>Archivos y Cámara</h1>
23

    
24
 <p>
25
  <label>
26
   Foto de Móvil
27
   <input type="file"
28
     accept="image/*"
29
     capture="camera">
30
  </label>
31
 </p>
32

    
33
 <p>
34
  <label>
35
   Video de Móvil
36
   <input type="file"
37
     accept="video/*"
38
     capture="">
39
  </label>
40
 </p>
41

    
42
 <p>
43
  <label>
44
   Archivo
45
   <input type="file">
46
  </label>
47
 </p>
48

    
49
</body>
50

    
51
</html>

D. Uso directo de la cámara

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

1
<!DOCTYPE html>
2
<html>
3

    
4
<head>
5

    
6
 <meta charset="utf-8">
7
 <meta name="viewport"
8
   content="width=device-width">
9

    
10
 <title>Cámara</title>
11

    
12
 <style>
13
  html {
14
   color-scheme: light dark;
15
  }
16
 </style>
17

    
18
</head>
19

    
20
<body>
21

    
22
 <h1>Cámara</h1>
23

    
24
 <p>
25
  Para grabar o capturar imagen,
26
  cliquea
27
  <strong>Inicia</strong>.
28
 </p>
29

    
30
 <p>
31
  Para grabar por 5 segundos
32
  cliquea
33
  <strong>Graba</strong>
34
  y cliquea
35
  <strong>Para</strong> para
36
  detener.
37
 </p>
38

    
39
 <p>
40
  Para capturar una imagen de la
41
  cámara, cliquea
42
  <strong>Captura</strong>.
43
 </p>
44

    
45
 <menu style="display: flex;
46
   flex-wrap: wrap;
47
   list-style: none;">
48
  <li>
49
   <button type="button"
50
     onclick="inicia()">
51
    Inicia
52
   </button>
53
  </li>
54
  <li>
55
   <button type="button"
56
     onclick="graba()">
57
    Graba
58
   </button>
59
  </li>
60
  <li>
61
   <button type="button"
62
     onclick="para();">
63
    Para
64
   </button>
65
  </li>
66
  <li>
67
   <button type="button"
68
     onclick="captura()">
69
    Captura
70
   </button>
71
  </li>
72
 </menu>
73

    
74
 <section
75
   style="display: inline-block;
76
     vertical-align: top;">
77

    
78
  <h1>Preview</h1>
79

    
80
  <video id="preview" width="160"
81
    height="120" autoplay
82
    muted></video>
83

    
84
 </section>
85

    
86
 <section
87
   style="display: inline-block;
88
     vertical-align: top;">
89

    
90
  <h1>Recording</h1>
91

    
92
  <video id="recording" width="160"
93
    height="120" controls></video>
94

    
95
  <p>
96
   <a id="descarga">Descarga</a>
97
  </p>
98

    
99
  <div id="mensajes"></div>
100

    
101
 </section>
102

    
103
 <section
104
   style="display: inline-block;
105
     vertical-align: top;">
106

    
107
  <h1>Imagen</h1>
108

    
109
  <canvas id="canvas" width="160"
110
    height="120"></canvas>
111

    
112
  <p>
113
   <a id="descargaImagen">
114
    Descarga</a>
115
  </p>
116

    
117
 </section>
118

    
119
 <script>
120

    
121
  let stream = null
122

    
123
  let TIEMPO_DE_GRABACION = 5000
124

    
125
  var context =
126
   canvas.getContext('2d')
127

    
128
  async function inicia() {
129
   try {
130
    stream = await navigator
131
     .mediaDevices.getUserMedia({
132
      video: true,
133
      audio: true
134
     })
135
    preview.srcObject = stream
136
    descarga.href = stream
137
    preview.captureStream =
138
     preview.captureStream
139
     || preview.mozCaptureStream
140
    await new Promise(
141
     resolve =>
142
      preview.onplaying = resolve)
143
   } catch (e) {
144
    log(e.message)
145
   }
146
  }
147

    
148
  async function graba() {
149
   try {
150
    const recordedChunks =
151
     await grabaClip(stream,
152
      TIEMPO_DE_GRABACION)
153
    let recordedBlob = new Blob(
154
     recordedChunks,
155
     { type: "video/webm" })
156
    recording.src =
157
     URL.createObjectURL(
158
      recordedBlob)
159
    descarga.href = recording.src
160
    descarga.download =
161
     "RecordedVideo.webm"
162

    
163
    log("Exitosamente grabados "
164
     + recordedBlob.size
165
     + " bytes de "
166
     + recordedBlob.type
167
     + " media.")
168
   } catch (e) {
169
    log(e.message)
170
   }
171
  }
172

    
173
  function para() {
174
   detiene(preview.srcObject)
175
  }
176

    
177
  function captura() {
178
   context.drawImage(preview,
179
    0, 0, 160, 120)
180
   descargaImagen.href =
181
    canvas.toDataURL('image/jpeg')
182
   descargaImagen.download =
183
    "imagen.jpg"
184
  }
185

    
186
  function grabaClip(stream,
187
   milisegundos) {
188
   let recorder =
189
    new MediaRecorder(stream)
190
   let data = []
191
   recorder.ondataavailable =
192
    event => data.push(event.data)
193
   recorder.start()
194
   log(recorder.state
195
    + " durante "
196
    + (milisegundos / 1000)
197
    + " segundos…")
198
   let detenido = new Promise(
199
    (resolve, reject) => {
200
     recorder.onstop = resolve
201
     recorder.onerror =
202
      event => reject(event.name)
203
    })
204
   let grabado =
205
    espera(milisegundos)
206
     .then(() => recorder.state
207
      === "recording"
208
      && recorder.stop()
209
     )
210

    
211
   return Promise.all([
212
    detenido,
213
    grabado
214
   ])
215
    .then(() => data)
216
  }
217

    
218
  function detiene(stream) {
219
   stream.getTracks().forEach(
220
    track => track.stop())
221
  }
222

    
223
  function log(msg) {
224
   mensajes.innerHTML +=
225
    msg + "<br>"
226
  }
227

    
228
  function espera(milisegundos) {
229
   return new Promise(
230
    resolve => setTimeout(
231
     resolve, milisegundos))
232
  }
233

    
234
 </script>
235

    
236
</body>
237

    
238
</html>

E. Resumen

  • En esta lección se revisaron:

    • Acceso al GPS.

    • Acceso a la cámara.

    • Acceso a los archivos.

9. IoT

Versión para imprimir.

A. Introduccion

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar (con videos)

  1. Prueba el ejemplo en https://iothtml.rf.gd/.

  2. Copia la url de la app y pégala en varias pestañas, navegadores y dispositivos para que veas como entre todas estas vistas se puede controlar el foquito.

  3. Este proyecto puede correr simultáneamente en varios navegadores y computadoras. Todos interactuan con el servidor test.mosquitto.org.

  4. Este ejercicio usa la librería Eclipse Paho JavaScript Client para conectar el JavaScript del navegador web para conectarse al servidor de mqtt. Puedes profundizar en este tema en la URL https://eclipse.dev/paho/clients/js/

  5. Descarga el archivo /src/iothtml.zip y descompáctalo.

  6. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  7. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  8. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  9. Importa el proyecto de GitHub a Visual Studio Code

  10. Edita los archivos que desees.

  11. Prueba tu sitio localmente.

  12. Publica tu sitio. En este ejemplo no hay archivo sw.js ni necesitas esperar 11 o más minutos.

D. Hazlo funcionar (texto)

  1. Prueba el ejemplo en https://iothtml.rf.gd/.

  2. Copia la url de la app y pégala en varias pestañas, navegadores y dispositivos para que veas como entre todas estas vistas se puede chatear.

  3. Este proyecto puede correr simultáneamente en varios navegadores y computadoras. Todos interactuan con el servidor test.mosquitto.org.

  4. Este ejercicio usa la librería Eclipse Paho JavaScript Client para conectar el JavaScript del navegador web para conectarse al servidor de mqtt. Puedes profundizar en este tema en la URL https://eclipse.dev/paho/clients/js/

  5. Descarga el archivo /src/iothtml.zip y descompáctalo.

  6. Crea tu proyecto en GitHub pages:

    1. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

      • Mantén la selección Public.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

    5. Entra al repositorio y selecciona ⚙️ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto.Si haces cambios, clic en Save.

    6. En la misma pestaña de ⚙️ Settings selecciona ◯ Actions, luego selecciona General y en Actions permissions selecciona el rediobotón Allow xxxx, and select non-xxxx, actions and reusable workflows y abajo la casilla de verificación Allow actions created by GitHub y cliquea el botón Save.

  7. Importa el proyecto de GitHub a Visual Studio Code:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.

  8. Edita los archivos que desees.

  9. Haz clic derecho en index.html, selecciona Open with Live Server y se abre el navegador para que puedas probar localmente el ejemplo.

  10. Los cambios tardan 10 minutos en reflejarse en el navegador. Cuando desarrolles, es incómodo esperar a que se reflejen los cambios; en vez de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

  11. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push
  12. Si usas GitHub pages:

    1. Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.

    2. Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.

E. Archivos

F. index.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="utf-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>IoT</title>
10
11
 <script type="module" src="js/lib/manejaErrores.js"></script>
12
 <script src="paho.javascript-1.0.3/paho-mqtt-min.js"></script>
13
14
</head>
15
16
<body>
17
18
 <h1>IoT</h1>
19
20
 <p>
21
  <output id="salida">
22
   <progress max="100">Cargando…</progress>
23
  </output>
24
 </p>
25
26
 <p>
27
  <button id="boton" type="button">
28
   Enviar
29
  </button>
30
 </p>
31
32
 <script type="module">
33
34
  import { creaIdCliente } from "./js/lib/creaIdCliente.js"
35
  import { falloEnLaConexionMqtt } from "./js/lib/falloEnLaConexionMqtt.js"
36
  import { conexionMqttPerdida } from "./js/lib/conexionMqttPerdida.js"
37
  import { enviaMensajeMqtt } from "./js/lib/enviaMensajeMqtt.js"
38
39
  // A cada control le corresponde un tópico diferente.
40
  const TOPICO_FOCO = "gilpgdm/IoT/foco"
41
42
  let valor = "0"
43
44
  // Cambia por una raíz para tu proyecto.
45
  const clientId = creaIdCliente("gilpgdmIoT-")
46
47
  // Si usas un servidor de MQTT diferente, necesitas cambiar los parámetros.
48
  const cliente = new Paho.MQTT.Client("test.mosquitto.org", 8081, clientId)
49
50
  function clicEnBoton() {
51
    enviaMensajeMqtt(cliente, valor === '1' ? '0' : '1', TOPICO_FOCO)
52
  }
53
54
  // Acciones al recibir un mensaje.
55
  cliente.onMessageArrived = mensaje => {
56
   if (mensaje.destinationName === TOPICO_FOCO) {
57
    valor = mensaje.payloadString
58
    salida.value = valor === "1" ? "🔴" : "⚪"
59
   }
60
  }
61
62
  // Acciones al perder la conexión.
63
  cliente.onConnectionLost = conexionMqttPerdida
64
65
  // Configura el cliente.
66
  cliente.connect({
67
68
   // Manda un mensaje cada número de segundos indicados por este valor,
69
   // para que el sistema sepa que este dispositivo está activo.
70
   keepAliveInterval: 10,
71
72
   useSSL: true,
73
74
   // Acciones al fallar la conexión.
75
   onFailure: falloEnLaConexionMqtt,
76
77
   // Acciones al lograr la conexión.
78
   onSuccess: () => {
79
    console.log("Conectado")
80
    // Se suscribe a uno o más tópicos.
81
    cliente.subscribe(TOPICO_FOCO)
82
    // Envía el valor inicial al tópico.
83
    enviaMensajeMqtt(cliente, valor, TOPICO_FOCO)
84
    boton.addEventListener("click", clicEnBoton)
85
   },
86
87
  })
88
89
 </script>
90
91
</body>
92
93
</html>

G. Carpeta « js »

A. Carpeta « js / lib »

1. js / lib / conexionMqttPerdida.js

1
/**
2
 * @param {{
3
 *   errorCode: number,
4
 *   errorMessage: string
5
 *  }} responseObject
6
 */
7
export function conexionMqttPerdida(responseObject) {
8
 if (responseObject.errorCode !== 0) {
9
  const mensaje = "Conexión MQTT terminada " + responseObject.errorMessage
10
  console.error(mensaje)
11
  alert(mensaje)
12
 }
13
}

2. js / lib / creaIdCliente.js

1
/**
2
 * Añade caracteres al azar a una raíz, para obtener un clientId único.
3
 * @param {string} raiz 
4
 */
5
export function creaIdCliente(raiz) {
6
 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
7
 for (var i = 0; i < 15; i++) {
8
  raiz += chars.charAt(Math.floor(Math.random() * chars.length))
9
 }
10
 return raiz
11
}

3. js / lib / enviaMensajeMqtt.js

1
// @ts-nocheck
2
/**
3
 * Envá un valor al servidor de MQTT y es reenviado a todos los dispositivos
4
 * suscritos al tópico indicado
5
 * @param {Paho.MQTT.Client} cliente
6
 * @param {string} mensaje
7
 * @param {string} topico
8
 */
9
export function enviaMensajeMqtt(cliente, mensaje, topico) {
10
 const mensajeMqtt = new Paho.MQTT.Message(mensaje)
11
 mensajeMqtt.destinationName = topico
12
 cliente.send(mensajeMqtt)
13
}

4. js / lib / falloEnLaConexionMqtt.js

1
/**
2
 * @param {{errorMessage: string}} res
3
 */
4
export function falloEnLaConexionMqtt(res) {
5
 const mensaje = "Fallo en conexión MQTT: " + res.errorMessage
6
 console.error(mensaje)
7
 alert(mensaje)
8
}

5. js / lib / manejaErrores.js

1
import { muestraError } from "./muestraError.js"
2
3
/**
4
 * Intercepta Response.prototype.json para capturar errores de parseo
5
 * y asegurar que se reporten correctamente en navegadores Chromium.
6
 */
7
{
8
 const originalJson = Response.prototype.json
9
10
 Response.prototype.json = function () {
11
  // Llamamos al método original usando el contexto (this) de la respuesta
12
  return originalJson.apply(this, arguments)
13
   .catch((/** @type {any} */ error) => {
14
    // Corrige un error de Chrome que evita el manejo correcto de errores.
15
    throw new Error(error)
16
   })
17
 }
18
}
19
20
window.onerror = function (
21
  /** @type {string} */ _message,
22
  /** @type {string} */ _url,
23
  /** @type {number} */ _line,
24
  /** @type {number} */ _column,
25
  /** @type {Error} */ errorObject
26
) {
27
 muestraError(errorObject)
28
 return true
29
}
30
31
window.addEventListener('unhandledrejection', event => {
32
 muestraError(event.reason)
33
 event.preventDefault()
34
})
35

6. js / lib / muestraError.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Muestra los datos de una Error en la consola y en un cuadro de alerta.
5
 * @param { ProblemDetailsError | Error | null } error descripción del error.
6
 */
7
export function muestraError(error) {
8
9
 if (error === null) {
10
11
  console.error("Error")
12
  alert("Error")
13
14
 } else if (error instanceof ProblemDetailsError) {
15
16
  const problemDetails = error.problemDetails
17
18
  let mensaje =
19
   typeof problemDetails["title"] === "string" ? problemDetails["title"] : ""
20
  if (typeof problemDetails["detail"] === "string") {
21
   if (mensaje !== "") {
22
    mensaje += "\n\n"
23
   }
24
   mensaje += problemDetails["detail"]
25
  }
26
  if (mensaje === "") {
27
   mensaje = "Error"
28
  }
29
  console.error(error, problemDetails)
30
  alert(mensaje)
31
32
 } else {
33
34
  console.error(error)
35
  alert(error.message)
36
37
 }
38
39
}

7. js / lib / ProblemDetailsError.js

1
export class ProblemDetailsError extends Error {
2
3
 /**
4
  * Detalle de los errores devueltos por un servicio.
5
  * Crea una instancia de ProblemDetailsError.
6
  * @param {object} problemDetails Objeto con la descripcipon del error.
7
  */
8
 constructor(problemDetails) {
9
10
  super(typeof problemDetails["detail"] === "string"
11
   ? problemDetails["detail"]
12
   : (typeof problemDetails["title"] === "string"
13
    ? problemDetails["title"]
14
    : "Error"))
15
16
  this.problemDetails = problemDetails
17
18
 }
19
20
}

H. Carpeta « paho.javascript-1.0.3 »

A. paho.javascript-1.0.3 / about.html

1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2
<html xmlns="http://www.w3.org/1999/xhtml"><head>
3
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
4
<title>About</title>
5
</head>
6
<body lang="EN-US">
7
<h2>About This Content</h2>
8
 
9
<p><em>December 9, 2013</em></p>	
10
<h3>License</h3>
11
12
<p>The Eclipse Foundation makes available all content in this plug-in ("Content").  Unless otherwise 
13
indicated below, the Content is provided to you under the terms and conditions of the
14
Eclipse Public License Version 1.0 ("EPL") and Eclipse Distribution License Version 1.0 ("EDL").
15
A copy of the EPL is available at 
16
<a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a> 
17
and a copy of the EDL is available at 
18
<a href="http://www.eclipse.org/org/documents/edl-v10.php">http://www.eclipse.org/org/documents/edl-v10.php</a>. 
19
For purposes of the EPL, "Program" will mean the Content.</p>
20
21
<p>If you did not receive this Content directly from the Eclipse Foundation, the Content is 
22
being redistributed by another party ("Redistributor") and different terms and conditions may
23
apply to your use of any object code in the Content.  Check the Redistributor's license that was 
24
provided with the Content.  If no such license exists, contact the Redistributor.  Unless otherwise
25
indicated below, the terms and conditions of the EPL still apply to any source code in the Content
26
and such source code may be obtained at <a href="http://www.eclipse.org/">http://www.eclipse.org</a>.</p>
27
28
</body></html>
29

B. paho.javascript-1.0.3 / CONTRIBUTING.md

1
# Contributing to Paho
2
3
Thanks for your interest in this project!
4
5
You can contribute bugfixes and new features by sending pull requests through GitHub.
6
7
## Legal
8
9
In order for your contribution to be accepted, it must comply with the Eclipse Foundation IP policy.
10
11
Please read the [Eclipse Foundation policy on accepting contributions via Git](http://wiki.eclipse.org/Development_Resources/Contributing_via_Git).
12
13
1. Sign the [Eclipse CLA](http://www.eclipse.org/legal/CLA.php)
14
  1. Register for an Eclipse Foundation User ID. You can register [here](https://dev.eclipse.org/site_login/createaccount.php).
15
  2. Log into the [Projects Portal](https://projects.eclipse.org/), and click on the '[Eclipse CLA](https://projects.eclipse.org/user/sign/cla)' link.
16
2. Go to your [account settings](https://dev.eclipse.org/site_login/myaccount.php#open_tab_accountsettings) and add your GitHub username to your account.
17
3. Make sure that you _sign-off_ your Git commits in the following format:
18
  ``` Signed-off-by: John Smith  ``` This is usually at the bottom of the commit message. You can automate this by adding the '-s' flag when you make the commits. e.g.   ```git commit -s -m "Adding a cool feature"```
19
4. Ensure that the email address that you make your commits with is the same one you used to sign up to the Eclipse Foundation website with.
20
21
## Contributing a change
22
23
## Contributing a change
24
25
1. [Fork the repository on GitHub](https://github.com/eclipse/paho.mqtt.javascript/fork)
26
2. Clone the forked repository onto your computer: ``` git clone https://github.com//paho.mqtt.javascript.git ```
27
3. Create a new branch from the latest ```develop``` branch with ```git checkout -b YOUR_BRANCH_NAME origin/develop```
28
4. Make your changes
29
5. If developing a new feature, make sure to include JUnit tests.
30
6. Ensure that all new and existing tests pass.
31
7. Commit the changes into the branch: ``` git commit -s ``` Make sure that your commit message is meaningful and describes your changes correctly.
32
8. If you have a lot of commits for the change, squash them into a single / few commits.
33
9. Push the changes in your branch to your forked repository.
34
10. Finally, go to [https://github.com/eclipse/paho.mqtt.javascript](https://github.com/eclipse/paho.mqtt.javascript) and create a pull request from your "YOUR_BRANCH_NAME" branch to the ```develop``` one to request review and merge of the commits in your pushed branch.
35
36
37
What happens next depends on the content of the patch. If it is 100% authored
38
by the contributor and is less than 1000 lines (and meets the needs of the
39
project), then it can be pulled into the main repository. If not, more steps
40
are required. These are detailed in the
41
[legal process poster](http://www.eclipse.org/legal/EclipseLegalProcessPoster.pdf).
42
43
44
45
## Developer resources:
46
47
48
Information regarding source code management, builds, coding standards, and more.
49
50
- [https://projects.eclipse.org/projects/iot.paho/developer](https://projects.eclipse.org/projects/iot.paho/developer)
51
52
Contact:
53
--------
54
55
Contact the project developers via the project's development
56
[mailing list](https://dev.eclipse.org/mailman/listinfo/paho-dev).
57
58
Search for bugs:
59
----------------
60
61
This project uses GitHub Issues here: [github.com/eclipse/paho.mqtt.javascript/issues](https://github.com/eclipse/paho.mqtt.javascript/issues) to track ongoing development and issues.
62
63
Create a new bug:
64
-----------------
65
66
Be sure to search for existing bugs before you create another one. Remember that contributions are always welcome!
67
68
- [Create new Paho bug](https://github.com/eclipse/paho.mqtt.javascript/issues/new)
69

C. paho.javascript-1.0.3 / edl-v10

1
2
Eclipse Distribution License - v 1.0
3
4
Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
5
6
All rights reserved.
7
8
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9
10
    Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
11
    Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12
    Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
16

D. paho.javascript-1.0.3 / epl-v10

1
Eclipse Public License - v 1.0
2
3
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4
5
1. DEFINITIONS
6
7
"Contribution" means:
8
9
a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and
10
b) in the case of each subsequent Contributor:
11
i) changes to the Program, and
12
ii) additions to the Program;
13
where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program.
14
"Contributor" means any person or entity that distributes the Program.
15
16
"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program.
17
18
"Program" means the Contributions distributed in accordance with this Agreement.
19
20
"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.
21
22
2. GRANT OF RIGHTS
23
24
a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.
25
b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder.
26
c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.
27
d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement.
28
3. REQUIREMENTS
29
30
A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:
31
32
a) it complies with the terms and conditions of this Agreement; and
33
b) its license agreement:
34
i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose;
35
ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits;
36
iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and
37
iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.
38
When the Program is made available in source code form:
39
40
a) it must be made available under this Agreement; and
41
b) a copy of this Agreement must be included with each copy of the Program.
42
Contributors may not remove or alter any copyright notices contained within the Program.
43
44
Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution.
45
46
4. COMMERCIAL DISTRIBUTION
47
48
Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense.
49
50
For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.
51
52
5. NO WARRANTY
53
54
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations.
55
56
6. DISCLAIMER OF LIABILITY
57
58
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
59
60
7. GENERAL
61
62
If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
63
64
If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed.
65
66
All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive.
67
68
Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved.
69
70
This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.
71

E. paho.javascript-1.0.3 / paho-mqtt-min.js

1
/*******************************************************************************
2
 * Copyright (c) 2013, 2016 IBM Corp.
3
 *
4
 * All rights reserved. This program and the accompanying materials
5
 * are made available under the terms of the Eclipse Public License v1.0
6
 * and Eclipse Distribution License v1.0 which accompany this distribution.
7
 *
8
 * The Eclipse Public License is available at
9
 *    http://www.eclipse.org/legal/epl-v10.html
10
 * and the Eclipse Distribution License is available at
11
 *   http://www.eclipse.org/org/documents/edl-v10.php.
12
 *
13
 *******************************************************************************/
14
(function(p,s){"object"===typeof exports&&"object"===typeof module?module.exports=s():"function"===typeof define&&define.amd?define(s):"object"===typeof exports?exports=s():("undefined"===typeof p.Paho&&(p.Paho={}),p.Paho.MQTT=s())})(this,function(){return function(p){function s(a,b,c){b[c++]=a>>8;b[c++]=a%256;return c}function u(a,b,c,k){k=s(b,c,k);D(a,c,k);return k+b}function n(a){for(var b=0,c=0;c<a.length;c++){var k=a.charCodeAt(c);2047<k?(55296<=k&&56319>=k&&(c++,b++),b+=3):127<k?b+=2:b++}return b}
15
function D(a,b,c){for(var k=0;k<a.length;k++){var e=a.charCodeAt(k);if(55296<=e&&56319>=e){var g=a.charCodeAt(++k);if(isNaN(g))throw Error(f(h.MALFORMED_UNICODE,[e,g]));e=(e-55296<<10)+(g-56320)+65536}127>=e?b[c++]=e:(2047>=e?b[c++]=e>>6&31|192:(65535>=e?b[c++]=e>>12&15|224:(b[c++]=e>>18&7|240,b[c++]=e>>12&63|128),b[c++]=e>>6&63|128),b[c++]=e&63|128)}return b}function E(a,b,c){for(var k="",e,g=b;g<b+c;){e=a[g++];if(!(128>e)){var m=a[g++]-128;if(0>m)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),
16
""]));if(224>e)e=64*(e-192)+m;else{var d=a[g++]-128;if(0>d)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16)]));if(240>e)e=4096*(e-224)+64*m+d;else{var l=a[g++]-128;if(0>l)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16),l.toString(16)]));if(248>e)e=262144*(e-240)+4096*m+64*d+l;else throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16),l.toString(16)]));}}}65535<e&&(e-=65536,k+=String.fromCharCode(55296+(e>>10)),e=56320+(e&
17
1023));k+=String.fromCharCode(e)}return k}var z=function(a,b){for(var c in a)if(a.hasOwnProperty(c))if(b.hasOwnProperty(c)){if(typeof a[c]!==b[c])throw Error(f(h.INVALID_TYPE,[typeof a[c],c]));}else{c="Unknown property, "+c+". Valid properties are:";for(var k in b)b.hasOwnProperty(k)&&(c=c+" "+k);throw Error(c);}},v=function(a,b){return function(){return a.apply(b,arguments)}},h={OK:{code:0,text:"AMQJSC0000I OK."},CONNECT_TIMEOUT:{code:1,text:"AMQJSC0001E Connect timed out."},SUBSCRIBE_TIMEOUT:{code:2,
18
text:"AMQJS0002E Subscribe timed out."},UNSUBSCRIBE_TIMEOUT:{code:3,text:"AMQJS0003E Unsubscribe timed out."},PING_TIMEOUT:{code:4,text:"AMQJS0004E Ping timed out."},INTERNAL_ERROR:{code:5,text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"},CONNACK_RETURNCODE:{code:6,text:"AMQJS0006E Bad Connack return code:{0} {1}."},SOCKET_ERROR:{code:7,text:"AMQJS0007E Socket error:{0}."},SOCKET_CLOSE:{code:8,text:"AMQJS0008I Socket closed."},MALFORMED_UTF:{code:9,text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."},
19
UNSUPPORTED:{code:10,text:"AMQJS0010E {0} is not supported by this browser."},INVALID_STATE:{code:11,text:"AMQJS0011E Invalid state {0}."},INVALID_TYPE:{code:12,text:"AMQJS0012E Invalid type {0} for {1}."},INVALID_ARGUMENT:{code:13,text:"AMQJS0013E Invalid argument {0} for {1}."},UNSUPPORTED_OPERATION:{code:14,text:"AMQJS0014E Unsupported operation."},INVALID_STORED_DATA:{code:15,text:"AMQJS0015E Invalid data in local storage key\x3d{0} value\x3d{1}."},INVALID_MQTT_MESSAGE_TYPE:{code:16,text:"AMQJS0016E Invalid MQTT message type {0}."},
20
MALFORMED_UNICODE:{code:17,text:"AMQJS0017E Malformed Unicode string:{0} {1}."},BUFFER_FULL:{code:18,text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."}},H={0:"Connection Accepted",1:"Connection Refused: unacceptable protocol version",2:"Connection Refused: identifier rejected",3:"Connection Refused: server unavailable",4:"Connection Refused: bad user name or password",5:"Connection Refused: not authorized"},f=function(a,b){var c=a.text;if(b)for(var k,e,g=0;g<b.length;g++)if(k="{"+
21
g+"}",e=c.indexOf(k),0<e)var h=c.substring(0,e),c=c.substring(e+k.length),c=h+b[g]+c;return c},A=[0,6,77,81,73,115,100,112,3],B=[0,4,77,81,84,84,4],q=function(a,b){this.type=a;for(var c in b)b.hasOwnProperty(c)&&(this[c]=b[c])};q.prototype.encode=function(){var a=(this.type&15)<<4,b=0,c=[],k=0,e;void 0!==this.messageIdentifier&&(b+=2);switch(this.type){case 1:switch(this.mqttVersion){case 3:b+=A.length+3;break;case 4:b+=B.length+3}b+=n(this.clientId)+2;void 0!==this.willMessage&&(b+=n(this.willMessage.destinationName)+
22
2,e=this.willMessage.payloadBytes,e instanceof Uint8Array||(e=new Uint8Array(h)),b+=e.byteLength+2);void 0!==this.userName&&(b+=n(this.userName)+2);void 0!==this.password&&(b+=n(this.password)+2);break;case 8:for(var a=a|2,g=0;g<this.topics.length;g++)c[g]=n(this.topics[g]),b+=c[g]+2;b+=this.requestedQos.length;break;case 10:a|=2;for(g=0;g<this.topics.length;g++)c[g]=n(this.topics[g]),b+=c[g]+2;break;case 6:a|=2;break;case 3:this.payloadMessage.duplicate&&(a|=8);a=a|=this.payloadMessage.qos<<1;this.payloadMessage.retained&&
23
(a|=1);var k=n(this.payloadMessage.destinationName),h=this.payloadMessage.payloadBytes,b=b+(k+2)+h.byteLength;h instanceof ArrayBuffer?h=new Uint8Array(h):h instanceof Uint8Array||(h=new Uint8Array(h.buffer))}var f=b,g=Array(1),d=0;do{var t=f%128,f=f>>7;0<f&&(t|=128);g[d++]=t}while(0<f&&4>d);f=g.length+1;b=new ArrayBuffer(b+f);d=new Uint8Array(b);d[0]=a;d.set(g,1);if(3==this.type)f=u(this.payloadMessage.destinationName,k,d,f);else if(1==this.type){switch(this.mqttVersion){case 3:d.set(A,f);f+=A.length;
24
break;case 4:d.set(B,f),f+=B.length}a=0;this.cleanSession&&(a=2);void 0!==this.willMessage&&(a=a|4|this.willMessage.qos<<3,this.willMessage.retained&&(a|=32));void 0!==this.userName&&(a|=128);void 0!==this.password&&(a|=64);d[f++]=a;f=s(this.keepAliveInterval,d,f)}void 0!==this.messageIdentifier&&(f=s(this.messageIdentifier,d,f));switch(this.type){case 1:f=u(this.clientId,n(this.clientId),d,f);void 0!==this.willMessage&&(f=u(this.willMessage.destinationName,n(this.willMessage.destinationName),d,f),
25
f=s(e.byteLength,d,f),d.set(e,f),f+=e.byteLength);void 0!==this.userName&&(f=u(this.userName,n(this.userName),d,f));void 0!==this.password&&u(this.password,n(this.password),d,f);break;case 3:d.set(h,f);break;case 8:for(g=0;g<this.topics.length;g++)f=u(this.topics[g],c[g],d,f),d[f++]=this.requestedQos[g];break;case 10:for(g=0;g<this.topics.length;g++)f=u(this.topics[g],c[g],d,f)}return b};var F=function(a,b,c){this._client=a;this._window=b;this._keepAliveInterval=1E3*c;this.isReset=!1;var k=(new q(12)).encode(),
26
e=function(a){return function(){return g.apply(a)}},g=function(){this.isReset?(this.isReset=!1,this._client._trace("Pinger.doPing","send PINGREQ"),this._client.socket.send(k),this.timeout=this._window.setTimeout(e(this),this._keepAliveInterval)):(this._client._trace("Pinger.doPing","Timed out"),this._client._disconnected(h.PING_TIMEOUT.code,f(h.PING_TIMEOUT)))};this.reset=function(){this.isReset=!0;this._window.clearTimeout(this.timeout);0<this._keepAliveInterval&&(this.timeout=setTimeout(e(this),
27
this._keepAliveInterval))};this.cancel=function(){this._window.clearTimeout(this.timeout)}},w=function(a,b,c,f,e){this._window=b;c||(c=30);this.timeout=setTimeout(function(a,b,c){return function(){return a.apply(b,c)}}(f,a,e),1E3*c);this.cancel=function(){this._window.clearTimeout(this.timeout)}},d=function(a,b,c,d,e){if(!("WebSocket"in p&&null!==p.WebSocket))throw Error(f(h.UNSUPPORTED,["WebSocket"]));if(!("localStorage"in p&&null!==p.localStorage))throw Error(f(h.UNSUPPORTED,["localStorage"]));
28
if(!("ArrayBuffer"in p&&null!==p.ArrayBuffer))throw Error(f(h.UNSUPPORTED,["ArrayBuffer"]));this._trace("Paho.MQTT.Client",a,b,c,d,e);this.host=b;this.port=c;this.path=d;this.uri=a;this.clientId=e;this._wsuri=null;this._localKey=b+":"+c+("/mqtt"!=d?":"+d:"")+":"+e+":";this._msg_queue=[];this._buffered_msg_queue=[];this._sentMessages={};this._receivedMessages={};this._notify_msg_sent={};this._message_identifier=1;this._sequence=0;for(var g in localStorage)0!==g.indexOf("Sent:"+this._localKey)&&0!==
29
g.indexOf("Received:"+this._localKey)||this.restore(g)};d.prototype.host=null;d.prototype.port=null;d.prototype.path=null;d.prototype.uri=null;d.prototype.clientId=null;d.prototype.socket=null;d.prototype.connected=!1;d.prototype.maxMessageIdentifier=65536;d.prototype.connectOptions=null;d.prototype.hostIndex=null;d.prototype.onConnected=null;d.prototype.onConnectionLost=null;d.prototype.onMessageDelivered=null;d.prototype.onMessageArrived=null;d.prototype.traceFunction=null;d.prototype._msg_queue=
30
null;d.prototype._buffered_msg_queue=null;d.prototype._connectTimeout=null;d.prototype.sendPinger=null;d.prototype.receivePinger=null;d.prototype._reconnectInterval=1;d.prototype._reconnecting=!1;d.prototype._reconnectTimeout=null;d.prototype.disconnectedPublishing=!1;d.prototype.disconnectedBufferSize=5E3;d.prototype.receiveBuffer=null;d.prototype._traceBuffer=null;d.prototype._MAX_TRACE_ENTRIES=100;d.prototype.connect=function(a){var b=this._traceMask(a,"password");this._trace("Client.connect",
31
b,this.socket,this.connected);if(this.connected)throw Error(f(h.INVALID_STATE,["already connected"]));if(this.socket)throw Error(f(h.INVALID_STATE,["already connected"]));this._reconnecting&&(this._reconnectTimeout.cancel(),this._reconnectTimeout=null,this._reconnecting=!1);this.connectOptions=a;this._reconnectInterval=1;this._reconnecting=!1;a.uris?(this.hostIndex=0,this._doConnect(a.uris[0])):this._doConnect(this.uri)};d.prototype.subscribe=function(a,b){this._trace("Client.subscribe",a,b);if(!this.connected)throw Error(f(h.INVALID_STATE,
32
["not connected"]));var c=new q(8);c.topics=[a];c.requestedQos=void 0!==b.qos?[b.qos]:[0];b.onSuccess&&(c.onSuccess=function(a){b.onSuccess({invocationContext:b.invocationContext,grantedQos:a})});b.onFailure&&(c.onFailure=function(a){b.onFailure({invocationContext:b.invocationContext,errorCode:a,errorMessage:f(a)})});b.timeout&&(c.timeOut=new w(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,errorCode:h.SUBSCRIBE_TIMEOUT.code,errorMessage:f(h.SUBSCRIBE_TIMEOUT)}]));this._requires_ack(c);
33
this._schedule_message(c)};d.prototype.unsubscribe=function(a,b){this._trace("Client.unsubscribe",a,b);if(!this.connected)throw Error(f(h.INVALID_STATE,["not connected"]));var c=new q(10);c.topics=[a];b.onSuccess&&(c.callback=function(){b.onSuccess({invocationContext:b.invocationContext})});b.timeout&&(c.timeOut=new w(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,errorCode:h.UNSUBSCRIBE_TIMEOUT.code,errorMessage:f(h.UNSUBSCRIBE_TIMEOUT)}]));this._requires_ack(c);this._schedule_message(c)};
34
d.prototype.send=function(a){this._trace("Client.send",a);wireMessage=new q(3);wireMessage.payloadMessage=a;if(this.connected)0<a.qos?this._requires_ack(wireMessage):this.onMessageDelivered&&(this._notify_msg_sent[wireMessage]=this.onMessageDelivered(wireMessage.payloadMessage)),this._schedule_message(wireMessage);else if(this._reconnecting&&this.disconnectedPublishing){if(Object.keys(this._sentMessages).length+this._buffered_msg_queue.length>this.disconnectedBufferSize)throw Error(f(h.BUFFER_FULL,
35
[this.disconnectedBufferSize]));0<a.qos?this._requires_ack(wireMessage):(wireMessage.sequence=++this._sequence,this._buffered_msg_queue.push(wireMessage))}else throw Error(f(h.INVALID_STATE,["not connected"]));};d.prototype.disconnect=function(){this._trace("Client.disconnect");this._reconnecting&&(this._reconnectTimeout.cancel(),this._reconnectTimeout=null,this._reconnecting=!1);if(!this.socket)throw Error(f(h.INVALID_STATE,["not connecting or connected"]));wireMessage=new q(14);this._notify_msg_sent[wireMessage]=
36
v(this._disconnected,this);this._schedule_message(wireMessage)};d.prototype.getTraceLog=function(){if(null!==this._traceBuffer){this._trace("Client.getTraceLog",new Date);this._trace("Client.getTraceLog in flight messages",this._sentMessages.length);for(var a in this._sentMessages)this._trace("_sentMessages ",a,this._sentMessages[a]);for(a in this._receivedMessages)this._trace("_receivedMessages ",a,this._receivedMessages[a]);return this._traceBuffer}};d.prototype.startTrace=function(){null===this._traceBuffer&&
37
(this._traceBuffer=[]);this._trace("Client.startTrace",new Date,"1.0.3")};d.prototype.stopTrace=function(){delete this._traceBuffer};d.prototype._doConnect=function(a){this.connectOptions.useSSL&&(a=a.split(":"),a[0]="wss",a=a.join(":"));this._wsuri=a;this.connected=!1;this.socket=4>this.connectOptions.mqttVersion?new WebSocket(a,["mqttv3.1"]):new WebSocket(a,["mqtt"]);this.socket.binaryType="arraybuffer";this.socket.onopen=v(this._on_socket_open,this);this.socket.onmessage=v(this._on_socket_message,
38
this);this.socket.onerror=v(this._on_socket_error,this);this.socket.onclose=v(this._on_socket_close,this);this.sendPinger=new F(this,window,this.connectOptions.keepAliveInterval);this.receivePinger=new F(this,window,this.connectOptions.keepAliveInterval);this._connectTimeout&&(this._connectTimeout.cancel(),this._connectTimeout=null);this._connectTimeout=new w(this,window,this.connectOptions.timeout,this._disconnected,[h.CONNECT_TIMEOUT.code,f(h.CONNECT_TIMEOUT)])};d.prototype._schedule_message=function(a){this._msg_queue.push(a);
39
this.connected&&this._process_queue()};d.prototype.store=function(a,b){var c={type:b.type,messageIdentifier:b.messageIdentifier,version:1};switch(b.type){case 3:b.pubRecReceived&&(c.pubRecReceived=!0);c.payloadMessage={};for(var d="",e=b.payloadMessage.payloadBytes,g=0;g<e.length;g++)d=15>=e[g]?d+"0"+e[g].toString(16):d+e[g].toString(16);c.payloadMessage.payloadHex=d;c.payloadMessage.qos=b.payloadMessage.qos;c.payloadMessage.destinationName=b.payloadMessage.destinationName;b.payloadMessage.duplicate&&
40
(c.payloadMessage.duplicate=!0);b.payloadMessage.retained&&(c.payloadMessage.retained=!0);0===a.indexOf("Sent:")&&(void 0===b.sequence&&(b.sequence=++this._sequence),c.sequence=b.sequence);break;default:throw Error(f(h.INVALID_STORED_DATA,[key,c]));}localStorage.setItem(a+this._localKey+b.messageIdentifier,JSON.stringify(c))};d.prototype.restore=function(a){var b=localStorage.getItem(a),c=JSON.parse(b),d=new q(c.type,c);switch(c.type){case 3:for(var b=c.payloadMessage.payloadHex,e=new ArrayBuffer(b.length/
41
2),e=new Uint8Array(e),g=0;2<=b.length;){var m=parseInt(b.substring(0,2),16),b=b.substring(2,b.length);e[g++]=m}b=new Paho.MQTT.Message(e);b.qos=c.payloadMessage.qos;b.destinationName=c.payloadMessage.destinationName;c.payloadMessage.duplicate&&(b.duplicate=!0);c.payloadMessage.retained&&(b.retained=!0);d.payloadMessage=b;break;default:throw Error(f(h.INVALID_STORED_DATA,[a,b]));}0===a.indexOf("Sent:"+this._localKey)?(d.payloadMessage.duplicate=!0,this._sentMessages[d.messageIdentifier]=d):0===a.indexOf("Received:"+
42
this._localKey)&&(this._receivedMessages[d.messageIdentifier]=d)};d.prototype._process_queue=function(){for(var a=null,b=this._msg_queue.reverse();a=b.pop();)this._socket_send(a),this._notify_msg_sent[a]&&(this._notify_msg_sent[a](),delete this._notify_msg_sent[a])};d.prototype._requires_ack=function(a){var b=Object.keys(this._sentMessages).length;if(b>this.maxMessageIdentifier)throw Error("Too many messages:"+b);for(;void 0!==this._sentMessages[this._message_identifier];)this._message_identifier++;
43
a.messageIdentifier=this._message_identifier;this._sentMessages[a.messageIdentifier]=a;3===a.type&&this.store("Sent:",a);this._message_identifier===this.maxMessageIdentifier&&(this._message_identifier=1)};d.prototype._on_socket_open=function(){var a=new q(1,this.connectOptions);a.clientId=this.clientId;this._socket_send(a)};d.prototype._on_socket_message=function(a){this._trace("Client._on_socket_message",a.data);a=this._deframeMessages(a.data);for(var b=0;b<a.length;b+=1)this._handleMessage(a[b])};
44
d.prototype._deframeMessages=function(a){a=new Uint8Array(a);var b=[];if(this.receiveBuffer){var c=new Uint8Array(this.receiveBuffer.length+a.length);c.set(this.receiveBuffer);c.set(a,this.receiveBuffer.length);a=c;delete this.receiveBuffer}try{for(c=0;c<a.length;){var d;a:{var e=a,g=c,m=g,n=e[g],l=n>>4,t=n&15,g=g+1,x=void 0,C=0,p=1;do{if(g==e.length){d=[null,m];break a}x=e[g++];C+=(x&127)*p;p*=128}while(0!==(x&128));x=g+C;if(x>e.length)d=[null,m];else{var y=new q(l);switch(l){case 2:e[g++]&1&&(y.sessionPresent=
45
!0);y.returnCode=e[g++];break;case 3:var m=t>>1&3,s=256*e[g]+e[g+1],g=g+2,u=E(e,g,s),g=g+s;0<m&&(y.messageIdentifier=256*e[g]+e[g+1],g+=2);var r=new Paho.MQTT.Message(e.subarray(g,x));1==(t&1)&&(r.retained=!0);8==(t&8)&&(r.duplicate=!0);r.qos=m;r.destinationName=u;y.payloadMessage=r;break;case 4:case 5:case 6:case 7:case 11:y.messageIdentifier=256*e[g]+e[g+1];break;case 9:y.messageIdentifier=256*e[g]+e[g+1],g+=2,y.returnCode=e.subarray(g,x)}d=[y,x]}}var v=d[0],c=d[1];if(null!==v)b.push(v);else break}c<
46
a.length&&(this.receiveBuffer=a.subarray(c))}catch(w){d="undefined"==w.hasOwnProperty("stack")?w.stack.toString():"No Error Stack Available";this._disconnected(h.INTERNAL_ERROR.code,f(h.INTERNAL_ERROR,[w.message,d]));return}return b};d.prototype._handleMessage=function(a){this._trace("Client._handleMessage",a);try{switch(a.type){case 2:this._connectTimeout.cancel();this._reconnectTimeout&&this._reconnectTimeout.cancel();if(this.connectOptions.cleanSession){for(var b in this._sentMessages){var c=this._sentMessages[b];
47
localStorage.removeItem("Sent:"+this._localKey+c.messageIdentifier)}this._sentMessages={};for(b in this._receivedMessages){var d=this._receivedMessages[b];localStorage.removeItem("Received:"+this._localKey+d.messageIdentifier)}this._receivedMessages={}}if(0===a.returnCode)this.connected=!0,this.connectOptions.uris&&(this.hostIndex=this.connectOptions.uris.length);else{this._disconnected(h.CONNACK_RETURNCODE.code,f(h.CONNACK_RETURNCODE,[a.returnCode,H[a.returnCode]]));break}a=[];for(var e in this._sentMessages)this._sentMessages.hasOwnProperty(e)&&
48
a.push(this._sentMessages[e]);if(0<this._buffered_msg_queue.length){e=null;for(var g=this._buffered_msg_queue.reverse();e=g.pop();)a.push(e),this.onMessageDelivered&&(this._notify_msg_sent[e]=this.onMessageDelivered(e.payloadMessage))}a=a.sort(function(a,b){return a.sequence-b.sequence});for(var g=0,m=a.length;g<m;g++)if(c=a[g],3==c.type&&c.pubRecReceived){var n=new q(6,{messageIdentifier:c.messageIdentifier});this._schedule_message(n)}else this._schedule_message(c);if(this.connectOptions.onSuccess)this.connectOptions.onSuccess({invocationContext:this.connectOptions.invocationContext});
49
c=!1;this._reconnecting&&(c=!0,this._reconnectInterval=1,this._reconnecting=!1);this._connected(c,this._wsuri);this._process_queue();break;case 3:this._receivePublish(a);break;case 4:if(c=this._sentMessages[a.messageIdentifier])if(delete this._sentMessages[a.messageIdentifier],localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier),this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 5:if(c=this._sentMessages[a.messageIdentifier])c.pubRecReceived=!0,n=new q(6,{messageIdentifier:a.messageIdentifier}),
50
this.store("Sent:",c),this._schedule_message(n);break;case 6:d=this._receivedMessages[a.messageIdentifier];localStorage.removeItem("Received:"+this._localKey+a.messageIdentifier);d&&(this._receiveMessage(d),delete this._receivedMessages[a.messageIdentifier]);var l=new q(7,{messageIdentifier:a.messageIdentifier});this._schedule_message(l);break;case 7:c=this._sentMessages[a.messageIdentifier];delete this._sentMessages[a.messageIdentifier];localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier);
51
if(this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 9:if(c=this._sentMessages[a.messageIdentifier]){c.timeOut&&c.timeOut.cancel();if(128===a.returnCode[0]){if(c.onFailure)c.onFailure(a.returnCode)}else if(c.onSuccess)c.onSuccess(a.returnCode);delete this._sentMessages[a.messageIdentifier]}break;case 11:if(c=this._sentMessages[a.messageIdentifier])c.timeOut&&c.timeOut.cancel(),c.callback&&c.callback(),delete this._sentMessages[a.messageIdentifier];break;case 13:this.sendPinger.reset();
52
break;case 14:this._disconnected(h.INVALID_MQTT_MESSAGE_TYPE.code,f(h.INVALID_MQTT_MESSAGE_TYPE,[a.type]));break;default:this._disconnected(h.INVALID_MQTT_MESSAGE_TYPE.code,f(h.INVALID_MQTT_MESSAGE_TYPE,[a.type]))}}catch(t){c="undefined"==t.hasOwnProperty("stack")?t.stack.toString():"No Error Stack Available",this._disconnected(h.INTERNAL_ERROR.code,f(h.INTERNAL_ERROR,[t.message,c]))}};d.prototype._on_socket_error=function(a){this._reconnecting||this._disconnected(h.SOCKET_ERROR.code,f(h.SOCKET_ERROR,
53
[a.data]))};d.prototype._on_socket_close=function(){this._reconnecting||this._disconnected(h.SOCKET_CLOSE.code,f(h.SOCKET_CLOSE))};d.prototype._socket_send=function(a){if(1==a.type){var b=this._traceMask(a,"password");this._trace("Client._socket_send",b)}else this._trace("Client._socket_send",a);this.socket.send(a.encode());this.sendPinger.reset()};d.prototype._receivePublish=function(a){switch(a.payloadMessage.qos){case "undefined":case 0:this._receiveMessage(a);break;case 1:var b=new q(4,{messageIdentifier:a.messageIdentifier});
54
this._schedule_message(b);this._receiveMessage(a);break;case 2:this._receivedMessages[a.messageIdentifier]=a;this.store("Received:",a);a=new q(5,{messageIdentifier:a.messageIdentifier});this._schedule_message(a);break;default:throw Error("Invaild qos\x3d"+wireMmessage.payloadMessage.qos);}};d.prototype._receiveMessage=function(a){if(this.onMessageArrived)this.onMessageArrived(a.payloadMessage)};d.prototype._connected=function(a,b){if(this.onConnected)this.onConnected(a,b)};d.prototype._reconnect=
55
function(){this._trace("Client._reconnect");this.connected||(this._reconnecting=!0,this.sendPinger.cancel(),this.receivePinger.cancel(),128>this._reconnectInterval&&(this._reconnectInterval*=2),this.connectOptions.uris?(this.hostIndex=0,this._doConnect(this.connectOptions.uris[0])):this._doConnect(this.uri))};d.prototype._disconnected=function(a,b){this._trace("Client._disconnected",a,b);if(void 0!==a&&this._reconnecting)this._reconnectTimeout=new w(this,window,this._reconnectInterval,this._reconnect);
56
else if(this.sendPinger.cancel(),this.receivePinger.cancel(),this._connectTimeout&&(this._connectTimeout.cancel(),this._connectTimeout=null),this._msg_queue=[],this._buffered_msg_queue=[],this._notify_msg_sent={},this.socket&&(this.socket.onopen=null,this.socket.onmessage=null,this.socket.onerror=null,this.socket.onclose=null,1===this.socket.readyState&&this.socket.close(),delete this.socket),this.connectOptions.uris&&this.hostIndex<this.connectOptions.uris.length-1)this.hostIndex++,this._doConnect(this.connectOptions.uris[this.hostIndex]);
57
else if(void 0===a&&(a=h.OK.code,b=f(h.OK)),this.connected){this.connected=!1;if(this.onConnectionLost)this.onConnectionLost({errorCode:a,errorMessage:b,reconnect:this.connectOptions.reconnect,uri:this._wsuri});a!==h.OK.code&&this.connectOptions.reconnect&&(this._reconnectInterval=1,this._reconnect())}else if(4===this.connectOptions.mqttVersion&&!1===this.connectOptions.mqttVersionExplicit)this._trace("Failed to connect V4, dropping back to V3"),this.connectOptions.mqttVersion=3,this.connectOptions.uris?
58
(this.hostIndex=0,this._doConnect(this.connectOptions.uris[0])):this._doConnect(this.uri);else if(this.connectOptions.onFailure)this.connectOptions.onFailure({invocationContext:this.connectOptions.invocationContext,errorCode:a,errorMessage:b})};d.prototype._trace=function(){if(this.traceFunction){for(var a in arguments)"undefined"!==typeof arguments[a]&&arguments.splice(a,1,JSON.stringify(arguments[a]));a=Array.prototype.slice.call(arguments).join("");this.traceFunction({severity:"Debug",message:a})}if(null!==
59
this._traceBuffer){a=0;for(var b=arguments.length;a<b;a++)this._traceBuffer.length==this._MAX_TRACE_ENTRIES&&this._traceBuffer.shift(),0===a?this._traceBuffer.push(arguments[a]):"undefined"===typeof arguments[a]?this._traceBuffer.push(arguments[a]):this._traceBuffer.push("  "+JSON.stringify(arguments[a]))}};d.prototype._traceMask=function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&(c[d]=d==b?"******":a[d]);return c};var G=function(a,b,c,k){var e;if("string"!==typeof a)throw Error(f(h.INVALID_TYPE,
60
[typeof a,"host"]));if(2==arguments.length){k=b;e=a;var g=e.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/);if(g)a=g[4]||g[2],b=parseInt(g[7]),c=g[8];else throw Error(f(h.INVALID_ARGUMENT,[a,"host"]));}else{3==arguments.length&&(k=c,c="/mqtt");if("number"!==typeof b||0>b)throw Error(f(h.INVALID_TYPE,[typeof b,"port"]));if("string"!==typeof c)throw Error(f(h.INVALID_TYPE,[typeof c,"path"]));e="ws://"+(-1!==a.indexOf(":")&&"["!==a.slice(0,1)&&"]"!==a.slice(-1)?"["+a+"]":a)+":"+b+c}for(var m=
61
g=0;m<k.length;m++){var n=k.charCodeAt(m);55296<=n&&56319>=n&&m++;g++}if("string"!==typeof k||65535<g)throw Error(f(h.INVALID_ARGUMENT,[k,"clientId"]));var l=new d(e,a,b,c,k);this._getHost=function(){return a};this._setHost=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getPort=function(){return b};this._setPort=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getPath=function(){return c};this._setPath=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getURI=function(){return e};
62
this._setURI=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getClientId=function(){return l.clientId};this._setClientId=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getOnConnected=function(){return l.onConnected};this._setOnConnected=function(a){if("function"===typeof a)l.onConnected=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onConnected"]));};this._getDisconnectedPublishing=function(){return l.disconnectedPublishing};this._setDisconnectedPublishing=function(a){l.disconnectedPublishing=
63
a};this._getDisconnectedBufferSize=function(){return l.disconnectedBufferSize};this._setDisconnectedBufferSize=function(a){l.disconnectedBufferSize=a};this._getOnConnectionLost=function(){return l.onConnectionLost};this._setOnConnectionLost=function(a){if("function"===typeof a)l.onConnectionLost=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onConnectionLost"]));};this._getOnMessageDelivered=function(){return l.onMessageDelivered};this._setOnMessageDelivered=function(a){if("function"===typeof a)l.onMessageDelivered=
64
a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onMessageDelivered"]));};this._getOnMessageArrived=function(){return l.onMessageArrived};this._setOnMessageArrived=function(a){if("function"===typeof a)l.onMessageArrived=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onMessageArrived"]));};this._getTrace=function(){return l.traceFunction};this._setTrace=function(a){if("function"===typeof a)l.traceFunction=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onTrace"]));};this.connect=function(a){a=a||{};z(a,
65
{timeout:"number",userName:"string",password:"string",willMessage:"object",keepAliveInterval:"number",cleanSession:"boolean",useSSL:"boolean",invocationContext:"object",onSuccess:"function",onFailure:"function",hosts:"object",ports:"object",reconnect:"boolean",mqttVersion:"number",mqttVersionExplicit:"boolean",uris:"object"});void 0===a.keepAliveInterval&&(a.keepAliveInterval=60);if(4<a.mqttVersion||3>a.mqttVersion)throw Error(f(h.INVALID_ARGUMENT,[a.mqttVersion,"connectOptions.mqttVersion"]));void 0===
66
a.mqttVersion?(a.mqttVersionExplicit=!1,a.mqttVersion=4):a.mqttVersionExplicit=!0;if(void 0!==a.password&&void 0===a.userName)throw Error(f(h.INVALID_ARGUMENT,[a.password,"connectOptions.password"]));if(a.willMessage){if(!(a.willMessage instanceof r))throw Error(f(h.INVALID_TYPE,[a.willMessage,"connectOptions.willMessage"]));a.willMessage.stringPayload=null;if("undefined"===typeof a.willMessage.destinationName)throw Error(f(h.INVALID_TYPE,[typeof a.willMessage.destinationName,"connectOptions.willMessage.destinationName"]));
67
}"undefined"===typeof a.cleanSession&&(a.cleanSession=!0);if(a.hosts){if(!(a.hosts instanceof Array))throw Error(f(h.INVALID_ARGUMENT,[a.hosts,"connectOptions.hosts"]));if(1>a.hosts.length)throw Error(f(h.INVALID_ARGUMENT,[a.hosts,"connectOptions.hosts"]));for(var b=!1,d=0;d<a.hosts.length;d++){if("string"!==typeof a.hosts[d])throw Error(f(h.INVALID_TYPE,[typeof a.hosts[d],"connectOptions.hosts["+d+"]"]));if(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(a.hosts[d]))if(0===d)b=!0;else{if(!b)throw Error(f(h.INVALID_ARGUMENT,
68
[a.hosts[d],"connectOptions.hosts["+d+"]"]));}else if(b)throw Error(f(h.INVALID_ARGUMENT,[a.hosts[d],"connectOptions.hosts["+d+"]"]));}if(b)a.uris=a.hosts;else{if(!a.ports)throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(!(a.ports instanceof Array))throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(a.hosts.length!==a.ports.length)throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));a.uris=[];for(d=0;d<a.hosts.length;d++){if("number"!==typeof a.ports[d]||
69
0>a.ports[d])throw Error(f(h.INVALID_TYPE,[typeof a.ports[d],"connectOptions.ports["+d+"]"]));var b=a.hosts[d],g=a.ports[d];e="ws://"+(-1!==b.indexOf(":")?"["+b+"]":b)+":"+g+c;a.uris.push(e)}}}l.connect(a)};this.subscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};z(b,{qos:"number",invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("subscribeOptions.timeout specified with no onFailure callback.");
70
if("undefined"!==typeof b.qos&&0!==b.qos&&1!==b.qos&&2!==b.qos)throw Error(f(h.INVALID_ARGUMENT,[b.qos,"subscribeOptions.qos"]));l.subscribe(a,b)};this.unsubscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};z(b,{invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("unsubscribeOptions.timeout specified with no onFailure callback.");l.unsubscribe(a,b)};this.send=function(a,b,c,d){var e;if(0===
71
arguments.length)throw Error("Invalid argument.length");if(1==arguments.length){if(!(a instanceof r)&&"string"!==typeof a)throw Error("Invalid argument:"+typeof a);e=a;if("undefined"===typeof e.destinationName)throw Error(f(h.INVALID_ARGUMENT,[e.destinationName,"Message.destinationName"]));}else e=new r(b),e.destinationName=a,3<=arguments.length&&(e.qos=c),4<=arguments.length&&(e.retained=d);l.send(e)};this.publish=function(a,b,c,d){console.log("Publising message to: ",a);var e;if(0===arguments.length)throw Error("Invalid argument.length");
72
if(1==arguments.length){if(!(a instanceof r)&&"string"!==typeof a)throw Error("Invalid argument:"+typeof a);e=a;if("undefined"===typeof e.destinationName)throw Error(f(h.INVALID_ARGUMENT,[e.destinationName,"Message.destinationName"]));}else e=new r(b),e.destinationName=a,3<=arguments.length&&(e.qos=c),4<=arguments.length&&(e.retained=d);l.send(e)};this.disconnect=function(){l.disconnect()};this.getTraceLog=function(){return l.getTraceLog()};this.startTrace=function(){l.startTrace()};this.stopTrace=
73
function(){l.stopTrace()};this.isConnected=function(){return l.connected}};G.prototype={get host(){return this._getHost()},set host(a){this._setHost(a)},get port(){return this._getPort()},set port(a){this._setPort(a)},get path(){return this._getPath()},set path(a){this._setPath(a)},get clientId(){return this._getClientId()},set clientId(a){this._setClientId(a)},get onConnected(){return this._getOnConnected()},set onConnected(a){this._setOnConnected(a)},get disconnectedPublishing(){return this._getDisconnectedPublishing()},
74
set disconnectedPublishing(a){this._setDisconnectedPublishing(a)},get disconnectedBufferSize(){return this._getDisconnectedBufferSize()},set disconnectedBufferSize(a){this._setDisconnectedBufferSize(a)},get onConnectionLost(){return this._getOnConnectionLost()},set onConnectionLost(a){this._setOnConnectionLost(a)},get onMessageDelivered(){return this._getOnMessageDelivered()},set onMessageDelivered(a){this._setOnMessageDelivered(a)},get onMessageArrived(){return this._getOnMessageArrived()},set onMessageArrived(a){this._setOnMessageArrived(a)},
75
get trace(){return this._getTrace()},set trace(a){this._setTrace(a)}};var r=function(a){var b;if("string"===typeof a||a instanceof ArrayBuffer||a instanceof Int8Array||a instanceof Uint8Array||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array)b=a;else throw f(h.INVALID_ARGUMENT,[a,"newPayload"]);this._getPayloadString=function(){return"string"===typeof b?b:E(b,0,b.length)};this._getPayloadBytes=
76
function(){if("string"===typeof b){var a=new ArrayBuffer(n(b)),a=new Uint8Array(a);D(b,a,0);return a}return b};var c;this._getDestinationName=function(){return c};this._setDestinationName=function(a){if("string"===typeof a)c=a;else throw Error(f(h.INVALID_ARGUMENT,[a,"newDestinationName"]));};var d=0;this._getQos=function(){return d};this._setQos=function(a){if(0===a||1===a||2===a)d=a;else throw Error("Invalid argument:"+a);};var e=!1;this._getRetained=function(){return e};this._setRetained=function(a){if("boolean"===
77
typeof a)e=a;else throw Error(f(h.INVALID_ARGUMENT,[a,"newRetained"]));};var g=!1;this._getDuplicate=function(){return g};this._setDuplicate=function(a){g=a}};r.prototype={get payloadString(){return this._getPayloadString()},get payloadBytes(){return this._getPayloadBytes()},get destinationName(){return this._getDestinationName()},set destinationName(a){this._setDestinationName(a)},get topic(){return this._getDestinationName()},set topic(a){this._setDestinationName(a)},get qos(){return this._getQos()},
78
set qos(a){this._setQos(a)},get retained(){return this._getRetained()},set retained(a){this._setRetained(a)},get duplicate(){return this._getDuplicate()},set duplicate(a){this._setDuplicate(a)}};return{Client:G,Message:r}}(window)});

F. paho.javascript-1.0.3 / paho-mqtt.js

1
/*******************************************************************************
2
 * Copyright (c) 2013 IBM Corp.
3
 *
4
 * All rights reserved. This program and the accompanying materials
5
 * are made available under the terms of the Eclipse Public License v1.0
6
 * and Eclipse Distribution License v1.0 which accompany this distribution.
7
 *
8
 * The Eclipse Public License is available at
9
 *    http://www.eclipse.org/legal/epl-v10.html
10
 * and the Eclipse Distribution License is available at
11
 *   http://www.eclipse.org/org/documents/edl-v10.php.
12
 *
13
 * Contributors:
14
 *    Andrew Banks - initial API and implementation and initial documentation
15
 *******************************************************************************/
16
17
18
// Only expose a single object name in the global namespace.
19
// Everything must go through this module. Global Paho.MQTT module
20
// only has a single public function, client, which returns
21
// a Paho.MQTT client object given connection details.
22
23
/**
24
 * Send and receive messages using web browsers.
25
 * <p>
26
 * This programming interface lets a JavaScript client application use the MQTT V3.1 or
27
 * V3.1.1 protocol to connect to an MQTT-supporting messaging server.
28
 *
29
 * The function supported includes:
30
 * <ol>
31
 * <li>Connecting to and disconnecting from a server. The server is identified by its host name and port number.
32
 * <li>Specifying options that relate to the communications link with the server,
33
 * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required.
34
 * <li>Subscribing to and receiving messages from MQTT Topics.
35
 * <li>Publishing messages to MQTT Topics.
36
 * </ol>
37
 * <p>
38
 * The API consists of two main objects:
39
 * <dl>
40
 * <dt><b>{@link Paho.MQTT.Client}</b></dt>
41
 * <dd>This contains methods that provide the functionality of the API,
42
 * including provision of callbacks that notify the application when a message
43
 * arrives from or is delivered to the messaging server,
44
 * or when the status of its connection to the messaging server changes.</dd>
45
 * <dt><b>{@link Paho.MQTT.Message}</b></dt>
46
 * <dd>This encapsulates the payload of the message along with various attributes
47
 * associated with its delivery, in particular the destination to which it has
48
 * been (or is about to be) sent.</dd>
49
 * </dl>
50
 * <p>
51
 * The programming interface validates parameters passed to it, and will throw
52
 * an Error containing an error message intended for developer use, if it detects
53
 * an error with any parameter.
54
 * <p>
55
 * Example:
56
 *
57
 * <code><pre>
58
client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId");
59
client.onConnectionLost = onConnectionLost;
60
client.onMessageArrived = onMessageArrived;
61
client.connect({onSuccess:onConnect});
62
63
function onConnect() {
64
  // Once a connection has been made, make a subscription and send a message.
65
  console.log("onConnect");
66
  client.subscribe("/World");
67
  message = new Paho.MQTT.Message("Hello");
68
  message.destinationName = "/World";
69
  client.send(message);
70
};
71
function onConnectionLost(responseObject) {
72
  if (responseObject.errorCode !== 0)
73
	console.log("onConnectionLost:"+responseObject.errorMessage);
74
};
75
function onMessageArrived(message) {
76
  console.log("onMessageArrived:"+message.payloadString);
77
  client.disconnect();
78
};
79
 * </pre></code>
80
 * @namespace Paho.MQTT
81
 */
82
83
/* jshint shadow:true */
84
(function ExportLibrary(root, factory) {
85
	if(typeof exports === 'object' && typeof module === 'object'){
86
		module.exports = factory();
87
	} else if (typeof define === 'function' && define.amd){
88
		define(factory);
89
	} else if (typeof exports === 'object'){
90
		exports = factory();
91
	} else {
92
		if (typeof root.Paho === 'undefined'){
93
			root.Paho = {};
94
		}
95
		root.Paho.MQTT = factory();
96
	}
97
})(this, function LibraryFactory(){
98
99
100
var PahoMQTT = (function (global) {
101
102
	// Private variables below, these are only visible inside the function closure
103
	// which is used to define the module.
104
105
	var version = "@VERSION@";
106
	var buildLevel = "@BUILDLEVEL@";
107
108
	/**
109
	 * Unique message type identifiers, with associated
110
	 * associated integer values.
111
	 * @private
112
	 */
113
	var MESSAGE_TYPE = {
114
		CONNECT: 1,
115
		CONNACK: 2,
116
		PUBLISH: 3,
117
		PUBACK: 4,
118
		PUBREC: 5,
119
		PUBREL: 6,
120
		PUBCOMP: 7,
121
		SUBSCRIBE: 8,
122
		SUBACK: 9,
123
		UNSUBSCRIBE: 10,
124
		UNSUBACK: 11,
125
		PINGREQ: 12,
126
		PINGRESP: 13,
127
		DISCONNECT: 14
128
	};
129
130
	// Collection of utility methods used to simplify module code
131
	// and promote the DRY pattern.
132
133
	/**
134
	 * Validate an object's parameter names to ensure they
135
	 * match a list of expected variables name for this option
136
	 * type. Used to ensure option object passed into the API don't
137
	 * contain erroneous parameters.
138
	 * @param {Object} obj - User options object
139
	 * @param {Object} keys - valid keys and types that may exist in obj.
140
	 * @throws {Error} Invalid option parameter found.
141
	 * @private
142
	 */
143
	var validate = function(obj, keys) {
144
		for (var key in obj) {
145
			if (obj.hasOwnProperty(key)) {
146
				if (keys.hasOwnProperty(key)) {
147
					if (typeof obj[key] !== keys[key])
148
					   throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key]));
149
				} else {
150
					var errorStr = "Unknown property, " + key + ". Valid properties are:";
151
					for (var validKey in keys)
152
						if (keys.hasOwnProperty(validKey))
153
							errorStr = errorStr+" "+validKey;
154
					throw new Error(errorStr);
155
				}
156
			}
157
		}
158
	};
159
160
	/**
161
	 * Return a new function which runs the user function bound
162
	 * to a fixed scope.
163
	 * @param {function} User function
164
	 * @param {object} Function scope
165
	 * @return {function} User function bound to another scope
166
	 * @private
167
	 */
168
	var scope = function (f, scope) {
169
		return function () {
170
			return f.apply(scope, arguments);
171
		};
172
	};
173
174
	/**
175
	 * Unique message type identifiers, with associated
176
	 * associated integer values.
177
	 * @private
178
	 */
179
	var ERROR = {
180
		OK: {code:0, text:"AMQJSC0000I OK."},
181
		CONNECT_TIMEOUT: {code:1, text:"AMQJSC0001E Connect timed out."},
182
		SUBSCRIBE_TIMEOUT: {code:2, text:"AMQJS0002E Subscribe timed out."},
183
		UNSUBSCRIBE_TIMEOUT: {code:3, text:"AMQJS0003E Unsubscribe timed out."},
184
		PING_TIMEOUT: {code:4, text:"AMQJS0004E Ping timed out."},
185
		INTERNAL_ERROR: {code:5, text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"},
186
		CONNACK_RETURNCODE: {code:6, text:"AMQJS0006E Bad Connack return code:{0} {1}."},
187
		SOCKET_ERROR: {code:7, text:"AMQJS0007E Socket error:{0}."},
188
		SOCKET_CLOSE: {code:8, text:"AMQJS0008I Socket closed."},
189
		MALFORMED_UTF: {code:9, text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."},
190
		UNSUPPORTED: {code:10, text:"AMQJS0010E {0} is not supported by this browser."},
191
		INVALID_STATE: {code:11, text:"AMQJS0011E Invalid state {0}."},
192
		INVALID_TYPE: {code:12, text:"AMQJS0012E Invalid type {0} for {1}."},
193
		INVALID_ARGUMENT: {code:13, text:"AMQJS0013E Invalid argument {0} for {1}."},
194
		UNSUPPORTED_OPERATION: {code:14, text:"AMQJS0014E Unsupported operation."},
195
		INVALID_STORED_DATA: {code:15, text:"AMQJS0015E Invalid data in local storage key={0} value={1}."},
196
		INVALID_MQTT_MESSAGE_TYPE: {code:16, text:"AMQJS0016E Invalid MQTT message type {0}."},
197
		MALFORMED_UNICODE: {code:17, text:"AMQJS0017E Malformed Unicode string:{0} {1}."},
198
		BUFFER_FULL: {code:18, text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."},
199
	};
200
201
	/** CONNACK RC Meaning. */
202
	var CONNACK_RC = {
203
		0:"Connection Accepted",
204
		1:"Connection Refused: unacceptable protocol version",
205
		2:"Connection Refused: identifier rejected",
206
		3:"Connection Refused: server unavailable",
207
		4:"Connection Refused: bad user name or password",
208
		5:"Connection Refused: not authorized"
209
	};
210
211
	/**
212
	 * Format an error message text.
213
	 * @private
214
	 * @param {error} ERROR.KEY value above.
215
	 * @param {substitutions} [array] substituted into the text.
216
	 * @return the text with the substitutions made.
217
	 */
218
	var format = function(error, substitutions) {
219
		var text = error.text;
220
		if (substitutions) {
221
		  var field,start;
222
		  for (var i=0; i<substitutions.length; i++) {
223
			field = "{"+i+"}";
224
			start = text.indexOf(field);
225
			if(start > 0) {
226
				var part1 = text.substring(0,start);
227
				var part2 = text.substring(start+field.length);
228
				text = part1+substitutions[i]+part2;
229
			}
230
		  }
231
		}
232
		return text;
233
	};
234
235
	//MQTT protocol and version          6    M    Q    I    s    d    p    3
236
	var MqttProtoIdentifierv3 = [0x00,0x06,0x4d,0x51,0x49,0x73,0x64,0x70,0x03];
237
	//MQTT proto/version for 311         4    M    Q    T    T    4
238
	var MqttProtoIdentifierv4 = [0x00,0x04,0x4d,0x51,0x54,0x54,0x04];
239
240
	/**
241
	 * Construct an MQTT wire protocol message.
242
	 * @param type MQTT packet type.
243
	 * @param options optional wire message attributes.
244
	 *
245
	 * Optional properties
246
	 *
247
	 * messageIdentifier: message ID in the range [0..65535]
248
	 * payloadMessage:	Application Message - PUBLISH only
249
	 * connectStrings:	array of 0 or more Strings to be put into the CONNECT payload
250
	 * topics:			array of strings (SUBSCRIBE, UNSUBSCRIBE)
251
	 * requestQoS:		array of QoS values [0..2]
252
	 *
253
	 * "Flag" properties
254
	 * cleanSession:	true if present / false if absent (CONNECT)
255
	 * willMessage:  	true if present / false if absent (CONNECT)
256
	 * isRetained:		true if present / false if absent (CONNECT)
257
	 * userName:		true if present / false if absent (CONNECT)
258
	 * password:		true if present / false if absent (CONNECT)
259
	 * keepAliveInterval:	integer [0..65535]  (CONNECT)
260
	 *
261
	 * @private
262
	 * @ignore
263
	 */
264
	var WireMessage = function (type, options) {
265
		this.type = type;
266
		for (var name in options) {
267
			if (options.hasOwnProperty(name)) {
268
				this[name] = options[name];
269
			}
270
		}
271
	};
272
273
	WireMessage.prototype.encode = function() {
274
		// Compute the first byte of the fixed header
275
		var first = ((this.type & 0x0f) << 4);
276
277
		/*
278
		 * Now calculate the length of the variable header + payload by adding up the lengths
279
		 * of all the component parts
280
		 */
281
282
		var remLength = 0;
283
		var topicStrLength = [];
284
		var destinationNameLength = 0;
285
		var willMessagePayloadBytes;
286
287
		// if the message contains a messageIdentifier then we need two bytes for that
288
		if (this.messageIdentifier !== undefined)
289
			remLength += 2;
290
291
		switch(this.type) {
292
			// If this a Connect then we need to include 12 bytes for its header
293
			case MESSAGE_TYPE.CONNECT:
294
				switch(this.mqttVersion) {
295
					case 3:
296
						remLength += MqttProtoIdentifierv3.length + 3;
297
						break;
298
					case 4:
299
						remLength += MqttProtoIdentifierv4.length + 3;
300
						break;
301
				}
302
303
				remLength += UTF8Length(this.clientId) + 2;
304
				if (this.willMessage !== undefined) {
305
					remLength += UTF8Length(this.willMessage.destinationName) + 2;
306
					// Will message is always a string, sent as UTF-8 characters with a preceding length.
307
					willMessagePayloadBytes = this.willMessage.payloadBytes;
308
					if (!(willMessagePayloadBytes instanceof Uint8Array))
309
						willMessagePayloadBytes = new Uint8Array(payloadBytes);
310
					remLength += willMessagePayloadBytes.byteLength +2;
311
				}
312
				if (this.userName !== undefined)
313
					remLength += UTF8Length(this.userName) + 2;
314
				if (this.password !== undefined)
315
					remLength += UTF8Length(this.password) + 2;
316
			break;
317
318
			// Subscribe, Unsubscribe can both contain topic strings
319
			case MESSAGE_TYPE.SUBSCRIBE:
320
				first |= 0x02; // Qos = 1;
321
				for ( var i = 0; i < this.topics.length; i++) {
322
					topicStrLength[i] = UTF8Length(this.topics[i]);
323
					remLength += topicStrLength[i] + 2;
324
				}
325
				remLength += this.requestedQos.length; // 1 byte for each topic's Qos
326
				// QoS on Subscribe only
327
				break;
328
329
			case MESSAGE_TYPE.UNSUBSCRIBE:
330
				first |= 0x02; // Qos = 1;
331
				for ( var i = 0; i < this.topics.length; i++) {
332
					topicStrLength[i] = UTF8Length(this.topics[i]);
333
					remLength += topicStrLength[i] + 2;
334
				}
335
				break;
336
337
			case MESSAGE_TYPE.PUBREL:
338
				first |= 0x02; // Qos = 1;
339
				break;
340
341
			case MESSAGE_TYPE.PUBLISH:
342
				if (this.payloadMessage.duplicate) first |= 0x08;
343
				first  = first |= (this.payloadMessage.qos << 1);
344
				if (this.payloadMessage.retained) first |= 0x01;
345
				destinationNameLength = UTF8Length(this.payloadMessage.destinationName);
346
				remLength += destinationNameLength + 2;
347
				var payloadBytes = this.payloadMessage.payloadBytes;
348
				remLength += payloadBytes.byteLength;
349
				if (payloadBytes instanceof ArrayBuffer)
350
					payloadBytes = new Uint8Array(payloadBytes);
351
				else if (!(payloadBytes instanceof Uint8Array))
352
					payloadBytes = new Uint8Array(payloadBytes.buffer);
353
				break;
354
355
			case MESSAGE_TYPE.DISCONNECT:
356
				break;
357
358
			default:
359
				break;
360
		}
361
362
		// Now we can allocate a buffer for the message
363
364
		var mbi = encodeMBI(remLength);  // Convert the length to MQTT MBI format
365
		var pos = mbi.length + 1;        // Offset of start of variable header
366
		var buffer = new ArrayBuffer(remLength + pos);
367
		var byteStream = new Uint8Array(buffer);    // view it as a sequence of bytes
368
369
		//Write the fixed header into the buffer
370
		byteStream[0] = first;
371
		byteStream.set(mbi,1);
372
373
		// If this is a PUBLISH then the variable header starts with a topic
374
		if (this.type == MESSAGE_TYPE.PUBLISH)
375
			pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos);
376
		// If this is a CONNECT then the variable header contains the protocol name/version, flags and keepalive time
377
378
		else if (this.type == MESSAGE_TYPE.CONNECT) {
379
			switch (this.mqttVersion) {
380
				case 3:
381
					byteStream.set(MqttProtoIdentifierv3, pos);
382
					pos += MqttProtoIdentifierv3.length;
383
					break;
384
				case 4:
385
					byteStream.set(MqttProtoIdentifierv4, pos);
386
					pos += MqttProtoIdentifierv4.length;
387
					break;
388
			}
389
			var connectFlags = 0;
390
			if (this.cleanSession)
391
				connectFlags = 0x02;
392
			if (this.willMessage !== undefined ) {
393
				connectFlags |= 0x04;
394
				connectFlags |= (this.willMessage.qos<<3);
395
				if (this.willMessage.retained) {
396
					connectFlags |= 0x20;
397
				}
398
			}
399
			if (this.userName !== undefined)
400
				connectFlags |= 0x80;
401
			if (this.password !== undefined)
402
				connectFlags |= 0x40;
403
			byteStream[pos++] = connectFlags;
404
			pos = writeUint16 (this.keepAliveInterval, byteStream, pos);
405
		}
406
407
		// Output the messageIdentifier - if there is one
408
		if (this.messageIdentifier !== undefined)
409
			pos = writeUint16 (this.messageIdentifier, byteStream, pos);
410
411
		switch(this.type) {
412
			case MESSAGE_TYPE.CONNECT:
413
				pos = writeString(this.clientId, UTF8Length(this.clientId), byteStream, pos);
414
				if (this.willMessage !== undefined) {
415
					pos = writeString(this.willMessage.destinationName, UTF8Length(this.willMessage.destinationName), byteStream, pos);
416
					pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos);
417
					byteStream.set(willMessagePayloadBytes, pos);
418
					pos += willMessagePayloadBytes.byteLength;
419
420
				}
421
			if (this.userName !== undefined)
422
				pos = writeString(this.userName, UTF8Length(this.userName), byteStream, pos);
423
			if (this.password !== undefined)
424
				pos = writeString(this.password, UTF8Length(this.password), byteStream, pos);
425
			break;
426
427
			case MESSAGE_TYPE.PUBLISH:
428
				// PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters.
429
				byteStream.set(payloadBytes, pos);
430
431
				break;
432
433
//    	    case MESSAGE_TYPE.PUBREC:
434
//    	    case MESSAGE_TYPE.PUBREL:
435
//    	    case MESSAGE_TYPE.PUBCOMP:
436
//    	    	break;
437
438
			case MESSAGE_TYPE.SUBSCRIBE:
439
				// SUBSCRIBE has a list of topic strings and request QoS
440
				for (var i=0; i<this.topics.length; i++) {
441
					pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos);
442
					byteStream[pos++] = this.requestedQos[i];
443
				}
444
				break;
445
446
			case MESSAGE_TYPE.UNSUBSCRIBE:
447
				// UNSUBSCRIBE has a list of topic strings
448
				for (var i=0; i<this.topics.length; i++)
449
					pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos);
450
				break;
451
452
			default:
453
				// Do nothing.
454
		}
455
456
		return buffer;
457
	};
458
459
	function decodeMessage(input,pos) {
460
	    var startingPos = pos;
461
		var first = input[pos];
462
		var type = first >> 4;
463
		var messageInfo = first &= 0x0f;
464
		pos += 1;
465
466
467
		// Decode the remaining length (MBI format)
468
469
		var digit;
470
		var remLength = 0;
471
		var multiplier = 1;
472
		do {
473
			if (pos == input.length) {
474
			    return [null,startingPos];
475
			}
476
			digit = input[pos++];
477
			remLength += ((digit & 0x7F) * multiplier);
478
			multiplier *= 128;
479
		} while ((digit & 0x80) !== 0);
480
481
		var endPos = pos+remLength;
482
		if (endPos > input.length) {
483
		    return [null,startingPos];
484
		}
485
486
		var wireMessage = new WireMessage(type);
487
		switch(type) {
488
			case MESSAGE_TYPE.CONNACK:
489
				var connectAcknowledgeFlags = input[pos++];
490
				if (connectAcknowledgeFlags & 0x01)
491
					wireMessage.sessionPresent = true;
492
				wireMessage.returnCode = input[pos++];
493
				break;
494
495
			case MESSAGE_TYPE.PUBLISH:
496
				var qos = (messageInfo >> 1) & 0x03;
497
498
				var len = readUint16(input, pos);
499
				pos += 2;
500
				var topicName = parseUTF8(input, pos, len);
501
				pos += len;
502
				// If QoS 1 or 2 there will be a messageIdentifier
503
				if (qos > 0) {
504
					wireMessage.messageIdentifier = readUint16(input, pos);
505
					pos += 2;
506
				}
507
508
				var message = new Paho.MQTT.Message(input.subarray(pos, endPos));
509
				if ((messageInfo & 0x01) == 0x01)
510
					message.retained = true;
511
				if ((messageInfo & 0x08) == 0x08)
512
					message.duplicate =  true;
513
				message.qos = qos;
514
				message.destinationName = topicName;
515
				wireMessage.payloadMessage = message;
516
				break;
517
518
			case  MESSAGE_TYPE.PUBACK:
519
			case  MESSAGE_TYPE.PUBREC:
520
			case  MESSAGE_TYPE.PUBREL:
521
			case  MESSAGE_TYPE.PUBCOMP:
522
			case  MESSAGE_TYPE.UNSUBACK:
523
				wireMessage.messageIdentifier = readUint16(input, pos);
524
				break;
525
526
			case  MESSAGE_TYPE.SUBACK:
527
				wireMessage.messageIdentifier = readUint16(input, pos);
528
				pos += 2;
529
				wireMessage.returnCode = input.subarray(pos, endPos);
530
				break;
531
532
			default:
533
				break;
534
		}
535
536
		return [wireMessage,endPos];
537
	}
538
539
	function writeUint16(input, buffer, offset) {
540
		buffer[offset++] = input >> 8;      //MSB
541
		buffer[offset++] = input % 256;     //LSB
542
		return offset;
543
	}
544
545
	function writeString(input, utf8Length, buffer, offset) {
546
		offset = writeUint16(utf8Length, buffer, offset);
547
		stringToUTF8(input, buffer, offset);
548
		return offset + utf8Length;
549
	}
550
551
	function readUint16(buffer, offset) {
552
		return 256*buffer[offset] + buffer[offset+1];
553
	}
554
555
	/**
556
	 * Encodes an MQTT Multi-Byte Integer
557
	 * @private
558
	 */
559
	function encodeMBI(number) {
560
		var output = new Array(1);
561
		var numBytes = 0;
562
563
		do {
564
			var digit = number % 128;
565
			number = number >> 7;
566
			if (number > 0) {
567
				digit |= 0x80;
568
			}
569
			output[numBytes++] = digit;
570
		} while ( (number > 0) && (numBytes<4) );
571
572
		return output;
573
	}
574
575
	/**
576
	 * Takes a String and calculates its length in bytes when encoded in UTF8.
577
	 * @private
578
	 */
579
	function UTF8Length(input) {
580
		var output = 0;
581
		for (var i = 0; i<input.length; i++)
582
		{
583
			var charCode = input.charCodeAt(i);
584
				if (charCode > 0x7FF)
585
				   {
586
					  // Surrogate pair means its a 4 byte character
587
					  if (0xD800 <= charCode && charCode <= 0xDBFF)
588
						{
589
						  i++;
590
						  output++;
591
						}
592
				   output +=3;
593
				   }
594
			else if (charCode > 0x7F)
595
				output +=2;
596
			else
597
				output++;
598
		}
599
		return output;
600
	}
601
602
	/**
603
	 * Takes a String and writes it into an array as UTF8 encoded bytes.
604
	 * @private
605
	 */
606
	function stringToUTF8(input, output, start) {
607
		var pos = start;
608
		for (var i = 0; i<input.length; i++) {
609
			var charCode = input.charCodeAt(i);
610
611
			// Check for a surrogate pair.
612
			if (0xD800 <= charCode && charCode <= 0xDBFF) {
613
				var lowCharCode = input.charCodeAt(++i);
614
				if (isNaN(lowCharCode)) {
615
					throw new Error(format(ERROR.MALFORMED_UNICODE, [charCode, lowCharCode]));
616
				}
617
				charCode = ((charCode - 0xD800)<<10) + (lowCharCode - 0xDC00) + 0x10000;
618
619
			}
620
621
			if (charCode <= 0x7F) {
622
				output[pos++] = charCode;
623
			} else if (charCode <= 0x7FF) {
624
				output[pos++] = charCode>>6  & 0x1F | 0xC0;
625
				output[pos++] = charCode     & 0x3F | 0x80;
626
			} else if (charCode <= 0xFFFF) {
627
				output[pos++] = charCode>>12 & 0x0F | 0xE0;
628
				output[pos++] = charCode>>6  & 0x3F | 0x80;
629
				output[pos++] = charCode     & 0x3F | 0x80;
630
			} else {
631
				output[pos++] = charCode>>18 & 0x07 | 0xF0;
632
				output[pos++] = charCode>>12 & 0x3F | 0x80;
633
				output[pos++] = charCode>>6  & 0x3F | 0x80;
634
				output[pos++] = charCode     & 0x3F | 0x80;
635
			}
636
		}
637
		return output;
638
	}
639
640
	function parseUTF8(input, offset, length) {
641
		var output = "";
642
		var utf16;
643
		var pos = offset;
644
645
		while (pos < offset+length)
646
		{
647
			var byte1 = input[pos++];
648
			if (byte1 < 128)
649
				utf16 = byte1;
650
			else
651
			{
652
				var byte2 = input[pos++]-128;
653
				if (byte2 < 0)
654
					throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16),""]));
655
				if (byte1 < 0xE0)             // 2 byte character
656
					utf16 = 64*(byte1-0xC0) + byte2;
657
				else
658
				{
659
					var byte3 = input[pos++]-128;
660
					if (byte3 < 0)
661
						throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)]));
662
					if (byte1 < 0xF0)        // 3 byte character
663
						utf16 = 4096*(byte1-0xE0) + 64*byte2 + byte3;
664
								else
665
								{
666
								   var byte4 = input[pos++]-128;
667
								   if (byte4 < 0)
668
						throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)]));
669
								   if (byte1 < 0xF8)        // 4 byte character
670
										   utf16 = 262144*(byte1-0xF0) + 4096*byte2 + 64*byte3 + byte4;
671
					   else                     // longer encodings are not supported
672
						throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)]));
673
								}
674
				}
675
			}
676
677
				if (utf16 > 0xFFFF)   // 4 byte character - express as a surrogate pair
678
				  {
679
					 utf16 -= 0x10000;
680
					 output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character
681
					 utf16 = 0xDC00 + (utf16 & 0x3FF);  // trail character
682
				  }
683
			output += String.fromCharCode(utf16);
684
		}
685
		return output;
686
	}
687
688
	/**
689
	 * Repeat keepalive requests, monitor responses.
690
	 * @ignore
691
	 */
692
	var Pinger = function(client, window, keepAliveInterval) {
693
		this._client = client;
694
		this._window = window;
695
		this._keepAliveInterval = keepAliveInterval*1000;
696
		this.isReset = false;
697
698
		var pingReq = new WireMessage(MESSAGE_TYPE.PINGREQ).encode();
699
700
		var doTimeout = function (pinger) {
701
			return function () {
702
				return doPing.apply(pinger);
703
			};
704
		};
705
706
		/** @ignore */
707
		var doPing = function() {
708
			if (!this.isReset) {
709
				this._client._trace("Pinger.doPing", "Timed out");
710
				this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT));
711
			} else {
712
				this.isReset = false;
713
				this._client._trace("Pinger.doPing", "send PINGREQ");
714
				this._client.socket.send(pingReq);
715
				this.timeout = this._window.setTimeout(doTimeout(this), this._keepAliveInterval);
716
			}
717
		};
718
719
		this.reset = function() {
720
			this.isReset = true;
721
			this._window.clearTimeout(this.timeout);
722
			if (this._keepAliveInterval > 0)
723
				this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval);
724
		};
725
726
		this.cancel = function() {
727
			this._window.clearTimeout(this.timeout);
728
		};
729
	 };
730
731
	/**
732
	 * Monitor request completion.
733
	 * @ignore
734
	 */
735
	var Timeout = function(client, window, timeoutSeconds, action, args) {
736
		this._window = window;
737
		if (!timeoutSeconds)
738
			timeoutSeconds = 30;
739
740
		var doTimeout = function (action, client, args) {
741
			return function () {
742
				return action.apply(client, args);
743
			};
744
		};
745
		this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000);
746
747
		this.cancel = function() {
748
			this._window.clearTimeout(this.timeout);
749
		};
750
	};
751
752
	/*
753
	 * Internal implementation of the Websockets MQTT V3.1 client.
754
	 *
755
	 * @name Paho.MQTT.ClientImpl @constructor
756
	 * @param {String} host the DNS nameof the webSocket host.
757
	 * @param {Number} port the port number for that host.
758
	 * @param {String} clientId the MQ client identifier.
759
	 */
760
	var ClientImpl = function (uri, host, port, path, clientId) {
761
		// Check dependencies are satisfied in this browser.
762
		if (!("WebSocket" in global && global.WebSocket !== null)) {
763
			throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"]));
764
		}
765
		if (!("localStorage" in global && global.localStorage !== null)) {
766
			throw new Error(format(ERROR.UNSUPPORTED, ["localStorage"]));
767
		}
768
		if (!("ArrayBuffer" in global && global.ArrayBuffer !== null)) {
769
			throw new Error(format(ERROR.UNSUPPORTED, ["ArrayBuffer"]));
770
		}
771
		this._trace("Paho.MQTT.Client", uri, host, port, path, clientId);
772
773
		this.host = host;
774
		this.port = port;
775
		this.path = path;
776
		this.uri = uri;
777
		this.clientId = clientId;
778
		this._wsuri = null;
779
780
		// Local storagekeys are qualified with the following string.
781
		// The conditional inclusion of path in the key is for backward
782
		// compatibility to when the path was not configurable and assumed to
783
		// be /mqtt
784
		this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":";
785
786
		// Create private instance-only message queue
787
		// Internal queue of messages to be sent, in sending order.
788
		this._msg_queue = [];
789
		this._buffered_msg_queue = [];
790
791
		// Messages we have sent and are expecting a response for, indexed by their respective message ids.
792
		this._sentMessages = {};
793
794
		// Messages we have received and acknowleged and are expecting a confirm message for
795
		// indexed by their respective message ids.
796
		this._receivedMessages = {};
797
798
		// Internal list of callbacks to be executed when messages
799
		// have been successfully sent over web socket, e.g. disconnect
800
		// when it doesn't have to wait for ACK, just message is dispatched.
801
		this._notify_msg_sent = {};
802
803
		// Unique identifier for SEND messages, incrementing
804
		// counter as messages are sent.
805
		this._message_identifier = 1;
806
807
		// Used to determine the transmission sequence of stored sent messages.
808
		this._sequence = 0;
809
810
811
		// Load the local state, if any, from the saved version, only restore state relevant to this client.
812
		for (var key in localStorage)
813
			if (   key.indexOf("Sent:"+this._localKey) === 0 || key.indexOf("Received:"+this._localKey) === 0)
814
			this.restore(key);
815
	};
816
817
	// Messaging Client public instance members.
818
	ClientImpl.prototype.host = null;
819
	ClientImpl.prototype.port = null;
820
	ClientImpl.prototype.path = null;
821
	ClientImpl.prototype.uri = null;
822
	ClientImpl.prototype.clientId = null;
823
824
	// Messaging Client private instance members.
825
	ClientImpl.prototype.socket = null;
826
	/* true once we have received an acknowledgement to a CONNECT packet. */
827
	ClientImpl.prototype.connected = false;
828
	/* The largest message identifier allowed, may not be larger than 2**16 but
829
	 * if set smaller reduces the maximum number of outbound messages allowed.
830
	 */
831
	ClientImpl.prototype.maxMessageIdentifier = 65536;
832
	ClientImpl.prototype.connectOptions = null;
833
	ClientImpl.prototype.hostIndex = null;
834
	ClientImpl.prototype.onConnected = null;
835
	ClientImpl.prototype.onConnectionLost = null;
836
	ClientImpl.prototype.onMessageDelivered = null;
837
	ClientImpl.prototype.onMessageArrived = null;
838
	ClientImpl.prototype.traceFunction = null;
839
	ClientImpl.prototype._msg_queue = null;
840
	ClientImpl.prototype._buffered_msg_queue = null;
841
	ClientImpl.prototype._connectTimeout = null;
842
	/* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */
843
	ClientImpl.prototype.sendPinger = null;
844
	/* The receivePinger monitors how long we allow before we require evidence that the server is alive. */
845
	ClientImpl.prototype.receivePinger = null;
846
	ClientImpl.prototype._reconnectInterval = 1; // Reconnect Delay, starts at 1 second
847
	ClientImpl.prototype._reconnecting = false;
848
	ClientImpl.prototype._reconnectTimeout = null;
849
	ClientImpl.prototype.disconnectedPublishing = false;
850
	ClientImpl.prototype.disconnectedBufferSize = 5000;
851
852
	ClientImpl.prototype.receiveBuffer = null;
853
854
	ClientImpl.prototype._traceBuffer = null;
855
	ClientImpl.prototype._MAX_TRACE_ENTRIES = 100;
856
857
	ClientImpl.prototype.connect = function (connectOptions) {
858
		var connectOptionsMasked = this._traceMask(connectOptions, "password");
859
		this._trace("Client.connect", connectOptionsMasked, this.socket, this.connected);
860
861
		if (this.connected)
862
			throw new Error(format(ERROR.INVALID_STATE, ["already connected"]));
863
		if (this.socket)
864
			throw new Error(format(ERROR.INVALID_STATE, ["already connected"]));
865
866
		if (this._reconnecting) {
867
			// connect() function is called while reconnect is in progress.
868
			// Terminate the auto reconnect process to use new connect options.
869
			this._reconnectTimeout.cancel();
870
			this._reconnectTimeout = null;
871
			this._reconnecting = false;
872
		}
873
874
		this.connectOptions = connectOptions;
875
		this._reconnectInterval = 1;
876
		this._reconnecting = false;
877
		if (connectOptions.uris) {
878
			this.hostIndex = 0;
879
			this._doConnect(connectOptions.uris[0]);
880
		} else {
881
			this._doConnect(this.uri);
882
		}
883
884
	};
885
886
	ClientImpl.prototype.subscribe = function (filter, subscribeOptions) {
887
		this._trace("Client.subscribe", filter, subscribeOptions);
888
889
		if (!this.connected)
890
			throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
891
892
		var wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE);
893
		wireMessage.topics=[filter];
894
		if (subscribeOptions.qos !== undefined)
895
			wireMessage.requestedQos = [subscribeOptions.qos];
896
		else
897
			wireMessage.requestedQos = [0];
898
899
		if (subscribeOptions.onSuccess) {
900
			wireMessage.onSuccess = function(grantedQos) {subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos});};
901
		}
902
903
		if (subscribeOptions.onFailure) {
904
			wireMessage.onFailure = function(errorCode) {subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode, errorMessage:format(errorCode)});};
905
		}
906
907
		if (subscribeOptions.timeout) {
908
			wireMessage.timeOut = new Timeout(this, window, subscribeOptions.timeout, subscribeOptions.onFailure,
909
						[{invocationContext:subscribeOptions.invocationContext,
910
						errorCode:ERROR.SUBSCRIBE_TIMEOUT.code,
911
						errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}]);
912
		}
913
914
		// All subscriptions return a SUBACK.
915
		this._requires_ack(wireMessage);
916
		this._schedule_message(wireMessage);
917
	};
918
919
	/** @ignore */
920
	ClientImpl.prototype.unsubscribe = function(filter, unsubscribeOptions) {
921
		this._trace("Client.unsubscribe", filter, unsubscribeOptions);
922
923
		if (!this.connected)
924
		   throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
925
926
		var wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE);
927
		wireMessage.topics = [filter];
928
929
		if (unsubscribeOptions.onSuccess) {
930
			wireMessage.callback = function() {unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext});};
931
		}
932
		if (unsubscribeOptions.timeout) {
933
			wireMessage.timeOut = new Timeout(this, window, unsubscribeOptions.timeout, unsubscribeOptions.onFailure,
934
					  [{invocationContext:unsubscribeOptions.invocationContext,
935
						errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code,
936
						errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}]);
937
		}
938
939
		// All unsubscribes return a SUBACK.
940
		this._requires_ack(wireMessage);
941
		this._schedule_message(wireMessage);
942
	};
943
944
	ClientImpl.prototype.send = function (message) {
945
		this._trace("Client.send", message);
946
947
		wireMessage = new WireMessage(MESSAGE_TYPE.PUBLISH);
948
		wireMessage.payloadMessage = message;
949
950
		if (this.connected) {
951
			// Mark qos 1 & 2 message as "ACK required"
952
			// For qos 0 message, invoke onMessageDelivered callback if there is one.
953
			// Then schedule the message.
954
			if (message.qos > 0) {
955
			this._requires_ack(wireMessage);
956
			} else if (this.onMessageDelivered) {
957
			this._notify_msg_sent[wireMessage] = this.onMessageDelivered(wireMessage.payloadMessage);
958
			}
959
		this._schedule_message(wireMessage);
960
		} else {
961
			// Currently disconnected, will not schedule this message
962
			// Check if reconnecting is in progress and disconnected publish is enabled.
963
			if (this._reconnecting && this.disconnectedPublishing) {
964
				// Check the limit which include the "required ACK" messages
965
				var messageCount = Object.keys(this._sentMessages).length + this._buffered_msg_queue.length;
966
				if (messageCount > this.disconnectedBufferSize) {
967
					throw new Error(format(ERROR.BUFFER_FULL, [this.disconnectedBufferSize]));
968
				} else {
969
					if (message.qos > 0) {
970
						// Mark this message as "ACK required"
971
						this._requires_ack(wireMessage);
972
					} else {
973
						wireMessage.sequence = ++this._sequence;
974
						this._buffered_msg_queue.push(wireMessage);
975
					}
976
				}
977
			} else {
978
				throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
979
			}
980
		}
981
	};
982
983
	ClientImpl.prototype.disconnect = function () {
984
		this._trace("Client.disconnect");
985
986
		if (this._reconnecting) {
987
			// disconnect() function is called while reconnect is in progress.
988
			// Terminate the auto reconnect process.
989
			this._reconnectTimeout.cancel();
990
			this._reconnectTimeout = null;
991
			this._reconnecting = false;
992
		}
993
994
		if (!this.socket)
995
			throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"]));
996
997
		wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT);
998
999
		// Run the disconnected call back as soon as the message has been sent,
1000
		// in case of a failure later on in the disconnect processing.
1001
		// as a consequence, the _disconected call back may be run several times.
1002
		this._notify_msg_sent[wireMessage] = scope(this._disconnected, this);
1003
1004
		this._schedule_message(wireMessage);
1005
	};
1006
1007
	ClientImpl.prototype.getTraceLog = function () {
1008
		if ( this._traceBuffer !== null ) {
1009
			this._trace("Client.getTraceLog", new Date());
1010
			this._trace("Client.getTraceLog in flight messages", this._sentMessages.length);
1011
			for (var key in this._sentMessages)
1012
				this._trace("_sentMessages ",key, this._sentMessages[key]);
1013
			for (var key in this._receivedMessages)
1014
				this._trace("_receivedMessages ",key, this._receivedMessages[key]);
1015
1016
			return this._traceBuffer;
1017
		}
1018
	};
1019
1020
	ClientImpl.prototype.startTrace = function () {
1021
		if ( this._traceBuffer === null ) {
1022
			this._traceBuffer = [];
1023
		}
1024
		this._trace("Client.startTrace", new Date(), version);
1025
	};
1026
1027
	ClientImpl.prototype.stopTrace = function () {
1028
		delete this._traceBuffer;
1029
	};
1030
1031
	ClientImpl.prototype._doConnect = function (wsurl) {
1032
		// When the socket is open, this client will send the CONNECT WireMessage using the saved parameters.
1033
		if (this.connectOptions.useSSL) {
1034
		    var uriParts = wsurl.split(":");
1035
		    uriParts[0] = "wss";
1036
		    wsurl = uriParts.join(":");
1037
		}
1038
		this._wsuri = wsurl;
1039
		this.connected = false;
1040
1041
1042
1043
		if (this.connectOptions.mqttVersion < 4) {
1044
			this.socket = new WebSocket(wsurl, ["mqttv3.1"]);
1045
		} else {
1046
			this.socket = new WebSocket(wsurl, ["mqtt"]);
1047
		}
1048
		this.socket.binaryType = 'arraybuffer';
1049
		this.socket.onopen = scope(this._on_socket_open, this);
1050
		this.socket.onmessage = scope(this._on_socket_message, this);
1051
		this.socket.onerror = scope(this._on_socket_error, this);
1052
		this.socket.onclose = scope(this._on_socket_close, this);
1053
1054
		this.sendPinger = new Pinger(this, window, this.connectOptions.keepAliveInterval);
1055
		this.receivePinger = new Pinger(this, window, this.connectOptions.keepAliveInterval);
1056
		if (this._connectTimeout) {
1057
			this._connectTimeout.cancel();
1058
			this._connectTimeout = null;
1059
		}
1060
		this._connectTimeout = new Timeout(this, window, this.connectOptions.timeout, this._disconnected,  [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]);
1061
	};
1062
1063
1064
	// Schedule a new message to be sent over the WebSockets
1065
	// connection. CONNECT messages cause WebSocket connection
1066
	// to be started. All other messages are queued internally
1067
	// until this has happened. When WS connection starts, process
1068
	// all outstanding messages.
1069
	ClientImpl.prototype._schedule_message = function (message) {
1070
		this._msg_queue.push(message);
1071
		// Process outstanding messages in the queue if we have an  open socket, and have received CONNACK.
1072
		if (this.connected) {
1073
			this._process_queue();
1074
		}
1075
	};
1076
1077
	ClientImpl.prototype.store = function(prefix, wireMessage) {
1078
		var storedMessage = {type:wireMessage.type, messageIdentifier:wireMessage.messageIdentifier, version:1};
1079
1080
		switch(wireMessage.type) {
1081
		  case MESSAGE_TYPE.PUBLISH:
1082
			  if(wireMessage.pubRecReceived)
1083
				  storedMessage.pubRecReceived = true;
1084
1085
			  // Convert the payload to a hex string.
1086
			  storedMessage.payloadMessage = {};
1087
			  var hex = "";
1088
			  var messageBytes = wireMessage.payloadMessage.payloadBytes;
1089
			  for (var i=0; i<messageBytes.length; i++) {
1090
				if (messageBytes[i] <= 0xF)
1091
				  hex = hex+"0"+messageBytes[i].toString(16);
1092
				else
1093
				  hex = hex+messageBytes[i].toString(16);
1094
			  }
1095
			  storedMessage.payloadMessage.payloadHex = hex;
1096
1097
			  storedMessage.payloadMessage.qos = wireMessage.payloadMessage.qos;
1098
			  storedMessage.payloadMessage.destinationName = wireMessage.payloadMessage.destinationName;
1099
			  if (wireMessage.payloadMessage.duplicate)
1100
				  storedMessage.payloadMessage.duplicate = true;
1101
			  if (wireMessage.payloadMessage.retained)
1102
				  storedMessage.payloadMessage.retained = true;
1103
1104
			  // Add a sequence number to sent messages.
1105
			  if ( prefix.indexOf("Sent:") === 0 ) {
1106
				  if ( wireMessage.sequence === undefined )
1107
					  wireMessage.sequence = ++this._sequence;
1108
				  storedMessage.sequence = wireMessage.sequence;
1109
			  }
1110
			  break;
1111
1112
			default:
1113
				throw Error(format(ERROR.INVALID_STORED_DATA, [key, storedMessage]));
1114
		}
1115
		localStorage.setItem(prefix+this._localKey+wireMessage.messageIdentifier, JSON.stringify(storedMessage));
1116
	};
1117
1118
	ClientImpl.prototype.restore = function(key) {
1119
		var value = localStorage.getItem(key);
1120
		var storedMessage = JSON.parse(value);
1121
1122
		var wireMessage = new WireMessage(storedMessage.type, storedMessage);
1123
1124
		switch(storedMessage.type) {
1125
		  case MESSAGE_TYPE.PUBLISH:
1126
			  // Replace the payload message with a Message object.
1127
			  var hex = storedMessage.payloadMessage.payloadHex;
1128
			  var buffer = new ArrayBuffer((hex.length)/2);
1129
			  var byteStream = new Uint8Array(buffer);
1130
			  var i = 0;
1131
			  while (hex.length >= 2) {
1132
				  var x = parseInt(hex.substring(0, 2), 16);
1133
				  hex = hex.substring(2, hex.length);
1134
				  byteStream[i++] = x;
1135
			  }
1136
			  var payloadMessage = new Paho.MQTT.Message(byteStream);
1137
1138
			  payloadMessage.qos = storedMessage.payloadMessage.qos;
1139
			  payloadMessage.destinationName = storedMessage.payloadMessage.destinationName;
1140
			  if (storedMessage.payloadMessage.duplicate)
1141
				  payloadMessage.duplicate = true;
1142
			  if (storedMessage.payloadMessage.retained)
1143
				  payloadMessage.retained = true;
1144
			  wireMessage.payloadMessage = payloadMessage;
1145
1146
			  break;
1147
1148
			default:
1149
			  throw Error(format(ERROR.INVALID_STORED_DATA, [key, value]));
1150
		}
1151
1152
		if (key.indexOf("Sent:"+this._localKey) === 0) {
1153
			wireMessage.payloadMessage.duplicate = true;
1154
			this._sentMessages[wireMessage.messageIdentifier] = wireMessage;
1155
		} else if (key.indexOf("Received:"+this._localKey) === 0) {
1156
			this._receivedMessages[wireMessage.messageIdentifier] = wireMessage;
1157
		}
1158
	};
1159
1160
	ClientImpl.prototype._process_queue = function () {
1161
		var message = null;
1162
		// Process messages in order they were added
1163
		var fifo = this._msg_queue.reverse();
1164
1165
		// Send all queued messages down socket connection
1166
		while ((message = fifo.pop())) {
1167
			this._socket_send(message);
1168
			// Notify listeners that message was successfully sent
1169
			if (this._notify_msg_sent[message]) {
1170
				this._notify_msg_sent[message]();
1171
				delete this._notify_msg_sent[message];
1172
			}
1173
		}
1174
	};
1175
1176
	/**
1177
	 * Expect an ACK response for this message. Add message to the set of in progress
1178
	 * messages and set an unused identifier in this message.
1179
	 * @ignore
1180
	 */
1181
	ClientImpl.prototype._requires_ack = function (wireMessage) {
1182
		var messageCount = Object.keys(this._sentMessages).length;
1183
		if (messageCount > this.maxMessageIdentifier)
1184
			throw Error ("Too many messages:"+messageCount);
1185
1186
		while(this._sentMessages[this._message_identifier] !== undefined) {
1187
			this._message_identifier++;
1188
		}
1189
		wireMessage.messageIdentifier = this._message_identifier;
1190
		this._sentMessages[wireMessage.messageIdentifier] = wireMessage;
1191
		if (wireMessage.type === MESSAGE_TYPE.PUBLISH) {
1192
			this.store("Sent:", wireMessage);
1193
		}
1194
		if (this._message_identifier === this.maxMessageIdentifier) {
1195
			this._message_identifier = 1;
1196
		}
1197
	};
1198
1199
	/**
1200
	 * Called when the underlying websocket has been opened.
1201
	 * @ignore
1202
	 */
1203
	ClientImpl.prototype._on_socket_open = function () {
1204
		// Create the CONNECT message object.
1205
		var wireMessage = new WireMessage(MESSAGE_TYPE.CONNECT, this.connectOptions);
1206
		wireMessage.clientId = this.clientId;
1207
		this._socket_send(wireMessage);
1208
	};
1209
1210
	/**
1211
	 * Called when the underlying websocket has received a complete packet.
1212
	 * @ignore
1213
	 */
1214
	ClientImpl.prototype._on_socket_message = function (event) {
1215
		this._trace("Client._on_socket_message", event.data);
1216
		var messages = this._deframeMessages(event.data);
1217
		for (var i = 0; i < messages.length; i+=1) {
1218
		    this._handleMessage(messages[i]);
1219
		}
1220
	};
1221
1222
	ClientImpl.prototype._deframeMessages = function(data) {
1223
		var byteArray = new Uint8Array(data);
1224
		var messages = [];
1225
	    if (this.receiveBuffer) {
1226
	        var newData = new Uint8Array(this.receiveBuffer.length+byteArray.length);
1227
	        newData.set(this.receiveBuffer);
1228
	        newData.set(byteArray,this.receiveBuffer.length);
1229
	        byteArray = newData;
1230
	        delete this.receiveBuffer;
1231
	    }
1232
		try {
1233
		    var offset = 0;
1234
		    while(offset < byteArray.length) {
1235
		        var result = decodeMessage(byteArray,offset);
1236
		        var wireMessage = result[0];
1237
		        offset = result[1];
1238
		        if (wireMessage !== null) {
1239
		            messages.push(wireMessage);
1240
		        } else {
1241
		            break;
1242
		        }
1243
		    }
1244
		    if (offset < byteArray.length) {
1245
		    	this.receiveBuffer = byteArray.subarray(offset);
1246
		    }
1247
		} catch (error) {
1248
			var errorStack = ((error.hasOwnProperty('stack') == 'undefined') ? error.stack.toString() : "No Error Stack Available");
1249
			this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack]));
1250
			return;
1251
		}
1252
		return messages;
1253
	};
1254
1255
	ClientImpl.prototype._handleMessage = function(wireMessage) {
1256
1257
		this._trace("Client._handleMessage", wireMessage);
1258
1259
		try {
1260
			switch(wireMessage.type) {
1261
			case MESSAGE_TYPE.CONNACK:
1262
				this._connectTimeout.cancel();
1263
				if (this._reconnectTimeout)
1264
					this._reconnectTimeout.cancel();
1265
1266
				// If we have started using clean session then clear up the local state.
1267
				if (this.connectOptions.cleanSession) {
1268
					for (var key in this._sentMessages) {
1269
						var sentMessage = this._sentMessages[key];
1270
						localStorage.removeItem("Sent:"+this._localKey+sentMessage.messageIdentifier);
1271
					}
1272
					this._sentMessages = {};
1273
1274
					for (var key in this._receivedMessages) {
1275
						var receivedMessage = this._receivedMessages[key];
1276
						localStorage.removeItem("Received:"+this._localKey+receivedMessage.messageIdentifier);
1277
					}
1278
					this._receivedMessages = {};
1279
				}
1280
				// Client connected and ready for business.
1281
				if (wireMessage.returnCode === 0) {
1282
1283
					this.connected = true;
1284
					// Jump to the end of the list of uris and stop looking for a good host.
1285
1286
					if (this.connectOptions.uris)
1287
            this.hostIndex = this.connectOptions.uris.length;
1288
1289
				} else {
1290
					this._disconnected(ERROR.CONNACK_RETURNCODE.code , format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]]));
1291
					break;
1292
				}
1293
1294
				// Resend messages.
1295
				var sequencedMessages = [];
1296
				for (var msgId in this._sentMessages) {
1297
					if (this._sentMessages.hasOwnProperty(msgId))
1298
						sequencedMessages.push(this._sentMessages[msgId]);
1299
				}
1300
1301
				// Also schedule qos 0 buffered messages if any
1302
				if (this._buffered_msg_queue.length > 0) {
1303
					var msg = null;
1304
					var fifo = this._buffered_msg_queue.reverse();
1305
					while ((msg = fifo.pop())) {
1306
						sequencedMessages.push(msg);
1307
						if (this.onMessageDelivered)
1308
							this._notify_msg_sent[msg] = this.onMessageDelivered(msg.payloadMessage);
1309
					}
1310
				}
1311
1312
				// Sort sentMessages into the original sent order.
1313
				var sequencedMessages = sequencedMessages.sort(function(a,b) {return a.sequence - b.sequence;} );
1314
				for (var i=0, len=sequencedMessages.length; i<len; i++) {
1315
					var sentMessage = sequencedMessages[i];
1316
					if (sentMessage.type == MESSAGE_TYPE.PUBLISH && sentMessage.pubRecReceived) {
1317
						var pubRelMessage = new WireMessage(MESSAGE_TYPE.PUBREL, {messageIdentifier:sentMessage.messageIdentifier});
1318
						this._schedule_message(pubRelMessage);
1319
					} else {
1320
						this._schedule_message(sentMessage);
1321
				}
1322
				}
1323
1324
				// Execute the connectOptions.onSuccess callback if there is one.
1325
        // Will also now return if this connection was the result of an automatic
1326
        // reconnect and which URI was successfully connected to.
1327
				if (this.connectOptions.onSuccess) {
1328
					this.connectOptions.onSuccess({invocationContext:this.connectOptions.invocationContext});
1329
				}
1330
1331
				var reconnected = false;
1332
				if (this._reconnecting) {
1333
					reconnected = true;
1334
					this._reconnectInterval = 1;
1335
					this._reconnecting = false;
1336
				}
1337
1338
				// Execute the onConnected callback if there is one.
1339
				this._connected(reconnected, this._wsuri);
1340
1341
				// Process all queued messages now that the connection is established.
1342
				this._process_queue();
1343
				break;
1344
1345
			case MESSAGE_TYPE.PUBLISH:
1346
				this._receivePublish(wireMessage);
1347
				break;
1348
1349
			case MESSAGE_TYPE.PUBACK:
1350
				var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1351
				 // If this is a re flow of a PUBACK after we have restarted receivedMessage will not exist.
1352
				if (sentMessage) {
1353
					delete this._sentMessages[wireMessage.messageIdentifier];
1354
					localStorage.removeItem("Sent:"+this._localKey+wireMessage.messageIdentifier);
1355
					if (this.onMessageDelivered)
1356
						this.onMessageDelivered(sentMessage.payloadMessage);
1357
				}
1358
				break;
1359
1360
			case MESSAGE_TYPE.PUBREC:
1361
				var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1362
				// If this is a re flow of a PUBREC after we have restarted receivedMessage will not exist.
1363
				if (sentMessage) {
1364
					sentMessage.pubRecReceived = true;
1365
					var pubRelMessage = new WireMessage(MESSAGE_TYPE.PUBREL, {messageIdentifier:wireMessage.messageIdentifier});
1366
					this.store("Sent:", sentMessage);
1367
					this._schedule_message(pubRelMessage);
1368
				}
1369
				break;
1370
1371
			case MESSAGE_TYPE.PUBREL:
1372
				var receivedMessage = this._receivedMessages[wireMessage.messageIdentifier];
1373
				localStorage.removeItem("Received:"+this._localKey+wireMessage.messageIdentifier);
1374
				// If this is a re flow of a PUBREL after we have restarted receivedMessage will not exist.
1375
				if (receivedMessage) {
1376
					this._receiveMessage(receivedMessage);
1377
					delete this._receivedMessages[wireMessage.messageIdentifier];
1378
				}
1379
				// Always flow PubComp, we may have previously flowed PubComp but the server lost it and restarted.
1380
				var pubCompMessage = new WireMessage(MESSAGE_TYPE.PUBCOMP, {messageIdentifier:wireMessage.messageIdentifier});
1381
				this._schedule_message(pubCompMessage);
1382
1383
1384
				break;
1385
1386
			case MESSAGE_TYPE.PUBCOMP:
1387
				var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1388
				delete this._sentMessages[wireMessage.messageIdentifier];
1389
				localStorage.removeItem("Sent:"+this._localKey+wireMessage.messageIdentifier);
1390
				if (this.onMessageDelivered)
1391
					this.onMessageDelivered(sentMessage.payloadMessage);
1392
				break;
1393
1394
			case MESSAGE_TYPE.SUBACK:
1395
				var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1396
				if (sentMessage) {
1397
					if(sentMessage.timeOut)
1398
						sentMessage.timeOut.cancel();
1399
					// This will need to be fixed when we add multiple topic support
1400
          			if (wireMessage.returnCode[0] === 0x80) {
1401
						if (sentMessage.onFailure) {
1402
							sentMessage.onFailure(wireMessage.returnCode);
1403
						}
1404
					} else if (sentMessage.onSuccess) {
1405
						sentMessage.onSuccess(wireMessage.returnCode);
1406
					}
1407
					delete this._sentMessages[wireMessage.messageIdentifier];
1408
				}
1409
				break;
1410
1411
			case MESSAGE_TYPE.UNSUBACK:
1412
				var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1413
				if (sentMessage) {
1414
					if (sentMessage.timeOut)
1415
						sentMessage.timeOut.cancel();
1416
					if (sentMessage.callback) {
1417
						sentMessage.callback();
1418
					}
1419
					delete this._sentMessages[wireMessage.messageIdentifier];
1420
				}
1421
1422
				break;
1423
1424
			case MESSAGE_TYPE.PINGRESP:
1425
				/* The sendPinger or receivePinger may have sent a ping, the receivePinger has already been reset. */
1426
				this.sendPinger.reset();
1427
				break;
1428
1429
			case MESSAGE_TYPE.DISCONNECT:
1430
				// Clients do not expect to receive disconnect packets.
1431
				this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code , format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type]));
1432
				break;
1433
1434
			default:
1435
				this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code , format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type]));
1436
			}
1437
		} catch (error) {
1438
			var errorStack = ((error.hasOwnProperty('stack') == 'undefined') ? error.stack.toString() : "No Error Stack Available");
1439
			this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack]));
1440
			return;
1441
		}
1442
	};
1443
1444
	/** @ignore */
1445
	ClientImpl.prototype._on_socket_error = function (error) {
1446
		if (!this._reconnecting) {
1447
		this._disconnected(ERROR.SOCKET_ERROR.code , format(ERROR.SOCKET_ERROR, [error.data]));
1448
		}
1449
	};
1450
1451
	/** @ignore */
1452
	ClientImpl.prototype._on_socket_close = function () {
1453
		if (!this._reconnecting) {
1454
		this._disconnected(ERROR.SOCKET_CLOSE.code , format(ERROR.SOCKET_CLOSE));
1455
		}
1456
	};
1457
1458
	/** @ignore */
1459
	ClientImpl.prototype._socket_send = function (wireMessage) {
1460
1461
		if (wireMessage.type == 1) {
1462
			var wireMessageMasked = this._traceMask(wireMessage, "password");
1463
			this._trace("Client._socket_send", wireMessageMasked);
1464
		}
1465
		else this._trace("Client._socket_send", wireMessage);
1466
1467
		this.socket.send(wireMessage.encode());
1468
		/* We have proved to the server we are alive. */
1469
		this.sendPinger.reset();
1470
	};
1471
1472
	/** @ignore */
1473
	ClientImpl.prototype._receivePublish = function (wireMessage) {
1474
		switch(wireMessage.payloadMessage.qos) {
1475
			case "undefined":
1476
			case 0:
1477
				this._receiveMessage(wireMessage);
1478
				break;
1479
1480
			case 1:
1481
				var pubAckMessage = new WireMessage(MESSAGE_TYPE.PUBACK, {messageIdentifier:wireMessage.messageIdentifier});
1482
				this._schedule_message(pubAckMessage);
1483
				this._receiveMessage(wireMessage);
1484
				break;
1485
1486
			case 2:
1487
				this._receivedMessages[wireMessage.messageIdentifier] = wireMessage;
1488
				this.store("Received:", wireMessage);
1489
				var pubRecMessage = new WireMessage(MESSAGE_TYPE.PUBREC, {messageIdentifier:wireMessage.messageIdentifier});
1490
				this._schedule_message(pubRecMessage);
1491
1492
				break;
1493
1494
			default:
1495
				throw Error("Invaild qos="+wireMmessage.payloadMessage.qos);
1496
		}
1497
	};
1498
1499
	/** @ignore */
1500
	ClientImpl.prototype._receiveMessage = function (wireMessage) {
1501
		if (this.onMessageArrived) {
1502
			this.onMessageArrived(wireMessage.payloadMessage);
1503
		}
1504
	};
1505
1506
	/**
1507
	 * Client has connected.
1508
	 * @param {reconnect} [boolean] indicate if this was a result of reconnect operation.
1509
	 * @param {uri} [string] fully qualified WebSocket URI of the server.
1510
	 */
1511
	ClientImpl.prototype._connected = function (reconnect, uri) {
1512
		// Execute the onConnected callback if there is one.
1513
		if (this.onConnected)
1514
			this.onConnected(reconnect, uri);
1515
	};
1516
1517
	/**
1518
	 * Attempts to reconnect the client to the server.
1519
   * For each reconnect attempt, will double the reconnect interval
1520
   * up to 128 seconds.
1521
	 */
1522
	ClientImpl.prototype._reconnect = function () {
1523
		this._trace("Client._reconnect");
1524
		if (!this.connected) {
1525
			this._reconnecting = true;
1526
			this.sendPinger.cancel();
1527
			this.receivePinger.cancel();
1528
			if (this._reconnectInterval < 128)
1529
				this._reconnectInterval = this._reconnectInterval * 2;
1530
			if (this.connectOptions.uris) {
1531
				this.hostIndex = 0;
1532
				this._doConnect(this.connectOptions.uris[0]);
1533
			} else {
1534
				this._doConnect(this.uri);
1535
			}
1536
		}
1537
	};
1538
1539
	/**
1540
	 * Client has disconnected either at its own request or because the server
1541
	 * or network disconnected it. Remove all non-durable state.
1542
	 * @param {errorCode} [number] the error number.
1543
	 * @param {errorText} [string] the error text.
1544
	 * @ignore
1545
	 */
1546
	ClientImpl.prototype._disconnected = function (errorCode, errorText) {
1547
		this._trace("Client._disconnected", errorCode, errorText);
1548
1549
		if (errorCode !== undefined && this._reconnecting) {
1550
      //Continue automatic reconnect process
1551
  		this._reconnectTimeout = new Timeout(this, window, this._reconnectInterval, this._reconnect);
1552
			return;
1553
		}
1554
1555
		this.sendPinger.cancel();
1556
		this.receivePinger.cancel();
1557
		if (this._connectTimeout) {
1558
			this._connectTimeout.cancel();
1559
			this._connectTimeout = null;
1560
		}
1561
1562
		// Clear message buffers.
1563
		this._msg_queue = [];
1564
		this._buffered_msg_queue = [];
1565
		this._notify_msg_sent = {};
1566
1567
		if (this.socket) {
1568
			// Cancel all socket callbacks so that they cannot be driven again by this socket.
1569
			this.socket.onopen = null;
1570
			this.socket.onmessage = null;
1571
			this.socket.onerror = null;
1572
			this.socket.onclose = null;
1573
			if (this.socket.readyState === 1)
1574
				this.socket.close();
1575
			delete this.socket;
1576
		}
1577
1578
		if (this.connectOptions.uris && this.hostIndex < this.connectOptions.uris.length-1) {
1579
			// Try the next host.
1580
			this.hostIndex++;
1581
			this._doConnect(this.connectOptions.uris[this.hostIndex]);
1582
		} else {
1583
1584
			if (errorCode === undefined) {
1585
				errorCode = ERROR.OK.code;
1586
				errorText = format(ERROR.OK);
1587
			}
1588
1589
			// Run any application callbacks last as they may attempt to reconnect and hence create a new socket.
1590
			if (this.connected) {
1591
				this.connected = false;
1592
				// Execute the connectionLostCallback if there is one, and we were connected.
1593
				if (this.onConnectionLost) {
1594
					this.onConnectionLost({errorCode:errorCode, errorMessage:errorText, reconnect:this.connectOptions.reconnect, uri:this._wsuri});
1595
				}
1596
				if (errorCode !== ERROR.OK.code && this.connectOptions.reconnect) {
1597
					// Start automatic reconnect process for the very first time since last successful connect.
1598
					this._reconnectInterval = 1;
1599
					this._reconnect();
1600
					return;
1601
				}
1602
			} else {
1603
				// Otherwise we never had a connection, so indicate that the connect has failed.
1604
				if (this.connectOptions.mqttVersion === 4 && this.connectOptions.mqttVersionExplicit === false) {
1605
					this._trace("Failed to connect V4, dropping back to V3");
1606
					this.connectOptions.mqttVersion = 3;
1607
					if (this.connectOptions.uris) {
1608
						this.hostIndex = 0;
1609
						this._doConnect(this.connectOptions.uris[0]);
1610
					} else {
1611
						this._doConnect(this.uri);
1612
					}
1613
				} else if(this.connectOptions.onFailure) {
1614
					this.connectOptions.onFailure({invocationContext:this.connectOptions.invocationContext, errorCode:errorCode, errorMessage:errorText});
1615
				}
1616
			}
1617
		}
1618
	};
1619
1620
	/** @ignore */
1621
	ClientImpl.prototype._trace = function () {
1622
		// Pass trace message back to client's callback function
1623
		if (this.traceFunction) {
1624
			for (var i in arguments)
1625
			{
1626
				if (typeof arguments[i] !== "undefined")
1627
					arguments.splice(i, 1, JSON.stringify(arguments[i]));
1628
			}
1629
			var record = Array.prototype.slice.call(arguments).join("");
1630
			this.traceFunction ({severity: "Debug", message: record	});
1631
		}
1632
1633
		//buffer style trace
1634
		if ( this._traceBuffer !== null ) {
1635
			for (var i = 0, max = arguments.length; i < max; i++) {
1636
				if ( this._traceBuffer.length == this._MAX_TRACE_ENTRIES ) {
1637
					this._traceBuffer.shift();
1638
				}
1639
				if (i === 0) this._traceBuffer.push(arguments[i]);
1640
				else if (typeof arguments[i] === "undefined" ) this._traceBuffer.push(arguments[i]);
1641
				else this._traceBuffer.push("  "+JSON.stringify(arguments[i]));
1642
		   }
1643
		}
1644
	};
1645
1646
	/** @ignore */
1647
	ClientImpl.prototype._traceMask = function (traceObject, masked) {
1648
		var traceObjectMasked = {};
1649
		for (var attr in traceObject) {
1650
			if (traceObject.hasOwnProperty(attr)) {
1651
				if (attr == masked)
1652
					traceObjectMasked[attr] = "******";
1653
				else
1654
					traceObjectMasked[attr] = traceObject[attr];
1655
			}
1656
		}
1657
		return traceObjectMasked;
1658
	};
1659
1660
	// ------------------------------------------------------------------------
1661
	// Public Programming interface.
1662
	// ------------------------------------------------------------------------
1663
1664
	/**
1665
	 * The JavaScript application communicates to the server using a {@link Paho.MQTT.Client} object.
1666
	 * <p>
1667
	 * Most applications will create just one Client object and then call its connect() method,
1668
	 * however applications can create more than one Client object if they wish.
1669
	 * In this case the combination of host, port and clientId attributes must be different for each Client object.
1670
	 * <p>
1671
	 * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods
1672
	 * (even though the underlying protocol exchange might be synchronous in nature).
1673
	 * This means they signal their completion by calling back to the application,
1674
	 * via Success or Failure callback functions provided by the application on the method in question.
1675
	 * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime
1676
	 * of the script that made the invocation.
1677
	 * <p>
1678
	 * In contrast there are some callback functions, most notably <i>onMessageArrived</i>,
1679
	 * that are defined on the {@link Paho.MQTT.Client} object.
1680
	 * These may get called multiple times, and aren't directly related to specific method invocations made by the client.
1681
	 *
1682
	 * @name Paho.MQTT.Client
1683
	 *
1684
	 * @constructor
1685
	 *
1686
	 * @param {string} host - the address of the messaging server, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address.
1687
	 * @param {number} port - the port number to connect to - only required if host is not a URI
1688
	 * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'.
1689
	 * @param {string} clientId - the Messaging client identifier, between 1 and 23 characters in length.
1690
	 *
1691
	 * @property {string} host - <i>read only</i> the server's DNS hostname or dotted decimal IP address.
1692
	 * @property {number} port - <i>read only</i> the server's port.
1693
	 * @property {string} path - <i>read only</i> the server's path.
1694
	 * @property {string} clientId - <i>read only</i> used when connecting to the server.
1695
	 * @property {function} onConnectionLost - called when a connection has been lost.
1696
	 *                            after a connect() method has succeeded.
1697
	 *                            Establish the call back used when a connection has been lost. The connection may be
1698
	 *                            lost because the client initiates a disconnect or because the server or network
1699
	 *                            cause the client to be disconnected. The disconnect call back may be called without
1700
	 *                            the connectionComplete call back being invoked if, for example the client fails to
1701
	 *                            connect.
1702
	 *                            A single response object parameter is passed to the onConnectionLost callback containing the following fields:
1703
	 *                            <ol>
1704
	 *                            <li>errorCode
1705
	 *                            <li>errorMessage
1706
	 *                            </ol>
1707
	 * @property {function} onMessageDelivered - called when a message has been delivered.
1708
	 *                            All processing that this Client will ever do has been completed. So, for example,
1709
	 *                            in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server
1710
	 *                            and the message has been removed from persistent storage before this callback is invoked.
1711
	 *                            Parameters passed to the onMessageDelivered callback are:
1712
	 *                            <ol>
1713
	 *                            <li>{@link Paho.MQTT.Message} that was delivered.
1714
	 *                            </ol>
1715
	 * @property {function} onMessageArrived - called when a message has arrived in this Paho.MQTT.client.
1716
	 *                            Parameters passed to the onMessageArrived callback are:
1717
	 *                            <ol>
1718
	 *                            <li>{@link Paho.MQTT.Message} that has arrived.
1719
	 *                            </ol>
1720
	 * @property {function} onConnected - called when a connection is successfully made to the server.
1721
	 *                                  after a connect() method.
1722
	 *                                  Parameters passed to the onConnected callback are:
1723
	 *                                  <ol>
1724
	 *                                  <li>reconnect (boolean) - If true, the connection was the result of a reconnect.</li>
1725
	 *                                  <li>URI (string) - The URI used to connect to the server.</li>
1726
	 *                                  </ol>
1727
	 * @property {boolean} disconnectedPublishing - if set, will enable disconnected publishing in
1728
	 *                                            in the event that the connection to the server is lost.
1729
	 * @property {number} disconnectedBufferSize - Used to set the maximum number of messages that the disconnected
1730
	 *                                             buffer will hold before rejecting new messages. Default size: 5000 messages
1731
	 * @property {function} trace - called whenever trace is called. TODO
1732
	 */
1733
	var Client = function (host, port, path, clientId) {
1734
1735
	    var uri;
1736
1737
		if (typeof host !== "string")
1738
			throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"]));
1739
1740
	    if (arguments.length == 2) {
1741
	        // host: must be full ws:// uri
1742
	        // port: clientId
1743
	        clientId = port;
1744
	        uri = host;
1745
	        var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/);
1746
	        if (match) {
1747
	            host = match[4]||match[2];
1748
	            port = parseInt(match[7]);
1749
	            path = match[8];
1750
	        } else {
1751
	            throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"]));
1752
	        }
1753
	    } else {
1754
	        if (arguments.length == 3) {
1755
				clientId = path;
1756
				path = "/mqtt";
1757
			}
1758
			if (typeof port !== "number" || port < 0)
1759
				throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"]));
1760
			if (typeof path !== "string")
1761
				throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"]));
1762
1763
			var ipv6AddSBracket = (host.indexOf(":") !== -1 && host.slice(0,1) !== "[" && host.slice(-1) !== "]");
1764
			uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path;
1765
		}
1766
1767
		var clientIdLength = 0;
1768
		for (var i = 0; i<clientId.length; i++) {
1769
			var charCode = clientId.charCodeAt(i);
1770
			if (0xD800 <= charCode && charCode <= 0xDBFF)  {
1771
				 i++; // Surrogate pair.
1772
			}
1773
			clientIdLength++;
1774
		}
1775
		if (typeof clientId !== "string" || clientIdLength > 65535)
1776
			throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"]));
1777
1778
		var client = new ClientImpl(uri, host, port, path, clientId);
1779
		this._getHost =  function() { return host; };
1780
		this._setHost = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1781
1782
		this._getPort = function() { return port; };
1783
		this._setPort = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1784
1785
		this._getPath = function() { return path; };
1786
		this._setPath = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1787
1788
		this._getURI = function() { return uri; };
1789
		this._setURI = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1790
1791
		this._getClientId = function() { return client.clientId; };
1792
		this._setClientId = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1793
1794
		this._getOnConnected = function() { return client.onConnected; };
1795
		this._setOnConnected = function(newOnConnected) {
1796
			if (typeof newOnConnected === "function")
1797
				client.onConnected = newOnConnected;
1798
			else
1799
				throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnected, "onConnected"]));
1800
		};
1801
1802
		this._getDisconnectedPublishing = function() { return client.disconnectedPublishing; };
1803
		this._setDisconnectedPublishing = function(newDisconnectedPublishing) {
1804
			client.disconnectedPublishing = newDisconnectedPublishing;
1805
		};
1806
1807
		this._getDisconnectedBufferSize = function() { return client.disconnectedBufferSize; };
1808
		this._setDisconnectedBufferSize = function(newDisconnectedBufferSize) {
1809
			client.disconnectedBufferSize = newDisconnectedBufferSize;
1810
		};
1811
1812
		this._getOnConnectionLost = function() { return client.onConnectionLost; };
1813
		this._setOnConnectionLost = function(newOnConnectionLost) {
1814
			if (typeof newOnConnectionLost === "function")
1815
				client.onConnectionLost = newOnConnectionLost;
1816
			else
1817
				throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"]));
1818
		};
1819
1820
		this._getOnMessageDelivered = function() { return client.onMessageDelivered; };
1821
		this._setOnMessageDelivered = function(newOnMessageDelivered) {
1822
			if (typeof newOnMessageDelivered === "function")
1823
				client.onMessageDelivered = newOnMessageDelivered;
1824
			else
1825
				throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageDelivered, "onMessageDelivered"]));
1826
		};
1827
1828
		this._getOnMessageArrived = function() { return client.onMessageArrived; };
1829
		this._setOnMessageArrived = function(newOnMessageArrived) {
1830
			if (typeof newOnMessageArrived === "function")
1831
				client.onMessageArrived = newOnMessageArrived;
1832
			else
1833
				throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"]));
1834
		};
1835
1836
		this._getTrace = function() { return client.traceFunction; };
1837
		this._setTrace = function(trace) {
1838
			if(typeof trace === "function"){
1839
				client.traceFunction = trace;
1840
			}else{
1841
				throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, "onTrace"]));
1842
			}
1843
		};
1844
1845
		/**
1846
		 * Connect this Messaging client to its server.
1847
		 *
1848
		 * @name Paho.MQTT.Client#connect
1849
		 * @function
1850
		 * @param {object} connectOptions - Attributes used with the connection.
1851
		 * @param {number} connectOptions.timeout - If the connect has not succeeded within this
1852
		 *                    number of seconds, it is deemed to have failed.
1853
		 *                    The default is 30 seconds.
1854
		 * @param {string} connectOptions.userName - Authentication username for this connection.
1855
		 * @param {string} connectOptions.password - Authentication password for this connection.
1856
		 * @param {Paho.MQTT.Message} connectOptions.willMessage - sent by the server when the client
1857
		 *                    disconnects abnormally.
1858
		 * @param {number} connectOptions.keepAliveInterval - the server disconnects this client if
1859
		 *                    there is no activity for this number of seconds.
1860
		 *                    The default value of 60 seconds is assumed if not set.
1861
		 * @param {boolean} connectOptions.cleanSession - if true(default) the client and server
1862
		 *                    persistent state is deleted on successful connect.
1863
		 * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection.
1864
		 * @param {object} connectOptions.invocationContext - passed to the onSuccess callback or onFailure callback.
1865
		 * @param {function} connectOptions.onSuccess - called when the connect acknowledgement
1866
		 *                    has been received from the server.
1867
		 * A single response object parameter is passed to the onSuccess callback containing the following fields:
1868
		 * <ol>
1869
		 * <li>invocationContext as passed in to the onSuccess method in the connectOptions.
1870
		 * </ol>
1871
     * @param {function} connectOptions.onFailure - called when the connect request has failed or timed out.
1872
		 * A single response object parameter is passed to the onFailure callback containing the following fields:
1873
		 * <ol>
1874
		 * <li>invocationContext as passed in to the onFailure method in the connectOptions.
1875
		 * <li>errorCode a number indicating the nature of the error.
1876
		 * <li>errorMessage text describing the error.
1877
		 * </ol>
1878
     * @param {array} connectOptions.hosts - If present this contains either a set of hostnames or fully qualified
1879
		 * WebSocket URIs (ws://iot.eclipse.org:80/ws), that are tried in order in place
1880
		 * of the host and port paramater on the construtor. The hosts are tried one at at time in order until
1881
		 * one of then succeeds.
1882
     * @param {array} connectOptions.ports - If present the set of ports matching the hosts. If hosts contains URIs, this property
1883
		 * is not used.
1884
     * @param {boolean} connectOptions.reconnect - Sets whether the client will automatically attempt to reconnect
1885
     * to the server if the connection is lost.
1886
     *<ul>
1887
     *<li>If set to false, the client will not attempt to automatically reconnect to the server in the event that the
1888
     * connection is lost.</li>
1889
     *<li>If set to true, in the event that the connection is lost, the client will attempt to reconnect to the server.
1890
     * It will initially wait 1 second before it attempts to reconnect, for every failed reconnect attempt, the delay
1891
     * will double until it is at 2 minutes at which point the delay will stay at 2 minutes.</li>
1892
     *</ul>
1893
     * @param {number} connectOptions.mqttVersion - The version of MQTT to use to connect to the MQTT Broker.
1894
     *<ul>
1895
     *<li>3 - MQTT V3.1</li>
1896
     *<li>4 - MQTT V3.1.1</li>
1897
     *</ul>
1898
     * @param {boolean} connectOptions.mqttVersionExplicit - If set to true, will force the connection to use the
1899
     * selected MQTT Version or will fail to connect.
1900
     * @param {array} connectOptions.uris - If present, should contain a list of fully qualified WebSocket uris
1901
     * (e.g. ws://iot.eclipse.org:80/ws), that are tried in order in place of the host and port parameter of the construtor.
1902
     * The uris are tried one at a time in order until one of them succeeds. Do not use this in conjunction with hosts as
1903
     * the hosts array will be converted to uris and will overwrite this property.
1904
		 * @throws {InvalidState} If the client is not in disconnected state. The client must have received connectionLost
1905
		 * or disconnected before calling connect for a second or subsequent time.
1906
		 */
1907
		this.connect = function (connectOptions) {
1908
			connectOptions = connectOptions || {} ;
1909
			validate(connectOptions,  {timeout:"number",
1910
									   userName:"string",
1911
									   password:"string",
1912
									   willMessage:"object",
1913
									   keepAliveInterval:"number",
1914
									   cleanSession:"boolean",
1915
									   useSSL:"boolean",
1916
									   invocationContext:"object",
1917
									   onSuccess:"function",
1918
									   onFailure:"function",
1919
									   hosts:"object",
1920
									   ports:"object",
1921
									   reconnect:"boolean",
1922
									   mqttVersion:"number",
1923
									   mqttVersionExplicit:"boolean",
1924
									   uris: "object"});
1925
1926
			// If no keep alive interval is set, assume 60 seconds.
1927
			if (connectOptions.keepAliveInterval === undefined)
1928
				connectOptions.keepAliveInterval = 60;
1929
1930
			if (connectOptions.mqttVersion > 4 || connectOptions.mqttVersion < 3) {
1931
				throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.mqttVersion, "connectOptions.mqttVersion"]));
1932
			}
1933
1934
			if (connectOptions.mqttVersion === undefined) {
1935
				connectOptions.mqttVersionExplicit = false;
1936
				connectOptions.mqttVersion = 4;
1937
			} else {
1938
				connectOptions.mqttVersionExplicit = true;
1939
			}
1940
1941
			//Check that if password is set, so is username
1942
			if (connectOptions.password !== undefined && connectOptions.userName === undefined)
1943
				throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.password, "connectOptions.password"]));
1944
1945
			if (connectOptions.willMessage) {
1946
				if (!(connectOptions.willMessage instanceof Message))
1947
					throw new Error(format(ERROR.INVALID_TYPE, [connectOptions.willMessage, "connectOptions.willMessage"]));
1948
				// The will message must have a payload that can be represented as a string.
1949
				// Cause the willMessage to throw an exception if this is not the case.
1950
				connectOptions.willMessage.stringPayload = null;
1951
1952
				if (typeof connectOptions.willMessage.destinationName === "undefined")
1953
					throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.willMessage.destinationName, "connectOptions.willMessage.destinationName"]));
1954
			}
1955
			if (typeof connectOptions.cleanSession === "undefined")
1956
				connectOptions.cleanSession = true;
1957
			if (connectOptions.hosts) {
1958
1959
				if (!(connectOptions.hosts instanceof Array) )
1960
					throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"]));
1961
				if (connectOptions.hosts.length <1 )
1962
					throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"]));
1963
1964
				var usingURIs = false;
1965
				for (var i = 0; i<connectOptions.hosts.length; i++) {
1966
					if (typeof connectOptions.hosts[i] !== "string")
1967
						throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.hosts[i], "connectOptions.hosts["+i+"]"]));
1968
					if (/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(connectOptions.hosts[i])) {
1969
						if (i === 0) {
1970
							usingURIs = true;
1971
						} else if (!usingURIs) {
1972
							throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts[i], "connectOptions.hosts["+i+"]"]));
1973
						}
1974
					} else if (usingURIs) {
1975
						throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts[i], "connectOptions.hosts["+i+"]"]));
1976
					}
1977
				}
1978
1979
				if (!usingURIs) {
1980
					if (!connectOptions.ports)
1981
						throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"]));
1982
					if (!(connectOptions.ports instanceof Array) )
1983
						throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"]));
1984
					if (connectOptions.hosts.length !== connectOptions.ports.length)
1985
						throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"]));
1986
1987
					connectOptions.uris = [];
1988
1989
					for (var i = 0; i<connectOptions.hosts.length; i++) {
1990
						if (typeof connectOptions.ports[i] !== "number" || connectOptions.ports[i] < 0)
1991
							throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.ports[i], "connectOptions.ports["+i+"]"]));
1992
						var host = connectOptions.hosts[i];
1993
						var port = connectOptions.ports[i];
1994
1995
						var ipv6 = (host.indexOf(":") !== -1);
1996
						uri = "ws://"+(ipv6?"["+host+"]":host)+":"+port+path;
1997
						connectOptions.uris.push(uri);
1998
					}
1999
				} else {
2000
					connectOptions.uris = connectOptions.hosts;
2001
				}
2002
			}
2003
2004
			client.connect(connectOptions);
2005
		};
2006
2007
		/**
2008
		 * Subscribe for messages, request receipt of a copy of messages sent to the destinations described by the filter.
2009
		 *
2010
		 * @name Paho.MQTT.Client#subscribe
2011
		 * @function
2012
		 * @param {string} filter describing the destinations to receive messages from.
2013
		 * <br>
2014
		 * @param {object} subscribeOptions - used to control the subscription
2015
		 *
2016
		 * @param {number} subscribeOptions.qos - the maiximum qos of any publications sent
2017
		 *                                  as a result of making this subscription.
2018
		 * @param {object} subscribeOptions.invocationContext - passed to the onSuccess callback
2019
		 *                                  or onFailure callback.
2020
		 * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement
2021
		 *                                  has been received from the server.
2022
		 *                                  A single response object parameter is passed to the onSuccess callback containing the following fields:
2023
		 *                                  <ol>
2024
		 *                                  <li>invocationContext if set in the subscribeOptions.
2025
		 *                                  </ol>
2026
		 * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out.
2027
		 *                                  A single response object parameter is passed to the onFailure callback containing the following fields:
2028
		 *                                  <ol>
2029
		 *                                  <li>invocationContext - if set in the subscribeOptions.
2030
		 *                                  <li>errorCode - a number indicating the nature of the error.
2031
		 *                                  <li>errorMessage - text describing the error.
2032
		 *                                  </ol>
2033
		 * @param {number} subscribeOptions.timeout - which, if present, determines the number of
2034
		 *                                  seconds after which the onFailure calback is called.
2035
		 *                                  The presence of a timeout does not prevent the onSuccess
2036
		 *                                  callback from being called when the subscribe completes.
2037
		 * @throws {InvalidState} if the client is not in connected state.
2038
		 */
2039
		this.subscribe = function (filter, subscribeOptions) {
2040
			if (typeof filter !== "string")
2041
				throw new Error("Invalid argument:"+filter);
2042
			subscribeOptions = subscribeOptions || {} ;
2043
			validate(subscribeOptions,  {qos:"number",
2044
										 invocationContext:"object",
2045
										 onSuccess:"function",
2046
										 onFailure:"function",
2047
										 timeout:"number"
2048
										});
2049
			if (subscribeOptions.timeout && !subscribeOptions.onFailure)
2050
				throw new Error("subscribeOptions.timeout specified with no onFailure callback.");
2051
			if (typeof subscribeOptions.qos !== "undefined" && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 ))
2052
				throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"]));
2053
			client.subscribe(filter, subscribeOptions);
2054
		};
2055
2056
		/**
2057
		 * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter.
2058
		 *
2059
		 * @name Paho.MQTT.Client#unsubscribe
2060
		 * @function
2061
		 * @param {string} filter - describing the destinations to receive messages from.
2062
		 * @param {object} unsubscribeOptions - used to control the subscription
2063
		 * @param {object} unsubscribeOptions.invocationContext - passed to the onSuccess callback
2064
		                                      or onFailure callback.
2065
		 * @param {function} unsubscribeOptions.onSuccess - called when the unsubscribe acknowledgement has been received from the server.
2066
		 *                                    A single response object parameter is passed to the
2067
		 *                                    onSuccess callback containing the following fields:
2068
		 *                                    <ol>
2069
		 *                                    <li>invocationContext - if set in the unsubscribeOptions.
2070
		 *                                    </ol>
2071
		 * @param {function} unsubscribeOptions.onFailure called when the unsubscribe request has failed or timed out.
2072
		 *                                    A single response object parameter is passed to the onFailure callback containing the following fields:
2073
		 *                                    <ol>
2074
		 *                                    <li>invocationContext - if set in the unsubscribeOptions.
2075
		 *                                    <li>errorCode - a number indicating the nature of the error.
2076
		 *                                    <li>errorMessage - text describing the error.
2077
		 *                                    </ol>
2078
		 * @param {number} unsubscribeOptions.timeout - which, if present, determines the number of seconds
2079
		 *                                    after which the onFailure callback is called. The presence of
2080
		 *                                    a timeout does not prevent the onSuccess callback from being
2081
		 *                                    called when the unsubscribe completes
2082
		 * @throws {InvalidState} if the client is not in connected state.
2083
		 */
2084
		this.unsubscribe = function (filter, unsubscribeOptions) {
2085
			if (typeof filter !== "string")
2086
				throw new Error("Invalid argument:"+filter);
2087
			unsubscribeOptions = unsubscribeOptions || {} ;
2088
			validate(unsubscribeOptions,  {invocationContext:"object",
2089
										   onSuccess:"function",
2090
										   onFailure:"function",
2091
										   timeout:"number"
2092
										  });
2093
			if (unsubscribeOptions.timeout && !unsubscribeOptions.onFailure)
2094
				throw new Error("unsubscribeOptions.timeout specified with no onFailure callback.");
2095
			client.unsubscribe(filter, unsubscribeOptions);
2096
		};
2097
2098
		/**
2099
		 * Send a message to the consumers of the destination in the Message.
2100
		 *
2101
		 * @name Paho.MQTT.Client#send
2102
		 * @function
2103
		 * @param {string|Paho.MQTT.Message} topic - <b>mandatory</b> The name of the destination to which the message is to be sent.
2104
		 * 					   - If it is the only parameter, used as Paho.MQTT.Message object.
2105
		 * @param {String|ArrayBuffer} payload - The message data to be sent.
2106
		 * @param {number} qos The Quality of Service used to deliver the message.
2107
		 * 		<dl>
2108
		 * 			<dt>0 Best effort (default).
2109
		 *     			<dt>1 At least once.
2110
		 *     			<dt>2 Exactly once.
2111
		 * 		</dl>
2112
		 * @param {Boolean} retained If true, the message is to be retained by the server and delivered
2113
		 *                     to both current and future subscriptions.
2114
		 *                     If false the server only delivers the message to current subscribers, this is the default for new Messages.
2115
		 *                     A received message has the retained boolean set to true if the message was published
2116
		 *                     with the retained boolean set to true
2117
		 *                     and the subscrption was made after the message has been published.
2118
		 * @throws {InvalidState} if the client is not connected.
2119
		 */
2120
		this.send = function (topic,payload,qos,retained) {
2121
			var message ;
2122
2123
			if(arguments.length === 0){
2124
				throw new Error("Invalid argument."+"length");
2125
2126
			}else if(arguments.length == 1) {
2127
2128
				if (!(topic instanceof Message) && (typeof topic !== "string"))
2129
					throw new Error("Invalid argument:"+ typeof topic);
2130
2131
				message = topic;
2132
				if (typeof message.destinationName === "undefined")
2133
					throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"]));
2134
				client.send(message);
2135
2136
			}else {
2137
				//parameter checking in Message object
2138
				message = new Message(payload);
2139
				message.destinationName = topic;
2140
				if(arguments.length >= 3)
2141
					message.qos = qos;
2142
				if(arguments.length >= 4)
2143
					message.retained = retained;
2144
				client.send(message);
2145
			}
2146
		};
2147
2148
		/**
2149
		 * Publish a message to the consumers of the destination in the Message.
2150
		 * Synonym for Paho.Mqtt.Client#send
2151
		 *
2152
		 * @name Paho.MQTT.Client#publish
2153
		 * @function
2154
		 * @param {string|Paho.MQTT.Message} topic - <b>mandatory</b> The name of the topic to which the message is to be published.
2155
		 * 					   - If it is the only parameter, used as Paho.MQTT.Message object.
2156
		 * @param {String|ArrayBuffer} payload - The message data to be published.
2157
		 * @param {number} qos The Quality of Service used to deliver the message.
2158
		 * 		<dl>
2159
		 * 			<dt>0 Best effort (default).
2160
		 *     			<dt>1 At least once.
2161
		 *     			<dt>2 Exactly once.
2162
		 * 		</dl>
2163
		 * @param {Boolean} retained If true, the message is to be retained by the server and delivered
2164
		 *                     to both current and future subscriptions.
2165
		 *                     If false the server only delivers the message to current subscribers, this is the default for new Messages.
2166
		 *                     A received message has the retained boolean set to true if the message was published
2167
		 *                     with the retained boolean set to true
2168
		 *                     and the subscrption was made after the message has been published.
2169
		 * @throws {InvalidState} if the client is not connected.
2170
		 */
2171
		 this.publish = function(topic,payload,qos,retained) {
2172
			 console.log("Publising message to: ", topic);
2173
			 var message ;
2174
2175
 			if(arguments.length === 0){
2176
 				throw new Error("Invalid argument."+"length");
2177
2178
 			}else if(arguments.length == 1) {
2179
2180
 				if (!(topic instanceof Message) && (typeof topic !== "string"))
2181
 					throw new Error("Invalid argument:"+ typeof topic);
2182
2183
 				message = topic;
2184
 				if (typeof message.destinationName === "undefined")
2185
 					throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"]));
2186
 				client.send(message);
2187
2188
 			}else {
2189
 				//parameter checking in Message object
2190
 				message = new Message(payload);
2191
 				message.destinationName = topic;
2192
 				if(arguments.length >= 3)
2193
 					message.qos = qos;
2194
 				if(arguments.length >= 4)
2195
 					message.retained = retained;
2196
 				client.send(message);
2197
 			}
2198
		};
2199
2200
		/**
2201
		 * Normal disconnect of this Messaging client from its server.
2202
		 *
2203
		 * @name Paho.MQTT.Client#disconnect
2204
		 * @function
2205
		 * @throws {InvalidState} if the client is already disconnected.
2206
		 */
2207
		this.disconnect = function () {
2208
			client.disconnect();
2209
		};
2210
2211
		/**
2212
		 * Get the contents of the trace log.
2213
		 *
2214
		 * @name Paho.MQTT.Client#getTraceLog
2215
		 * @function
2216
		 * @return {Object[]} tracebuffer containing the time ordered trace records.
2217
		 */
2218
		this.getTraceLog = function () {
2219
			return client.getTraceLog();
2220
		};
2221
2222
		/**
2223
		 * Start tracing.
2224
		 *
2225
		 * @name Paho.MQTT.Client#startTrace
2226
		 * @function
2227
		 */
2228
		this.startTrace = function () {
2229
			client.startTrace();
2230
		};
2231
2232
		/**
2233
		 * Stop tracing.
2234
		 *
2235
		 * @name Paho.MQTT.Client#stopTrace
2236
		 * @function
2237
		 */
2238
		this.stopTrace = function () {
2239
			client.stopTrace();
2240
		};
2241
2242
		this.isConnected = function() {
2243
			return client.connected;
2244
		};
2245
	};
2246
2247
	Client.prototype = {
2248
		get host() { return this._getHost(); },
2249
		set host(newHost) { this._setHost(newHost); },
2250
2251
		get port() { return this._getPort(); },
2252
		set port(newPort) { this._setPort(newPort); },
2253
2254
		get path() { return this._getPath(); },
2255
		set path(newPath) { this._setPath(newPath); },
2256
2257
		get clientId() { return this._getClientId(); },
2258
		set clientId(newClientId) { this._setClientId(newClientId); },
2259
2260
		get onConnected() { return this._getOnConnected(); },
2261
		set onConnected(newOnConnected) { this._setOnConnected(newOnConnected); },
2262
2263
		get disconnectedPublishing() { return this._getDisconnectedPublishing(); },
2264
		set disconnectedPublishing(newDisconnectedPublishing) { this._setDisconnectedPublishing(newDisconnectedPublishing); },
2265
2266
		get disconnectedBufferSize() { return this._getDisconnectedBufferSize(); },
2267
		set disconnectedBufferSize(newDisconnectedBufferSize) { this._setDisconnectedBufferSize(newDisconnectedBufferSize); },
2268
2269
		get onConnectionLost() { return this._getOnConnectionLost(); },
2270
		set onConnectionLost(newOnConnectionLost) { this._setOnConnectionLost(newOnConnectionLost); },
2271
2272
		get onMessageDelivered() { return this._getOnMessageDelivered(); },
2273
		set onMessageDelivered(newOnMessageDelivered) { this._setOnMessageDelivered(newOnMessageDelivered); },
2274
2275
		get onMessageArrived() { return this._getOnMessageArrived(); },
2276
		set onMessageArrived(newOnMessageArrived) { this._setOnMessageArrived(newOnMessageArrived); },
2277
2278
		get trace() { return this._getTrace(); },
2279
		set trace(newTraceFunction) { this._setTrace(newTraceFunction); }
2280
2281
	};
2282
2283
	/**
2284
	 * An application message, sent or received.
2285
	 * <p>
2286
	 * All attributes may be null, which implies the default values.
2287
	 *
2288
	 * @name Paho.MQTT.Message
2289
	 * @constructor
2290
	 * @param {String|ArrayBuffer} payload The message data to be sent.
2291
	 * <p>
2292
	 * @property {string} payloadString <i>read only</i> The payload as a string if the payload consists of valid UTF-8 characters.
2293
	 * @property {ArrayBuffer} payloadBytes <i>read only</i> The payload as an ArrayBuffer.
2294
	 * <p>
2295
	 * @property {string} destinationName <b>mandatory</b> The name of the destination to which the message is to be sent
2296
	 *                    (for messages about to be sent) or the name of the destination from which the message has been received.
2297
	 *                    (for messages received by the onMessage function).
2298
	 * <p>
2299
	 * @property {number} qos The Quality of Service used to deliver the message.
2300
	 * <dl>
2301
	 *     <dt>0 Best effort (default).
2302
	 *     <dt>1 At least once.
2303
	 *     <dt>2 Exactly once.
2304
	 * </dl>
2305
	 * <p>
2306
	 * @property {Boolean} retained If true, the message is to be retained by the server and delivered
2307
	 *                     to both current and future subscriptions.
2308
	 *                     If false the server only delivers the message to current subscribers, this is the default for new Messages.
2309
	 *                     A received message has the retained boolean set to true if the message was published
2310
	 *                     with the retained boolean set to true
2311
	 *                     and the subscrption was made after the message has been published.
2312
	 * <p>
2313
	 * @property {Boolean} duplicate <i>read only</i> If true, this message might be a duplicate of one which has already been received.
2314
	 *                     This is only set on messages received from the server.
2315
	 *
2316
	 */
2317
	var Message = function (newPayload) {
2318
		var payload;
2319
		if (   typeof newPayload === "string" ||
2320
		newPayload instanceof ArrayBuffer ||
2321
		newPayload instanceof Int8Array   ||
2322
		newPayload instanceof Uint8Array  ||
2323
		newPayload instanceof Int16Array  ||
2324
		newPayload instanceof Uint16Array ||
2325
		newPayload instanceof Int32Array  ||
2326
		newPayload instanceof Uint32Array ||
2327
		newPayload instanceof Float32Array ||
2328
		newPayload instanceof Float64Array
2329
		   ) {
2330
			payload = newPayload;
2331
		} else {
2332
			throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"]));
2333
		}
2334
2335
		this._getPayloadString = function () {
2336
			if (typeof payload === "string")
2337
				return payload;
2338
			else
2339
				return parseUTF8(payload, 0, payload.length);
2340
		};
2341
2342
		this._getPayloadBytes = function() {
2343
			if (typeof payload === "string") {
2344
				var buffer = new ArrayBuffer(UTF8Length(payload));
2345
				var byteStream = new Uint8Array(buffer);
2346
				stringToUTF8(payload, byteStream, 0);
2347
2348
				return byteStream;
2349
			} else {
2350
				return payload;
2351
			}
2352
		};
2353
2354
		var destinationName;
2355
		this._getDestinationName = function() { return destinationName; };
2356
		this._setDestinationName = function(newDestinationName) {
2357
			if (typeof newDestinationName === "string")
2358
				destinationName = newDestinationName;
2359
			else
2360
				throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"]));
2361
		};
2362
2363
		var qos = 0;
2364
		this._getQos = function() { return qos; };
2365
		this._setQos = function(newQos) {
2366
			if (newQos === 0 || newQos === 1 || newQos === 2 )
2367
				qos = newQos;
2368
			else
2369
				throw new Error("Invalid argument:"+newQos);
2370
		};
2371
2372
		var retained = false;
2373
		this._getRetained = function() { return retained; };
2374
		this._setRetained = function(newRetained) {
2375
			if (typeof newRetained === "boolean")
2376
				retained = newRetained;
2377
			else
2378
				throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, "newRetained"]));
2379
		};
2380
2381
		var duplicate = false;
2382
		this._getDuplicate = function() { return duplicate; };
2383
		this._setDuplicate = function(newDuplicate) { duplicate = newDuplicate; };
2384
	};
2385
2386
	Message.prototype = {
2387
		get payloadString() { return this._getPayloadString(); },
2388
		get payloadBytes() { return this._getPayloadBytes(); },
2389
2390
		get destinationName() { return this._getDestinationName(); },
2391
		set destinationName(newDestinationName) { this._setDestinationName(newDestinationName); },
2392
2393
		get topic() { return this._getDestinationName(); },
2394
		set topic(newTopic) { this._setDestinationName(newTopic); },
2395
2396
		get qos() { return this._getQos(); },
2397
		set qos(newQos) { this._setQos(newQos); },
2398
2399
		get retained() { return this._getRetained(); },
2400
		set retained(newRetained) { this._setRetained(newRetained); },
2401
2402
		get duplicate() { return this._getDuplicate(); },
2403
		set duplicate(newDuplicate) { this._setDuplicate(newDuplicate); }
2404
	};
2405
2406
	// Module contents.
2407
	return {
2408
		Client: Client,
2409
		Message: Message
2410
	};
2411
})(window);
2412
return PahoMQTT;
2413
});
2414

I. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

J. Resumen

  • En esta lección se presentó el proyecto base para IoT.

10. ESP32

Versión para imprimir.

A. Introduccion

  • En esta lección se presentan programas para el NODE MCU ESP32 y como interactuar con el sistema de IoT.

B. Blink.ino

Video de la placa

Ve como capturar y ejecutar este ejemplo.

Ve como desplegar este ejemplo en Tinkercad y compartirlo.

1/* Este programa es un derivado de
2 ESP8266 Blink by Simon Peter */
3
4/** Función que se invoca una sola
5 vez al inicio del programa. */
6void setup() {
7 /* Todas las placas compatibles
8 con Arduino incluyen un led
9 de prueba ligado a un pin
10 cuyo número está definido por
11 la constante LED_BUILTIN.
12 Inicializa el pin para
13 LED_BUILTIN como salida. */
14 pinMode(LED_BUILTIN, OUTPUT);
15}
16
17/* Función que se invoca
18 repetidamente mientras el
19 programa esté activo. */
20void loop() {
21 /* Enciende el LED_BUILTIN (HIGH
22 es el nivel de voltaje). */
23 digitalWrite(LED_BUILTIN, HIGH);
24 delay(1000); // Espera 1 segundo
25 /* Apaga el LED_BUILTIN haciendo
26 que el nivel de voltaje sea
27 LOW. */
28 digitalWrite(LED_BUILTIN, LOW);
29 delay(1000);
30}
31

C. Button.ino

Video de la placa

Ve como desplegar este ejemplo en Tinkercad y compartirlo.

1/* Este es un derivado de Button
2 por DojoDave y Tom Igoe */
3
4/* Pin para el botón. */
5const int BOTON = 0;
6
7int estadoDelBoton = 0;
8
9void setup() {
10 /* Inicializa el pin para
11 LED_BUILTIN como salida. */
12 pinMode(LED_BUILTIN, OUTPUT);
13 /* Initializa el pin para
14 BOTON como entrada. */
15 pinMode(BOTON, INPUT);
16}
17
18void loop() {
19 estadoDelBoton =
20 digitalRead(BOTON);
21 /* Checa si el botón está
22 presionado. Esto es, si
23 estadoDelBotón es LOW. */
24 if (estadoDelBoton == LOW) {
25 // Enciende el LED_BUILTIN.
26 digitalWrite(LED_BUILTIN, HIGH);
27 } else {
28 // Apaga el LED_BUILTIN.
29 digitalWrite(LED_BUILTIN, LOW);
30 }
31}
32

D. Dispositivo.ino

  • En el siguiente código, sustituye la configuración por los datos de tu red WiFi.

  • La URL y el puerto deben coincidir con tu dervidor de MQTT. Si usas test.mosquitto.org, conserva los valores mostrados más adelante.

  • Para monitorear su funcionamiento, debes hacer clic en el botón de arriba a la derecha en la ventana de Arduino, que tiene una lupa y tiene el mensaje flotante que dice Monitor Serie. Debes ajustar los baudios a la misma velocidad que en el código, donde dice Serial.begin(115200); que en este caso es 115200 baudios. Si no se ven los mensajes, baja la velocidad tanto en el código, como en el monitor.

Video del dispositivo
Implementación del ejemplo de IoT con MQTT en Arduino IDE.

Ve como configurar y ejecutar este ejemplo. No hagas caso a la parte de los certificados,

1#include "EspMQTTClient.h"
2
3const char *const SSID =
4 "Galaxy A723C85";
5const char *const PASS =
6 "bdoi1764";
7const char *const URL =
8 "test.mosquitto.org";
9const uint16_t PUERTO =
10 1883;
11const char *const CLIENT_ID =
12 "gilpgdmIoT-esp32-1";
13const char *const TOPICO_FOCO =
14 "gilpgdm/IoT/foco";
15
16EspMQTTClient cliente(
17 SSID,
18 PASS,
19 URL,
20 0, // Usuario opcional
21 0, // Contraseña opcional
22 CLIENT_ID,
23 PUERTO);
24const int BOTON = 0;
25bool presionado = false;
26String valor = "0";
27
28void recibeMensaje(
29 const String &payload)
30{
31 valor = payload;
32 digitalWrite(LED_BUILTIN,
33 valor == "1"
34 ? HIGH
35 : LOW);
36}
37
38void enviaMensajeMqtt(
39 String valorAEnviar,
40 String topico)
41{
42 cliente.publish(topico,
43 valorAEnviar);
44}
45
46void onConnectionEstablished()
47{
48 cliente.subscribe(
49 TOPICO_FOCO, recibeMensaje);
50 enviaMensajeMqtt(
51 valor, TOPICO_FOCO);
52}
53
54void setup()
55{
56 Serial.begin(115200);
57 pinMode(BOTON, INPUT);
58 pinMode(LED_BUILTIN, OUTPUT);
59 // Funcionalidades opcionaes
60 // Mensajes para depurar conexión
61 cliente.enableDebuggingMessages();
62 cliente.enableHTTPWebUpdater();
63 // Actualizaciones
64 // OTA (Over The Air)
65 cliente.enableOTA();
66 // Mensaje de última voluntad.
67 cliente.enableLastWillMessage(
68 "gilpgdm/IoT/lastwill",
69 "Adios");
70}
71
72void loop()
73{
74 cliente.loop();
75 if (cliente.isConnected())
76 {
77 bool actual =
78 digitalRead(BOTON);
79 if (!presionado &&
80 actual == LOW)
81 {
82 enviaMensajeMqtt(
83 valor == "1" ? "0" : "1",
84 TOPICO_FOCO);
85 }
86 presionado = (actual == LOW);
87 }
88}
89

E. Resumen

  • En esta lección se presentaron los siguientes archivos:

    • Blinkt.ino

    • Button.ino

    • Dispositivo.ino

11. Simulación de un sistema IoT

Versión para imprimir.

A. Introduccion

  • En esta lección ejemplifica el funcionamiento de un sistema completo de IoT.

  • Aunque no se muestra, todas las páginsa están protegidas por un sistema de autenticación como en https://gilpgawoas.github.io/m19aut/ y no se pueden usar sin pasar por un inicio de desión.

  • En este ejemplo tendremos los siguientes usuarios y roles:

    cuca
    Vendedor
    pepito
    Cliente
    juan
    Instalador
    angela
    Operador
    perla
    Cobranza

B. Contratación

  • Los sistemas de IoT requieren recibir constantemente dinero. En este ejemplo, la financiación viene de estar pagando cada mes el servicio de alumbrado controlado por interruptores físicos y por un control remoto.

  • Para empezar, los clientes asisten a las oficinas de ventas de la empresa, firman un contrato y agendan una cita, pues en este caso el hacer funcionar este tipo de sistemas no es sencillo.

  • En este caso, la página está operada por el usuario cuca, con el rol Vandedor, que recibe los datos proporcionados por los contratantes.


Contrato y cita

Usuario: Cuca

C. Instalación del sistema IoT

  • El instalador asiste al domicilio del contratante y realiza la instalación del hardware y del software.

  • Se debe dejar funcionando todo el hardware, todo el software y la conexión al servidor.

  • En este caso, la instalación es realzada el usuario juan, con el rol Instalador.

  • En teste caso, el sistema debe quedar operado por el usuario pepito, con el rol Cliente. Si es necesario, debe recibir capacitación por parte del instalador.

  • El proceso de instalación se parece a lo mostrado en este video: Configurar e instalar IoT.

D. Uso del sistema de IoT

  • El cliente, que en este caso es el usuario pepito con el rol Cliente utiliza el sistema.

  • Se transmiten los datos de uso a la empresa de IoT. En este caso, los datos se envían a un servidor MQTT, que a su vez los reenvía a la empresa.

Video del dispositivo
Implementación del ejemplo de IoT con MQTT en Arduino IDE.

Foco

Nombre del cliente

Pepito

Usu del cliente

pepito

Id del dispositivo

169

E. Monitoreo

  • Los datos enviados por MQTT se reciben en la empresa y se almacenan en la base de datos. Se puede usar un mecanismo parecido a la interfaz de uso, pero enviando los datos a un servicio que los almacene.

  • En este caso, el sistema es operado por el usuario angela, con el rol Operador, que observa el comportamiento de la página y revisa si hay fallos.


Monitoreo

  • Botón presionado.

    Id del dispositivo = 169.

    Valor = 0.

    Guardando valor en la base de datos.

F. Almacenamiento en tabla

  • Los datos de todos los dispositivos se almacenan en la tabla de histórico.

  • El campo timestamp representa hora y fecha en que se tomó la medición..


Tabla de consumo

HISTORICO_ID DISPOSITIVO_ID HISTORICO_VALOR HISTORICO_TIMESTAMP

G. Facturación

  • Los datos en el sistema se utilizan para extraer conocimiento de los datos almacenados.

  • En este caso, el sistema es operado por el usuario perla, con el rol Cobranza para generar la facturación.


Factura de noviembre de 2024

Nombre del cliente

Pepito

Id del dispositivo

169

Tiempo de encendido del foco

3000 minutos

Total a pagar

$600.00

H. Resumen

  • En esta lección ejemplificó el funcionamiento de un sistema completo de IoT

12. Notificaciones

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

1
<!DOCTYPE html>
2
<html lang="es">
3

    
4
<head>
5

    
6
 <meta charset="UTF-8">
7
 <meta name="viewport"
8
   content="width=device-width">
9

    
10
 <title>Notificaciones</title>
11

    
12
 <style>
13
  html {
14
   color-scheme: light dark;
15
  }
16
 </style>
17

    
18
</head>
19

    
20
<body>
21

    
22
 <h1>Notificaciones</h1>
23

    
24
 <button type="button"
25
   onclick="notifica()">
26
  Muestra
27
 </button>
28

    
29
 <script>
30

    
31
  const MENSAJE = "Hola"
32

    
33
  async function notifica() {
34
   let permitida = false
35
   if ("Notification" in window) {
36
    let permiso =
37
     Notification.permission
38
    if (permiso === "default") {
39
     permiso = await Notification
40
      .requestPermission()
41
    }
42
    permitida =
43
     permiso === "granted"
44
      ? true
45
      : false
46
   }
47
   if (permitida) {
48
    notificacion =
49
     new Notification(MENSAJE)
50
   } else {
51
    alert(MENSAJE)
52
   }
53
  }
54

    
55
 </script>
56

    
57
</body>
58

    
59
</html>

13. Notificaciones push

Versión para imprimir.

1. Introduccion

  • En esta lección se muestra un ejemplo de notificaciones push.

  • Puedes probar el ejemplo en https://notipush.rf.gd/.

  • Primero suscríbete y luego envía notificaciones a todos los que estén conectados.

2. Diagrama entidad relación

Diagrama entidad relación

3. Diagrama de despliegue

Diagrama de despliegue

4. Hazlo funcionar (con videos)

  1. Prueba el ejemplo en https://notipush.rf.gd/.

  2. Copia la url de la app y pégala en varios navegadores y dispositivos.

  3. Suscríbanse y envíen notificaciones ntre los dispositivos conectados.

  4. Descarga el archivo /src/notipush.zip y descompáctalo.

  5. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  6. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  7. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  8. Importa el proyecto de GitHub a Visual Studio Code

  9. Edita los archivos que desees.

  10. Prueba tu sitio localmente.

  11. Necesitas un hosting. En este ejemplo se muestra como usar el hosting. Si no lo has usado, lo primero que tienes que hacer es entrar a registrar tu email con el botón Registrar. Si ya tienes tu email registrado, omite este paso.

  12. Crea una cuenta. Si ya tienes cuenta, entra a ella y crea un nuevo domino. En este ejemplo no se crean los archivos directamente en el hosting.

  13. Sube tus archivos al hosting usando ftp.

  14. Sube tus archivos a GitHub. En este ejemplo no hay archivo sw.js ni necesitas esperar 11 o más minutos.

5. Hazlo funcionar (texto)

  1. Este ejercicio usa la librería WebPush para PHP. Puedes profundizar en este tema en la URL https://github.com/web-push-libs/web-push-php

  2. Prueba el ejemplo en https://notipush.rf.gd/.

  3. Copia la url de la app y pégala en varios navegadores y dispositivos.

  4. Suscríbanse y envíen notificaciones ntre los dispositivos conectados.

  5. Descarga el archivo /src/notipush.zip y descompáctalo.

  6. Crea tu proyecto en GitHub:

    1. Crea una cuenta de email, por ejemplo, pepito@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.

      • Mantén la selección Public para que otros usuarios puedan ver tu proyecto.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

  7. Importa el proyecto en GitHub:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.

  8. Edita los archivos que desees.

  9. El ejercicio usa una llave pública en el servidor y la repite el código de JavaScript en el navegador. También se requiere la correspondiente llave privada que se almacena solo en el servidor.

  10. Las llaves se pueden generar con el enlace Genera llaves del ejemplo. Para copiar los valores despliega el código fuente de la página, porque en el navegador algunos caracteres pueden tener interpretación diferente.

  11. El proyecto ya contiene la carpeta vendor y el archivo composer.lock, pero es posible crearlos con estos pasos:

    1. Instalar composer. Para Windows, usa el instalador de https://getcomposer.org/download/.

    2. Abre una terminal y ejecuta el comando
      composer update

  12. Haz clic derecho en index.html, selecciona PHP Server: serve project y se abre el navegador para que puedas probar localmente el ejemplo.

  13. Para depurar paso a paso haz lo siguiente:

    1. En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.

    2. Recarga la página, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    3. Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).

    4. Selecciona el archivo donde vas a empezar a depurar.

    5. Haz clic en el número de la línea donde vas a empezar a depurar.

    6. En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.

    7. Haz clic en Run and Debug .

    8. Si no está configurada la depuración, haz clic en create a launch json file.

    9. Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .

    10. Aparece un cuadro con los controles de depuración.

    11. Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.

    12. Regresa al navegador, recarga la página de manera normal y empieza a usarla.

    13. Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.

  14. Sube el proyecto al hosting que elijas.

    1. Crea una nueva carpeta para crear un nuevo proyecto que estará conectado directamente al servidor web por ftp.

    2. Abre la nueva carpeta con Visual Studio Code.

    3. Tecle al mismo tiempo SFTP: Config. Aparece un archivo de configuración de FTP. Llena los datos con la configuración de FTP de su servidor, excepto la contraseña.

    4. Cliquea el botón de SFTP y luego haz clic en la URL de tu servidos. En la barra superior te pide la contraseña y ENTER.

    5. Pásate a la parte de archivos y coloca tus archivos.

    6. Cliquea con el botón derecho en la sección de archivos y selecciona Sync: Local -> Remote.

  15. Abre un navegador y prueba el proyecto en tu hosting.

  16. En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.

  17. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push

6. Archivos

7. index.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Notificaciones Push</title>
10
11
 <script type="module" src="js/lib/manejaErrores.js"></script>
12
13
</head>
14
15
<body>
16
17
 <h1>Notificaciones Push</h1>
18
19
 <menu style="display: flex; list-style: none; flex-wrap: wrap; gap: 0.5rem;">
20
  <li>
21
   <button id="btnSuscribe" type="button" hidden>
22
    Suscríbete
23
   </button>
24
  </li>
25
  <li>
26
   <button id="btnCancela" type="button" hidden>
27
    Cancela suscripción
28
   </button>
29
  </li>
30
  <li>
31
   <button id="btnNotifica" type="button" hidden>
32
    Notifica
33
   </button>
34
  </li>
35
  <li>
36
   <a href="srv/genera-llaves.php" target="_blank">Genera llaves</a>
37
  </li>
38
 </menu>
39
40
 <p>
41
  <output id="outEstado">
42
   <progress max="100">Cargando…</progress>
43
  </output>
44
 </p>
45
46
 <fieldset>
47
  <legend>Reporte de envío a endpoints</legend>
48
49
  <dl id="reporte"></dl>
50
51
 </fieldset>
52
53
 <script type="module">
54
55
  import {
56
   activaNotificacionesPush
57
  } from "./js/lib/activaNotificacionesPush.js"
58
  import { getSuscripcionPush } from "./js/lib/getSuscripcionPush.js"
59
  import { suscribeAPush } from "./js/lib/suscribeAPush.js"
60
  import { cancelaSuscripcionPush } from "./js/lib/cancelaSuscripcionPush.js"
61
  import { urlBase64ToUint8Array } from "./js/lib/urlBase64ToUint8Array.js"
62
  import {
63
   calculaDtoParaSuscripcion
64
  } from "./js/lib/calculaDtoParaSuscripcion.js"
65
  import { consume } from "./js/lib/consume.js"
66
  import { enviaJsonRecibeJson } from "./js/lib/enviaJsonRecibeJson.js"
67
  import { descargaVista } from "./js/lib/descargaVista.js"
68
  import { muestraError } from "./js/lib/muestraError.js"
69
70
  const applicationServerKey = urlBase64ToUint8Array(
71
   "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0")
72
  /** @enum {string} */
73
  const Estado = {
74
   CALCULANDO: "Calculando…",
75
   SUSCRITO: "Suscrito",
76
   DESUSCRITO: "Sin suscripción",
77
   INCOMPATIBLE: "Incompatible"
78
  }
79
80
  preparaVista()
81
  btnSuscribe.addEventListener("click", suscribe)
82
  btnCancela.addEventListener("click", cancela)
83
  btnNotifica.addEventListener("click", notificaDesdeElServidor)
84
85
  async function preparaVista() {
86
   try {
87
    await activaNotificacionesPush("sw.js")
88
    const suscripcion = await getSuscripcionPush()
89
    if (suscripcion === null) {
90
     muestraEstado(Estado.DESUSCRITO)
91
    } else {
92
     // Modifica la suscripción en el servidor,
93
     const dto = calculaDtoParaSuscripcion(suscripcion)
94
     await consume(enviaJsonRecibeJson("php/suscripcion-modifica.php", dto))
95
     muestraEstado(Estado.SUSCRITO)
96
    }
97
   } catch (error) {
98
    muestraEstado(Estado.INCOMPATIBLE)
99
    muestraError(error)
100
   }
101
  }
102
103
  async function notificaDesdeElServidor() {
104
   reporte.innerHTML =
105
     /* html */ `<progress max="100">Cargando…</progress>`
106
   await descargaVista("php/notifica.php", "POST")
107
  }
108
109
  async function suscribe() {
110
   muestraEstado(Estado.CALCULANDO)
111
   const suscripcion = await suscribeAPush(applicationServerKey)
112
   // Agrega la suscripción al servidor,
113
   const dto = calculaDtoParaSuscripcion(suscripcion)
114
   await consume(enviaJsonRecibeJson("php/suscripcion-modifica.php", dto)
115
   )
116
   muestraEstado(Estado.SUSCRITO)
117
  }
118
119
  async function cancela() {
120
   muestraEstado(Estado.CALCULANDO)
121
   const suscripcion = await cancelaSuscripcionPush()
122
   if (suscripcion !== null) {
123
    // Elimina la suscripción en el servidor,
124
    const dto = calculaDtoParaSuscripcion(suscripcion)
125
    await consume(enviaJsonRecibeJson("php/suscripcion-elimina.php", dto))
126
    muestraEstado(Estado.DESUSCRITO)
127
   }
128
  }
129
130
  /**
131
   * @param {Estado} estado
132
   */
133
  function muestraEstado(estado) {
134
   outEstado.value = estado
135
   if (estado === Estado.INCOMPATIBLE || estado === Estado.CALCULANDO) {
136
    btnSuscribe.hidden = true
137
    btnCancela.hidden = true
138
    btnNotifica.hidden = true
139
   } else if (estado === Estado.SUSCRITO) {
140
    btnSuscribe.hidden = true
141
    btnCancela.hidden = false
142
    btnNotifica.hidden = false
143
   } else if (estado === Estado.DESUSCRITO) {
144
    btnSuscribe.hidden = false
145
    btnCancela.hidden = true
146
    btnNotifica.hidden = true
147
   }
148
  }
149
150
 </script>
151
152
 </body>
153
154
</html>

8. sw.js

1
const URL_SERVIDOR = "https://notipush.rf.gd"
2
3
if (self instanceof ServiceWorkerGlobalScope) {
4
5
 // El siguiente código se activa cuando llega una notificación push.
6
 self.addEventListener("push", (/** @type {PushEvent} */ event) => {
7
  const notificacion = event.data
8
  /* Si el navegador tiene permitido mostrar notificaciones push,
9
   * nuestra la que se ha recibido. */
10
  if (notificacion !== null && self.Notification.permission === 'granted') {
11
   event.waitUntil(muestraNotificacion(notificacion))
12
  }
13
 })
14
15
 self.addEventListener("notificationclick",
16
  (/** @type {NotificationEvent} */ event) => {
17
   event.notification.close()
18
   event.waitUntil(muestraVentana())
19
  })
20
}
21
22
/**
23
 * @param {PushMessageData} notificacion
24
 */
25
async function muestraNotificacion(notificacion) {
26
 if (self instanceof ServiceWorkerGlobalScope) {
27
  const mensaje = notificacion.text()
28
  await self.registration.showNotification(mensaje)
29
 }
30
}
31
32
async function muestraVentana() {
33
 if (self instanceof ServiceWorkerGlobalScope) {
34
  const clientes = await self.clients.matchAll({ type: "window" })
35
  for (const cliente of clientes) {
36
   if (cliente.url.startsWith(URL_SERVIDOR)) {
37
    return cliente.focus()
38
   }
39
  }
40
  return self.clients.openWindow("/")
41
 }
42
}

9. Carpeta « php »

A. php / Bd.php

1
<?php
2
3
class Bd
4
{
5
6
 private static ?PDO $pdo = null;
7
8
 static function pdo(): PDO
9
 {
10
  if (self::$pdo === null) {
11
12
   self::$pdo = new PDO(
13
    // cadena de conexión
14
    "sqlite:" . __DIR__ . "/notipush.db",
15
    // usuario
16
    null,
17
    // contraseña
18
    null,
19
    // Opciones: pdos no persistentes y lanza excepciones.
20
    [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
21
   );
22
23
   self::$pdo->exec(
24
    'CREATE TABLE IF NOT EXISTS SUSCRIPCION (
25
       SUS_ENDPOINT TEXT NOT NULL,
26
       SUS_PUB_KEY TEXT NOT NULL,
27
       SUS_AUT_TOK TEXT NOT NULL,
28
       SUS_CONT_ENCOD TEXT NOT NULL,
29
      CONSTRAINT SUS_PK
30
       PRIMARY KEY(SUS_ENDPOINT),
31
      CONSTRAINT SUS_ENDPNT_NV
32
       CHECK(LENGTH(SUS_ENDPOINT) > 0)
33
     )'
34
   );
35
  }
36
37
  return self::$pdo;
38
 }
39
}
40

B. php / genera-llaves.php

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Llaves VAPID</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Llaves VAPID</h1>
16
17
 <pre>
18
 <?php
19
20
 require __DIR__ . "/../vendor/autoload.php";
21
22
 use Minishlink\WebPush\VAPID;
23
24
 var_dump(VAPID::createVapidKeys());
25
26
 ?>
27
 </pre>
28
29
</body>
30
31
</html>

C. php / notifica.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/../vendor/autoload.php";
5
require_once  __DIR__ . "/lib/devuelveJson.php";
6
require_once  __DIR__ . "/Bd.php";
7
require_once __DIR__ . "/Suscripcion.php";
8
require_once __DIR__ . "/suscripcionElimina.php";
9
10
use Minishlink\WebPush\WebPush;
11
12
const AUTH = [
13
 "VAPID" => [
14
  "subject" => "https://notipush.rf.gd/",
15
  "publicKey" => "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0",
16
  "privateKey" => "vplfkITvu0cwHqzK9Kj-DYStbCH_9AhGx9LqMyaeI6w"
17
 ]
18
];
19
20
$webPush = new WebPush(AUTH);
21
$mensaje = "Hola! 👋";
22
23
// Envia el mensaje a todas las suscripciones.
24
25
$bd = Bd::pdo();
26
$stmt = $bd->query("SELECT * FROM SUSCRIPCION");
27
$suscripciones =
28
 $stmt->fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, Suscripcion::class);
29
30
foreach ($suscripciones as $suscripcion) {
31
 $webPush->queueNotification($suscripcion, $mensaje);
32
}
33
$reportes = $webPush->flush();
34
35
// Genera el reporte de envio a cada suscripcion.
36
$reporteDeEnvios = "";
37
foreach ($reportes as $reporte) {
38
 $endpoint = $reporte->getRequest()->getUri();
39
 $htmlEndpoint = htmlentities($endpoint);
40
 if ($reporte->isSuccess()) {
41
  // Reporte de éxito.
42
  $reporteDeEnvios .= "<dt>$htmlEndpoint</dt><dd>Éxito</dd>";
43
 } else {
44
  if ($reporte->isSubscriptionExpired()) {
45
   suscripcionElimina($bd, $endpoint);
46
  }
47
  // Reporte de fallo.
48
  $explicacion = htmlentities($reporte->getReason());
49
  $reporteDeEnvios .= "<dt>$endpoint</dt><dd>Fallo: $explicacion</dd>";
50
 }
51
}
52
53
devuelveJson(["reporte" => ["innerHTML" => $reporteDeEnvios]]);
54

D. php / recibeSuscripcion.php

1
<?php
2
3
require_once __DIR__ . "/lib/BAD_REQUEST.php";
4
require_once __DIR__ . "/lib/recibeJson.php";
5
require_once __DIR__ . "/lib/ProblemDetailsException.php";
6
7
function recibeSuscripcion()
8
{
9
10
 $objeto = recibeJson();
11
12
 if (
13
  !isset($objeto->authToken)
14
  || !is_string($objeto->authToken)
15
  || $objeto->authToken === ""
16
 )
17
  throw new ProblemDetailsException([
18
   "status" => BAD_REQUEST,
19
   "title" => "El authToken debe ser texto que no esté en blanco.",
20
   "type" => "/errors/authtokenincorrecto.html",
21
  ]);
22
23
 if (
24
  !isset($objeto->contentEncoding)
25
  || !is_string($objeto->contentEncoding)
26
  || $objeto->contentEncoding === ""
27
 )
28
  throw new ProblemDetailsException([
29
   "status" => BAD_REQUEST,
30
   "title" => "La contentEncoding debe ser texto que no esté en blanco.",
31
   "type" => "/errors/contentencodingincorrecta.html",
32
  ]);
33
34
 if (
35
  !isset($objeto->endpoint)
36
  || !is_string($objeto->endpoint)
37
  || $objeto->endpoint === ""
38
 )
39
  throw new ProblemDetailsException([
40
   "status" => BAD_REQUEST,
41
   "title" => "El endpoint debe ser texto que no esté en blanco.",
42
   "type" => "/errors/endpointincorrecto.html",
43
  ]);
44
45
 if (
46
  !isset($objeto->publicKey)
47
  || !is_string($objeto->publicKey)
48
  || $objeto->publicKey === ""
49
 )
50
  throw new ProblemDetailsException([
51
   "status" => BAD_REQUEST,
52
   "title" => "La publicKey debe ser texto que no esté en blanco.",
53
   "type" => "/errors/publickeyincorrecta.html",
54
  ]);
55
56
 return [
57
  "SUS_AUT_TOK" => $objeto->authToken,
58
  "SUS_CONT_ENCOD" => $objeto->contentEncoding,
59
  "SUS_ENDPOINT" => $objeto->endpoint,
60
  "SUS_PUB_KEY" => $objeto->publicKey,
61
 ];
62
}
63

E. php / suscripcion-elimina.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/lib/devuelveNoContent.php";
5
require_once  __DIR__ . "/Bd.php";
6
require_once  __DIR__ . "/recibeSuscripcion.php";
7
require_once  __DIR__ . "/suscripcionElimina.php";
8
9
$modelo = recibeSuscripcion();
10
suscripcionElimina(Bd::pdo(), $modelo["SUS_ENDPOINT"]);
11
devuelveNoContent();
12

F. php / suscripcion-modifica.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/lib/devuelveCreated.php";
5
require_once __DIR__ . "/lib/devuelveJson.php";
6
require_once  __DIR__ . "/Bd.php";
7
require_once  __DIR__ . "/recibeSuscripcion.php";
8
9
$modelo = recibeSuscripcion();
10
11
$bd = Bd::pdo();
12
13
$stmt =
14
 $bd->prepare("SELECT * FROM SUSCRIPCION WHERE SUS_ENDPOINT = :SUS_ENDPOINT");
15
$stmt->execute([":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"]]);
16
$anterior = $stmt->fetch(PDO::FETCH_ASSOC);
17
18
if ($anterior === false) {
19
20
 $stmt = $bd->prepare(
21
  "INSERT INTO SUSCRIPCION (
22
    SUS_ENDPOINT, SUS_PUB_KEY, SUS_AUT_TOK, SUS_CONT_ENCOD
23
   ) values (
24
    :SUS_ENDPOINT, :SUS_PUB_KEY, :SUS_AUT_TOK, :SUS_CONT_ENCOD
25
   )"
26
 );
27
 $stmt->execute([
28
  ":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"],
29
  ":SUS_PUB_KEY" => $modelo["SUS_PUB_KEY"],
30
  ":SUS_AUT_TOK" => $modelo["SUS_AUT_TOK"],
31
  ":SUS_CONT_ENCOD" => $modelo["SUS_CONT_ENCOD"],
32
 ]);
33
34
 devuelveCreated("", $modelo);
35
} else {
36
37
 $stmt = $bd->prepare(
38
  "UPDATE SUSCRIPCION
39
   SET
40
    SUS_PUB_KEY = :SUS_PUB_KEY,
41
    SUS_AUT_TOK = :SUS_AUT_TOK,
42
    SUS_CONT_ENCOD = :SUS_CONT_ENCOD
43
   WHERE
44
    SUS_ENDPOINT = :SUS_ENDPOINT"
45
 );
46
 $stmt->execute([
47
  ":SUS_PUB_KEY" => $modelo["SUS_PUB_KEY"],
48
  ":SUS_AUT_TOK" => $modelo["SUS_AUT_TOK"],
49
  ":SUS_CONT_ENCOD" => $modelo["SUS_CONT_ENCOD"],
50
  ":SUS_ENDPOINT" => $modelo["SUS_ENDPOINT"],
51
 ]);
52
53
 devuelveJson($modelo);
54
}
55

G. php / Suscripcion.php

1
<?php
2
3
require_once __DIR__ . "/../vendor/autoload.php";
4
5
use Minishlink\WebPush\SubscriptionInterface;
6
7
class Suscripcion implements SubscriptionInterface
8
{
9
10
 public string $SUS_ENDPOINT;
11
 public string $SUS_PUB_KEY;
12
 public string $SUS_AUT_TOK;
13
 public string $SUS_CONT_ENCOD;
14
15
 public function __construct(
16
  string $SUS_ENDPOINT = "",
17
  string $SUS_PUB_KEY = "",
18
  string $SUS_AUT_TOK = "",
19
  string $SUS_CONT_ENCOD = ""
20
 ) {
21
  $this->SUS_ENDPOINT = $SUS_ENDPOINT;
22
  $this->SUS_PUB_KEY = $SUS_PUB_KEY;
23
  $this->SUS_AUT_TOK = $SUS_AUT_TOK;
24
  $this->SUS_CONT_ENCOD = $SUS_CONT_ENCOD;
25
 }
26
27
 public function getEndpoint(): string
28
 {
29
  return $this->SUS_ENDPOINT;
30
 }
31
32
 public function getPublicKey(): ?string
33
 {
34
  return $this->SUS_PUB_KEY;
35
 }
36
37
 public function getAuthToken(): ?string
38
 {
39
  return $this->SUS_AUT_TOK;
40
 }
41
42
 public function getContentEncoding(): ?string
43
 {
44
  return $this->SUS_CONT_ENCOD;
45
 }
46
}
47

H. php / suscripcionElimina.php

1
<?php
2
3
function suscripcionElimina(\PDO $bd, string $endpoint)
4
{
5
 $stmt =
6
  $bd->prepare("DELETE FROM SUSCRIPCION WHERE SUS_ENDPOINT = :SUS_ENDPOINT");
7
 $stmt->execute([":SUS_ENDPOINT" => $endpoint]);
8
}
9

I. Carpeta « php / lib »

1. php / lib / BAD_REQUEST.php

1
<?php
2
3
const BAD_REQUEST = 400;
4

2. php / lib / devuelveCreated.php

1
<?php
2
3
require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5
function devuelveCreated($urlDelNuevo, $resultado)
6
{
7
 $json = json_encode($resultado);
8
 if ($json === false) {
9
  devuelveResultadoNoJson();
10
 } else {
11
  http_response_code(201);
12
  header("Location: $urlDelNuevo");
13
  header("Content-Type: application/json; charset=utf-8");
14
  echo $json;
15
 }
16
}
17

3. php / lib / devuelveJson.php

1
<?php
2
3
require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5
function devuelveJson($resultado)
6
{
7
 $json = json_encode($resultado);
8
 if ($json === false) {
9
  devuelveResultadoNoJson();
10
 } else {
11
  header("Content-Type: application/json; charset=utf-8");
12
  echo $json;
13
 }
14
 exit();
15
}
16

4. php / lib / devuelveNoContent.php

1
<?php
2
3
function devuelveNoContent()
4
{
5
 http_response_code(204);
6
}
7

5. php / lib / devuelveResultadoNoJson.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
function devuelveResultadoNoJson()
6
{
7
 http_response_code(INTERNAL_SERVER_ERROR);
8
 header("Content-Type: application/problem+json; charset=utf-8");
9
10
 echo '{' .
11
  "status: " . INTERNAL_SERVER_ERROR .
12
  '"title": "El resultado no puede representarse como JSON."' .
13
  '"type": "/errors/resultadonojson.html"' .
14
  '}';
15
}
16

6. php / lib / INTERNAL_SERVER_ERROR.php

1
<?php
2
3
const INTERNAL_SERVER_ERROR = 500;

7. php / lib / manejaErrores.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
require_once __DIR__ . "/ProblemDetailsException.php";
5
6
// Hace que se lance una excepción automáticamente cuando se genere un error.
7
set_error_handler(function ($severity, $message, $file, $line) {
8
 throw new ErrorException($message, 0, $severity, $file, $line);
9
});
10
11
// Código cuando una excepción no es atrapada.
12
set_exception_handler(function (Throwable $excepcion) {
13
 if ($excepcion instanceof ProblemDetailsException) {
14
  devuelveProblemDetails($excepcion->problemDetails);
15
 } else {
16
  devuelveProblemDetails([
17
   "status" => INTERNAL_SERVER_ERROR,
18
   "title" => "Error interno del servidor",
19
   "detail" => $excepcion->getMessage(),
20
   "type" => "/errors/errorinterno.html",
21
  ]);
22
 }
23
 exit();
24
});
25
26
function devuelveProblemDetails(array $array)
27
{
28
 $json = json_encode($array);
29
 if ($json === false) {
30
  devuelveResultadoNoJson();
31
 } else {
32
  http_response_code(isset($array["status"]) ? $array["status"] : 500);
33
  header("Content-Type: application/problem+json; charset=utf-8");
34
  echo $json;
35
 }
36
}
37

8. php / lib / ProblemDetailsException.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
/**
6
 * Detalle de los errores devueltos por un servicio.
7
 */
8
class ProblemDetailsException extends Exception
9
{
10
11
 public array $problemDetails;
12
13
 public function __construct(
14
  array $problemDetails,
15
 ) {
16
  
17
  parent::__construct(
18
   isset($problemDetails["detail"])
19
    ? $problemDetails["detail"]
20
    : (isset($problemDetails["title"])
21
     ? $problemDetails["title"]
22
     : "Error"),
23
   $problemDetails["status"]
24
    ? $problemDetails["status"]
25
    : INTERNAL_SERVER_ERROR
26
  );
27
28
  $this->problemDetails = $problemDetails;
29
 }
30
}
31

9. php / lib / recibeJson.php

1
<?php
2
3
require_once __DIR__ . "/BAD_REQUEST.php";
4
5
function recibeJson()
6
{
7
 $json = json_decode(file_get_contents("php://input"));
8
9
 if ($json === null) {
10
11
  http_response_code(BAD_REQUEST);
12
  header("Content-Type: application/problem+json; charset=utf-8");
13
14
  echo '{' .
15
   "status: " . BAD_REQUEST .
16
   '"title": "Los datos recibidos no están en formato JSON."' .
17
   '"type": "/errors/datosnojson.html"' .
18
   '}';
19
20
  exit();
21
 }
22
23
 return $json;
24
}
25

10. Carpeta « js »

A. Carpeta « js / lib »

1. js / lib / activaNotificacionesPush.js

1
/**
2
 * @param { string | URL } urlDeServiceWorkerQueRecibeNotificaciones
3
 */
4
export async function activaNotificacionesPush(
5
6
 urlDeServiceWorkerQueRecibeNotificaciones) {
7
8
 // Valida que el navegador soporte notificaciones push.
9
 if (!('PushManager' in window))
10
  throw new Error("Este navegador no soporta notificaciones push.")
11
12
 // Valida que el navegador soporte notificaciones,
13
 if (!("Notification" in window))
14
  throw new Error("Este navegador no soporta notificaciones push.")
15
16
 // Valida que el navegador soporte service workers,
17
 if (!("serviceWorker" in navigator))
18
  throw new Error("Este navegador no soporta service workers.")
19
20
 // Recupera el permiso para usar notificaciones
21
 let permiso = Notification.permission
22
 if (permiso === "default") {
23
  // Permiso no asignado. Pide al usuario su autorización.
24
  permiso = await Notification.requestPermission()
25
 }
26
27
 // Valida que el usuario haya permitido usar notificaciones..
28
 if (permiso === "denied")
29
  throw new Error("Notificaciones bloqueadas.")
30
31
 const registro = await navigator.serviceWorker.register(
32
  urlDeServiceWorkerQueRecibeNotificaciones)
33
 console.log(urlDeServiceWorkerQueRecibeNotificaciones, "registrado.")
34
 console.log(registro)
35
36
 if (!("showNotification" in registro))
37
  throw new Error("Este navegador no soporta notificaciones.")
38
}

2. js / lib / calculaDtoParaSuscripcion.js

1
/**
2
 * Devuelve una literal de objeto que se puede usar para enviar
3
 * en formato JSON al servidor.
4
 * DTO es un acrónimo para Data Transder Object, u
5
 * objeto para transferencia de datos.
6
 * @param { PushSubscription } suscripcion
7
 */
8
export function calculaDtoParaSuscripcion(suscripcion) {
9
 const key = suscripcion.getKey("p256dh")
10
 const token = suscripcion.getKey("auth")
11
 const supported = PushManager.supportedContentEncodings
12
 const encodings = Array.isArray(supported) && supported.length > 0
13
  ? supported
14
  : ["aesgcm"]
15
 const endpoint = suscripcion.endpoint
16
 const publicKey = key === null
17
  ? null
18
  : btoa(String.fromCharCode.apply(null, new Uint8Array(key)))
19
 const authToken = token === null
20
  ? null
21
  : btoa(String.fromCharCode.apply(null, new Uint8Array(token)))
22
 const contentEncoding = encodings[0]
23
 return {
24
  endpoint,
25
  publicKey,
26
  authToken,
27
  contentEncoding
28
 }
29
}
30

3. js / lib / cancelaSuscripcionPush.js

1
import { getSuscripcionPush } from "./getSuscripcionPush.js"
2
3
export async function cancelaSuscripcionPush() {
4
 const suscripcion = await getSuscripcionPush()
5
 const resultado = suscripcion === null
6
  ? false
7
  : await suscripcion.unsubscribe()
8
 return resultado === true ? suscripcion : null
9
}

4. js / lib / consume.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Espera a que la promesa de un fetch termine. Si
5
 * hay error, lanza una excepción.
6
 * 
7
 * @param {Promise<Response> } servicio
8
 */
9
export async function consume(servicio) {
10
 const respuesta = await servicio
11
 if (respuesta.ok) {
12
  return respuesta
13
 } else {
14
  const contentType = respuesta.headers.get("Content-Type")
15
  if (contentType === "application/problem+json; charset=utf-8")
16
   throw new ProblemDetailsError(await respuesta.json())
17
  else
18
   throw new Error(respuesta.statusText)
19
 }
20
}

5. js / lib / descargaVista.js

1
import { consume } from "./consume.js"
2
import { muestraObjeto } from "./muestraObjeto.js"
3
import { recibeJson } from "./recibeJson.js"
4
5
/**
6
 * @param {string} url
7
 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
8
 *  | "CONNECT" | "HEAD" } metodoHttp
9
 */
10
export async function descargaVista(url, metodoHttp = "GET") {
11
 const respuesta = await consume(recibeJson(url, metodoHttp))
12
 const json = await respuesta.json()
13
 muestraObjeto(document, json)
14
 return json
15
}

6. js / lib / enviaJsonRecibeJson.js

1
2
/**
3
 * @param { string } url
4
 * @param { Object } body
5
 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
6
 *  | "CONNECT" | "HEAD" } metodoHttp
7
 */
8
export async function enviaJsonRecibeJson(url, body, metodoHttp = "POST") {
9
 return fetch(
10
  url,
11
  {
12
   method: metodoHttp,
13
   headers: {
14
    "Content-Type": "application/json",
15
    "Accept": "application/json, application/problem+json"
16
   },
17
   body: JSON.stringify(body)
18
  }
19
 )
20
}

7. js / lib / getSuscripcionPush.js

1
export async function getSuscripcionPush() {
2
 // Recupera el service worker registrado.
3
 const registro = await navigator.serviceWorker.ready
4
 return registro.pushManager.getSubscription()
5
}

8. js / lib / manejaErrores.js

1
import { muestraError } from "./muestraError.js"
2
3
/**
4
 * Intercepta Response.prototype.json para capturar errores de parseo
5
 * y asegurar que se reporten correctamente en navegadores Chromium.
6
 */
7
{
8
 const originalJson = Response.prototype.json
9
10
 Response.prototype.json = function () {
11
  // Llamamos al método original usando el contexto (this) de la respuesta
12
  return originalJson.apply(this, arguments)
13
   .catch((/** @type {any} */ error) => {
14
    // Corrige un error de Chrome que evita el manejo correcto de errores.
15
    throw new Error(error)
16
   })
17
 }
18
}
19
20
window.onerror = function (
21
  /** @type {string} */ _message,
22
  /** @type {string} */ _url,
23
  /** @type {number} */ _line,
24
  /** @type {number} */ _column,
25
  /** @type {Error} */ errorObject
26
) {
27
 muestraError(errorObject)
28
 return true
29
}
30
31
window.addEventListener('unhandledrejection', event => {
32
 muestraError(event.reason)
33
 event.preventDefault()
34
})
35

9. js / lib / muestraError.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Muestra los datos de una Error en la consola y en un cuadro de alerta.
5
 * @param { ProblemDetailsError | Error | null } error descripción del error.
6
 */
7
export function muestraError(error) {
8
9
 if (error === null) {
10
11
  console.error("Error")
12
  alert("Error")
13
14
 } else if (error instanceof ProblemDetailsError) {
15
16
  const problemDetails = error.problemDetails
17
18
  let mensaje =
19
   typeof problemDetails["title"] === "string" ? problemDetails["title"] : ""
20
  if (typeof problemDetails["detail"] === "string") {
21
   if (mensaje !== "") {
22
    mensaje += "\n\n"
23
   }
24
   mensaje += problemDetails["detail"]
25
  }
26
  if (mensaje === "") {
27
   mensaje = "Error"
28
  }
29
  console.error(error, problemDetails)
30
  alert(mensaje)
31
32
 } else {
33
34
  console.error(error)
35
  alert(error.message)
36
37
 }
38
39
}

10. js / lib / muestraObjeto.js

1
/**
2
 * @param {Document | HTMLElement | ShadowRoot} raizHtml
3
 * @param { any } objeto
4
 */
5
export function muestraObjeto(raizHtml, objeto) {
6
 for (const [nombre, definiciones] of Object.entries(objeto)) {
7
  if (Array.isArray(definiciones)) {
8
   muestraArray(raizHtml, nombre, definiciones)
9
  } else if (definiciones !== undefined && definiciones !== null) {
10
   muestraElemento(raizHtml, nombre, definiciones)
11
  }
12
 }
13
}
14
15
/**
16
 * @param { string } nombre
17
 */
18
export function selectorDeNombre(nombre) {
19
 return `[id="${nombre}"],[name="${nombre}"],[data-name="${nombre}"]`
20
}
21
22
/**
23
 * @param { Document | HTMLElement | ShadowRoot } raizHtml
24
 * @param { string } propiedad
25
 * @param {any[]} valores
26
 */
27
function muestraArray(raizHtml, propiedad, valores) {
28
 const conjunto = new Set(valores)
29
 const elementos = raizHtml.querySelectorAll(selectorDeNombre(propiedad))
30
 if (elementos.length === 1 && elementos[0] instanceof HTMLSelectElement) {
31
  muestraOptions(elementos[0], conjunto)
32
 } else {
33
  muestraInputs(elementos, conjunto)
34
 }
35
36
}
37
38
/**
39
 * @param {HTMLSelectElement} select
40
 * @param {Set<any>} conjunto
41
 */
42
function muestraOptions(select, conjunto) {
43
 for (let i = 0, options = select.options, len = options.length; i < len; i++) {
44
  const option = options[i]
45
  option.selected = conjunto.has(option.value)
46
 }
47
}
48
49
/**
50
 * @param {NodeListOf<Element>} elementos
51
 * @param {Set<any>} conjunto
52
 */
53
function muestraInputs(elementos, conjunto) {
54
 for (let i = 0, len = elementos.length; i < len; i++) {
55
  const elemento = elementos[i]
56
  if (elemento instanceof HTMLInputElement) {
57
   elemento.checked = conjunto.has(elemento.value)
58
  }
59
 }
60
}
61
62
const data_ = "data-"
63
const data_Length = data_.length
64
65
/**
66
 * @param {Document | HTMLElement | ShadowRoot} raizHtml
67
 * @param {string} nombre
68
 * @param {{ [s: string]: any; } } definiciones
69
 */
70
function muestraElemento(raizHtml, nombre, definiciones) {
71
 const elemento = raizHtml.querySelector(selectorDeNombre(nombre))
72
 if (elemento !== null) {
73
  for (const [propiedad, valor] of Object.entries(definiciones)) {
74
   if (propiedad in elemento) {
75
    elemento[propiedad] = valor
76
   } else if (
77
    propiedad.length > data_Length
78
    && propiedad.startsWith(data_)
79
    && elemento instanceof HTMLElement
80
   ) {
81
    elemento.dataset[propiedad.substring(data_Length)] = valor
82
   }
83
  }
84
 }
85
}

11. js / lib / ProblemDetailsError.js

1
export class ProblemDetailsError extends Error {
2
3
 /**
4
  * Detalle de los errores devueltos por un servicio.
5
  * Crea una instancia de ProblemDetailsError.
6
  * @param {object} problemDetails Objeto con la descripcipon del error.
7
  */
8
 constructor(problemDetails) {
9
10
  super(typeof problemDetails["detail"] === "string"
11
   ? problemDetails["detail"]
12
   : (typeof problemDetails["title"] === "string"
13
    ? problemDetails["title"]
14
    : "Error"))
15
16
  this.problemDetails = problemDetails
17
18
 }
19
20
}

12. js / lib / recibeJson.js

1
2
/**
3
 * @param {string} url
4
 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
5
 *  | "CONNECT" | "HEAD" } metodoHttp
6
 */
7
export async function recibeJson(url, metodoHttp = "GET") {
8
 return fetch(
9
  url,
10
  {
11
   method: metodoHttp,
12
   headers: { "Accept": "application/json, application/problem+json" }
13
  }
14
 )
15
}

13. js / lib / suscribeAPush.js

1
/**
2
 * @param { ArrayBuffer } applicationServerKey
3
 */
4
export async function suscribeAPush(applicationServerKey) {
5
 // Recupera el service worker registrado.
6
 const registro = await navigator.serviceWorker.ready
7
 return registro.pushManager.subscribe({
8
  userVisibleOnly: true,
9
  applicationServerKey
10
 })
11
}

14. js / lib / urlBase64ToUint8Array.js

1
/**
2
 * @param { string } base64String
3
 */
4
export function urlBase64ToUint8Array(base64String) {
5
 const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
6
 const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/')
7
 const rawData = atob(base64)
8
 const outputArray = new Uint8Array(rawData.length)
9
 for (let i = 0; i < rawData.length; ++i) {
10
  outputArray[i] = rawData.charCodeAt(i)
11
 }
12
 return outputArray
13
}

11. Carpeta « errors »

A. errors / authtokenincorrecto.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El authToken debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>El authToken debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

B. errors / contentencodingincorrecta.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>La contentEncoding debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>La contentEncoding debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

C. errors / datosnojson.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Los datos recibidos no están en formato JSON</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Los datos recibidos no están en formato JSON</h1>
16
17
</body>
18
19
</html>

D. errors / endpointincorrecto.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El endpoint debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>El endpoint debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

E. errors / errorinterno.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Error interno del servidor</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Error interno del servidor</h1>
16
17
 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19
</body>
20
21
</html>

F. errors / publickeyincorrecta.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>La publicKey debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>La publicKey debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

G. errors / resultadonojson.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El resultado no puede representarse como JSON</title>
10
11
</head>
12
13
<body>
14
15
 <h1>El resultado no puede representarse como JSON</h1>
16
17
 <p>
18
  Debido a un error interno del servidor, el resultado generado, no se puede
19
  recuperar.
20
 </p>
21
22
</body>
23
24
</html>

12. composer.json

1
{
2
 "require": {
3
  "minishlink/web-push": "^10.0.1"
4
 }
5
}

13. composer.lock

1
{
2
    "_readme": [
3
        "This file locks the dependencies of your project to a known state",
4
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5
        "This file is @generated automatically"
6
    ],
7
    "content-hash": "5d03d83a0c68b97c1b883f5e5d450861",
8
    "packages": [
9
        {
10
            "name": "brick/math",
11
            "version": "0.14.1",
12
            "source": {
13
                "type": "git",
14
                "url": "https://github.com/brick/math.git",
15
                "reference": "f05858549e5f9d7bb45875a75583240a38a281d0"
16
            },
17
            "dist": {
18
                "type": "zip",
19
                "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0",
20
                "reference": "f05858549e5f9d7bb45875a75583240a38a281d0",
21
                "shasum": ""
22
            },
23
            "require": {
24
                "php": "^8.2"
25
            },
26
            "require-dev": {
27
                "php-coveralls/php-coveralls": "^2.2",
28
                "phpstan/phpstan": "2.1.22",
29
                "phpunit/phpunit": "^11.5"
30
            },
31
            "type": "library",
32
            "autoload": {
33
                "psr-4": {
34
                    "Brick\\Math\\": "src/"
35
                }
36
            },
37
            "notification-url": "https://packagist.org/downloads/",
38
            "license": [
39
                "MIT"
40
            ],
41
            "description": "Arbitrary-precision arithmetic library",
42
            "keywords": [
43
                "Arbitrary-precision",
44
                "BigInteger",
45
                "BigRational",
46
                "arithmetic",
47
                "bigdecimal",
48
                "bignum",
49
                "bignumber",
50
                "brick",
51
                "decimal",
52
                "integer",
53
                "math",
54
                "mathematics",
55
                "rational"
56
            ],
57
            "support": {
58
                "issues": "https://github.com/brick/math/issues",
59
                "source": "https://github.com/brick/math/tree/0.14.1"
60
            },
61
            "funding": [
62
                {
63
                    "url": "https://github.com/BenMorel",
64
                    "type": "github"
65
                }
66
            ],
67
            "time": "2025-11-24T14:40:29+00:00"
68
        },
69
        {
70
            "name": "guzzlehttp/guzzle",
71
            "version": "7.10.0",
72
            "source": {
73
                "type": "git",
74
                "url": "https://github.com/guzzle/guzzle.git",
75
                "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
76
            },
77
            "dist": {
78
                "type": "zip",
79
                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
80
                "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
81
                "shasum": ""
82
            },
83
            "require": {
84
                "ext-json": "*",
85
                "guzzlehttp/promises": "^2.3",
86
                "guzzlehttp/psr7": "^2.8",
87
                "php": "^7.2.5 || ^8.0",
88
                "psr/http-client": "^1.0",
89
                "symfony/deprecation-contracts": "^2.2 || ^3.0"
90
            },
91
            "provide": {
92
                "psr/http-client-implementation": "1.0"
93
            },
94
            "require-dev": {
95
                "bamarni/composer-bin-plugin": "^1.8.2",
96
                "ext-curl": "*",
97
                "guzzle/client-integration-tests": "3.0.2",
98
                "php-http/message-factory": "^1.1",
99
                "phpunit/phpunit": "^8.5.39 || ^9.6.20",
100
                "psr/log": "^1.1 || ^2.0 || ^3.0"
101
            },
102
            "suggest": {
103
                "ext-curl": "Required for CURL handler support",
104
                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
105
                "psr/log": "Required for using the Log middleware"
106
            },
107
            "type": "library",
108
            "extra": {
109
                "bamarni-bin": {
110
                    "bin-links": true,
111
                    "forward-command": false
112
                }
113
            },
114
            "autoload": {
115
                "files": [
116
                    "src/functions_include.php"
117
                ],
118
                "psr-4": {
119
                    "GuzzleHttp\\": "src/"
120
                }
121
            },
122
            "notification-url": "https://packagist.org/downloads/",
123
            "license": [
124
                "MIT"
125
            ],
126
            "authors": [
127
                {
128
                    "name": "Graham Campbell",
129
                    "email": "hello@gjcampbell.co.uk",
130
                    "homepage": "https://github.com/GrahamCampbell"
131
                },
132
                {
133
                    "name": "Michael Dowling",
134
                    "email": "mtdowling@gmail.com",
135
                    "homepage": "https://github.com/mtdowling"
136
                },
137
                {
138
                    "name": "Jeremy Lindblom",
139
                    "email": "jeremeamia@gmail.com",
140
                    "homepage": "https://github.com/jeremeamia"
141
                },
142
                {
143
                    "name": "George Mponos",
144
                    "email": "gmponos@gmail.com",
145
                    "homepage": "https://github.com/gmponos"
146
                },
147
                {
148
                    "name": "Tobias Nyholm",
149
                    "email": "tobias.nyholm@gmail.com",
150
                    "homepage": "https://github.com/Nyholm"
151
                },
152
                {
153
                    "name": "Márk Sági-Kazár",
154
                    "email": "mark.sagikazar@gmail.com",
155
                    "homepage": "https://github.com/sagikazarmark"
156
                },
157
                {
158
                    "name": "Tobias Schultze",
159
                    "email": "webmaster@tubo-world.de",
160
                    "homepage": "https://github.com/Tobion"
161
                }
162
            ],
163
            "description": "Guzzle is a PHP HTTP client library",
164
            "keywords": [
165
                "client",
166
                "curl",
167
                "framework",
168
                "http",
169
                "http client",
170
                "psr-18",
171
                "psr-7",
172
                "rest",
173
                "web service"
174
            ],
175
            "support": {
176
                "issues": "https://github.com/guzzle/guzzle/issues",
177
                "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
178
            },
179
            "funding": [
180
                {
181
                    "url": "https://github.com/GrahamCampbell",
182
                    "type": "github"
183
                },
184
                {
185
                    "url": "https://github.com/Nyholm",
186
                    "type": "github"
187
                },
188
                {
189
                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
190
                    "type": "tidelift"
191
                }
192
            ],
193
            "time": "2025-08-23T22:36:01+00:00"
194
        },
195
        {
196
            "name": "guzzlehttp/promises",
197
            "version": "2.3.0",
198
            "source": {
199
                "type": "git",
200
                "url": "https://github.com/guzzle/promises.git",
201
                "reference": "481557b130ef3790cf82b713667b43030dc9c957"
202
            },
203
            "dist": {
204
                "type": "zip",
205
                "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
206
                "reference": "481557b130ef3790cf82b713667b43030dc9c957",
207
                "shasum": ""
208
            },
209
            "require": {
210
                "php": "^7.2.5 || ^8.0"
211
            },
212
            "require-dev": {
213
                "bamarni/composer-bin-plugin": "^1.8.2",
214
                "phpunit/phpunit": "^8.5.44 || ^9.6.25"
215
            },
216
            "type": "library",
217
            "extra": {
218
                "bamarni-bin": {
219
                    "bin-links": true,
220
                    "forward-command": false
221
                }
222
            },
223
            "autoload": {
224
                "psr-4": {
225
                    "GuzzleHttp\\Promise\\": "src/"
226
                }
227
            },
228
            "notification-url": "https://packagist.org/downloads/",
229
            "license": [
230
                "MIT"
231
            ],
232
            "authors": [
233
                {
234
                    "name": "Graham Campbell",
235
                    "email": "hello@gjcampbell.co.uk",
236
                    "homepage": "https://github.com/GrahamCampbell"
237
                },
238
                {
239
                    "name": "Michael Dowling",
240
                    "email": "mtdowling@gmail.com",
241
                    "homepage": "https://github.com/mtdowling"
242
                },
243
                {
244
                    "name": "Tobias Nyholm",
245
                    "email": "tobias.nyholm@gmail.com",
246
                    "homepage": "https://github.com/Nyholm"
247
                },
248
                {
249
                    "name": "Tobias Schultze",
250
                    "email": "webmaster@tubo-world.de",
251
                    "homepage": "https://github.com/Tobion"
252
                }
253
            ],
254
            "description": "Guzzle promises library",
255
            "keywords": [
256
                "promise"
257
            ],
258
            "support": {
259
                "issues": "https://github.com/guzzle/promises/issues",
260
                "source": "https://github.com/guzzle/promises/tree/2.3.0"
261
            },
262
            "funding": [
263
                {
264
                    "url": "https://github.com/GrahamCampbell",
265
                    "type": "github"
266
                },
267
                {
268
                    "url": "https://github.com/Nyholm",
269
                    "type": "github"
270
                },
271
                {
272
                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
273
                    "type": "tidelift"
274
                }
275
            ],
276
            "time": "2025-08-22T14:34:08+00:00"
277
        },
278
        {
279
            "name": "guzzlehttp/psr7",
280
            "version": "2.8.0",
281
            "source": {
282
                "type": "git",
283
                "url": "https://github.com/guzzle/psr7.git",
284
                "reference": "21dc724a0583619cd1652f673303492272778051"
285
            },
286
            "dist": {
287
                "type": "zip",
288
                "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
289
                "reference": "21dc724a0583619cd1652f673303492272778051",
290
                "shasum": ""
291
            },
292
            "require": {
293
                "php": "^7.2.5 || ^8.0",
294
                "psr/http-factory": "^1.0",
295
                "psr/http-message": "^1.1 || ^2.0",
296
                "ralouphie/getallheaders": "^3.0"
297
            },
298
            "provide": {
299
                "psr/http-factory-implementation": "1.0",
300
                "psr/http-message-implementation": "1.0"
301
            },
302
            "require-dev": {
303
                "bamarni/composer-bin-plugin": "^1.8.2",
304
                "http-interop/http-factory-tests": "0.9.0",
305
                "phpunit/phpunit": "^8.5.44 || ^9.6.25"
306
            },
307
            "suggest": {
308
                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
309
            },
310
            "type": "library",
311
            "extra": {
312
                "bamarni-bin": {
313
                    "bin-links": true,
314
                    "forward-command": false
315
                }
316
            },
317
            "autoload": {
318
                "psr-4": {
319
                    "GuzzleHttp\\Psr7\\": "src/"
320
                }
321
            },
322
            "notification-url": "https://packagist.org/downloads/",
323
            "license": [
324
                "MIT"
325
            ],
326
            "authors": [
327
                {
328
                    "name": "Graham Campbell",
329
                    "email": "hello@gjcampbell.co.uk",
330
                    "homepage": "https://github.com/GrahamCampbell"
331
                },
332
                {
333
                    "name": "Michael Dowling",
334
                    "email": "mtdowling@gmail.com",
335
                    "homepage": "https://github.com/mtdowling"
336
                },
337
                {
338
                    "name": "George Mponos",
339
                    "email": "gmponos@gmail.com",
340
                    "homepage": "https://github.com/gmponos"
341
                },
342
                {
343
                    "name": "Tobias Nyholm",
344
                    "email": "tobias.nyholm@gmail.com",
345
                    "homepage": "https://github.com/Nyholm"
346
                },
347
                {
348
                    "name": "Márk Sági-Kazár",
349
                    "email": "mark.sagikazar@gmail.com",
350
                    "homepage": "https://github.com/sagikazarmark"
351
                },
352
                {
353
                    "name": "Tobias Schultze",
354
                    "email": "webmaster@tubo-world.de",
355
                    "homepage": "https://github.com/Tobion"
356
                },
357
                {
358
                    "name": "Márk Sági-Kazár",
359
                    "email": "mark.sagikazar@gmail.com",
360
                    "homepage": "https://sagikazarmark.hu"
361
                }
362
            ],
363
            "description": "PSR-7 message implementation that also provides common utility methods",
364
            "keywords": [
365
                "http",
366
                "message",
367
                "psr-7",
368
                "request",
369
                "response",
370
                "stream",
371
                "uri",
372
                "url"
373
            ],
374
            "support": {
375
                "issues": "https://github.com/guzzle/psr7/issues",
376
                "source": "https://github.com/guzzle/psr7/tree/2.8.0"
377
            },
378
            "funding": [
379
                {
380
                    "url": "https://github.com/GrahamCampbell",
381
                    "type": "github"
382
                },
383
                {
384
                    "url": "https://github.com/Nyholm",
385
                    "type": "github"
386
                },
387
                {
388
                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
389
                    "type": "tidelift"
390
                }
391
            ],
392
            "time": "2025-08-23T21:21:41+00:00"
393
        },
394
        {
395
            "name": "minishlink/web-push",
396
            "version": "v10.0.1",
397
            "source": {
398
                "type": "git",
399
                "url": "https://github.com/web-push-libs/web-push-php.git",
400
                "reference": "08463189d3501cbd78a8625c87ab6680a7397aad"
401
            },
402
            "dist": {
403
                "type": "zip",
404
                "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/08463189d3501cbd78a8625c87ab6680a7397aad",
405
                "reference": "08463189d3501cbd78a8625c87ab6680a7397aad",
406
                "shasum": ""
407
            },
408
            "require": {
409
                "ext-curl": "*",
410
                "ext-json": "*",
411
                "ext-mbstring": "*",
412
                "ext-openssl": "*",
413
                "guzzlehttp/guzzle": "^7.9.2",
414
                "php": ">=8.2",
415
                "spomky-labs/base64url": "^2.0.4",
416
                "web-token/jwt-library": "^3.4.9|^4.0.6"
417
            },
418
            "require-dev": {
419
                "friendsofphp/php-cs-fixer": "^v3.91.3",
420
                "phpstan/phpstan": "^2.1.33",
421
                "phpstan/phpstan-strict-rules": "^2.0",
422
                "phpunit/phpunit": "^11.5.46|^12.5.2",
423
                "symfony/polyfill-iconv": "^1.33"
424
            },
425
            "suggest": {
426
                "ext-bcmath": "Optional for performance.",
427
                "ext-gmp": "Optional for performance."
428
            },
429
            "type": "library",
430
            "autoload": {
431
                "psr-4": {
432
                    "Minishlink\\WebPush\\": "src"
433
                }
434
            },
435
            "notification-url": "https://packagist.org/downloads/",
436
            "license": [
437
                "MIT"
438
            ],
439
            "authors": [
440
                {
441
                    "name": "Louis Lagrange",
442
                    "email": "lagrange.louis@gmail.com",
443
                    "homepage": "https://github.com/Minishlink"
444
                }
445
            ],
446
            "description": "Web Push library for PHP",
447
            "homepage": "https://github.com/web-push-libs/web-push-php",
448
            "keywords": [
449
                "Push API",
450
                "WebPush",
451
                "notifications",
452
                "push",
453
                "web"
454
            ],
455
            "support": {
456
                "issues": "https://github.com/web-push-libs/web-push-php/issues",
457
                "source": "https://github.com/web-push-libs/web-push-php/tree/v10.0.1"
458
            },
459
            "time": "2025-12-15T10:04:28+00:00"
460
        },
461
        {
462
            "name": "psr/clock",
463
            "version": "1.0.0",
464
            "source": {
465
                "type": "git",
466
                "url": "https://github.com/php-fig/clock.git",
467
                "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
468
            },
469
            "dist": {
470
                "type": "zip",
471
                "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
472
                "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
473
                "shasum": ""
474
            },
475
            "require": {
476
                "php": "^7.0 || ^8.0"
477
            },
478
            "type": "library",
479
            "autoload": {
480
                "psr-4": {
481
                    "Psr\\Clock\\": "src/"
482
                }
483
            },
484
            "notification-url": "https://packagist.org/downloads/",
485
            "license": [
486
                "MIT"
487
            ],
488
            "authors": [
489
                {
490
                    "name": "PHP-FIG",
491
                    "homepage": "https://www.php-fig.org/"
492
                }
493
            ],
494
            "description": "Common interface for reading the clock.",
495
            "homepage": "https://github.com/php-fig/clock",
496
            "keywords": [
497
                "clock",
498
                "now",
499
                "psr",
500
                "psr-20",
501
                "time"
502
            ],
503
            "support": {
504
                "issues": "https://github.com/php-fig/clock/issues",
505
                "source": "https://github.com/php-fig/clock/tree/1.0.0"
506
            },
507
            "time": "2022-11-25T14:36:26+00:00"
508
        },
509
        {
510
            "name": "psr/http-client",
511
            "version": "1.0.3",
512
            "source": {
513
                "type": "git",
514
                "url": "https://github.com/php-fig/http-client.git",
515
                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
516
            },
517
            "dist": {
518
                "type": "zip",
519
                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
520
                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
521
                "shasum": ""
522
            },
523
            "require": {
524
                "php": "^7.0 || ^8.0",
525
                "psr/http-message": "^1.0 || ^2.0"
526
            },
527
            "type": "library",
528
            "extra": {
529
                "branch-alias": {
530
                    "dev-master": "1.0.x-dev"
531
                }
532
            },
533
            "autoload": {
534
                "psr-4": {
535
                    "Psr\\Http\\Client\\": "src/"
536
                }
537
            },
538
            "notification-url": "https://packagist.org/downloads/",
539
            "license": [
540
                "MIT"
541
            ],
542
            "authors": [
543
                {
544
                    "name": "PHP-FIG",
545
                    "homepage": "https://www.php-fig.org/"
546
                }
547
            ],
548
            "description": "Common interface for HTTP clients",
549
            "homepage": "https://github.com/php-fig/http-client",
550
            "keywords": [
551
                "http",
552
                "http-client",
553
                "psr",
554
                "psr-18"
555
            ],
556
            "support": {
557
                "source": "https://github.com/php-fig/http-client"
558
            },
559
            "time": "2023-09-23T14:17:50+00:00"
560
        },
561
        {
562
            "name": "psr/http-factory",
563
            "version": "1.1.0",
564
            "source": {
565
                "type": "git",
566
                "url": "https://github.com/php-fig/http-factory.git",
567
                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
568
            },
569
            "dist": {
570
                "type": "zip",
571
                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
572
                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
573
                "shasum": ""
574
            },
575
            "require": {
576
                "php": ">=7.1",
577
                "psr/http-message": "^1.0 || ^2.0"
578
            },
579
            "type": "library",
580
            "extra": {
581
                "branch-alias": {
582
                    "dev-master": "1.0.x-dev"
583
                }
584
            },
585
            "autoload": {
586
                "psr-4": {
587
                    "Psr\\Http\\Message\\": "src/"
588
                }
589
            },
590
            "notification-url": "https://packagist.org/downloads/",
591
            "license": [
592
                "MIT"
593
            ],
594
            "authors": [
595
                {
596
                    "name": "PHP-FIG",
597
                    "homepage": "https://www.php-fig.org/"
598
                }
599
            ],
600
            "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
601
            "keywords": [
602
                "factory",
603
                "http",
604
                "message",
605
                "psr",
606
                "psr-17",
607
                "psr-7",
608
                "request",
609
                "response"
610
            ],
611
            "support": {
612
                "source": "https://github.com/php-fig/http-factory"
613
            },
614
            "time": "2024-04-15T12:06:14+00:00"
615
        },
616
        {
617
            "name": "psr/http-message",
618
            "version": "2.0",
619
            "source": {
620
                "type": "git",
621
                "url": "https://github.com/php-fig/http-message.git",
622
                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
623
            },
624
            "dist": {
625
                "type": "zip",
626
                "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
627
                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
628
                "shasum": ""
629
            },
630
            "require": {
631
                "php": "^7.2 || ^8.0"
632
            },
633
            "type": "library",
634
            "extra": {
635
                "branch-alias": {
636
                    "dev-master": "2.0.x-dev"
637
                }
638
            },
639
            "autoload": {
640
                "psr-4": {
641
                    "Psr\\Http\\Message\\": "src/"
642
                }
643
            },
644
            "notification-url": "https://packagist.org/downloads/",
645
            "license": [
646
                "MIT"
647
            ],
648
            "authors": [
649
                {
650
                    "name": "PHP-FIG",
651
                    "homepage": "https://www.php-fig.org/"
652
                }
653
            ],
654
            "description": "Common interface for HTTP messages",
655
            "homepage": "https://github.com/php-fig/http-message",
656
            "keywords": [
657
                "http",
658
                "http-message",
659
                "psr",
660
                "psr-7",
661
                "request",
662
                "response"
663
            ],
664
            "support": {
665
                "source": "https://github.com/php-fig/http-message/tree/2.0"
666
            },
667
            "time": "2023-04-04T09:54:51+00:00"
668
        },
669
        {
670
            "name": "ralouphie/getallheaders",
671
            "version": "3.0.3",
672
            "source": {
673
                "type": "git",
674
                "url": "https://github.com/ralouphie/getallheaders.git",
675
                "reference": "120b605dfeb996808c31b6477290a714d356e822"
676
            },
677
            "dist": {
678
                "type": "zip",
679
                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
680
                "reference": "120b605dfeb996808c31b6477290a714d356e822",
681
                "shasum": ""
682
            },
683
            "require": {
684
                "php": ">=5.6"
685
            },
686
            "require-dev": {
687
                "php-coveralls/php-coveralls": "^2.1",
688
                "phpunit/phpunit": "^5 || ^6.5"
689
            },
690
            "type": "library",
691
            "autoload": {
692
                "files": [
693
                    "src/getallheaders.php"
694
                ]
695
            },
696
            "notification-url": "https://packagist.org/downloads/",
697
            "license": [
698
                "MIT"
699
            ],
700
            "authors": [
701
                {
702
                    "name": "Ralph Khattar",
703
                    "email": "ralph.khattar@gmail.com"
704
                }
705
            ],
706
            "description": "A polyfill for getallheaders.",
707
            "support": {
708
                "issues": "https://github.com/ralouphie/getallheaders/issues",
709
                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
710
            },
711
            "time": "2019-03-08T08:55:37+00:00"
712
        },
713
        {
714
            "name": "spomky-labs/base64url",
715
            "version": "v2.0.4",
716
            "source": {
717
                "type": "git",
718
                "url": "https://github.com/Spomky-Labs/base64url.git",
719
                "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d"
720
            },
721
            "dist": {
722
                "type": "zip",
723
                "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d",
724
                "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d",
725
                "shasum": ""
726
            },
727
            "require": {
728
                "php": ">=7.1"
729
            },
730
            "require-dev": {
731
                "phpstan/extension-installer": "^1.0",
732
                "phpstan/phpstan": "^0.11|^0.12",
733
                "phpstan/phpstan-beberlei-assert": "^0.11|^0.12",
734
                "phpstan/phpstan-deprecation-rules": "^0.11|^0.12",
735
                "phpstan/phpstan-phpunit": "^0.11|^0.12",
736
                "phpstan/phpstan-strict-rules": "^0.11|^0.12"
737
            },
738
            "type": "library",
739
            "autoload": {
740
                "psr-4": {
741
                    "Base64Url\\": "src/"
742
                }
743
            },
744
            "notification-url": "https://packagist.org/downloads/",
745
            "license": [
746
                "MIT"
747
            ],
748
            "authors": [
749
                {
750
                    "name": "Florent Morselli",
751
                    "homepage": "https://github.com/Spomky-Labs/base64url/contributors"
752
                }
753
            ],
754
            "description": "Base 64 URL Safe Encoding/Decoding PHP Library",
755
            "homepage": "https://github.com/Spomky-Labs/base64url",
756
            "keywords": [
757
                "base64",
758
                "rfc4648",
759
                "safe",
760
                "url"
761
            ],
762
            "support": {
763
                "issues": "https://github.com/Spomky-Labs/base64url/issues",
764
                "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4"
765
            },
766
            "funding": [
767
                {
768
                    "url": "https://github.com/Spomky",
769
                    "type": "github"
770
                },
771
                {
772
                    "url": "https://www.patreon.com/FlorentMorselli",
773
                    "type": "patreon"
774
                }
775
            ],
776
            "time": "2020-11-03T09:10:25+00:00"
777
        },
778
        {
779
            "name": "spomky-labs/pki-framework",
780
            "version": "1.4.1",
781
            "source": {
782
                "type": "git",
783
                "url": "https://github.com/Spomky-Labs/pki-framework.git",
784
                "reference": "f0e9a548df4e3942886adc9b7830581a46334631"
785
            },
786
            "dist": {
787
                "type": "zip",
788
                "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/f0e9a548df4e3942886adc9b7830581a46334631",
789
                "reference": "f0e9a548df4e3942886adc9b7830581a46334631",
790
                "shasum": ""
791
            },
792
            "require": {
793
                "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14",
794
                "ext-mbstring": "*",
795
                "php": ">=8.1"
796
            },
797
            "require-dev": {
798
                "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0",
799
                "ext-gmp": "*",
800
                "ext-openssl": "*",
801
                "infection/infection": "^0.28|^0.29|^0.31",
802
                "php-parallel-lint/php-parallel-lint": "^1.3",
803
                "phpstan/extension-installer": "^1.3|^2.0",
804
                "phpstan/phpstan": "^1.8|^2.0",
805
                "phpstan/phpstan-deprecation-rules": "^1.0|^2.0",
806
                "phpstan/phpstan-phpunit": "^1.1|^2.0",
807
                "phpstan/phpstan-strict-rules": "^1.3|^2.0",
808
                "phpunit/phpunit": "^10.1|^11.0|^12.0",
809
                "rector/rector": "^1.0|^2.0",
810
                "roave/security-advisories": "dev-latest",
811
                "symfony/string": "^6.4|^7.0|^8.0",
812
                "symfony/var-dumper": "^6.4|^7.0|^8.0",
813
                "symplify/easy-coding-standard": "^12.0"
814
            },
815
            "suggest": {
816
                "ext-bcmath": "For better performance (or GMP)",
817
                "ext-gmp": "For better performance (or BCMath)",
818
                "ext-openssl": "For OpenSSL based cyphering"
819
            },
820
            "type": "library",
821
            "autoload": {
822
                "psr-4": {
823
                    "SpomkyLabs\\Pki\\": "src/"
824
                }
825
            },
826
            "notification-url": "https://packagist.org/downloads/",
827
            "license": [
828
                "MIT"
829
            ],
830
            "authors": [
831
                {
832
                    "name": "Joni Eskelinen",
833
                    "email": "jonieske@gmail.com",
834
                    "role": "Original developer"
835
                },
836
                {
837
                    "name": "Florent Morselli",
838
                    "email": "florent.morselli@spomky-labs.com",
839
                    "role": "Spomky-Labs PKI Framework developer"
840
                }
841
            ],
842
            "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.",
843
            "homepage": "https://github.com/spomky-labs/pki-framework",
844
            "keywords": [
845
                "DER",
846
                "Private Key",
847
                "ac",
848
                "algorithm identifier",
849
                "asn.1",
850
                "asn1",
851
                "attribute certificate",
852
                "certificate",
853
                "certification request",
854
                "cryptography",
855
                "csr",
856
                "decrypt",
857
                "ec",
858
                "encrypt",
859
                "pem",
860
                "pkcs",
861
                "public key",
862
                "rsa",
863
                "sign",
864
                "signature",
865
                "verify",
866
                "x.509",
867
                "x.690",
868
                "x509",
869
                "x690"
870
            ],
871
            "support": {
872
                "issues": "https://github.com/Spomky-Labs/pki-framework/issues",
873
                "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.1"
874
            },
875
            "funding": [
876
                {
877
                    "url": "https://github.com/Spomky",
878
                    "type": "github"
879
                },
880
                {
881
                    "url": "https://www.patreon.com/FlorentMorselli",
882
                    "type": "patreon"
883
                }
884
            ],
885
            "time": "2025-12-20T12:57:40+00:00"
886
        },
887
        {
888
            "name": "symfony/deprecation-contracts",
889
            "version": "v3.6.0",
890
            "source": {
891
                "type": "git",
892
                "url": "https://github.com/symfony/deprecation-contracts.git",
893
                "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
894
            },
895
            "dist": {
896
                "type": "zip",
897
                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
898
                "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
899
                "shasum": ""
900
            },
901
            "require": {
902
                "php": ">=8.1"
903
            },
904
            "type": "library",
905
            "extra": {
906
                "thanks": {
907
                    "url": "https://github.com/symfony/contracts",
908
                    "name": "symfony/contracts"
909
                },
910
                "branch-alias": {
911
                    "dev-main": "3.6-dev"
912
                }
913
            },
914
            "autoload": {
915
                "files": [
916
                    "function.php"
917
                ]
918
            },
919
            "notification-url": "https://packagist.org/downloads/",
920
            "license": [
921
                "MIT"
922
            ],
923
            "authors": [
924
                {
925
                    "name": "Nicolas Grekas",
926
                    "email": "p@tchwork.com"
927
                },
928
                {
929
                    "name": "Symfony Community",
930
                    "homepage": "https://symfony.com/contributors"
931
                }
932
            ],
933
            "description": "A generic function and convention to trigger deprecation notices",
934
            "homepage": "https://symfony.com",
935
            "support": {
936
                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
937
            },
938
            "funding": [
939
                {
940
                    "url": "https://symfony.com/sponsor",
941
                    "type": "custom"
942
                },
943
                {
944
                    "url": "https://github.com/fabpot",
945
                    "type": "github"
946
                },
947
                {
948
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
949
                    "type": "tidelift"
950
                }
951
            ],
952
            "time": "2024-09-25T14:21:43+00:00"
953
        },
954
        {
955
            "name": "web-token/jwt-library",
956
            "version": "4.1.3",
957
            "source": {
958
                "type": "git",
959
                "url": "https://github.com/web-token/jwt-library.git",
960
                "reference": "690d4dd47b78f423cb90457f858e4106e1deb728"
961
            },
962
            "dist": {
963
                "type": "zip",
964
                "url": "https://api.github.com/repos/web-token/jwt-library/zipball/690d4dd47b78f423cb90457f858e4106e1deb728",
965
                "reference": "690d4dd47b78f423cb90457f858e4106e1deb728",
966
                "shasum": ""
967
            },
968
            "require": {
969
                "brick/math": "^0.12|^0.13|^0.14",
970
                "php": ">=8.2",
971
                "psr/clock": "^1.0",
972
                "spomky-labs/pki-framework": "^1.2.1"
973
            },
974
            "conflict": {
975
                "spomky-labs/jose": "*"
976
            },
977
            "suggest": {
978
                "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance",
979
                "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance",
980
                "ext-openssl": "For key management (creation, optimization, etc.) and some algorithms (AES, RSA, ECDSA, etc.)",
981
                "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
982
                "paragonie/sodium_compat": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
983
                "spomky-labs/aes-key-wrap": "For all Key Wrapping algorithms (AxxxKW, AxxxGCMKW, PBES2-HSxxx+AyyyKW...)",
984
                "symfony/console": "Needed to use console commands",
985
                "symfony/http-client": "To enable JKU/X5U support."
986
            },
987
            "type": "library",
988
            "autoload": {
989
                "psr-4": {
990
                    "Jose\\Component\\": ""
991
                }
992
            },
993
            "notification-url": "https://packagist.org/downloads/",
994
            "license": [
995
                "MIT"
996
            ],
997
            "authors": [
998
                {
999
                    "name": "Florent Morselli",
1000
                    "homepage": "https://github.com/Spomky"
1001
                },
1002
                {
1003
                    "name": "All contributors",
1004
                    "homepage": "https://github.com/web-token/jwt-framework/contributors"
1005
                }
1006
            ],
1007
            "description": "JWT library",
1008
            "homepage": "https://github.com/web-token",
1009
            "keywords": [
1010
                "JOSE",
1011
                "JWE",
1012
                "JWK",
1013
                "JWKSet",
1014
                "JWS",
1015
                "Jot",
1016
                "RFC7515",
1017
                "RFC7516",
1018
                "RFC7517",
1019
                "RFC7518",
1020
                "RFC7519",
1021
                "RFC7520",
1022
                "bundle",
1023
                "jwa",
1024
                "jwt",
1025
                "symfony"
1026
            ],
1027
            "support": {
1028
                "issues": "https://github.com/web-token/jwt-library/issues",
1029
                "source": "https://github.com/web-token/jwt-library/tree/4.1.3"
1030
            },
1031
            "funding": [
1032
                {
1033
                    "url": "https://github.com/Spomky",
1034
                    "type": "github"
1035
                },
1036
                {
1037
                    "url": "https://www.patreon.com/FlorentMorselli",
1038
                    "type": "patreon"
1039
                }
1040
            ],
1041
            "time": "2025-12-18T14:27:35+00:00"
1042
        }
1043
    ],
1044
    "packages-dev": [],
1045
    "aliases": [],
1046
    "minimum-stability": "stable",
1047
    "stability-flags": [],
1048
    "prefer-stable": false,
1049
    "prefer-lowest": false,
1050
    "platform": [],
1051
    "platform-dev": [],
1052
    "plugin-api-version": "2.6.0"
1053
}
1054

14. Carpeta « vendor »

A. vendor / -- No se muestra el contenido de esta carpeta --

1

15. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

16. Resumen

  • En esta lección se muestró un ejemplo de notificaciones push.

14. PWA con vistas CRUD y Material Design 3 Expressive

Versión para imprimir.

A. Introduccion

  • En esta lección se presenta una PWA que despliega vistas CRUD y usa Material Design 3 Expressive.

  • Puedes probar el ejemplo en https://pwacrud.rf.gd/.

B. Referencias

Sitios de Material Design
Material.io

Material Design es una guía de diseño para aplicaciones multiplataforma. La encuentras en https://m3.material.io/.

Herramienta para selección de colores
Material Theme Builder

https://material-foundation.github.io/material-theme-builder/

Adaptación multiplataforma

La forma de adaptar Material Design en distintas plataformas está en https://material.io/design/platform-guidance/cross-platform-adaptation.html

C. Hazlo funcionar (con videos)

  1. Prueba e instala, de preferencia con Chrome, el sitio https://pwacrud.rf.gd/.

  2. Descarga el archivo /src/pwacrud.zip y descompáctalo.

  3. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  4. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  5. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  6. Importa el proyecto de GitHub a Visual Studio Code

  7. Edita los archivos que desees.

  8. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  9. Crea los íconos del proyecto con https://www.photopea.com/.

  10. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  11. Coloca el archivo favicon.ico en la raíz del proyecto.

  12. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  13. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  14. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  15. Prueba tu PWA.

  16. Publica tu PWA.

  17. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  18. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  19. Instala y usa tu PWA en iOS (iPhone y iPad).

D. Hazlo funcionar (texto)

  1. Prueba e instala, de preferencia con Chrome, el sitio https://pwacrud.rf.gd/.

  2. Descarga el archivo /src/pwacrud.zip y descompáctalo.

  3. Crea tu proyecto en GitHub pages:

    1. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

      • Mantén la selección Public.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

    5. Entra al repositorio y selecciona ⚙️ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto. Si haces cambios, clic en Save.

    6. En la misma pestaña de ⚙️ Settings selecciona ◯ Actions, luego selecciona General y en Actions permissions selecciona el rediobotón Allow xxxx, and select non-xxxx, actions and reusable workflows y abajo la casilla de verificación Allow actions created by GitHub y cliquea el botón Save.

  4. Importa el proyecto de GitHub a Visual Studio Code:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo, excepto el archivo .htaccess.

  5. Edita los archivos que desees.

  6. Crea los íconos del proyecto con https://www.photopea.com/.

  7. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  8. Coloca el archivo favicon.ico en la raíz del proyecto.

  9. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  10. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  11. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  12. Haz clic derecho en index.html, selecciona Open with Live Server y se abre el navegador para que puedas probar localmente el ejemplo.

  13. Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.

  14. Coloca las capturas en la carpeta img y asegúrate de que estén declaradas en el archivo site.webmanifest.

  15. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en vez de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

    5. En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error.

    6. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.

  16. Cuando usas GitHub pages, antes de subir los archivos, no debes modificar el valor de VERSION en el archivo sw.js.

  17. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push
  18. Si usas GitHub pages:

    1. Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.

    2. Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.

    3. A partir de este momento, espera al menos 11 minutos para modificar el valor de VERSION en el archivo sw.js y volver a subir el proyecto.

  19. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  20. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de agregar a la pantalla principal que aparece en el menú de extensión de Chrome.

  21. Instala y usa tu PWA en iOS (iPhone y iPad).

E. Archivos

F. index.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <title>PWA con Crud</title>
8
9
 <meta name="description" content="Ejemplo de PWA con Formulario">
10
11
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
12
 <script type="module" src="js/lib/manejaErrores.js"></script>
13
14
 <meta name="viewport" content="width=device-width">
15
 <meta name="theme-color" content="#fffbfe">
16
 <link rel="icon" sizes="32x32" href="favicon.ico">
17
 <link rel="manifest" href="site.webmanifest">
18
 <script src="ungap/custom-elements.js"></script>
19
20
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
21
 <script type="module" src="js/nav-tab-fixed.js"></script>
22
23
 <link rel="stylesheet" href="css/estilos.css">
24
 <link rel="stylesheet" href="css/transicion_pestanas.css">
25
 <link rel="stylesheet" href="css/md-tab.css">
26
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
27
 <link rel="stylesheet" href="css/md-list.css">
28
 <link rel="stylesheet" href="css/md-fab-primary.css">
29
30
</head>
31
32
<body>
33
34
 <md-app-bar class="centered" adicional="tab">
35
36
  <h1>PWA con Crud</h1>
37
38
 </md-app-bar>
39
40
 <nav-tab-fixed id="tab"></nav-tab-fixed>
41
42
 <section>
43
44
  <ul class="md-list">
45
46
   <li>
47
    <a class="md-one-line" href="modifica.html?id=1">
48
     <span class="headline">
49
      Fut
50
     </span>
51
    </a>
52
   </li>
53
54
   <li>
55
    <a class="md-one-line" href="modifica.html?id=2">
56
     <span class="headline">
57
      Basket
58
     </span>
59
    </a>
60
   </li>
61
62
  </ul>
63
64
  <a href="agrega.html" class="md-fab-primary" title="Agregar"
65
    style="position: fixed; bottom: 1rem; right: 1rem;">
66
   <span class="material-symbols-outlined">add</span>
67
  </a>
68
69
 </section>
70
71
72
</body>
73
74
</html>

G. agrega.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>Agregar - PWA con Crud</title>
9
10
 <meta name="description" content="Ejemplo de PWA con Formulario">
11
12
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
13
 <script type="module" src="js/lib/manejaErrores.js"></script>
14
15
 <meta name="viewport" content="width=device-width">
16
 <meta name="theme-color" content="#fffbfe">
17
 <link rel="icon" sizes="32x32" href="favicon.ico">
18
 <link rel="manifest" href="site.webmanifest">
19
 <script src="ungap/custom-elements.js"></script>
20
21
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
22
23
 <link rel="stylesheet" href="css/estilos.css">
24
 <link rel="stylesheet" href="css/transicion_pestanas.css">
25
 <link rel="stylesheet" href="css/md-filled-text-field.css">
26
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
27
 <link rel="stylesheet" href="css/md-standard-icon-button.css">
28
29
</head>
30
31
<body>
32
33
 <form id="formulario" novalidate>
34
35
  <md-app-bar adicional="tab">
36
37
   <a class="md-standard-icon-button" slot="navigation" title="Cancelar"
38
     href="index.html">
39
    <span class="material-symbols-outlined">cancel</span></a>
40
41
   <h1>Agregar</h1>
42
43
   <button type="submit" class="md-standard-icon-button" title="Guardar"
44
     slot="action">
45
    <span class="material-symbols-outlined">save</span>
46
   </button>
47
48
  </md-app-bar>
49
50
  <section>
51
52
   <p>
53
    <label class="md-filled-text-field">
54
     <input name="nombre" required placeholder="Nombre*">
55
     <span>Nombre *</span>
56
     <small id="supportingNombre">Obligatorio</small>
57
    </label>
58
   </p>
59
60
  </section>
61
62
 </form>
63
64
 <script type="module">
65
66
  import { muestraTextoDeAyuda } from "./js/lib/muestraTextoDeAyuda.js"
67
68
  formulario.nombre.addEventListener("input", copiaMensajes)
69
  formulario.addEventListener("submit", guardar)
70
71
  function copiaMensajes() {
72
   muestraTextoDeAyuda(formulario.nombre, supportingNombre, "Obligatorio")
73
  }
74
75
  /**
76
   * @param {SubmitEvent} evt
77
   */
78
  function guardar(evt) {
79
   evt.preventDefault()
80
   copiaMensajes()
81
   if (formulario.nombre.validity.valid) {
82
    location.href = "index.html"
83
   }
84
  }
85
86
 </script>
87
88
</body>
89
90
</html>

H. modifica.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>Modificar - PWA con Crud</title>
9
10
 <meta name="description" content="Ejemplo de PWA con Formulario">
11
12
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
13
 <script type="module" src="js/lib/manejaErrores.js"></script>
14
15
 <meta name="viewport" content="width=device-width">
16
 <meta name="theme-color" content="#fffbfe">
17
 <link rel="icon" sizes="32x32" href="favicon.ico">
18
 <link rel="manifest" href="site.webmanifest">
19
 <script src="ungap/custom-elements.js"></script>
20
21
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
22
23
 <link rel="stylesheet" href="css/estilos.css">
24
 <link rel="stylesheet" href="css/transicion_pestanas.css">
25
 <link rel="stylesheet" href="css/md-filled-text-field.css">
26
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
27
 <link rel="stylesheet" href="css/md-standard-icon-button.css">
28
29
</head>
30
31
<body>
32
33
 <form id="formulario" novalidate>
34
35
  <md-app-bar adicional="tab">
36
37
   <a class="md-standard-icon-button" slot="navigation" title="Cancelar"
38
     href="index.html">
39
    <span class="material-symbols-outlined">cancel</span></a>
40
41
   <h1>Modificar</h1>
42
43
   <button type="submit" class="md-standard-icon-button" title="Guardar"
44
     slot="action">
45
    <span class="material-symbols-outlined">save</span>
46
   </button>
47
48
   <button id="botonEliminar" type="button" class="md-standard-icon-button"
49
     title="Eliminar" slot="action">
50
    <span class="material-symbols-outlined">delete_forever</span>
51
   </button>
52
53
  </md-app-bar>
54
55
  <section>
56
57
   <p>
58
    <label class="md-filled-text-field">
59
     <input name="nombre" required placeholder="Nombre*">
60
     <span>Nombre *</span>
61
     <small id="supportingNombre">Obligatorio</small>
62
    </label>
63
   </p>
64
65
  </section>
66
67
 </form>
68
69
 <script type="module">
70
  import { muestraTextoDeAyuda } from "./js/lib/muestraTextoDeAyuda.js"
71
72
  formulario.nombre.addEventListener("input", copiaMensajes)
73
  formulario.addEventListener("submit", guardar)
74
  botonEliminar.addEventListener("click", eliminar)
75
76
  function copiaMensajes() {
77
   muestraTextoDeAyuda(formulario.nombre, supportingNombre, "Obligatorio")
78
  }
79
80
  /**
81
   * @param {SubmitEvent} evt
82
   */
83
  function guardar(evt) {
84
   evt.preventDefault()
85
   copiaMensajes()
86
   if (formulario.nombre.validity.valid) {
87
    location.href = "index.html"
88
   }
89
  }
90
91
  function eliminar() {
92
   location.href = "index.html"
93
  }
94
95
 </script>
96
97
</body>
98
99
</html>

I. ayuda.html

1
<!DOCTYPE html>
2
<html lang="es" class="light dark">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <title>Ayuda - PWA con Formulario</title>
8
9
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
10
11
 <meta name="viewport" content="width=device-width">
12
 <meta name="theme-color" content="#fffbfe">
13
 <link rel="icon" sizes="32x32" href="favicon.ico">
14
 <link rel="manifest" href="site.webmanifest">
15
 <script src="ungap/custom-elements.js"></script>
16
17
 <script type="module" src="js/lib/custom/md-app-bar.js"></script>
18
 <script type="module" src="js/nav-tab-fixed.js"></script>
19
20
 <link rel="stylesheet" href="css/estilos.css">
21
 <link rel="stylesheet" href="css/transicion_pestanas.css">
22
 <link rel="stylesheet" href="css/md-headline.css">
23
 <link rel="stylesheet" href="css/md-tab.css">
24
 <link rel="stylesheet" href="css/material-symbols-outlined.css">
25
 <link rel="stylesheet" href="css/md-list.css">
26
27
</head>
28
29
<body>
30
31
 <md-app-bar adicional="tab">
32
33
  <h1>Ayuda</h1>
34
35
 </md-app-bar>
36
37
 <nav-tab-fixed id="tab"></nav-tab-fixed>
38
39
 <section>
40
41
  <ul class="md-list">
42
   <li class="md-two-line">
43
    <span class="headline">
44
     Título
45
    </span>
46
    <span class="supporting">
47
     PWA con Material Design
48
    </span>
49
   </li>
50
   <li class="md-two-line">
51
    <span class="headline">
52
     Descripción
53
    </span>
54
    <span class="supporting">
55
     Ejemplos de vistas móviles.
56
    </span>
57
   </li>
58
   <li class="md-two-line">
59
    <span class="headline">
60
     Autor
61
    </span>
62
    <span class="supporting">
63
     Gilberto Pacheco Gallegos
64
    </span>
65
   </li>
66
   <li class="md-two-line">
67
    <span class="headline">
68
     Derechos de autor
69
    </span>
70
    <span class="supporting">
71
     © 2025 Gilberto Pacheco Gallegos
72
    </span>
73
   </li>
74
   <li class="md-three-line">
75
    <span class="headline">
76
     Este software usa la librería para PWA
77
    </span>
78
    <span class="supporting">
79
     Esta obra de Gilberto Pacheco Gallegos está bajo una
80
     <a target="_blank" rel="license noreferrer"
81
       href="http://creativecommons.org/licenses/by/4.0/">
82
      Licencia Creative Commons Atribución 4.0 Internacional</a></span>
83
   </li>
84
   <li>
85
    <a class="md-three-line" target="_blank" rel=”noreferrer”
86
      href="https://fonts.google.com/icons">
87
     <span class="headline">
88
      También usa Material Symbols
89
     </span>
90
     <span class="supporting">
91
      Desarrollada por Google bajo licencia Apache 2.0
92
     </span>
93
    </a>
94
   </li>
95
  </ul>
96
97
 </section>
98
99
 <nav-drw></nav-drw>
100
101
</body>
102
103
</html>

J. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

short_name

Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.

name

Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.

id

Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.

start_url

Ruta del archivo inicial de la app.

display

Forma de mostrar la app. El término standalone significa que no se muestra la barra de navegación del navegador web.

theme_color

Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.

background_color

Color de fondo de la pantalla desplah en dispositivos móviles.

description

Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

screenshots

Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo que muestra el navegador al instalar la app. Debes incluir al menos una con "form_factor": "wide" y otra con "form_factor": "narrow".

icons

Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.

src

Url de la imagen dentro de la app.

sizes

Dimensiones en pixeles de la imagen, anchoxalto.

type

Tipo mime de la imagen.

purpose

Forma en que se usa la imagen.

maskable

La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.

any

No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.

Normalmente debe proporcionarse un juego de íconos con purpose any y otro juego de íconos con purpose maskable.

form_factor

Orientación de una screenshot.

wide

La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.

narrow

La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.

Debes incluir al menos una screenshot con "form_factor": "wide" y otra con "form_factor": "narrow".

label

Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

1
{
2
 "short_name": "Crud PWA",
3
 "name": "PWA con Crud",
4
 "id": "/index.html",
5
 "start_url": "/index.html",
6
 "display": "standalone",
7
 "theme_color": "#fffbfe",
8
 "background_color": "#fffbfe",
9
 "display_override": [
10
  "window-controls-overlay"
11
 ],
12
 "description": "PWA con Crud.",
13
 "screenshots": [
14
  {
15
   "src": "img/screenshot_horizontal.png",
16
   "sizes": "661x360",
17
   "type": "image/png",
18
   "form_factor": "wide",
19
   "label": "PWA con Material Design"
20
  },
21
  {
22
   "src": "img/screenshot_vertical.png",
23
   "sizes": "555x757",
24
   "type": "image/png",
25
   "form_factor": "narrow",
26
   "label": "PWA con Material Design (2)"
27
  }
28
 ],
29
 "icons": [
30
  {
31
    "purpose": "maskable",
32
    "sizes": "3150x3150",
33
    "src": "img/maskable_icon.png",
34
    "type": "image/png"
35
  },
36
  {
37
    "purpose": "maskable",
38
    "sizes": "48x48",
39
    "src": "img/maskable_icon_x48.png",
40
    "type": "image/png"
41
  },
42
  {
43
    "purpose": "maskable",
44
    "sizes": "72x72",
45
    "src": "img/maskable_icon_x72.png",
46
    "type": "image/png"
47
  },
48
  {
49
    "purpose": "maskable",
50
    "sizes": "96x96",
51
    "src": "img/maskable_icon_x96.png",
52
    "type": "image/png"
53
  },
54
  {
55
    "purpose": "maskable",
56
    "sizes": "128x128",
57
    "src": "img/maskable_icon_x128.png",
58
    "type": "image/png"
59
  },
60
  {
61
    "purpose": "maskable",
62
    "sizes": "192x192",
63
    "src": "img/maskable_icon_x192.png",
64
    "type": "image/png"
65
  },
66
  {
67
    "purpose": "maskable",
68
    "sizes": "384x384",
69
    "src": "img/maskable_icon_x384.png",
70
    "type": "image/png"
71
  },
72
  {
73
    "purpose": "maskable",
74
    "sizes": "512x512",
75
    "src": "img/maskable_icon_x512.png",
76
    "type": "image/png"
77
  },
78
  {
79
    "purpose": "any",
80
    "sizes": "3150x3150",
81
    "src": "img/maskable_icon.png",
82
    "type": "image/png"
83
  },
84
  {
85
    "purpose": "any",
86
    "sizes": "48x48",
87
    "src": "img/maskable_icon_x48.png",
88
    "type": "image/png"
89
  },
90
  {
91
    "purpose": "any",
92
    "sizes": "72x72",
93
    "src": "img/maskable_icon_x72.png",
94
    "type": "image/png"
95
  },
96
  {
97
    "purpose": "any",
98
    "sizes": "96x96",
99
    "src": "img/maskable_icon_x96.png",
100
    "type": "image/png"
101
  },
102
  {
103
    "purpose": "any",
104
    "sizes": "128x128",
105
    "src": "img/maskable_icon_x128.png",
106
    "type": "image/png"
107
  },
108
  {
109
    "purpose": "any",
110
    "sizes": "192x192",
111
    "src": "img/maskable_icon_x192.png",
112
    "type": "image/png"
113
  },
114
  {
115
    "purpose": "any",
116
    "sizes": "384x384",
117
    "src": "img/maskable_icon_x384.png",
118
    "type": "image/png"
119
  },
120
  {
121
    "purpose": "any",
122
    "sizes": "512x512",
123
    "src": "img/maskable_icon_x512.png",
124
    "type": "image/png"
125
  },
126
  {
127
   "purpose": "any",
128
   "sizes": "2048x2048",
129
   "src": "img/icono2048.png",
130
   "type": "image/png"
131
  }
132
 ]
133
}

K. sw.js

1
/* Este archivo debe estar colocado en la carpeta raíz del sitio.
2
 * 
3
 * Cualquier cambio en el contenido de este archivo hace que el service
4
 * worker se reinstale. */
5
6
/**
7
 * Cambia el número de la versión cuando cambia el contenido de los
8
 * archivos.
9
 * 
10
 * El número a la izquierda del punto (.), en este caso <q>1</q>, se
11
 * conoce como número mayor y se cambia cuando se realizan
12
 * modificaciones grandes o importantes.
13
 * 
14
 * El número a la derecha del punto (.), en este caso <q>00</q>, se
15
 * conoce como número menor y se cambia cuando se realizan
16
 * modificaciones menores.
17
 */
18
const VERSION = "1.00"
19
20
/**
21
 * Nombre de la carpeta de caché.
22
 */
23
const CACHE = "pwamd"
24
25
/**
26
 * Archivos requeridos para que la aplicación funcione fuera de
27
 * línea.
28
 */
29
const ARCHIVOS = [
30
 "agrega.html",
31
 "ayuda.html",
32
 "favicon.ico",
33
 "index.html",
34
 "modifica.html",
35
 "site.webmanifest",
36
 "css/baseline.css",
37
 "css/colors.css",
38
 "css/elevation.css",
39
 "css/estilos.css",
40
 "css/material-symbols-outlined.css",
41
 "css/md-fab-primary.css",
42
 "css/md-filled-text-field.css",
43
 "css/md-headline.css",
44
 "css/md-list.css",
45
 "css/md-standard-icon-button.css",
46
 "css/md-tab.css",
47
 "css/motion.css",
48
 "css/palette.css",
49
 "css/roboto.css",
50
 "css/shape.css",
51
 "css/state.css",
52
 "css/transicion_pestanas.css",
53
 "css/typography.css",
54
 "css/theme/dark.css",
55
 "css/theme/light.css",
56
 "fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
57
 "fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
58
 "fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
59
 "fonts/roboto-v32-latin-regular.woff2",
60
 "img/icono2048.png",
61
 "img/maskable_icon.png",
62
 "img/maskable_icon_x128.png",
63
 "img/maskable_icon_x192.png",
64
 "img/maskable_icon_x384.png",
65
 "img/maskable_icon_x48.png",
66
 "img/maskable_icon_x512.png",
67
 "img/maskable_icon_x72.png",
68
 "img/maskable_icon_x96.png",
69
 "img/screenshot_horizontal.png",
70
 "img/screenshot_vertical.png",
71
 "js/nav-tab-fixed.js",
72
 "js/lib/ES_APPLE.js",
73
 "js/lib/getAttribute.js",
74
 "js/lib/manejaErrores.js",
75
 "js/lib/muestraError.js",
76
 "js/lib/muestraTextoDeAyuda.js",
77
 "js/lib/ProblemDetailsError.js",
78
 "js/lib/querySelector.js",
79
 "js/lib/registraServiceWorker.js",
80
 "js/lib/resaltaSiEstasEn.js",
81
 "js/lib/custom/md-app-bar.js",
82
 "ungap/custom-elements.js",
83
 "/"
84
]
85
86
// Verifica si el código corre dentro de un service worker.
87
if (self instanceof ServiceWorkerGlobalScope) {
88
 // Evento al empezar a instalar el servide worker,
89
 self.addEventListener("install",
90
  (/** @type {ExtendableEvent} */ evt) => {
91
   console.log("El service worker se está instalando.")
92
   evt.waitUntil(llenaElCache())
93
  })
94
95
 // Evento al solicitar información a la red.
96
 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
97
  if (evt.request.method === "GET") {
98
   evt.respondWith(buscaLaRespuestaEnElCache(evt))
99
  }
100
 })
101
102
 // Evento cuando el service worker se vuelve activo.
103
 self.addEventListener("activate",
104
  () => console.log("El service worker está activo."))
105
}
106
107
async function llenaElCache() {
108
 console.log("Intentando cargar caché:", CACHE)
109
 // Borra todos los cachés.
110
 const keys = await caches.keys()
111
 for (const key of keys) {
112
  await caches.delete(key)
113
 }
114
 // Abre el caché de este service worker.
115
 const cache = await caches.open(CACHE)
116
 // Carga el listado de ARCHIVOS.
117
 await cache.addAll(ARCHIVOS)
118
 console.log("Cache cargado:", CACHE)
119
 console.log("Versión:", VERSION)
120
}
121
122
/** @param {FetchEvent} evt */
123
async function buscaLaRespuestaEnElCache(evt) {
124
 // Abre el caché.
125
 const cache = await caches.open(CACHE)
126
 const request = evt.request
127
 /* Busca la respuesta a la solicitud en el contenido del caché, sin
128
  * tomar en cuenta la parte después del símbolo "?" en la URL. */
129
 const response = await cache.match(request, { ignoreSearch: true })
130
 if (response === undefined) {
131
  /* Si no la encuentra, empieza a descargar de la red y devuelve
132
   * la promesa. */
133
  return fetch(request)
134
 } else {
135
  // Si la encuentra, devuelve la respuesta encontrada en el caché.
136
  return response
137
 }
138
}

L. Carpeta « css »

A. css / baseline.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
@import url(palette.css);
18
@import url(typography.css);
19
@import url(colors.css);
20
@import url(shape.css);
21
@import url(motion.css);
22
@import url(state.css);
23
@import url(elevation.css);
24
@import url(theme/light.css) screen and (prefers-color-scheme: light);
25
@import url(theme/dark.css) screen and (prefers-color-scheme: dark);
26

B. css / colors.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
.primary {
18
  color: var(--md-sys-color-on-primary);
19
  background-color: var(--md-sys-color-primary);
20
}
21
.on-primary {
22
  color: var(--md-sys-color-primary);
23
  background-color: var(--md-sys-color-on-primary);
24
}
25
.primary-container {
26
  color: var(--md-sys-color-on-primary-container);
27
  background-color: var(--md-sys-color-primary-container);
28
}
29
.on-primary-container {
30
  color: var(--md-sys-color-primary-container);
31
  background-color: var(--md-sys-color-on-primary-container);
32
}
33
.secondary {
34
  color: var(--md-sys-color-on-secondary);
35
  background-color: var(--md-sys-color-secondary);
36
}
37
.on-secondary {
38
  color: var(--md-sys-color-secondary);
39
  background-color: var(--md-sys-color-on-secondary);
40
}
41
.secondary-container {
42
  color: var(--md-sys-color-on-secondary-container);
43
  background-color: var(--md-sys-color-secondary-container);
44
}
45
.on-secondary-container {
46
  color: var(--md-sys-color-secondary-container);
47
  background-color: var(--md-sys-color-on-secondary-container);
48
}
49
.tertiary {
50
  color: var(--md-sys-color-on-tertiary);
51
  background-color: var(--md-sys-color-tertiary);
52
}
53
.on-tertiary {
54
  color: var(--md-sys-color-tertiary);
55
  background-color: var(--md-sys-color-on-tertiary);
56
}
57
.tertiary-container {
58
  color: var(--md-sys-color-on-tertiary-container);
59
  background-color: var(--md-sys-color-tertiary-container);
60
}
61
.on-tertiary-container {
62
  color: var(--md-sys-color-tertiary-container);
63
  background-color: var(--md-sys-color-on-tertiary-container);
64
}
65
.background {
66
  color: var(--md-sys-color-on-background);
67
  background-color: var(--md-sys-color-background);
68
}
69
.surface {
70
  color: var(--md-sys-color-on-surface);
71
  background-color: var(--md-sys-color-surface);
72
}
73
.surface-variant {
74
  color: var(--md-sys-color-on-surface-variant);
75
  background-color: var(--md-sys-color-surface-variant);
76
}
77
.on-surface-variant {
78
  color: var(--md-sys-color-surface-variant);
79
  background-color: var(--md-sys-color-on-surface-variant);
80
}
81
.outline {
82
  border: 1px solid var(--md-sys-color-outline);
83
}
84
.inverse-surface {
85
  color: var(--md-sys-color-on-inverse-surface);
86
  background-color: var(--md-sys-color-inverse-surface);
87
}
88
.on-inverse-surface {
89
  color: var(--md-sys-color-inverse-surface);
90
  background-color: var(--md-sys-color-on-inverse-surface);
91
}
92
.inverse-primary {
93
  color: var(--md-sys-color-on-inverse-primary);
94
  background-color: var(--md-sys-color-inverse-primary);
95
}
96
.on-inverse-primary {
97
  color: var(--md-sys-color-inverse-primary);
98
  background-color: var(--md-sys-color-on-inverse-primary);
99
}
100
.surface-tint {
101
  background-color: var(--md-sys-color-on-surface-tint);
102
}
103
.error {
104
  color: var(--md-sys-color-on-error);
105
  background-color: var(--md-sys-color-error);
106
}
107
.on-error {
108
  color: var(--md-sys-color-error);
109
  background-color: var(--md-sys-color-on-error);
110
}
111
.error-container {
112
  color: var(--md-sys-color-on-error-container);
113
  background-color: var(--md-sys-color-error-container);
114
}
115
.on-error-container {
116
  color: var(--md-sys-color-error-container);
117
  background-color: var(--md-sys-color-on-error-container);
118
}
119
.black {
120
  background-color: var(--md-ref-palette-black);
121
}
122
.black-text {
123
  color: var(--md-ref-palette-black);
124
}
125
.white {
126
  background-color: var(--md-ref-palette-white);
127
}
128
.white-text {
129
  color: var(--md-ref-palette-white);
130
}
131

C. css / elevation.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Surface tint color */
19
  --md-sys-elevation-surface-tint-color: var(--md-sys-color-primary);
20
  /* +5 */
21
  --md-sys-elevation-level5-value: 12px;
22
  --md-sys-elevation-level5-unit: 1px;
23
  --md-sys-elevation-level5: 12px;
24
  /* +4 */
25
  --md-sys-elevation-level4-value: 8px;
26
  --md-sys-elevation-level4-unit: 1px;
27
  --md-sys-elevation-level4: 8px;
28
  /* +3 */
29
  --md-sys-elevation-level3-value: 6px;
30
  --md-sys-elevation-level3-unit: 1px;
31
  --md-sys-elevation-level3: 6px;
32
  /* +2 */
33
  --md-sys-elevation-level2-value: 3px;
34
  --md-sys-elevation-level2-unit: 1px;
35
  --md-sys-elevation-level2: 3px;
36
  /* +1 */
37
  --md-sys-elevation-level1-value: 1px;
38
  --md-sys-elevation-level1-unit: 1px;
39
  --md-sys-elevation-level1: 1px;
40
  /* 0 */
41
  --md-sys-elevation-level0-value: 0px;
42
  --md-sys-elevation-level0-unit: 1px;
43
  --md-sys-elevation-level0: 0px;
44
}
45
.elevation-0 {
46
  box-shadow: var(--md-sys-elevation-level0);
47
}
48
.elevation-1 {
49
  box-shadow: var(--md-sys-elevation-level1);
50
}
51
.elevation-2 {
52
  box-shadow: var(--md-sys-elevation-level2);
53
}
54
.elevation-3 {
55
  box-shadow: var(--md-sys-elevation-level3);
56
}
57
.elevation-4 {
58
  box-shadow: var(--md-sys-elevation-level4);
59
}
60
.elevation-5 {
61
  box-shadow: var(--md-sys-elevation-level5);
62
}
63

D. css / estilos.css

1
/* Definiciones para Material Design 3  */
2
@import url(baseline.css);
3
/* Fonts utilizados */
4
@import url(roboto.css);
5
6
html {
7
 /* Indica los temas del sistema operativo que son soportados. */
8
 color-scheme: light dark;
9
 --tabWidth: 3.75rem;
10
 --anchoNav: 22.5rem;
11
}
12
13
body>section,
14
form>section {
15
 max-width: 600px;
16
 margin-left: auto;
17
 margin-right: auto;
18
}
19
20
/* Quita un borde rojo que coloca Firefox. */
21
:-moz-ui-invalid {
22
 box-shadow: none;
23
}
24
25
body {
26
 margin: 0;
27
 font-family: var(--md-sys-typescale-body-large-font);
28
 font-weight: var(--md-sys-typescale-body-large-weight);
29
 font-size: var(--md-sys-typescale-body-large-size);
30
 font-style: var(--md-sys-typescale-body-large-font-style);
31
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
32
 line-height: var(--md-sys-typescale-body-large-line-height);
33
 text-transform: var(--md-sys-typescale-body-large-text-transform);
34
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
35
 color: var(--md-sys-color-on-background);
36
 background-color: var(--md-sys-color-background);
37
 /* Las siguientes líneas Evita los cambios de apariencia al cargar estilos y
38
 + custom elements, que son conocidos como Flash Of Unstyled Content (fouc). */
39
 opacity: 0;
40
 animation-name: fouc;
41
 animation-fill-mode: forwards;
42
 animation-duration: 1.5s;
43
}
44
45
@keyframes fouc {
46
 to {
47
  opacity: 1;
48
 }
49
}
50
51
html {
52
 --Font: -apple-system, BlinkMacSystemFont, roboto, sans-serif;
53
 --colIntIos: white;
54
 --colIntIosOnBk: #2acc2a;
55
 --colIntIosOnBkFc: #1bbb1b;
56
 --colIntIosOffBk: #dbdbdb;
57
 --colIntIosOffBkFc: #BDBDBD;
58
 /* Plain typeface */
59
 --md-ref-typeface-plain: var(--Font);
60
 /* Brand typeface */
61
 --md-ref-typeface-brand: var(--Font);
62
 --md-sys-typescale-label-large-weight-prominent:
63
  var(--md-ref-typeface-weight-bold);
64
 --md-box_shadow_level4:
65
  0 var(--md-sys-elevation-level4) var(--md-sys-elevation-level4) var(--md-sys-color-shadow);
66
 --md-box_shadow_level3:
67
  0 var(--md-sys-elevation-level3) var(--md-sys-elevation-level3) var(--md-sys-color-shadow);
68
 --md-box_shadow_level2:
69
  0 var(--md-sys-elevation-level2) var(--md-sys-elevation-level2) var(--md-sys-color-shadow);
70
 --md-box_shadow_level1:
71
  0 var(--md-sys-elevation-level1) var(--md-sys-elevation-level1) var(--md-sys-color-shadow);
72
 --md-box_shadow_level0: none;
73
 --iconSize: 1.5rem;
74
 --avatarSize: 2.5rem;
75
 --imageSize: 3.5rem;
76
 --videoWidth: 7.125rem;
77
 --videoHeight: 4rem;
78
 --scroll-headline-duracion: 2s;
79
 --md-sys-state-focus-indicator-outer-offset: 0.125rem;
80
 --md-sys-state-focus-indicator-thickness: 0.1875rem;
81
 /* Pressed state layer opacity */
82
 --state-pressed-transparency-percentage: 84%;
83
 /* Focus state layer opacity */
84
 --state-focus-transparency-percentage: 88%;
85
 /* Hover state layer opacity */
86
 --state-hover-transparency-percentage: 92%;
87
}
88
89
p {
90
 margin: 1rem;
91
}
92
93
a {
94
 color: var(--md-sys-color-on-background);
95
}
96
97
@media (prefers-color-scheme: light) {
98
 html {
99
  --md-riple-color: #00000020;
100
 }
101
}
102
103
@media (prefers-color-scheme: dark) {
104
 html {
105
  --md-riple-color: #ffffff40;
106
 }
107
}
108
109
@keyframes md-ripple {
110
111
 from {
112
  background-size: 100%;
113
 }
114
115
 to {
116
  background-size: 15000%;
117
 }
118
119
}
120
121
@keyframes salePorLaIzquierda {
122
 to {
123
  translate: -100vw 0;
124
 }
125
}
126
127
@keyframes entraPorLaDerecha {
128
 from {
129
  translate: 100vw 0;
130
 }
131
}

E. css / material-symbols-outlined.css

1
@font-face {
2
 font-family: 'Material Symbols Outlined';
3
 font-style: normal;
4
 src:
5
  url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].woff2) format('woff2'),
6
  url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].ttf) format('truetype');
7
}
8
9
.material-symbols-outlined {
10
 font-family: 'Material Symbols Outlined';
11
 font-weight: normal;
12
 font-style: normal;
13
 font-size: 1.5rem;
14
 width: 1.5rem;
15
 height: 1.5rem;
16
 /* Preferred icon size */
17
 display: inline-block;
18
 line-height: 1;
19
 text-transform: none;
20
 letter-spacing: normal;
21
 word-wrap: normal;
22
 white-space: nowrap;
23
 direction: ltr;
24
}

F. css / md-fab-primary.css

1
.md-fab-primary {
2
 position: relative;
3
 display: inline-flex;
4
 justify-content: center;
5
 align-items: center;
6
 border: none;
7
 width: 3.5rem;
8
 height: 3.5rem;
9
 border-radius: var(--md-sys-shape-corner-large-default-size);
10
 overflow: hidden;
11
 padding: 0;
12
 padding-block: 0;
13
 padding-inline: 0;
14
 text-decoration: none;
15
 background-color: var(--md-sys-color-primary-container);
16
 box-shadow: var(--md-box_shadow_level3);
17
}
18
19
.md-fab-primary[hidden] {
20
 display: none;
21
}
22
23
/* state layer */
24
.md-fab-primary::after {
25
 content: "";
26
 position: absolute;
27
 top: 0;
28
 right: 0;
29
 left: 0;
30
 bottom: 0;
31
}
32
33
.md-fab-primary span {
34
 position: relative;
35
 color: var(--md-sys-color-on-primary-container);
36
}
37
38
.md-fab-primary:hover {
39
 box-shadow: var(--md-box_shadow_level4);
40
}
41
42
.md-fab-primary:hover::after {
43
 background-color: var(--md-sys-color-on-primary-container);
44
 opacity: var(--md-sys-state-hover-state-layer-opacity);
45
}
46
47
.md-fab-primary:hover span {
48
 color: var(--md-sys-color-on-primary-container);
49
}
50
51
52
.md-fab-primary:focus {
53
 box-shadow: var(--md-box_shadow_level3);
54
 outline:none;
55
 /* outline:
56
  var(--md-sys-color-secondary) var(--md-sys-state-focus-indicator-thickness);
57
 outline-offset: var(--md-sys-state-focus-indicator-outer-offset); */
58
}
59
60
.md-fab-primary:focus::after {
61
 background-color: var(--md-sys-color-on-primary-container);
62
 opacity: var(--md-sys-state-focus-state-layer-opacity);
63
}
64
65
.md-fab-primary:focus span {
66
 color: var(--md-sys-color-on-primary-container);
67
}
68
69
.md-fab-primary:active {
70
 box-shadow: var(--md-box_shadow_level3);
71
 background-position: center;
72
 background-image:
73
  radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
74
 background-size: 100%;
75
 animation-name: md-ripple;
76
 animation-duration: var(--md-sys-motion-duration-500);
77
}
78
79
.md-fab-primary:active::after {
80
 background-color: var(--md-sys-color-on-primary-container);
81
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
82
}
83
84
.md-fab-primary:active span {
85
 color: var(--md-sys-color-on-primary-container);
86
}

G. css / md-filled-text-field.css

1
.md-filled-text-field {
2
 position: relative;
3
 overflow: hidden;
4
 display: flex;
5
 flex-direction: column;
6
 align-items: stretch;
7
 padding-top: calc(0.5rem + var(--md-sys-typescale-body-small-line-height));
8
 border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
9
 border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
10
 overflow: hidden;
11
}
12
13
/* container */
14
.md-filled-text-field::before {
15
 content: "";
16
 position: absolute;
17
 z-index: -2;
18
 top: 0;
19
 right: 0;
20
 left: 0;
21
 bottom: 0;
22
 background-color: var(--md-sys-color-surface-container-highest);
23
}
24
25
/* state layer */
26
.md-filled-text-field::after {
27
 content: "";
28
 position: absolute;
29
 z-index: -1;
30
 top: 0;
31
 right: 0;
32
 left: 0;
33
 bottom: 0;
34
 background-color: transparent;
35
}
36
37
.md-filled-text-field span,
38
.md-filled-text-field label {
39
 position: absolute;
40
 top: 0.5rem;
41
 left: 1rem;
42
 right: 1rem;
43
 display: block;
44
 transform: translateY(1rem);
45
 transition-property: transform;
46
 transition-duration: var(--md-sys-motion-duration-300);
47
 white-space: nowrap;
48
 text-overflow: ellipsis;
49
 overflow: hidden;
50
 color: var(--md-sys-color-on-surface-variant);
51
 font-family: var(--md-sys-typescale-body-large-font);
52
 font-weight: var(--md-sys-typescale-body-large-weight);
53
 font-size: var(--md-sys-typescale-body-large-size);
54
 font-style: var(--md-sys-typescale-body-large-font-style);
55
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
56
 line-height: var(--md-sys-typescale-body-large-line-height);
57
 text-transform: var(--md-sys-typescale-body-large-text-transform);
58
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
59
}
60
61
.md-filled-text-field :not(:placeholder-shown)+span,
62
.md-filled-text-field :not(:placeholder-shown)+label,
63
.md-filled-text-field .populated+span,
64
.md-filled-text-field .populated+label,
65
.md-filled-text-field:focus-within span,
66
.md-filled-text-field:focus-within label,
67
.md-filled-text-field.float span,
68
.md-filled-text-field.float label {
69
 transform: translateY(0);
70
 font-family: var(--md-sys-typescale-body-small-font);
71
 font-weight: var(--md-sys-typescale-body-small-weight);
72
 font-size: var(--md-sys-typescale-body-small-size);
73
 font-style: var(--md-sys-typescale-body-small-font-style);
74
 letter-spacing: var(--md-sys-typescale-body-small-tracking);
75
 line-height: var(--md-sys-typescale-body-small-line-height);
76
 text-transform: var(--md-sys-typescale-body-small-text-transform);
77
 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
78
}
79
80
.md-filled-text-field :not(label, span, small) {
81
 position: relative;
82
 caret-color: var(--md-sys-color-primary);
83
 min-height: 2rem;
84
 box-sizing: border-box;
85
 padding-left: 1rem;
86
 padding-bottom: 0.5rem;
87
 padding-right: 1rem;
88
 border: none;
89
 resize: none;
90
 color: var(--md-sys-color-on-surface);
91
 font-family: var(--md-sys-typescale-body-large-font);
92
 font-weight: var(--md-sys-typescale-body-large-weight);
93
 font-size: var(--md-sys-typescale-body-large-size);
94
 font-style: var(--md-sys-typescale-body-large-font-style);
95
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
96
 line-height: var(--md-sys-typescale-body-large-line-height);
97
 text-transform: var(--md-sys-typescale-body-large-text-transform);
98
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
99
 background-color: transparent;
100
 outline: none;
101
 border-bottom-width: 0.0625rem;
102
 border-bottom-style: solid;
103
 border-bottom-color: var(--md-sys-color-on-surface-variant);
104
}
105
106
.md-filled-text-field ::placeholder {
107
 color: transparent;
108
}
109
110
.md-filled-text-field small {
111
 display: block;
112
 color: var(--md-sys-color-on-surface-variant);
113
 background-color: var(--md-sys-color-background);
114
 font-family: var(--md-sys-typescale-body-small-font);
115
 font-weight: var(--md-sys-typescale-body-small-weight);
116
 font-size: var(--md-sys-typescale-body-small-size);
117
 font-style: var(--md-sys-typescale-body-small-font-style);
118
 letter-spacing: var(--md-sys-typescale-body-small-tracking);
119
 line-height: var(--md-sys-typescale-body-small-line-height);
120
 text-transform: var(--md-sys-typescale-body-small-text-transform);
121
 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
122
 padding: 0.25rem 1rem 0 1rem;
123
 white-space: nowrap;
124
 text-overflow: ellipsis;
125
 overflow: hidden;
126
}
127
128
.md-filled-text-field:hover span,
129
.md-filled-text-field:hover label {
130
 color: var(--md-sys-color-on-surface-variant);
131
}
132
133
.md-filled-text-field:hover :not(label, span, small) {
134
 padding-bottom: 0.5rem;
135
 border-bottom-width: 0.0625rem;
136
 border-bottom-color: var(--md-sys-color-on-surface);
137
}
138
139
.md-filled-text-field:hover::after {
140
 background-color: var(--md-sys-color-on-surface);
141
 opacity: var(--md-sys-state-hover-state-layer-opacity);
142
}
143
144
.md-filled-text-field:hover small {
145
 color: var(--md-sys-color-on-surface-variant);
146
}
147
148
.md-filled-text-field:focus-within span,
149
.md-filled-text-field:focus-within label {
150
 color: var(--md-sys-color-primary);
151
}
152
153
.md-filled-text-field :focus {
154
 color: var(--md-sys-color-on-surface);
155
 outline: none;
156
 padding-bottom: 0.4375rem;
157
 border-bottom-width: 0.125rem;
158
 border-bottom-color: var(--md-sys-color-primary);
159
}
160
161
.md-filled-text-field:hover :focus {
162
 padding-bottom: 0.4375rem;
163
 border-bottom-width: 0.125rem;
164
}
165
166
.md-filled-text-field:focus-within small {
167
 color: var(--md-sys-color-on-surface-variant);
168
}
169
170
.md-filled-text-field :invalid {
171
 color: var(--md-sys-color-on-surface);
172
 border-bottom-color: var(--md-sys-color-error);
173
}
174
175
.md-filled-text-field :invalid+span,
176
.md-filled-text-field :invalid+label {
177
 color: var(--md-sys-color-error);
178
}
179
180
.md-filled-text-field :invalid~small,
181
.md-filled-text-field:hover :invalid~small,
182
.md-filled-text-field:focus-within .input-text:invalid~small {
183
 color: var(--md-sys-color-error);
184
}
185
186
.md-filled-text-field :invalid:focus {
187
 caret-color: var(--md-sys-color-error);
188
 border-bottom-color: var(--md-sys-color-error);
189
}
190
191
.md-filled-text-field:hover :invalid {
192
 color: var(--md-sys-color-on-surface);
193
 border-bottom-color: var(--md-sys-color-error);
194
}

H. css / md-headline.css

1
.md-headline {
2
 box-sizing: border-box;
3
 margin: 0;
4
 color: var(--md-sys-color-on-surface);
5
 background-color: var(--md-sys-color-surface);
6
 transition-property: color;
7
 transition-duration: var(--md-sys-motion-duration-1000);
8
 transition-timing-function: ease-in;
9
}
10
11
.md-headline.scroll-adicional {
12
 color: var(--md-sys-color-surface-container-low);
13
 background-color: var(--md-sys-color-surface-container-low);
14
}
15
16
.md-headline.scroll {
17
 color: var(--md-sys-color-surface);
18
}
19
20
.md-headline.headline-small {
21
 padding: 0 1rem 1.5rem 1rem;
22
}
23
24
.md-headline.headline-medium {
25
 padding: 0 1rem 1.75rem 1rem;
26
}
27
28
md-app-bar.centered~.md-headline {
29
 text-align: center;
30
}
31
32
md-app-bar[headline] h1 {
33
 opacity: 0;
34
 transition-property: opacity;
35
 transition-duration: var(--md-sys-motion-duration-1000);
36
}
37
38
md-app-bar[headline].scroll h1 {
39
 opacity: 1;
40
 transition-timing-function: ease-in;
41
}

I. css / md-list.css

1
.md-list {
2
 margin: 0.5rem 0;
3
 padding: 0;
4
 list-style-type: none;
5
}
6
7
.md-list .md-one-line,
8
.md-list .md-two-line,
9
.md-list .md-three-line {
10
 position: relative;
11
 display: flex;
12
 box-sizing: border-box;
13
}
14
15
/* container */
16
.md-list .md-one-line::before,
17
.md-list .md-two-line::before,
18
.md-list .md-three-line::before {
19
 content: "";
20
 position: absolute;
21
 z-index: -2;
22
 top: 0;
23
 right: 0;
24
 left: 0;
25
 bottom: 0;
26
 background-color: var(--md-sys-color-surface);
27
}
28
29
/* state layer */
30
.md-list .md-one-line::after,
31
.md-list .md-two-line::after,
32
.md-list .md-three-line::after {
33
 content: "";
34
 position: absolute;
35
 z-index: -1;
36
 top: 0;
37
 right: 0;
38
 left: 0;
39
 bottom: 0;
40
 background-color: transparent;
41
}
42
43
.md-list .md-one-line {
44
 align-items: center;
45
 gap: 1rem;
46
 min-height: 3.5rem;
47
 padding: 0.5rem 1.5rem 0.5rem 1rem;
48
}
49
50
.md-list .md-one-line.video,
51
.md-list .md-two-line.video {
52
 padding: 0.75rem 1.5rem 0.75rem 0;
53
}
54
55
.md-list .md-two-line,
56
.md-list .md-three-line {
57
 flex-flow: column;
58
}
59
60
.md-list .md-two-line {
61
 justify-content: center;
62
 min-height: 4.5rem;
63
 padding: 0.5rem 1.5rem 0.5rem 1rem;
64
}
65
66
.md-list .md-two-line.icon,
67
.md-list .md-two-line.avatar,
68
.md-list .md-two-line.image,
69
.md-list .md-two-line.video,
70
.md-list .md-three-line.icon,
71
.md-list .md-three-line.avatar,
72
.md-list .md-three-line.image,
73
.md-list .md-three-line.video {
74
 display: grid;
75
 column-gap: 1rem;
76
 row-gap: 0;
77
 grid-template-areas:
78
  "img headline"
79
  "img supporting";
80
}
81
82
.md-list .md-two-line.icon,
83
.md-list .md-two-line.avatar,
84
.md-list .md-two-line.image,
85
.md-list .md-two-line.video {
86
 align-content: center;
87
 grid-template-rows: 1fr 1fr;
88
}
89
90
.md-list .md-two-line.icon,
91
.md-list .md-three-line.icon {
92
 grid-template-columns: var(--iconSize) 1fr;
93
}
94
95
.md-list .md-two-line.avatar,
96
.md-list .md-three-line.avatar {
97
 grid-template-columns: var(--avatarSize) 1fr;
98
}
99
100
.md-list .md-two-line.image,
101
.md-list .md-three-line.image {
102
 grid-template-columns: var(--imageSize) 1fr;
103
}
104
105
.md-list .md-two-line.video,
106
.md-list .md-three-line.video {
107
 grid-template-columns: var(--videoWidth) 1fr;
108
}
109
110
.md-list .md-three-line {
111
 align-content: flex-start;
112
 min-height: 5.5rem;
113
 padding: 0.75rem 1.5rem 0.75rem 1rem;
114
}
115
116
.md-list .md-three-line.video {
117
 padding: 0.75rem 1.5rem 0.75rem 0;
118
}
119
120
.md-list .md-three-line.icon,
121
.md-list .md-three-line.avatar,
122
.md-list .md-three-line.image,
123
.md-list .md-three-line.video {
124
 align-content: start;
125
 grid-template-rows: var(--md-sys-typescale-label-large-line-height) 1fr;
126
}
127
128
/* state layer */
129
.md-list .md-one-line:hover::after,
130
.md-list .md-two-line:hover::after,
131
.md-list .md-three-line:hover::after {
132
 background-color: var(--md-sys-color-on-surface);
133
 opacity: var(--md-sys-state-hover-state-layer-opacity);
134
}
135
136
/* state layer */
137
.md-list a.md-one-line:focus::after,
138
.md-list a.md-two-line:focus::after,
139
.md-list a.md-three-line:focus::after,
140
.md-list a.md-one-line:focus-visible::after,
141
.md-list a.md-two-line:focus-visible::after,
142
.md-list a.md-three-line:focus-visible::after {
143
 background-color: var(--md-sys-color-on-surface);
144
 opacity: var(--md-sys-state-focus-state-layer-opacity);
145
}
146
147
.md-list a:focus,
148
.md-list a:focus-visible {
149
 outline: none;
150
}
151
152
.md-list a:active {
153
 background-position: center;
154
 background-image:
155
  radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
156
 background-size: 100%;
157
 animation-name: md-ripple;
158
 animation-duration: var(--md-sys-motion-duration-500);
159
 box-shadow: var(--md-box_shadow_level0) !important;
160
}
161
162
/* state layer */
163
.md-list a.md-one-line:active::after,
164
.md-list a.md-two-line:active::after,
165
.md-list a.md-three-line:active::after {
166
 background-color: var(--md-sys-color-on-surface);
167
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
168
}
169
170
.md-list a.md-two-line,
171
.md-list a.md-three-line {
172
 text-decoration: none;
173
}
174
175
.md-list a.md-two-line .headline,
176
.md-list a.md-three-line .headline {
177
 text-decoration: underline;
178
}
179
180
.md-list .headline {
181
 grid-area: headline;
182
 display: block;
183
 box-sizing: border-box;
184
 color: var(--md-sys-color-on-surface);
185
 font-family: var(--md-sys-typescale-body-large-font);
186
 font-weight: var(--md-sys-typescale-body-large-weight);
187
 font-size: var(--md-sys-typescale-body-large-size);
188
 font-style: var(--md-sys-typescale-body-large-font-style);
189
 letter-spacing: var(--md-sys-typescale-body-large-tracking);
190
 line-height: var(--md-sys-typescale-body-large-line-height);
191
 text-transform: var(--md-sys-typescale-body-large-text-transform);
192
 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
193
 max-height: var(--md-sys-typescale-body-large-line-height);
194
 white-space: nowrap;
195
 text-overflow: ellipsis;
196
 overflow: hidden;
197
}
198
199
.md-list .md-two-line.icon .headline,
200
.md-list .md-two-line.avatar .headline,
201
.md-list .md-two-line.image .headline,
202
.md-list .md-two-line.video .headline,
203
.md-list .md-three-line.icon .headline,
204
.md-list .md-three-line.avatar .headline,
205
.md-list .md-three-line.image .headline,
206
.md-list .md-three-line.video .headline {
207
 align-self: end;
208
}
209
210
.md-list .supporting {
211
 grid-area: supporting;
212
 display: -webkit-box;
213
 -webkit-box-orient: vertical;
214
 overflow: hidden;
215
 box-sizing: border-box;
216
 align-self: start;
217
 font-family: var(--md-sys-typescale-body-medium-font);
218
 font-weight: var(--md-sys-typescale-body-medium-weight);
219
 font-size: var(--md-sys-typescale-body-medium-size);
220
 font-style: var(--md-sys-typescale-body-medium-font-style);
221
 letter-spacing: var(--md-sys-typescale-body-medium-tracking);
222
 line-height: var(--md-sys-typescale-body-medium-line-height);
223
 text-transform: var(--md-sys-typescale-body-medium-text-transform);
224
 text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
225
}
226
227
.md-list .md-two-line .supporting {
228
 max-height: var(--md-sys-typescale-body-medium-line-height);
229
 line-clamp: 1;
230
 -webkit-line-clamp: 1;
231
}
232
233
.md-list .md-three-line .supporting {
234
 max-height: calc(2 * var(--md-sys-typescale-body-medium-line-height));
235
 line-clamp: 2;
236
 -webkit-line-clamp: 2;
237
}
238
239
.md-list .avatar img,
240
.md-list .avatar label,
241
.md-list .avatar .material-symbols-outlined:first-child {
242
 flex-shrink: 0;
243
 background-color: var(--md-sys-color-primary-container);
244
 color: var(--md-sys-color-on-primary-container);
245
 border-radius: 50%;
246
 width: var(--avatarSize);
247
 height: var(--avatarSize);
248
}
249
250
.md-list .avatar label {
251
 display: inline-block;
252
 font-family: var(--md-sys-typescale-title-medium-font);
253
 font-weight: var(--md-sys-typescale-title-medium-weight);
254
 font-size: var(--md-sys-typescale-title-medium-size);
255
 font-style: var(--md-sys-typescale-title-medium-font-style);
256
 letter-spacing: var(--md-sys-typescale-title-medium-tracking);
257
 line-height: var(--md-sys-typescale-title-medium-line-height);
258
 text-transform: var(--md-sys-typescale-title-medium-text-transform);
259
 text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
260
 overflow: hidden;
261
}
262
263
.md-list .avatar .material-symbols-outlined:first-child {
264
 font-size: var(--avatarSize);
265
}
266
267
.md-list .avatar.md-two-line img,
268
.md-list .avatar.md-two-line label,
269
.md-list .avatar.md-two-line .material-symbols-outlined:first-child {
270
 grid-area: img;
271
 align-self: center;
272
}
273
274
.md-list .avatar.md-three-line img,
275
.md-list .avatar.md-three-line label,
276
.md-list .avatar.md-three-line .material-symbols-outlined:first-child {
277
 grid-area: img;
278
 align-self: start;
279
}
280
281
.md-list .icon img,
282
.md-list .icon .material-symbols-outlined:first-child {
283
 flex-shrink: 0;
284
 color: var(--md-sys-color-on-surface-variant);
285
 width: var(--iconSize);
286
 height: var(--iconSize);
287
}
288
289
.md-list .icon .material-symbols-outlined:first-child {
290
 font-size: var(--iconSize);
291
}
292
293
.md-list .icon.md-two-line img,
294
.md-list .icon.md-two-line .material-symbols-outlined:first-child {
295
 grid-area: img;
296
 align-self: center;
297
}
298
299
.md-list .icon.md-three-line img,
300
.md-list .icon.md-three-line .material-symbols-outlined:first-child {
301
 grid-area: img;
302
 align-self: start;
303
}
304
305
.md-list .video img {
306
 flex-shrink: 0;
307
 color: var(--md-sys-color-on-surface-variant);
308
 width: var(--videoWidth);
309
 height: var(--videoHeight);
310
}
311
312
.md-list .video.md-two-line img {
313
 grid-area: img;
314
 align-self: center;
315
}
316
317
.md-list .video.md-three-line img {
318
 grid-area: img;
319
 align-self: start;
320
}
321
322
.md-list .image img,
323
.md-list .image .material-symbols-outlined:first-child {
324
 flex-shrink: 0;
325
 color: var(--md-sys-color-on-surface-variant);
326
 width: var(--imageSize);
327
 height: var(--imageSize);
328
}
329
330
.md-list .image .material-symbols-outlined:first-child {
331
 font-size: var(--imageSize);
332
}
333
334
.md-list .image.md-two-line img,
335
.md-list .image.md-two-line .material-symbols-outlined:first-child {
336
 grid-area: img;
337
 align-self: center;
338
}
339
340
.md-list .image.md-three-line img,
341
.md-list .image.md-three-line .material-symbols-outlined:first-child {
342
 grid-area: img;
343
 align-self: start;
344
}

J. css / md-standard-icon-button.css

1
.md-standard-icon-button {
2
 position: relative;
3
 display: inline-block;
4
 border: none;
5
 padding: 0.25rem;
6
 background-color: transparent;
7
 text-decoration: none;
8
 border-radius: 50%;
9
 overflow: hidden;
10
}
11
12
.md-standard-icon-button[hidden] {
13
 display: none;
14
}
15
16
/* state layer */
17
.md-standard-icon-button::after {
18
 content: "";
19
 position: absolute;
20
 top: 0.25rem;
21
 right: 0.25rem;
22
 left: 0.25rem;
23
 bottom: 0.25rem;
24
 border-radius: 50%;
25
}
26
27
.md-standard-icon-button span {
28
 position: relative;
29
 padding: 0.5rem;
30
 color: var(--md-sys-color-on-surface-variant);
31
 font-size: 1.5rem;
32
 width: 1.5rem;
33
 height: 1.5rem;
34
}
35
36
.md-standard-icon-button.avatar span {
37
 padding: 0.3125rem;
38
 font-size: 1.875rem;
39
 width: 1.875rem;
40
 height: 1.875rem;
41
}
42
43
.md-standard-icon-button:hover::after {
44
 background-color: var(--md-sys-color-on-surface-variant);
45
 opacity: var(--md-sys-state-hover-state-layer-opacity);
46
}
47
48
.md-standard-icon-button:hover span {
49
 color: var(--md-sys-color-on-surface-variant);
50
}
51
52
.md-standard-icon-button:focus {
53
 outline: none;
54
}
55
56
.md-standard-icon-button:focus::after {
57
 background-color: var(--md-sys-color-on-surface-variant);
58
 opacity: var(--md-sys-state-focus-state-layer-opacity);
59
}
60
61
.md-standard-icon-button:focus span {
62
 color: var(--md-sys-color-on-surface-variant);
63
}
64
65
.md-standard-icon-button:active {
66
 background-position: center;
67
 background-image:
68
  radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
69
 background-size: 100%;
70
 animation-name: md-ripple;
71
 animation-duration: var(--md-sys-motion-duration-500);
72
}
73
74
.md-standard-icon-button:active::after {
75
 background-color: var(--md-sys-color-on-surface-variant);
76
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
77
}
78
79
.md-standard-icon-button:active span {
80
 -color: var(--md-sys-color-on-surface-variant);
81
}
82
83
.md-standard-icon-button:disabled::after {
84
 background-color: transparent !important;
85
 opacity: 1;
86
}
87
88
.md-standard-icon-button:disabled span {
89
 color: var(--md-sys-color-on-surface) !important;
90
 opacity: 0.38;
91
}
92
93
.md-standard-icon-button:disabled:active {
94
 background-image: none;
95
 animation-name: none;
96
 animation-duration: unset;
97
}

K. css / md-tab.css

1
.md-tab {
2
 display: flex;
3
 background-color: transparent;
4
 align-items: stretch;
5
 flex-wrap: nowrap;
6
 overflow-x: auto;
7
 position: sticky;
8
 z-index: 1;
9
}
10
11
.md-tab.fixed {
12
 justify-content: center;
13
}
14
15
.md-tab.scrollable {
16
 padding-left: 2rem;
17
 gap: 1rem;
18
}
19
20
.md-tab.scroll {
21
 background-color: var(--md-sys-color-surface-container-low);
22
}
23
24
.md-tab a {
25
 position: relative;
26
 display: flex;
27
 flex-direction: column;
28
 justify-content: start;
29
 align-items: center;
30
 color: var(--md-sys-color-on-surface-variant);
31
 font-family: var(--md-sys-typescale-title-small-font);
32
 font-weight: var(--md-sys-typescale-title-small-weight);
33
 font-size: var(--md-sys-typescale-title-small-size);
34
 font-style: var(--md-sys-typescale-title-small-font-style);
35
 letter-spacing: var(--md-sys-typescale-title-small-tracking);
36
 line-height: var(--md-sys-typescale-title-small-line-height);
37
 text-transform: var(--md-sys-typescale-title-small-text-transform);
38
 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
39
 text-align: center;
40
 box-sizing: border-box;
41
 border-bottom: 0.1875rem solid var(--md-sys-color-surface);
42
}
43
44
.md-tab.fixed a {
45
 flex: 0 0 var(--tabWidth);
46
}
47
48
.md-tab.scrollable a {
49
 flex: 0 0 auto;
50
}
51
52
.md-tab a.active {
53
 border-bottom-color: var(--md-sys-color-primary);
54
}
55
56
/* state layer */
57
.md-tab a::after {
58
 content: "";
59
 position: absolute;
60
 z-index: -1;
61
 top: 0;
62
 right: 0;
63
 left: 0;
64
 bottom: 0;
65
 background-color: transparent;
66
}
67
68
.md-tab span {
69
 font-size: var(--iconSize);
70
 height: var(--iconSize);
71
 width: var(--iconSize);
72
 color: var(--md-sys-color-on-surface-variant);
73
}
74
75
.md-tab .active span {
76
 color: var(--md-sys-color-primary);
77
}
78
79
.md-tab a:hover {
80
 color: var(--md-sys-color-on-surface);
81
}
82
83
/* state layer */
84
.md-tab a:hover::after {
85
 background-color: var(--md-sys-color-on-surface);
86
 opacity: var(--md-sys-state-hover-state-layer-opacity);
87
}
88
89
.md-tab a.active:hover {
90
 color: var(--md-sys-color-primary);
91
}
92
93
/* state layer */
94
.md-tab a.active:hover::after {
95
 background-color: var(--md-sys-color-primary);
96
 opacity: var(--md-sys-state-hover-state-layer-opacity);
97
}
98
99
.md-tab a:hover span {
100
 color: var(--md-sys-color-on-surface);
101
}
102
103
.md-tab a.active:hover span {
104
 color: var(--md-sys-color-primary);
105
}
106
107
.md-tab a:focus {
108
 outline: none;
109
}
110
111
/* state layer */
112
.md-tab a:focus::after {
113
 background-color: var(--md-sys-color-on-surface);
114
 opacity: var(--md-sys-state-focus-state-layer-opacity);
115
}
116
117
/* state layer */
118
.md-tab a.active:focus::after {
119
 background-color: var(--md-sys-color-primary);
120
 opacity: var(--md-sys-state-hover-state-layer-opacity);
121
}
122
123
.md-tab a:active {
124
 background-position: center;
125
 background-image:
126
   radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
127
 background-size: 100%;
128
 animation-name: md-ripple;
129
 animation-duration: var(--md-sys-motion-duration-500);
130
}
131
132
/* state layer */
133
.md-tab a:active::after {
134
 background-color: var(--md-sys-color-on-surface);
135
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
136
}
137
138
/* state layer */
139
.md-tab a.active:active::after {
140
 background-color: var(--md-sys-color-primary);
141
 opacity: var(--md-sys-state-pressed-state-layer-opacity);
142
}

L. css / motion.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Emphasized decelerate easing (out) */
19
  --md-sys-motion-easing-emphasized-decelerate-x0: 0.05000000074505806;
20
  --md-sys-motion-easing-emphasized-decelerate-y0: 0.699999988079071;
21
  --md-sys-motion-easing-emphasized-decelerate-x1: 0.10000000149011612;
22
  --md-sys-motion-easing-emphasized-decelerate-y1: 1;
23
  /* Emphasized accelerate easing (in) */
24
  --md-sys-motion-easing-emphasized-accelerate-x0: 0.30000001192092896;
25
  --md-sys-motion-easing-emphasized-accelerate-y0: 0;
26
  --md-sys-motion-easing-emphasized-accelerate-x1: 0.800000011920929;
27
  --md-sys-motion-easing-emphasized-accelerate-y1: 0.15000000596046448;
28
  /* Standard decelerate easing (out) */
29
  --md-sys-motion-easing-standard-decelerate-x0: 0;
30
  --md-sys-motion-easing-standard-decelerate-y0: 0;
31
  --md-sys-motion-easing-standard-decelerate-x1: 0;
32
  --md-sys-motion-easing-standard-decelerate-y1: 1;
33
  /* Standard accelerate easing (in) */
34
  --md-sys-motion-easing-standard-accelerate-x0: 0.30000001192092896;
35
  --md-sys-motion-easing-standard-accelerate-y0: 0;
36
  --md-sys-motion-easing-standard-accelerate-x1: 1;
37
  --md-sys-motion-easing-standard-accelerate-y1: 1;
38
  /* Duration 1000ms */
39
  --md-sys-motion-duration-1000: 1000ms;
40
  /* Duration 900ms */
41
  --md-sys-motion-duration-900: 900ms;
42
  /* Duration 800ms */
43
  --md-sys-motion-duration-800: 800ms;
44
  /* Duration 700ms */
45
  --md-sys-motion-duration-700: 700ms;
46
  /* Duration 600ms */
47
  --md-sys-motion-duration-600: 600ms;
48
  /* Duration 550ms */
49
  --md-sys-motion-duration-550: 550ms;
50
  /* Duration 500ms */
51
  --md-sys-motion-duration-500: 500ms;
52
  /* Duration 450ms */
53
  --md-sys-motion-duration-450: 450ms;
54
  /* Duration 400ms */
55
  --md-sys-motion-duration-400: 400ms;
56
  /* Duration 350ms */
57
  --md-sys-motion-duration-350: 350ms;
58
  /* Duration 300ms */
59
  --md-sys-motion-duration-300: 300ms;
60
  /* Duration 250ms */
61
  --md-sys-motion-duration-250: 250ms;
62
  /* Duration 200ms */
63
  --md-sys-motion-duration-200: 200ms;
64
  /* Duration 150ms */
65
  --md-sys-motion-duration-150: 150ms;
66
  /* Duration 100ms */
67
  --md-sys-motion-duration-100: 100ms;
68
  /* Duration 50ms */
69
  --md-sys-motion-duration-50: 50ms;
70
  /* Standard easing (in and out) */
71
  --md-sys-motion-easing-standard-x0: 0.20000000298023224;
72
  --md-sys-motion-easing-standard-y0: 0;
73
  --md-sys-motion-easing-standard-x1: 0;
74
  --md-sys-motion-easing-standard-y1: 1;
75
  /* Linear easing */
76
  --md-sys-motion-easing-linear-x0: 0;
77
  --md-sys-motion-easing-linear-y0: 0;
78
  --md-sys-motion-easing-linear-x1: 1;
79
  --md-sys-motion-easing-linear-y1: 1;
80
  /* Emphasized */
81
  --md-sys-motion-easing-emphasized-x0: 0.20000000298023224;
82
  --md-sys-motion-easing-emphasized-y0: 0;
83
  --md-sys-motion-easing-emphasized-x1: 0;
84
  --md-sys-motion-easing-emphasized-y1: 1;
85
  /* Motion path */
86
  --md-sys-motion-path-standard-path: 1;
87
}
88
.duration-50 {
89
  transition-duration: var(--md-sys-motion-duration-50);
90
}
91
.duration-100 {
92
  transition-duration: var(--md-sys-motion-duration-100);
93
}
94
.duration-150 {
95
  transition-duration: var(--md-sys-motion-duration-150);
96
}
97
.duration-200 {
98
  transition-duration: var(--md-sys-motion-duration-200);
99
}
100
.duration-250 {
101
  transition-duration: var(--md-sys-motion-duration-250);
102
}
103
.duration-300 {
104
  transition-duration: var(--md-sys-motion-duration-300);
105
}
106
.duration-350 {
107
  transition-duration: var(--md-sys-motion-duration-350);
108
}
109
.duration-400 {
110
  transition-duration: var(--md-sys-motion-duration-400);
111
}
112
.duration-450 {
113
  transition-duration: var(--md-sys-motion-duration-450);
114
}
115
.duration-500 {
116
  transition-duration: var(--md-sys-motion-duration-500);
117
}
118
.duration-550 {
119
  transition-duration: var(--md-sys-motion-duration-550);
120
}
121
.duration-600 {
122
  transition-duration: var(--md-sys-motion-duration-600);
123
}
124
.duration-700 {
125
  transition-duration: var(--md-sys-motion-duration-700);
126
}
127
.duration-800 {
128
  transition-duration: var(--md-sys-motion-duration-800);
129
}
130
.duration-900 {
131
  transition-duration: var(--md-sys-motion-duration-900);
132
}
133
.duration-1000 {
134
  transition-duration: var(--md-sys-motion-duration-1000);
135
}
136
.easing-standard {
137
  transition-timing-function: cubic-bezier(
138
    var(--md-sys-motion-easing-standard-x0),
139
    var(--md-sys-motion-easing-standard-y0),
140
    var(--md-sys-motion-easing-standard-x1),
141
    var(--md-sys-motion-easing-standard-y1)
142
  );
143
}
144
.easing-linear {
145
  transition-timing-function: cubic-bezier(
146
    var(--md-sys-motion-easing-linear-x0),
147
    var(--md-sys-motion-easing-linear-y0),
148
    var(--md-sys-motion-easing-linear-x1),
149
    var(--md-sys-motion-easing-linear-y1)
150
  );
151
}
152
.easing-standard-accelerate {
153
  transition-timing-function: cubic-bezier(
154
    var(--md-sys-motion-easing-standard-accelerate-x0),
155
    var(--md-sys-motion-easing-standard-accelerate-y0),
156
    var(--md-sys-motion-easing-standard-accelerate-x1),
157
    var(--md-sys-motion-easing-standard-accelerate-y1)
158
  );
159
}
160
.easing-standard-decelerate {
161
  transition-timing-function: cubic-bezier(
162
    var(--md-sys-motion-easing-standard-decelerate-x0),
163
    var(--md-sys-motion-easing-standard-decelerate-y0),
164
    var(--md-sys-motion-easing-standard-decelerate-x1),
165
    var(--md-sys-motion-easing-standard-decelerate-y1)
166
  );
167
}
168
.easing-emphasized {
169
  transition-timing-function: cubic-bezier(
170
    var(--md-sys-motion-easing-emphasized-x0),
171
    var(--md-sys-motion-easing-emphasized-y0),
172
    var(--md-sys-motion-easing-emphasized-x1),
173
    var(--md-sys-motion-easing-emphasized-y1)
174
  );
175
}
176

M. css / palette.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Error 0 */
19
  --md-ref-palette-error0: #000000ff;
20
  /* Error 10 */
21
  --md-ref-palette-error10: #410e0bff;
22
  /* Error 20 */
23
  --md-ref-palette-error20: #601410ff;
24
  /* Error 30 */
25
  --md-ref-palette-error30: #8c1d18ff;
26
  /* Error 40 */
27
  --md-ref-palette-error40: #b3261eff;
28
  /* Error 50 */
29
  --md-ref-palette-error50: #dc362eff;
30
  /* Error 60 */
31
  --md-ref-palette-error60: #e46962ff;
32
  /* Error 70 */
33
  --md-ref-palette-error70: #ec928eff;
34
  /* Error 80 */
35
  --md-ref-palette-error80: #f2b8b5ff;
36
  /* Error 90 */
37
  --md-ref-palette-error90: #f9dedcff;
38
  /* Error 95 */
39
  --md-ref-palette-error95: #fceeeeff;
40
  /* Error 99 */
41
  --md-ref-palette-error99: #fffbf9ff;
42
  /* Error 100 */
43
  --md-ref-palette-error100: #ffffffff;
44
  /* Tertiary 0 */
45
  --md-ref-palette-tertiary0: #000000ff;
46
  /* Tertiary 10 */
47
  --md-ref-palette-tertiary10: #31111dff;
48
  /* Tertiary 20 */
49
  --md-ref-palette-tertiary20: #492532ff;
50
  /* Tertiary 30 */
51
  --md-ref-palette-tertiary30: #633b48ff;
52
  /* Tertiary 40 */
53
  --md-ref-palette-tertiary40: #7d5260ff;
54
  /* Tertiary 50 */
55
  --md-ref-palette-tertiary50: #986977ff;
56
  /* Tertiary 60 */
57
  --md-ref-palette-tertiary60: #b58392ff;
58
  /* Tertiary 70 */
59
  --md-ref-palette-tertiary70: #d29dacff;
60
  /* Tertiary 80 */
61
  --md-ref-palette-tertiary80: #efb8c8ff;
62
  /* Tertiary 90 */
63
  --md-ref-palette-tertiary90: #ffd8e4ff;
64
  /* Tertiary 95 */
65
  --md-ref-palette-tertiary95: #ffecf1ff;
66
  /* Tertiary 99 */
67
  --md-ref-palette-tertiary99: #fffbfaff;
68
  /* Tertiary 100 */
69
  --md-ref-palette-tertiary100: #ffffffff;
70
  /* Secondary 0 */
71
  --md-ref-palette-secondary0: #000000ff;
72
  /* Secondary 10 */
73
  --md-ref-palette-secondary10: #1d192bff;
74
  /* Secondary 20 */
75
  --md-ref-palette-secondary20: #332d41ff;
76
  /* Secondary 30 */
77
  --md-ref-palette-secondary30: #4a4458ff;
78
  /* Secondary 40 */
79
  --md-ref-palette-secondary40: #625b71ff;
80
  /* Secondary 50 */
81
  --md-ref-palette-secondary50: #7a7289ff;
82
  /* Secondary 60 */
83
  --md-ref-palette-secondary60: #958da5ff;
84
  /* Secondary 70 */
85
  --md-ref-palette-secondary70: #b0a7c0ff;
86
  /* Secondary 80 */
87
  --md-ref-palette-secondary80: #ccc2dcff;
88
  /* Secondary 90 */
89
  --md-ref-palette-secondary90: #e8def8ff;
90
  /* Secondary 95 */
91
  --md-ref-palette-secondary95: #f6edffff;
92
  /* Secondary 99 */
93
  --md-ref-palette-secondary99: #fffbfeff;
94
  /* Secondary 100 */
95
  --md-ref-palette-secondary100: #ffffffff;
96
  /* Primary 0 */
97
  --md-ref-palette-primary0: #000000ff;
98
  /* Primary 10 */
99
  --md-ref-palette-primary10: #21005dff;
100
  /* Primary 20 */
101
  --md-ref-palette-primary20: #381e72ff;
102
  /* Primary 30 */
103
  --md-ref-palette-primary30: #4f378bff;
104
  /* Primary 40 */
105
  --md-ref-palette-primary40: #6750a4ff;
106
  /* Primary 50 */
107
  --md-ref-palette-primary50: #7f67beff;
108
  /* Primary 60 */
109
  --md-ref-palette-primary60: #9a82dbff;
110
  /* Primary 70 */
111
  --md-ref-palette-primary70: #b69df8ff;
112
  /* Primary 80 */
113
  --md-ref-palette-primary80: #d0bcffff;
114
  /* Primary 90 */
115
  --md-ref-palette-primary90: #eaddffff;
116
  /* Primary 95 */
117
  --md-ref-palette-primary95: #f6edffff;
118
  /* Primary 99 */
119
  --md-ref-palette-primary99: #fffbfeff;
120
  /* Primary 100 */
121
  --md-ref-palette-primary100: #ffffffff;
122
  /* Neutral Variant 0 */
123
  --md-ref-palette-neutral-variant0: #000000ff;
124
  /* Neutral Variant 10 */
125
  --md-ref-palette-neutral-variant10: #1d1a22ff;
126
  /* Neutral Variant 20 */
127
  --md-ref-palette-neutral-variant20: #322f37ff;
128
  /* Neutral Variant 30 */
129
  --md-ref-palette-neutral-variant30: #49454fff;
130
  /* Neutral Variant 40 */
131
  --md-ref-palette-neutral-variant40: #605d66ff;
132
  /* Neutral Variant 50 */
133
  --md-ref-palette-neutral-variant50: #79747eff;
134
  /* Neutral Variant 60 */
135
  --md-ref-palette-neutral-variant60: #938f99ff;
136
  /* Neutral Variant 70 */
137
  --md-ref-palette-neutral-variant70: #aea9b4ff;
138
  /* Neutral Variant 80 */
139
  --md-ref-palette-neutral-variant80: #cac4d0ff;
140
  /* Neutral Variant 90 */
141
  --md-ref-palette-neutral-variant90: #e7e0ecff;
142
  /* Neutral Variant 95 */
143
  --md-ref-palette-neutral-variant95: #f5eefaff;
144
  /* Neutral Variant 99 */
145
  --md-ref-palette-neutral-variant99: #fffbfeff;
146
  /* Neutral Variant 100 */
147
  --md-ref-palette-neutral-variant100: #ffffffff;
148
  /* Neutral 0 */
149
  --md-ref-palette-neutral0: #000000ff;
150
  /* Neutral 10 */
151
  --md-ref-palette-neutral10: #1c1b1fff;
152
  /* Neutral 20 */
153
  --md-ref-palette-neutral20: #313033ff;
154
  /* Neutral 30 */
155
  --md-ref-palette-neutral30: #484649ff;
156
  /* Neutral 40 */
157
  --md-ref-palette-neutral40: #605d62ff;
158
  /* Neutral 50 */
159
  --md-ref-palette-neutral50: #787579ff;
160
  /* Neutral 60 */
161
  --md-ref-palette-neutral60: #939094ff;
162
  /* Neutral 70 */
163
  --md-ref-palette-neutral70: #aeaaaeff;
164
  /* Neutral 80 */
165
  --md-ref-palette-neutral80: #c9c5caff;
166
  /* Neutral 90 */
167
  --md-ref-palette-neutral90: #e6e1e5ff;
168
  /* Neutral 95 */
169
  --md-ref-palette-neutral95: #f4eff4ff;
170
  /* Neutral 99 */
171
  --md-ref-palette-neutral99: #fffbfeff;
172
  /* Neutral 100 */
173
  --md-ref-palette-neutral100: #ffffffff;
174
  /* Black */
175
  --md-ref-palette-black: #000000ff;
176
  /* White */
177
  --md-ref-palette-white: #ffffffff;
178
}
179

N. css / roboto.css

1
/* roboto-regular - latin */
2
@font-face {
3
 /* Revisa
4
  * https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
5
  * para otras opciones. */
6
 font-display: swap;
7
 font-family: 'Roboto';
8
 font-style: normal;
9
 font-weight: 400;
10
 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
11
 src: url('../fonts/roboto-v32-latin-regular.woff2') format('woff2');
12
}

O. css / shape.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Fully rounded */
19
  --md-sys-shape-corner-full-family: 3px;
20
  /* Extra large top rounding */
21
  --md-sys-shape-corner-extra-large-top-family: 1px;
22
  --md-sys-shape-corner-extra-large-top-default-size: 0px;
23
  --md-sys-shape-corner-extra-large-top-top-left: 28px;
24
  --md-sys-shape-corner-extra-large-top-top-right-unit: 1px;
25
  --md-sys-shape-corner-extra-large-top-top-right: 28px;
26
  /* Extra large rounding */
27
  --md-sys-shape-corner-extra-large-family: 1px;
28
  --md-sys-shape-corner-extra-large-default-size-unit: 1px;
29
  --md-sys-shape-corner-extra-large-default-size: 28px;
30
  /* Large top rounding */
31
  --md-sys-shape-corner-large-top-family: 1px;
32
  --md-sys-shape-corner-large-top-default-size-unit: 1px;
33
  --md-sys-shape-corner-large-top-default-size: 0px;
34
  --md-sys-shape-corner-large-top-top-left-unit: 1px;
35
  --md-sys-shape-corner-large-top-top-left: 16px;
36
  --md-sys-shape-corner-large-top-top-right-unit: 1px;
37
  --md-sys-shape-corner-large-top-top-right: 16px;
38
  /* Large end rounding */
39
  --md-sys-shape-corner-large-end-family: 1px;
40
  --md-sys-shape-corner-large-end-default-size-unit: 1px;
41
  --md-sys-shape-corner-large-end-default-size: 0px;
42
  --md-sys-shape-corner-large-end-top-right-unit: 1px;
43
  --md-sys-shape-corner-large-end-top-right: 16px;
44
  --md-sys-shape-corner-large-end-bottom-right-unit: 1px;
45
  --md-sys-shape-corner-large-end-bottom-right: 16px;
46
  /* Large rounding */
47
  --md-sys-shape-corner-large-family: 1px;
48
  --md-sys-shape-corner-large-default-size-unit: 1px;
49
  --md-sys-shape-corner-large-default-size: 16px;
50
  /* Medium rounding */
51
  --md-sys-shape-corner-medium-family: 1px;
52
  --md-sys-shape-corner-medium-default-size-unit: 1px;
53
  --md-sys-shape-corner-medium-default-size: 12px;
54
  /* Small rounding */
55
  --md-sys-shape-corner-small-family: 1px;
56
  --md-sys-shape-corner-small-default-size-unit: 1px;
57
  --md-sys-shape-corner-small-default-size: 8px;
58
  /* Extra small top rounding */
59
  --md-sys-shape-corner-extra-small-top-family: 1px;
60
  --md-sys-shape-corner-extra-small-top-default-size-unit: 1px;
61
  --md-sys-shape-corner-extra-small-top-default-size: 0px;
62
  --md-sys-shape-corner-extra-small-top-top-left-unit: 1px;
63
  --md-sys-shape-corner-extra-small-top-top-left: 4px;
64
  --md-sys-shape-corner-extra-small-top-top-right-unit: 1px;
65
  --md-sys-shape-corner-extra-small-top-top-right: 4px;
66
  /* Extra small rounding */
67
  --md-sys-shape-corner-extra-small-family: 1px;
68
  --md-sys-shape-corner-extra-small-default-size-unit: 1px;
69
  --md-sys-shape-corner-extra-small-default-size: 4px;
70
  /* No rounding */
71
  --md-sys-shape-corner-none-family: 1px;
72
  --md-sys-shape-corner-none-default-size-unit: 1px;
73
  --md-sys-shape-corner-none-default-size: 0px;
74
75
  --md-sys-shape-small: var(--md-sys-shape-corner-small-default-size);
76
  --md-sys-shape-medium: var(--md-sys-shape-corner-medium-default-size);
77
  --md-sys-shape-large: var(--md-sys-shape-corner-large-default-size);
78
}
79
80
.shape-none {
81
  border-radius: var(--md-sys-shape-corner-none-default-size);
82
}
83
.shape-extra-small {
84
  border-radius: var(--md-sys-shape-corner-extra-small-default-size);
85
}
86
.shape-small {
87
  border-radius: var(--md-sys-shape-corner-small-default-size);
88
}
89
.shape-medium {
90
  border-radius: var(--md-sys-shape-corner-medium-default-size);
91
}
92
.shape-large {
93
  border-radius: var(--md-sys-shape-corner-large-default-size);
94
}
95
.shape-extra-large {
96
  border-radius: var(--md-sys-shape-corner-extra-large-default-size);
97
}
98
.extra-small-top {
99
  border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
100
  border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
101
}
102
.large-end {
103
  border-top-right-radius: var(--md-sys-shape-corner-large-end-top-right);
104
  border-bottom-right-radius: var(--md-sys-shape-corner-large-end-bottom-right);
105
}
106
.large-top {
107
  border-top-left-radius: var(--md-sys-shape-corner-large-top-top-left);
108
  border-top-right-radius: var(--md-sys-shape-corner-large-top-top-right);
109
}
110
.extra-large-top {
111
  border-top-left-radius: var(--md-sys-shape-corner-extra-large-top-top-left);
112
  border-top-right-radius: var(--md-sys-shape-corner-extra-large-top-top-right);
113
}
114

P. css / state.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
:root {
18
  /* Dragged state layer opacity */
19
  --md-sys-state-dragged-state-layer-opacity: 0.1599999964237213;
20
  /* Pressed state layer opacity */
21
  --md-sys-state-pressed-state-layer-opacity: 0.11999999731779099;
22
  /* Focus state layer opacity */
23
  --md-sys-state-focus-state-layer-opacity: 0.11999999731779099;
24
  /* Hover state layer opacity */
25
  --md-sys-state-hover-state-layer-opacity: 0.07999999821186066;
26
}
27
.hover-state-layer {
28
  opacity: var(--md-sys-state-hover-state-layer-opacity);
29
}
30
.pressed-state-layer {
31
  opacity: var(--md-sys-state-pressed-state-layer-opacity);
32
}
33
.dragged-state-layer {
34
  opacity: var(--md-sys-state-dragged-state-layer-opacity);
35
}
36
.focus-state-layer {
37
  opacity: var(--md-sys-state-focus-state-layer-opacity);
38
}
39

Q. css / transicion_pestanas.css

1
@view-transition {
2
 navigation: auto;
3
}
4
5
md-app-bar h1 {
6
 view-transition-name: encabezado;
7
}
8
9
main {
10
 view-transition-name: contenido;
11
}
12
13
::view-transition-group(encabezado) {
14
 animation-duration: var(--md-sys-motion-duration-1000);
15
}
16
17
::view-transition-group(contenido) {
18
 animation-duration: var(--md-sys-motion-duration-1000);
19
}
20
21
html::view-transition-old(encabezado) {
22
 animation-name: salePorLaIzquierda;
23
}
24
25
html::view-transition-new(encabezado) {
26
 animation-name: entraPorLaDerecha;
27
}
28
29
html::view-transition-old(contenido) {
30
 animation-name: salePorLaIzquierda;
31
}
32
33
html::view-transition-new(contenido) {
34
 animation-duration: var(--md-sys-motion-duration-700);
35
 animation-name: entraPorLaDerecha;
36
}

R. css / typography.css

1
/*
2
 Copyright 2016 Google Inc. All rights reserved.
3
4
 Licensed under the Apache License, Version 2.0 (the "License");
5
 you may not use this file except in compliance with the License.
6
 You may obtain a copy of the License at
7
8
     http://www.apache.org/licenses/LICENSE-2.0
9
10
 Unless required by applicable law or agreed to in writing, software
11
 distributed under the License is distributed on an "AS IS" BASIS,
12
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 See the License for the specific language governing permissions and
14
 limitations under the License.
15
*/
16
17
/* This file is generated */
18
19
/* DO NOT EDIT */
20
21
:root {
22
  /* Label Small */
23
  --md-sys-typescale-label-small-text-transform: unset;
24
  --md-sys-typescale-label-small-axis-value: unset;
25
  --md-sys-typescale-label-small-font-style: unset;
26
  --md-sys-typescale-label-small-text-decoration: unset;
27
  /* Label Small line height */
28
  --md-sys-typescale-label-small-line-height-value: 16px;
29
  --md-sys-typescale-label-small-line-height-unit: 2px;
30
  --md-sys-typescale-label-small-line-height: 16px;
31
  /* Label Small font tracking */
32
  --md-sys-typescale-label-small-tracking-value: 0.5px;
33
  --md-sys-typescale-label-small-tracking-unit: 2px;
34
  --md-sys-typescale-label-small-tracking: 0.5px;
35
  /* Label Small font size */
36
  --md-sys-typescale-label-small-size-value: 11px;
37
  --md-sys-typescale-label-small-size-unit: 2px;
38
  --md-sys-typescale-label-small-size: 11px;
39
  /* Label Small font weight */
40
  --md-sys-typescale-label-small-weight: var(--md-ref-typeface-weight-medium);
41
  /* Label Small font name */
42
  --md-sys-typescale-label-small-font: var(--md-ref-typeface-plain);
43
  /* Label Medium */
44
  --md-sys-typescale-label-medium-axis-value: unset;
45
  --md-sys-typescale-label-medium-font-style: unset;
46
  --md-sys-typescale-label-medium-text-decoration: unset;
47
  /* Label Medium text transform */
48
  --md-sys-typescale-label-medium-text-transform: 1;
49
  /* Label Medium line height */
50
  --md-sys-typescale-label-medium-line-height-value: 16px;
51
  --md-sys-typescale-label-medium-line-height-unit: 2px;
52
  --md-sys-typescale-label-medium-line-height: 16px;
53
  /* Label Medium font tracking */
54
  --md-sys-typescale-label-medium-tracking-value: 0.5px;
55
  --md-sys-typescale-label-medium-tracking-unit: 2px;
56
  --md-sys-typescale-label-medium-tracking: 0.5px;
57
  /* Label Medium font size */
58
  --md-sys-typescale-label-medium-size-value: 12px;
59
  --md-sys-typescale-label-medium-size-unit: 2px;
60
  --md-sys-typescale-label-medium-size: 12px;
61
  /* Label Medium font weight */
62
  --md-sys-typescale-label-medium-weight: var(--md-ref-typeface-weight-medium);
63
  /* Label Medium font name */
64
  --md-sys-typescale-label-medium-font: var(--md-ref-typeface-plain);
65
  /* Label Large */
66
  --md-sys-typescale-label-large-text-transform: unset;
67
  --md-sys-typescale-label-large-axis-value: unset;
68
  --md-sys-typescale-label-large-font-style: unset;
69
  --md-sys-typescale-label-large-text-decoration: unset;
70
  /* Label Large line height */
71
  --md-sys-typescale-label-large-line-height-value: 20px;
72
  --md-sys-typescale-label-large-line-height-unit: 2px;
73
  --md-sys-typescale-label-large-line-height: 20px;
74
  /* Label Large font tracking */
75
  --md-sys-typescale-label-large-tracking-value: 0.10000000149011612px;
76
  --md-sys-typescale-label-large-tracking-unit: 2px;
77
  --md-sys-typescale-label-large-tracking: 0.10000000149011612px;
78
  /* Label Large font size */
79
  --md-sys-typescale-label-large-size-value: 14px;
80
  --md-sys-typescale-label-large-size-unit: 2px;
81
  --md-sys-typescale-label-large-size: 14px;
82
  /* Label Large font weight */
83
  --md-sys-typescale-label-large-weight: var(--md-ref-typeface-weight-medium);
84
  /* Label Large font name */
85
  --md-sys-typescale-label-large-font: var(--md-ref-typeface-plain);
86
  /* Body Small */
87
  --md-sys-typescale-body-small-text-transform: unset;
88
  --md-sys-typescale-body-small-axis-value: unset;
89
  --md-sys-typescale-body-small-font-style: unset;
90
  --md-sys-typescale-body-small-text-decoration: unset;
91
  /* Body Small line height */
92
  --md-sys-typescale-body-small-line-height-value: 16px;
93
  --md-sys-typescale-body-small-line-height-unit: 2px;
94
  --md-sys-typescale-body-small-line-height: 16px;
95
  /* Body Small font tracking */
96
  --md-sys-typescale-body-small-tracking-value: 0.4000000059604645px;
97
  --md-sys-typescale-body-small-tracking-unit: 2px;
98
  --md-sys-typescale-body-small-tracking: 0.4000000059604645px;
99
  /* Body Small font size */
100
  --md-sys-typescale-body-small-size-value: 12px;
101
  --md-sys-typescale-body-small-size-unit: 2px;
102
  --md-sys-typescale-body-small-size: 12px;
103
  /* Body Small font weight */
104
  --md-sys-typescale-body-small-weight: var(--md-ref-typeface-weight-regular);
105
  /* Body Small font name */
106
  --md-sys-typescale-body-small-font: var(--md-ref-typeface-plain);
107
  /* Body Medium */
108
  --md-sys-typescale-body-medium-text-transform: unset;
109
  --md-sys-typescale-body-medium-axis-value: unset;
110
  --md-sys-typescale-body-medium-font-style: unset;
111
  --md-sys-typescale-body-medium-text-decoration: unset;
112
  /* Body Medium line height */
113
  --md-sys-typescale-body-medium-line-height-value: 20px;
114
  --md-sys-typescale-body-medium-line-height-unit: 2px;
115
  --md-sys-typescale-body-medium-line-height: 20px;
116
  /* Body Medium font tracking */
117
  --md-sys-typescale-body-medium-tracking-value: 0.25px;
118
  --md-sys-typescale-body-medium-tracking-unit: 2px;
119
  --md-sys-typescale-body-medium-tracking: 0.25px;
120
  /* Body Medium font size */
121
  --md-sys-typescale-body-medium-size-value: 14px;
122
  --md-sys-typescale-body-medium-size-unit: 2px;
123
  --md-sys-typescale-body-medium-size: 14px;
124
  /* Body Medium font weight */
125
  --md-sys-typescale-body-medium-weight: var(--md-ref-typeface-weight-regular);
126
  /* Body Medium font name */
127
  --md-sys-typescale-body-medium-font: var(--md-ref-typeface-plain);
128
  /* Body Large */
129
  --md-sys-typescale-body-large-text-transform: unset;
130
  --md-sys-typescale-body-large-axis-value: unset;
131
  --md-sys-typescale-body-large-font-style: unset;
132
  --md-sys-typescale-body-large-text-decoration: unset;
133
  /* Body Large line height */
134
  --md-sys-typescale-body-large-line-height-value: 24px;
135
  --md-sys-typescale-body-large-line-height-unit: 2px;
136
  --md-sys-typescale-body-large-line-height: 24px;
137
  /* Body Large font tracking */
138
  --md-sys-typescale-body-large-tracking-value: 0.5px;
139
  --md-sys-typescale-body-large-tracking-unit: 2px;
140
  --md-sys-typescale-body-large-tracking: 0.5px;
141
  /* Body Large font size */
142
  --md-sys-typescale-body-large-size-value: 16px;
143
  --md-sys-typescale-body-large-size-unit: 2px;
144
  --md-sys-typescale-body-large-size: 16px;
145
  /* Body Large font weight */
146
  --md-sys-typescale-body-large-weight: var(--md-ref-typeface-weight-regular);
147
  /* Body Large font name */
148
  --md-sys-typescale-body-large-font: var(--md-ref-typeface-plain);
149
  /* Title Small */
150
  --md-sys-typescale-title-small-text-transform: unset;
151
  --md-sys-typescale-title-small-axis-value: unset;
152
  --md-sys-typescale-title-small-font-style: unset;
153
  --md-sys-typescale-title-small-text-decoration: unset;
154
  /* Title Small line height */
155
  --md-sys-typescale-title-small-line-height-value: 20px;
156
  --md-sys-typescale-title-small-line-height-unit: 2px;
157
  --md-sys-typescale-title-small-line-height: 20px;
158
  /* Title Small font tracking */
159
  --md-sys-typescale-title-small-tracking-value: 0.10000000149011612px;
160
  --md-sys-typescale-title-small-tracking-unit: 2px;
161
  --md-sys-typescale-title-small-tracking: 0.10000000149011612px;
162
  /* Title Small font size */
163
  --md-sys-typescale-title-small-size-value: 14px;
164
  --md-sys-typescale-title-small-size-unit: 2px;
165
  --md-sys-typescale-title-small-size: 14px;
166
  /* Title Small font weight */
167
  --md-sys-typescale-title-small-weight: var(--md-ref-typeface-weight-medium);
168
  /* Title Small font name */
169
  --md-sys-typescale-title-small-font: var(--md-ref-typeface-plain);
170
  /* Title Medium */
171
  --md-sys-typescale-title-medium-text-transform: unset;
172
  --md-sys-typescale-title-medium-axis-value: unset;
173
  --md-sys-typescale-title-medium-font-style: unset;
174
  --md-sys-typescale-title-medium-text-decoration: unset;
175
  /* Title Medium line height */
176
  --md-sys-typescale-title-medium-line-height-value: 24px;
177
  --md-sys-typescale-title-medium-line-height-unit: 2px;
178
  --md-sys-typescale-title-medium-line-height: 24px;
179
  /* Title Medium font tracking */
180
  --md-sys-typescale-title-medium-tracking-value: 0.15000000596046448px;
181
  --md-sys-typescale-title-medium-tracking-unit: 2px;
182
  --md-sys-typescale-title-medium-tracking: 0.15000000596046448px;
183
  /* Title Medium font size */
184
  --md-sys-typescale-title-medium-size-value: 16px;
185
  --md-sys-typescale-title-medium-size-unit: 2px;
186
  --md-sys-typescale-title-medium-size: 16px;
187
  /* Title Medium font weight */
188
  --md-sys-typescale-title-medium-weight: var(--md-ref-typeface-weight-medium);
189
  /* Title Medium font name */
190
  --md-sys-typescale-title-medium-font: var(--md-ref-typeface-plain);
191
  /* Title Large */
192
  --md-sys-typescale-title-large-text-transform: unset;
193
  --md-sys-typescale-title-large-axis-value: unset;
194
  --md-sys-typescale-title-large-font-style: unset;
195
  --md-sys-typescale-title-large-text-decoration: unset;
196
  /* Title Large line height */
197
  --md-sys-typescale-title-large-line-height-value: 28px;
198
  --md-sys-typescale-title-large-line-height-unit: 2px;
199
  --md-sys-typescale-title-large-line-height: 28px;
200
  /* Title Large font tracking */
201
  --md-sys-typescale-title-large-tracking-value: 0px;
202
  --md-sys-typescale-title-large-tracking-unit: 2px;
203
  --md-sys-typescale-title-large-tracking: 0px;
204
  /* Title Large font size */
205
  --md-sys-typescale-title-large-size-value: 22px;
206
  --md-sys-typescale-title-large-size-unit: 2px;
207
  --md-sys-typescale-title-large-size: 22px;
208
  /* Title Large font weight */
209
  --md-sys-typescale-title-large-weight: var(--md-ref-typeface-weight-regular);
210
  /* Title Large font name */
211
  --md-sys-typescale-title-large-font: var(--md-ref-typeface-brand);
212
  /* Headline Small */
213
  --md-sys-typescale-headline-small-text-transform: unset;
214
  --md-sys-typescale-headline-small-axis-value: unset;
215
  --md-sys-typescale-headline-small-font-style: unset;
216
  --md-sys-typescale-headline-small-text-decoration: unset;
217
  /* Headline Small line height */
218
  --md-sys-typescale-headline-small-line-height-value: 32px;
219
  --md-sys-typescale-headline-small-line-height-unit: 2px;
220
  --md-sys-typescale-headline-small-line-height: 32px;
221
  /* Headline Small font tracking */
222
  --md-sys-typescale-headline-small-tracking-value: 0px;
223
  --md-sys-typescale-headline-small-tracking-unit: 2px;
224
  --md-sys-typescale-headline-small-tracking: 0px;
225
  /* Headline Small font size */
226
  --md-sys-typescale-headline-small-size-value: 24px;
227
  --md-sys-typescale-headline-small-size-unit: 2px;
228
  --md-sys-typescale-headline-small-size: 24px;
229
  /* Headline Small font weight */
230
  --md-sys-typescale-headline-small-weight: var(
231
    --md-ref-typeface-weight-regular
232
  );
233
  /* Headline Small font name */
234
  --md-sys-typescale-headline-small-font: var(--md-ref-typeface-brand);
235
  /* Headline Medium */
236
  --md-sys-typescale-headline-medium-text-transform: unset;
237
  --md-sys-typescale-headline-medium-axis-value: unset;
238
  --md-sys-typescale-headline-medium-font-style: unset;
239
  --md-sys-typescale-headline-medium-text-decoration: unset;
240
  /* Headline Medium line height */
241
  --md-sys-typescale-headline-medium-line-height-value: 36px;
242
  --md-sys-typescale-headline-medium-line-height-unit: 2px;
243
  --md-sys-typescale-headline-medium-line-height: 36px;
244
  /* Headline Medium font tracking */
245
  --md-sys-typescale-headline-medium-tracking-value: 0px;
246
  --md-sys-typescale-headline-medium-tracking-unit: 2px;
247
  --md-sys-typescale-headline-medium-tracking: 0px;
248
  /* Headline Medium font size */
249
  --md-sys-typescale-headline-medium-size-value: 28px;
250
  --md-sys-typescale-headline-medium-size-unit: 2px;
251
  --md-sys-typescale-headline-medium-size: 28px;
252
  /* Headline Medium font weight */
253
  --md-sys-typescale-headline-medium-weight: var(
254
    --md-ref-typeface-weight-regular
255
  );
256
  /* Headline Medium font name */
257
  --md-sys-typescale-headline-medium-font: var(--md-ref-typeface-brand);
258
  /* Headline Large */
259
  --md-sys-typescale-headline-large-text-transform: unset;
260
  --md-sys-typescale-headline-large-axis-value: unset;
261
  --md-sys-typescale-headline-large-font-style: unset;
262
  --md-sys-typescale-headline-large-text-decoration: unset;
263
  /* Headline Large line height */
264
  --md-sys-typescale-headline-large-line-height-value: 40px;
265
  --md-sys-typescale-headline-large-line-height-unit: 2px;
266
  --md-sys-typescale-headline-large-line-height: 40px;
267
  /* Headline Large font tracking */
268
  --md-sys-typescale-headline-large-tracking-value: 0px;
269
  --md-sys-typescale-headline-large-tracking-unit: 2px;
270
  --md-sys-typescale-headline-large-tracking: 0px;
271
  /* Headline Large font size */
272
  --md-sys-typescale-headline-large-size-value: 32px;
273
  --md-sys-typescale-headline-large-size-unit: 2px;
274
  --md-sys-typescale-headline-large-size: 32px;
275
  /* Headline Large font name */
276
  --md-sys-typescale-headline-large-font: var(--md-ref-typeface-brand);
277
  /* Headline Large font weight */
278
  --md-sys-typescale-headline-large-weight: var(
279
    --md-ref-typeface-weight-regular
280
  );
281
  /* Display Small */
282
  --md-sys-typescale-display-small-text-transform: unset;
283
  --md-sys-typescale-display-small-axis-value: unset;
284
  --md-sys-typescale-display-small-font-style: unset;
285
  --md-sys-typescale-display-small-text-decoration: unset;
286
  /* Display Small line height */
287
  --md-sys-typescale-display-small-line-height-value: 44px;
288
  --md-sys-typescale-display-small-line-height-unit: 2px;
289
  --md-sys-typescale-display-small-line-height: 44px;
290
  /* Display Small font tracking */
291
  --md-sys-typescale-display-small-tracking-value: 0px;
292
  --md-sys-typescale-display-small-tracking-unit: 2px;
293
  --md-sys-typescale-display-small-tracking: 0px;
294
  /* Display Small font size */
295
  --md-sys-typescale-display-small-size-value: 36px;
296
  --md-sys-typescale-display-small-size-unit: 2px;
297
  --md-sys-typescale-display-small-size: 36px;
298
  /* Display Small font weight */
299
  --md-sys-typescale-display-small-weight: var(
300
    --md-ref-typeface-weight-regular
301
  );
302
  /* Display Small font name */
303
  --md-sys-typescale-display-small-font: var(--md-ref-typeface-brand);
304
  /* Display Medium */
305
  --md-sys-typescale-display-medium-text-transform: unset;
306
  --md-sys-typescale-display-medium-axis-value: unset;
307
  --md-sys-typescale-display-medium-font-style: unset;
308
  --md-sys-typescale-display-medium-text-decoration: unset;
309
  /* Display Medium line height */
310
  --md-sys-typescale-display-medium-line-height-value: 52px;
311
  --md-sys-typescale-display-medium-line-height-unit: 2px;
312
  --md-sys-typescale-display-medium-line-height: 52px;
313
  /* Display Medium font tracking */
314
  --md-sys-typescale-display-medium-tracking-value: 0px;
315
  --md-sys-typescale-display-medium-tracking-unit: 2px;
316
  --md-sys-typescale-display-medium-tracking: 0px;
317
  /* Display Medium font size */
318
  --md-sys-typescale-display-medium-size-value: 45px;
319
  --md-sys-typescale-display-medium-size-unit: 2px;
320
  --md-sys-typescale-display-medium-size: 45px;
321
  /* Display Medium font weight */
322
  --md-sys-typescale-display-medium-weight: var(
323
    --md-ref-typeface-weight-regular
324
  );
325
  /* Display Medium font name */
326
  --md-sys-typescale-display-medium-font: var(--md-ref-typeface-brand);
327
  /* Display Large */
328
  --md-sys-typescale-display-large-text-transform: unset;
329
  --md-sys-typescale-display-large-axis-value: unset;
330
  --md-sys-typescale-display-large-font-style: unset;
331
  --md-sys-typescale-display-large-text-decoration: unset;
332
  /* Display Large line height */
333
  --md-sys-typescale-display-large-line-height-value: 64px;
334
  --md-sys-typescale-display-large-line-height-unit: 2px;
335
  --md-sys-typescale-display-large-line-height: 64px;
336
  /* Display Large font tracking */
337
  --md-sys-typescale-display-large-tracking-value: -0.25px;
338
  --md-sys-typescale-display-large-tracking-unit: 2px;
339
  --md-sys-typescale-display-large-tracking: -0.25px;
340
  /* Display Large font size */
341
  --md-sys-typescale-display-large-size-value: 57px;
342
  --md-sys-typescale-display-large-size-unit: 2px;
343
  --md-sys-typescale-display-large-size: 57px;
344
  /* Display Large font weight */
345
  --md-sys-typescale-display-large-weight: var(
346
    --md-ref-typeface-weight-regular
347
  );
348
  /* Display Large font name */
349
  --md-sys-typescale-display-large-font: var(--md-ref-typeface-brand);
350
  /* Plain typeface */
351
  --md-ref-typeface-plain: Roboto;
352
  /* Brand typeface */
353
  --md-ref-typeface-brand: Roboto;
354
  /* Bold weight */
355
  --md-ref-typeface-weight-bold: 700;
356
  /* Medium weight */
357
  --md-ref-typeface-weight-medium: 500;
358
  /* Regular weight */
359
  --md-ref-typeface-weight-regular: 400;
360
}
361
362
/* Label Small */
363
.label-small {
364
  font-family: var(--md-sys-typescale-label-small-font);
365
  font-weight: var(--md-sys-typescale-label-small-weight);
366
  font-size: var(--md-sys-typescale-label-small-size);
367
  font-style: var(--md-sys-typescale-label-small-font-style);
368
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
369
  line-height: var(--md-sys-typescale-label-small-line-height);
370
  text-transform: var(--md-sys-typescale-label-small-text-transform);
371
  text-decoration: var(--md-sys-typescale-label-small-text-decoration);
372
}
373
/* Label Medium */
374
.label-medium {
375
  font-family: var(--md-sys-typescale-label-medium-font);
376
  font-weight: var(--md-sys-typescale-label-medium-weight);
377
  font-size: var(--md-sys-typescale-label-medium-size);
378
  font-style: var(--md-sys-typescale-label-medium-font-style);
379
  letter-spacing: var(--md-sys-typescale-label-medium-tracking);
380
  line-height: var(--md-sys-typescale-label-medium-line-height);
381
  text-transform: var(--md-sys-typescale-label-medium-text-transform);
382
  text-decoration: var(--md-sys-typescale-label-medium-text-decoration);
383
}
384
/* Label Large */
385
.label-large {
386
  font-family: var(--md-sys-typescale-label-large-font);
387
  font-weight: var(--md-sys-typescale-label-large-weight);
388
  font-size: var(--md-sys-typescale-label-large-size);
389
  font-style: var(--md-sys-typescale-label-large-font-style);
390
  letter-spacing: var(--md-sys-typescale-label-large-tracking);
391
  line-height: var(--md-sys-typescale-label-large-line-height);
392
  text-transform: var(--md-sys-typescale-label-large-text-transform);
393
  text-decoration: var(--md-sys-typescale-label-large-text-decoration);
394
}
395
/* Body Small */
396
.body-small {
397
  font-family: var(--md-sys-typescale-body-small-font);
398
  font-weight: var(--md-sys-typescale-body-small-weight);
399
  font-size: var(--md-sys-typescale-body-small-size);
400
  font-style: var(--md-sys-typescale-body-small-font-style);
401
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
402
  line-height: var(--md-sys-typescale-body-small-line-height);
403
  text-transform: var(--md-sys-typescale-body-small-text-transform);
404
  text-decoration: var(--md-sys-typescale-body-small-text-decoration);
405
}
406
/* Body Medium */
407
.body-medium {
408
  font-family: var(--md-sys-typescale-body-medium-font);
409
  font-weight: var(--md-sys-typescale-body-medium-weight);
410
  font-size: var(--md-sys-typescale-body-medium-size);
411
  font-style: var(--md-sys-typescale-body-medium-font-style);
412
  letter-spacing: var(--md-sys-typescale-body-medium-tracking);
413
  line-height: var(--md-sys-typescale-body-medium-line-height);
414
  text-transform: var(--md-sys-typescale-body-medium-text-transform);
415
  text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
416
}
417
/* Body Large */
418
.body-large {
419
  font-family: var(--md-sys-typescale-body-large-font);
420
  font-weight: var(--md-sys-typescale-body-large-weight);
421
  font-size: var(--md-sys-typescale-body-large-size);
422
  font-style: var(--md-sys-typescale-body-large-font-style);
423
  letter-spacing: var(--md-sys-typescale-body-large-tracking);
424
  line-height: var(--md-sys-typescale-body-large-line-height);
425
  text-transform: var(--md-sys-typescale-body-large-text-transform);
426
  text-decoration: var(--md-sys-typescale-body-large-text-decoration);
427
}
428
/* Title Small */
429
.title-small {
430
  font-family: var(--md-sys-typescale-title-small-font);
431
  font-weight: var(--md-sys-typescale-title-small-weight);
432
  font-size: var(--md-sys-typescale-title-small-size);
433
  font-style: var(--md-sys-typescale-title-small-font-style);
434
  letter-spacing: var(--md-sys-typescale-title-small-tracking);
435
  line-height: var(--md-sys-typescale-title-small-line-height);
436
  text-transform: var(--md-sys-typescale-title-small-text-transform);
437
  text-decoration: var(--md-sys-typescale-title-small-text-decoration);
438
}
439
/* Title Medium */
440
.title-medium {
441
  font-family: var(--md-sys-typescale-title-medium-font);
442
  font-weight: var(--md-sys-typescale-title-medium-weight);
443
  font-size: var(--md-sys-typescale-title-medium-size);
444
  font-style: var(--md-sys-typescale-title-medium-font-style);
445
  letter-spacing: var(--md-sys-typescale-title-medium-tracking);
446
  line-height: var(--md-sys-typescale-title-medium-line-height);
447
  text-transform: var(--md-sys-typescale-title-medium-text-transform);
448
  text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
449
}
450
/* Title Large */
451
.title-large {
452
  font-family: var(--md-sys-typescale-title-large-font);
453
  font-weight: var(--md-sys-typescale-title-large-weight);
454
  font-size: var(--md-sys-typescale-title-large-size);
455
  font-style: var(--md-sys-typescale-title-large-font-style);
456
  letter-spacing: var(--md-sys-typescale-title-large-tracking);
457
  line-height: var(--md-sys-typescale-title-large-line-height);
458
  text-transform: var(--md-sys-typescale-title-large-text-transform);
459
  text-decoration: var(--md-sys-typescale-title-large-text-decoration);
460
}
461
/* Headline Small */
462
.headline-small {
463
  font-family: var(--md-sys-typescale-headline-small-font);
464
  font-weight: var(--md-sys-typescale-headline-small-weight);
465
  font-size: var(--md-sys-typescale-headline-small-size);
466
  font-style: var(--md-sys-typescale-headline-small-font-style);
467
  letter-spacing: var(--md-sys-typescale-headline-small-tracking);
468
  line-height: var(--md-sys-typescale-headline-small-line-height);
469
  text-transform: var(--md-sys-typescale-headline-small-text-transform);
470
  text-decoration: var(--md-sys-typescale-headline-small-text-decoration);
471
}
472
/* Headline Medium */
473
.headline-medium {
474
  font-family: var(--md-sys-typescale-headline-medium-font);
475
  font-weight: var(--md-sys-typescale-headline-medium-weight);
476
  font-size: var(--md-sys-typescale-headline-medium-size);
477
  font-style: var(--md-sys-typescale-headline-medium-font-style);
478
  letter-spacing: var(--md-sys-typescale-headline-medium-tracking);
479
  line-height: var(--md-sys-typescale-headline-medium-line-height);
480
  text-transform: var(--md-sys-typescale-headline-medium-text-transform);
481
  text-decoration: var(--md-sys-typescale-headline-medium-text-decoration);
482
}
483
/* Headline Large */
484
.headline-large {
485
  font-family: var(--md-sys-typescale-headline-large-font);
486
  font-weight: var(--md-sys-typescale-headline-large-weight);
487
  font-size: var(--md-sys-typescale-headline-large-size);
488
  font-style: var(--md-sys-typescale-headline-large-font-style);
489
  letter-spacing: var(--md-sys-typescale-headline-large-tracking);
490
  line-height: var(--md-sys-typescale-headline-large-line-height);
491
  text-transform: var(--md-sys-typescale-headline-large-text-transform);
492
  text-decoration: var(--md-sys-typescale-headline-large-text-decoration);
493
}
494
/* Display Small */
495
.display-small {
496
  font-family: var(--md-sys-typescale-display-small-font);
497
  font-weight: var(--md-sys-typescale-display-small-weight);
498
  font-size: var(--md-sys-typescale-display-small-size);
499
  font-style: var(--md-sys-typescale-display-small-font-style);
500
  letter-spacing: var(--md-sys-typescale-display-small-tracking);
501
  line-height: var(--md-sys-typescale-display-small-line-height);
502
  text-transform: var(--md-sys-typescale-display-small-text-transform);
503
  text-decoration: var(--md-sys-typescale-display-small-text-decoration);
504
}
505
/* Display Medium */
506
.display-medium {
507
  font-family: var(--md-sys-typescale-display-medium-font);
508
  font-weight: var(--md-sys-typescale-display-medium-weight);
509
  font-size: var(--md-sys-typescale-display-medium-size);
510
  font-style: var(--md-sys-typescale-display-medium-font-style);
511
  letter-spacing: var(--md-sys-typescale-display-medium-tracking);
512
  line-height: var(--md-sys-typescale-display-medium-line-height);
513
  text-transform: var(--md-sys-typescale-display-medium-text-transform);
514
  text-decoration: var(--md-sys-typescale-display-medium-text-decoration);
515
}
516
/* Display Large */
517
.display-large {
518
  font-family: var(--md-sys-typescale-display-large-font);
519
  font-weight: var(--md-sys-typescale-display-large-weight);
520
  font-size: var(--md-sys-typescale-display-large-size);
521
  font-style: var(--md-sys-typescale-display-large-font-style);
522
  letter-spacing: var(--md-sys-typescale-display-large-tracking);
523
  line-height: var(--md-sys-typescale-display-large-line-height);
524
  text-transform: var(--md-sys-typescale-display-large-text-transform);
525
  text-decoration: var(--md-sys-typescale-display-large-text-decoration);
526
}
527

S. Carpeta « css / theme »

1. css / theme / dark.css

1
.dark {
2
  --md-sys-color-primary: rgb(170 199 255);
3
  --md-sys-color-surface-tint: rgb(170 199 255);
4
  --md-sys-color-on-primary: rgb(10 48 95);
5
  --md-sys-color-primary-container: rgb(40 71 119);
6
  --md-sys-color-on-primary-container: rgb(214 227 255);
7
  --md-sys-color-secondary: rgb(190 198 220);
8
  --md-sys-color-on-secondary: rgb(40 49 65);
9
  --md-sys-color-secondary-container: rgb(62 71 89);
10
  --md-sys-color-on-secondary-container: rgb(218 226 249);
11
  --md-sys-color-tertiary: rgb(221 188 224);
12
  --md-sys-color-on-tertiary: rgb(63 40 68);
13
  --md-sys-color-tertiary-container: rgb(87 62 92);
14
  --md-sys-color-on-tertiary-container: rgb(250 216 253);
15
  --md-sys-color-error: rgb(255 180 171);
16
  --md-sys-color-on-error: rgb(105 0 5);
17
  --md-sys-color-error-container: rgb(147 0 10);
18
  --md-sys-color-on-error-container: rgb(255 218 214);
19
  --md-sys-color-background: rgb(17 19 24);
20
  --md-sys-color-on-background: rgb(226 226 233);
21
  --md-sys-color-surface: rgb(17 19 24);
22
  --md-sys-color-on-surface: rgb(226 226 233);
23
  --md-sys-color-surface-variant: rgb(68 71 78);
24
  --md-sys-color-on-surface-variant: rgb(196 198 208);
25
  --md-sys-color-outline: rgb(142 144 153);
26
  --md-sys-color-outline-variant: rgb(68 71 78);
27
  --md-sys-color-shadow: rgb(0 0 0);
28
  --md-sys-color-scrim: rgb(0 0 0);
29
  --md-sys-color-inverse-surface: rgb(226 226 233);
30
  --md-sys-color-inverse-on-surface: rgb(46 48 54);
31
  --md-sys-color-inverse-primary: rgb(65 95 145);
32
  --md-sys-color-primary-fixed: rgb(214 227 255);
33
  --md-sys-color-on-primary-fixed: rgb(0 27 62);
34
  --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35
  --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36
  --md-sys-color-secondary-fixed: rgb(218 226 249);
37
  --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38
  --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39
  --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40
  --md-sys-color-tertiary-fixed: rgb(250 216 253);
41
  --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42
  --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43
  --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44
  --md-sys-color-surface-dim: rgb(17 19 24);
45
  --md-sys-color-surface-bright: rgb(55 57 62);
46
  --md-sys-color-surface-container-lowest: rgb(12 14 19);
47
  --md-sys-color-surface-container-low: rgb(25 28 32);
48
  --md-sys-color-surface-container: rgb(29 32 36);
49
  --md-sys-color-surface-container-high: rgb(40 42 47);
50
  --md-sys-color-surface-container-highest: rgb(51 53 58);
51
}
52

2. css / theme / light.css

1
.light {
2
  --md-sys-color-primary: rgb(65 95 145);
3
  --md-sys-color-surface-tint: rgb(65 95 145);
4
  --md-sys-color-on-primary: rgb(255 255 255);
5
  --md-sys-color-primary-container: rgb(214 227 255);
6
  --md-sys-color-on-primary-container: rgb(40 71 119);
7
  --md-sys-color-secondary: rgb(86 95 113);
8
  --md-sys-color-on-secondary: rgb(255 255 255);
9
  --md-sys-color-secondary-container: rgb(218 226 249);
10
  --md-sys-color-on-secondary-container: rgb(62 71 89);
11
  --md-sys-color-tertiary: rgb(112 85 117);
12
  --md-sys-color-on-tertiary: rgb(255 255 255);
13
  --md-sys-color-tertiary-container: rgb(250 216 253);
14
  --md-sys-color-on-tertiary-container: rgb(87 62 92);
15
  --md-sys-color-error: rgb(186 26 26);
16
  --md-sys-color-on-error: rgb(255 255 255);
17
  --md-sys-color-error-container: rgb(255 218 214);
18
  --md-sys-color-on-error-container: rgb(147 0 10);
19
  --md-sys-color-background: rgb(249 249 255);
20
  --md-sys-color-on-background: rgb(25 28 32);
21
  --md-sys-color-surface: rgb(249 249 255);
22
  --md-sys-color-on-surface: rgb(25 28 32);
23
  --md-sys-color-surface-variant: rgb(224 226 236);
24
  --md-sys-color-on-surface-variant: rgb(68 71 78);
25
  --md-sys-color-outline: rgb(116 119 127);
26
  --md-sys-color-outline-variant: rgb(196 198 208);
27
  --md-sys-color-shadow: rgb(0 0 0);
28
  --md-sys-color-scrim: rgb(0 0 0);
29
  --md-sys-color-inverse-surface: rgb(46 48 54);
30
  --md-sys-color-inverse-on-surface: rgb(240 240 247);
31
  --md-sys-color-inverse-primary: rgb(170 199 255);
32
  --md-sys-color-primary-fixed: rgb(214 227 255);
33
  --md-sys-color-on-primary-fixed: rgb(0 27 62);
34
  --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35
  --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36
  --md-sys-color-secondary-fixed: rgb(218 226 249);
37
  --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38
  --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39
  --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40
  --md-sys-color-tertiary-fixed: rgb(250 216 253);
41
  --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42
  --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43
  --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44
  --md-sys-color-surface-dim: rgb(217 217 224);
45
  --md-sys-color-surface-bright: rgb(249 249 255);
46
  --md-sys-color-surface-container-lowest: rgb(255 255 255);
47
  --md-sys-color-surface-container-low: rgb(243 243 250);
48
  --md-sys-color-surface-container: rgb(237 237 244);
49
  --md-sys-color-surface-container-high: rgb(231 232 238);
50
  --md-sys-color-surface-container-highest: rgb(226 226 233);
51
}
52

M. Carpeta « fonts »

A. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints

B. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf

C. fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2

D. fonts / roboto-v32-latin-regular.woff2

N. Carpeta « img »

A. img / icono2048.png

icono2048.png

B. img / maskable_icon.png

maskable_icon.png

C. img / maskable_icon_x128.png

maskable_icon_x128.png

D. img / maskable_icon_x192.png

maskable_icon_x192.png

E. img / maskable_icon_x384.png

maskable_icon_x384.png

F. img / maskable_icon_x48.png

maskable_icon_x48.png

G. img / maskable_icon_x512.png

maskable_icon_x512.png

H. img / maskable_icon_x72.png

maskable_icon_x72.png

I. img / maskable_icon_x96.png

maskable_icon_x96.png

J. img / screenshot_horizontal.png

screenshot_horizontal.png

K. img / screenshot_vertical.png

screenshot_vertical.png

O. Carpeta « js »

A. js / nav-tab-fixed.js

1
import { resaltaSiEstasEn } from "./lib/resaltaSiEstasEn.js"
2
3
export class NavTabFixed extends HTMLElement {
4
5
 constructor() {
6
  super()
7
  this.creado = false
8
 }
9
10
 connectedCallback() {
11
  this.classList.add("md-tab", "fixed")
12
13
  if (!this.creado) {
14
15
   this.innerHTML = /* HTML */`
16
    <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html">
17
     <span class="material-symbols-outlined">home</span>
18
     Inicio
19
    </a>
20
 
21
    <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html">
22
     <span class="material-symbols-outlined">help</span>
23
     Ayuda
24
    </a>`
25
26
   this.creado = true
27
28
  }
29
30
 }
31
32
}
33
34
customElements.define("nav-tab-fixed", NavTabFixed)

B. Carpeta « js / lib »

1. js / lib / ES_APPLE.js

1
export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent)

2. js / lib / getAttribute.js

1
/**
2
 * @param {HTMLElement} elementoHtml
3
 * @param {string} nombre
4
 * @returns {string}
5
 */
6
export function getAttribute(elementoHtml, nombre) {
7
 const valor = elementoHtml.getAttribute(nombre)
8
 return valor === null ? "" : valor
9
}

3. js / lib / manejaErrores.js

1
import { muestraError } from "./muestraError.js"
2
3
/**
4
 * Intercepta Response.prototype.json para capturar errores de parseo
5
 * y asegurar que se reporten correctamente en navegadores Chromium.
6
 */
7
{
8
 const originalJson = Response.prototype.json
9
10
 Response.prototype.json = function () {
11
  // Llamamos al método original usando el contexto (this) de la respuesta
12
  return originalJson.apply(this, arguments)
13
   .catch((/** @type {any} */ error) => {
14
    // Corrige un error de Chrome que evita el manejo correcto de errores.
15
    throw new Error(error)
16
   })
17
 }
18
}
19
20
window.onerror = function (
21
  /** @type {string} */ _message,
22
  /** @type {string} */ _url,
23
  /** @type {number} */ _line,
24
  /** @type {number} */ _column,
25
  /** @type {Error} */ errorObject
26
) {
27
 muestraError(errorObject)
28
 return true
29
}
30
31
window.addEventListener('unhandledrejection', event => {
32
 muestraError(event.reason)
33
 event.preventDefault()
34
})
35

4. js / lib / muestraError.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Muestra los datos de una Error en la consola y en un cuadro de alerta.
5
 * @param { ProblemDetailsError | Error | null } error descripción del error.
6
 */
7
export function muestraError(error) {
8
9
 if (error === null) {
10
11
  console.error("Error")
12
  alert("Error")
13
14
 } else if (error instanceof ProblemDetailsError) {
15
16
  const problemDetails = error.problemDetails
17
18
  let mensaje =
19
   typeof problemDetails["title"] === "string" ? problemDetails["title"] : ""
20
  if (typeof problemDetails["detail"] === "string") {
21
   if (mensaje !== "") {
22
    mensaje += "\n\n"
23
   }
24
   mensaje += problemDetails["detail"]
25
  }
26
  if (mensaje === "") {
27
   mensaje = "Error"
28
  }
29
  console.error(error, problemDetails)
30
  alert(mensaje)
31
32
 } else {
33
34
  console.error(error)
35
  alert(error.message)
36
37
 }
38
39
}

5. js / lib / muestraTextoDeAyuda.js

1
/**
2
 * Si un elemento HTML tiene un mensaje de validación, lo
3
 * muestra en su elemento de ayuda; en caso contrario, muestra
4
 * un mensaje de ayuda. 
5
 * @param { {
6
 *   validity: { valid: boolean };
7
 *   validationMessage: string
8
 *  } } elementoHtml elemento que contiene datos de validación.
9
 * @param { HTMLElement } elementoDeAyuda elemento fonde
10
 * se muestran los elementos de validación para elementoHtml.
11
 * @param { string } mensajeDeAyuda mensaje de ayuda cuando el
12
 *  estado de elementoHtml es válido.
13
 */
14
export function muestraTextoDeAyuda(elementoHtml, elementoDeAyuda,
15
 mensajeDeAyuda) {
16
 if (elementoHtml.validity.valid) {
17
  elementoDeAyuda.textContent = mensajeDeAyuda
18
 } else {
19
  elementoDeAyuda.textContent = elementoHtml.validationMessage
20
 }
21
}

6. js / lib / ProblemDetailsError.js

1
export class ProblemDetailsError extends Error {
2
3
 /**
4
  * Detalle de los errores devueltos por un servicio.
5
  * Crea una instancia de ProblemDetailsError.
6
  * @param {object} problemDetails Objeto con la descripcipon del error.
7
  */
8
 constructor(problemDetails) {
9
10
  super(typeof problemDetails["detail"] === "string"
11
   ? problemDetails["detail"]
12
   : (typeof problemDetails["title"] === "string"
13
    ? problemDetails["title"]
14
    : "Error"))
15
16
  this.problemDetails = problemDetails
17
18
 }
19
20
}

7. js / lib / querySelector.js

1
/**
2
 * @template { HTMLElement } T
3
 * @param { Document | Element | ShadowRoot } raiz
4
 * @param { string } query
5
 * @returns { T }
6
 */
7
export function querySelector(raiz, query) {
8
 /** @type { T | null } */
9
 const resutado = raiz.querySelector(query)
10
 if (resutado === null)
11
  throw new Error(`No se encuentra ${query}.`)
12
 return resutado
13
}

8. js / lib / registraServiceWorker.js

1
const nombreDeServiceWorker = "sw.js"
2
3
try {
4
 navigator.serviceWorker.register(nombreDeServiceWorker)
5
  .then(registro => {
6
   console.log(nombreDeServiceWorker, "registrado.")
7
   console.log(registro)
8
  })
9
  .catch(error => console.log(error))
10
} catch (error) {
11
 console.log(error)
12
}

9. js / lib / resaltaSiEstasEn.js

1
/**
2
 * @param {string[]} paginas
3
 */
4
export function resaltaSiEstasEn(paginas) {
5
6
 const pathname = location.pathname
7
8
 for (const pagina of paginas) {
9
10
  if (pathname === pagina) {
11
   queueMicrotask(() => {
12
    const tab = document.querySelector(".active")
13
    if (tab !== null && tab.closest(".scrollable") !== null) {
14
     tab.scrollIntoView({ inline: "center", block: "end" })
15
    }
16
   })
17
   return `class="active"`
18
  }
19
20
 }
21
22
 return ""
23
24
}

10. Carpeta « js / lib / custom »

A. js / lib / custom / md-app-bar.js

1
import { ES_APPLE } from "../ES_APPLE.js"
2
import { getAttribute } from "../getAttribute.js"
3
import { querySelector } from "../querySelector.js"
4
5
class MdAppBar extends HTMLElement {
6
7
 getContent() {
8
  return /* HTML */`
9
   <style>
10
11
    :host {
12
     display: flex;
13
     box-sizing: border-box;
14
     align-items: center;
15
     padding: 0 0.25rem;
16
     background-color: var(--md-sys-color-surface);
17
     position: sticky;
18
     z-index: 1;
19
     left: env(titlebar-area-x, 0);
20
     top: env(titlebar-area-y, 0);
21
     height: env(titlebar-area-height, 4rem);
22
     width: env(titlebar-area-width, 100%);
23
    }
24
    
25
    :host(.apple) {
26
     height: env(titlebar-area-height, 3rem);
27
    }
28
29
    :host(.scroll) {
30
     background-color: var(--md-sys-color-surface-container-low);
31
    }
32
33
    #navigation {
34
     flex: 0 0 auto;
35
     overflow: hidden
36
    }
37
38
    #navigation ::slotted(*) {
39
     color: var(--md-sys-color-on-surface);
40
    }
41
42
    #acciones {
43
     margin-left: auto;
44
     flex: 0 0 auto;
45
     overflow: hidden
46
    }
47
48
    :host(.centered) #navigation,
49
    :host(.centered) #acciones {
50
     flex: 0 0 6rem;
51
     overflow: hidden
52
    }
53
54
    #headline::slotted(*) {
55
     -webkit-app-region: drag;
56
     flex: 1 1 auto;
57
     white-space: nowrap;
58
     text-overflow: ellipsis;
59
     overflow: hidden;
60
     font-family: var(--md-sys-typescale-title-large-font);
61
     font-weight: var(--md-sys-typescale-title-large-weight);
62
     font-size: var(--md-sys-typescale-title-large-size);
63
     font-style: var(--md-sys-typescale-title-large-font-style);
64
     letter-spacing: var(--md-sys-typescale-title-large-tracking);
65
     line-height: var(--md-sys-typescale-title-large-line-height);
66
     text-transform: var(--md-sys-typescale-title-large-text-transform);
67
     text-decoration: var(--md-sys-typescale-title-large-text-decoration);
68
     color: var(--md-sys-color-on-surface);
69
    }
70
71
    :host(.centered) #headline::slotted(*) {
72
     flex: 1 1 auto;
73
     text-align: center
74
    }
75
76
   </style>
77
78
   <span id="navigation">
79
    <slot name="navigation"></slot>
80
   </span>
81
   <slot id="headline"></slot>
82
   <span id="acciones">
83
    <slot name="action"></slot>
84
   </span>`
85
 }
86
87
 constructor() {
88
  super()
89
  if (ES_APPLE) {
90
   document.body.classList.add("apple")
91
   document.body.classList.remove("material")
92
  } else {
93
   document.body.classList.add("material")
94
   document.body.classList.remove("apple")
95
  }
96
97
  /**
98
   * @private
99
   * @readonly
100
   */
101
  const shadow = this.attachShadow({ mode: "open" })
102
  shadow.innerHTML = this.getContent()
103
  this._configuraAction = this._configuraAction.bind(this)
104
  /**
105
   * @private
106
   * @type {number}
107
   */
108
  this._posY = 0
109
  /**
110
   * @private
111
   * @type {boolean}
112
   */
113
  this._scrolling = false
114
  /**
115
    * @private
116
    * @type { HTMLSlotElement }
117
    */
118
  this._navigation = querySelector(shadow, '[name="navigation"]')
119
  /**
120
    * @private
121
    * @type { HTMLSlotElement }
122
    */
123
  this._action = querySelector(shadow, '[name="action"]')
124
  /**
125
    * @private
126
    * @type { HTMLHeadingElement | null }
127
    */
128
  this._headline = null
129
  /**
130
    * @private
131
    * @type { HTMLElement | null }
132
    */
133
  this._adicional = null
134
  this._action.addEventListener("slotchange", this._configuraAction)
135
  addEventListener("scroll", () => this._onScroll())
136
  addEventListener("load", () => this.configurOtros())
137
 }
138
139
 connectedCallback() {
140
  this.role = "toolbar"
141
  this._configuraAction()
142
 }
143
144
 configurOtros() {
145
  const idHeadline = getAttribute(this, "headline")
146
  if (idHeadline !== "") {
147
   const headline = document.getElementById(idHeadline)
148
   if (headline instanceof HTMLHeadingElement) {
149
    this._headline = headline
150
    if (this.classList.contains("apple") || this.classList.contains("medium")) {
151
     headline.classList.add("md-headline", "headline-small")
152
    } else {
153
     headline.classList.add("md-headline", "headline-medium")
154
    }
155
   }
156
  }
157
  const idAdicional = getAttribute(this, "adicional")
158
  if (idAdicional !== "") {
159
   this._adicional = document.getElementById(idAdicional)
160
   if (this._adicional !== null) {
161
    if (this.classList.contains("apple")) {
162
     this._adicional.style.top = "env(titlebar-area-height, 3rem)"
163
    } else {
164
     this._adicional.style.top = "env(titlebar-area-height, 4rem)"
165
    }
166
   }
167
  }
168
 }
169
170
 _configuraAction() {
171
  const assignedElements = this._action.assignedElements()
172
  if (this.isConnected) {
173
   if (ES_APPLE) {
174
    this.classList.add("apple")
175
    this.classList.remove("material")
176
   } else {
177
    this.classList.add("material")
178
    this.classList.remove("apple")
179
   }
180
   if (this.classList.contains("centered")) {
181
    this.classList.remove("centrado")
182
    this.classList.remove("justificado")
183
   } else {
184
    if (ES_APPLE && assignedElements.length <= 1) {
185
     this.classList.add("centrado")
186
     this.classList.remove("justificado")
187
    } else {
188
     this.classList.add("justificado")
189
     this.classList.remove("centrado")
190
    }
191
   }
192
  }
193
 }
194
195
 /** @private */
196
 _onScroll() {
197
  this._posY = scrollY
198
  if (!this._scrolling) {
199
   requestAnimationFrame(() => this._avanza())
200
  }
201
  this._scrolling = true
202
 }
203
204
 /** @private */
205
 _avanza() {
206
  if (this._posY === 0) {
207
   this.classList.remove("scroll")
208
   if (this._headline !== null) {
209
    if (this._adicional === null) {
210
     this._headline.classList.remove("scroll")
211
    } else {
212
     this._headline.classList.remove("scroll-adicional")
213
    }
214
   }
215
   if (this._adicional !== null) {
216
    this._adicional.classList.remove("scroll")
217
   }
218
  } else {
219
   this.classList.add("scroll")
220
   if (this._headline !== null) {
221
    if (this._adicional === null) {
222
     this._headline.classList.add("scroll")
223
    } else {
224
     this._headline.classList.add("scroll-adicional")
225
    }
226
   }
227
   if (this._adicional !== null) {
228
    this._adicional.classList.add("scroll")
229
   }
230
  }
231
  this._scrolling = false
232
 }
233
234
}
235
236
customElements.define("md-app-bar", MdAppBar)

P. favicon.ico

favicon.ico

Q. .htaccess

  • Este archivo configura las respuestas del servidor..

  • Lo utilizan principalmente servidores como Apache o Nginx.

  • Configura el mime type para el archivo de manifiesto y sedhabilita el uso de la cache general de http..

1
AddType application/manifest+json .webmanifest
2
3
ExpiresActive On
4
5
Header set Cache-Control "max-age=1, must-revalidate"
6

R. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

S. instruccionesListadoSw.txt

1
Generar el listado de archivos del sw.js desde Visual Studio Code.
2
1. Abrir una terminal desde el menú 
3
    Terminal > New Terminal
4
5
2. Desde la terminal introducir la orden:
6
    Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
8
3. Abrir el archivo generado, que se llama
9
    archivos.txt
10
   y sobre este, realizar los pasos que siguen:
11
12
4. Quita del archivo archivos.txt:
13
    * el encabezado,
14
    * todas las carpetas,
15
    * todos los archivos dentro de .vscode como:
16
       * el archivo .vscode/settings.json,
17
       * el archivo .vscode/launch.json,
18
    * el archivo .htaccess,
19
    * el archivo archivos.txt,
20
    * este archivo (instruccionesListadoSw.txt),
21
    * el archivo jsconfig.json,
22
    * el archivo sw.js,
23
    * el archivo de la base de datos, que termina en ".db" y
24
      está en la carpeta php,
25
    * todos los archivos de php y
26
    * las líneas en blanco del final
27
28
5. Cambia los \ por / desde Visual Studio Code con las siguientes
29
   combinaciones de teclas:
30
31
    Ctrl+H En el diálogo que aparece introduce lo siguiente:
32
    Find:\
33
    Replace:/
34
35
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
36
 
37
6. Coloca las comillas y coma del final de cada línea desde Visual
38
   Studio Code con las siguientes combinaciones de teclas:
39
40
    Ctrl+H En el diálogo que aparece, selecciona el botón
41
            ".*"
42
           e introduce lo siguiente:
43
    Find:\s*$
44
    Replace:",
45
46
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
47
48
7. Marca la carpeta inicial, presiona la combinación de teclas:
49
50
    Shift+Ctrl+L
51
52
    borra la selección, teclea " y luego ESC
53
54
8. Cambia las secuencias de espacios por / con las siguientes
55
   combinaciones de teclas:
56
57
    Ctrl+H En el diálogo que aparece, selecciona el botón
58
            ".*"
59
           e introduce lo siguiente:
60
    Find:\s+
61
    Replace:/
62
63
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
64
65
9. Cambia las "/ por " con las siguientes combinaciones de teclas:
66
67
    Ctrl+H En el diálogo que aparece, quita la selección del botón
68
            ".*"
69
           e introduce lo siguiente:
70
    Find:"/
71
    Replace:"
72
73
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
74
75
10. Copia el texto al archivo
76
     sw.js
77
    en el contenido del arreglo llamado ARCHIVOS, pero recuerda
78
    mantener el último elemento, que dice:
79
     "/"

T. archivos.txt

1
"agrega.html",
2
"ayuda.html",
3
"favicon.ico",
4
"index.html",
5
"modifica.html",
6
"site.webmanifest",
7
"css/baseline.css",
8
"css/colors.css",
9
"css/elevation.css",
10
"css/estilos.css",
11
"css/material-symbols-outlined.css",
12
"css/md-fab-primary.css",
13
"css/md-filled-text-field.css",
14
"css/md-headline.css",
15
"css/md-list.css",
16
"css/md-standard-icon-button.css",
17
"css/md-tab.css",
18
"css/motion.css",
19
"css/palette.css",
20
"css/roboto.css",
21
"css/shape.css",
22
"css/state.css",
23
"css/transicion_pestanas.css",
24
"css/typography.css",
25
"css/theme/dark.css",
26
"css/theme/light.css",
27
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
28
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
29
"fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
30
"fonts/roboto-v32-latin-regular.woff2",
31
"img/icono2048.png",
32
"img/maskable_icon.png",
33
"img/maskable_icon_x128.png",
34
"img/maskable_icon_x192.png",
35
"img/maskable_icon_x384.png",
36
"img/maskable_icon_x48.png",
37
"img/maskable_icon_x512.png",
38
"img/maskable_icon_x72.png",
39
"img/maskable_icon_x96.png",
40
"img/screenshot_horizontal.png",
41
"img/screenshot_vertical.png",
42
"js/nav-tab-fixed.js",
43
"js/lib/ES_APPLE.js",
44
"js/lib/getAttribute.js",
45
"js/lib/manejaErrores.js",
46
"js/lib/muestraError.js",
47
"js/lib/muestraTextoDeAyuda.js",
48
"js/lib/ProblemDetailsError.js",
49
"js/lib/querySelector.js",
50
"js/lib/registraServiceWorker.js",
51
"js/lib/resaltaSiEstasEn.js",
52
"js/lib/custom/md-app-bar.js",
53
"ungap/custom-elements.js",

U. Carpeta « ungap »

A. ungap / custom-elements.js

V. Resumen

  • En esta lección se presentó la estructura básica de una PWA con vistas CRUD y Material Design 3 Expressive.

15. Sincronización

Versión para imprimir.

A. Introducción

  • En esta lección se muestra un ejemplo de sincronización de bases de datos.

  • Puedes probar el ejemplo en varios navegadores y dispositivos abriendo la url https://sincro.rf.gd/.

  • Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.

  • Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.

  • Comercialmente hay algunos productos como:

    Firebase
    PouchDb (navegador) + CouchDb (servidor)

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama de despliegue

Diagrama de despliegue

D. Hazlo funcionar (con videos)

  1. Prueba el ejemplo en https://sincro.rf.gd/.

  2. Copia la url de la app y pégala en varios navegadores y dispositivos.

  3. Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.

  4. Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.

  5. Descarga el archivo /src/sincro.zip y descompáctalo.

  6. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

  7. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

  8. Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

  9. Importa el proyecto de GitHub a Visual Studio Code

  10. Edita los archivos que desees.

  11. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  12. Prueba tu sitio localmente.

  13. Necesitas un hosting. En este ejemplo se muestra como usar el hosting. Si no lo has usado, lo primero que tienes que hacer es entrar a registrar tu email con el botón Registrar. Si ya tienes tu email registrado, omite este paso.

  14. Crea una cuenta. Si ya tienes cuenta, entra a ella y crea un nuevo domino. En este ejemplo no se crean los archivos directamente en el hosting.

  15. Sube tus archivos al hosting usando ftp.

  16. Sube tus archivos a GitHub. En este ejemplo no hay archivo sw.js ni necesitas esperar 11 o más minutos.

E. Hazlo funcionar (texto)

  1. Prueba el ejemplo en https://sincro.rf.gd/.

  2. Copia la url de la app y pégala en varios navegadores y dispositivos.

  3. Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.

  4. Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.

  5. Descarga el archivo /src/sincro.zip y descompáctalo.

  6. Crea tu proyecto en GitHub:

    1. Crea una cuenta de email, por ejemplo, pepito@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.

      • Mantén la selección Public para que otros usuarios puedan ver tu proyecto.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

  7. Importa el proyecto en GitHub:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.

  8. Edita los archivos que desees.

  9. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  10. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  11. Haz clic derecho en index.html, selecciona PHP Server: serve project y se abre el navegador para que puedas probar localmente el ejemplo.

  12. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en vez de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

    5. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.

  13. Para depurar paso a paso haz lo siguiente:

    1. En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.

    2. Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).

    3. Selecciona el archivo donde vas a empezar a depurar.

    4. Haz clic en el número de la línea donde vas a empezar a depurar.

    5. En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.

    6. Haz clic en Run and Debug .

    7. Si no está configurada la depuración, haz clic en create a launch json file.

    8. Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .

    9. Aparece un cuadro con los controles de depuración

    10. Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.

    11. Regresa al navegador, recarga la página de manera normal y empieza a usarla.

    12. Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.

  14. Sube el proyecto al hosting que elijas.

    1. Crea una nueva carpeta para crear un nuevo proyecto que estará conectado directamente al servidor web por ftp.

    2. Abre la nueva carpeta con Visual Studio Code.

    3. Tecle al mismo tiempo SFTP: Config. Aparece un archivo de configuración de FTP. Llena los datos con la configuración de FTP de su servidor, excepto la contraseña.

    4. Cliquea el botón de SFTP y luego haz clic en la URL de tu servidos. En la barra superior te pide la contraseña y ENTER.

    5. Pásate a la parte de archivos y coloca tus archivos.

    6. Cliquea con el botón derecho en la sección de archivos y selecciona Sync: Local -> Remote.

  15. Abre un navegador y prueba el proyecto en tu hosting.

  16. En el hosting InfinityFree, la primera vez que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.

  17. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push

F. Archivos

G. index.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
8
 <title>Sincronizacion</title>
9
10
 <meta name="viewport" content="width=device-width">
11
12
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
13
 <script type="module" src="js/lib/manejaErrores.js"></script>
14
15
</head>
16
17
<body>
18
19
 <h1>Sincronizacion</h1>
20
21
 <p><a href="agrega.html">Agregar</a></p>
22
23
 <ul id="lista">
24
  <li><progress max="100">Cargando…</progress></li>
25
 </ul>
26
27
 <script type="module">
28
29
  import { muestraError } from "./js/lib/muestraError.js"
30
  import { esperaUnPocoYSincroniza } from "./js/esperaUnPocoYSincroniza.js"
31
  import {
32
   pasatiempoConsultaNoEliminados
33
  } from "./js/pasatiempoConsultaNoEliminados.js"
34
  import { renderiza } from "./js/renderiza.js"
35
  import { sincroniza } from "./js/sincroniza.js"
36
37
  pasatiempoVistaIndex()
38
39
  export async function pasatiempoVistaIndex() {
40
   try {
41
    const pasatiempos = await pasatiempoConsultaNoEliminados()
42
    renderiza(pasatiempos)
43
    sincroniza()
44
   } catch (error) {
45
    muestraError(error)
46
    esperaUnPocoYSincroniza()
47
   }
48
  }
49
50
 </script>
51
52
</body>
53
54
</html>

H. agrega.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Agregar</title>
10
11
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
12
 <script type="module" src="js/lib/manejaErrores.js"></script>
13
14
</head>
15
16
<body>
17
18
 <form id="formulario">
19
20
  <h1>Agregar</h1>
21
22
  <p><a href="index.html">Cancelar</a></p>
23
24
  <p>
25
   <label>
26
    Nombre *
27
    <input name="nombre">
28
   </label>
29
  </p>
30
  <p>* Obligatorio</p>
31
  <p><button type="submit">Agregar</button></p>
32
33
 </form>
34
35
 <script type="module">
36
37
  import { pasatiempoAgrega } from "./js/pasatiempoAgrega.js"
38
39
  formulario.addEventListener("submit", pasatiempoAgrega)
40
41
 </script>
42
43
</body>
44
45
</html>

I. modifica.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Modificar</title>
10
11
 <script type="module" src="js/lib/registraServiceWorker.js"></script>
12
 <script type="module" src="js/lib/manejaErrores.js"></script>
13
14
</head>
15
16
<body>
17
18
 <form id="formulario">
19
20
  <h1>Modificar</h1>
21
22
  <p><a href="index.html">Cancelar</a></p>
23
24
  <p>
25
   <label>
26
    Nombre *
27
    <input name="nombre" value="Cargando…">
28
   </label>
29
  </p>
30
31
  <p>* Obligatorio</p>
32
33
  <p>
34
35
   <button type="submit">Guardar</button>
36
37
   <button id="botonEliminar" type="button">
38
    Eliminar
39
   </button>
40
41
  </p>
42
43
 </form>
44
45
 <script type="module">
46
47
  import {
48
   validaEntidadObligatoria
49
  } from "./js/lib/validaEntidadObligatoria.js"
50
  import { muestraObjeto } from "./js/lib/muestraObjeto.js"
51
  import { pasatiempoBusca } from "./js/pasatiempoBusca.js"
52
  import { pasatiempoElimina } from "./js/pasatiempoElimina.js"
53
  import { pasatiempoModifica } from "./js/pasatiempoModifica.js"
54
55
  const params = new URL(location.href).searchParams
56
  const id = params.get("id")
57
58
  descargaDatos()
59
60
  export async function descargaDatos() {
61
   if (id !== null && id !== "") {
62
    let modelo = await pasatiempoBusca(id)
63
    modelo = validaEntidadObligatoria("Pasatiempo", modelo)
64
    muestraObjeto(document, { nombre: { value: modelo.PAS_NOMBRE } })
65
    formulario
66
     .addEventListener("submit", event => pasatiempoModifica(event, id))
67
    botonEliminar.addEventListener("click", () => pasatiempoElimina(id))
68
   }
69
  }
70
71
 </script>
72
73
</body>
74
75
</html>

J. instruccionesListadoSw.txt

1
Generar el listado de archivos del sw.js desde Visual Studio Code.
2
1. Abrir una terminal desde el menú 
3
    Terminal > New Terminal
4
5
2. Desde la terminal introducir la orden:
6
    Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
8
3. Abrir el archivo generado, que se llama
9
    archivos.txt
10
   y sobre este, realizar los pasos que siguen:
11
12
4. Quita del archivo archivos.txt:
13
    * el encabezado,
14
    * todas las carpetas,
15
    * todos los archivos dentro de .vscode como:
16
       * el archivo .vscode/settings.json,
17
       * el archivo .vscode/launch.json,
18
    * el archivo .htaccess,
19
    * el archivo archivos.txt,
20
    * este archivo (instruccionesListadoSw.txt),
21
    * el archivo jsconfig.json,
22
    * el archivo sw.js,
23
    * el archivo de la base de datos, que termina en ".db" y
24
      está en la carpeta php,
25
    * todos los archivos de php y
26
    * las líneas en blanco del final
27
28
5. Cambia los \ por / desde Visual Studio Code con las siguientes
29
   combinaciones de teclas:
30
31
    Ctrl+H En el diálogo que aparece introduce lo siguiente:
32
    Find:\
33
    Replace:/
34
35
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
36
 
37
6. Coloca las comillas y coma del final de cada línea desde Visual
38
   Studio Code con las siguientes combinaciones de teclas:
39
40
    Ctrl+H En el diálogo que aparece, selecciona el botón
41
            ".*"
42
           e introduce lo siguiente:
43
    Find:\s*$
44
    Replace:",
45
46
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
47
48
7. Marca la carpeta inicial, presiona la combinación de teclas:
49
50
    Shift+Ctrl+L
51
52
    borra la selección, teclea " y luego ESC
53
54
8. Cambia las secuencias de espacios por / con las siguientes
55
   combinaciones de teclas:
56
57
    Ctrl+H En el diálogo que aparece, selecciona el botón
58
            ".*"
59
           e introduce lo siguiente:
60
    Find:\s+
61
    Replace:/
62
63
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
64
65
9. Cambia las "/ por " con las siguientes combinaciones de teclas:
66
67
    Ctrl+H En el diálogo que aparece, quita la selección del botón
68
            ".*"
69
           e introduce lo siguiente:
70
    Find:"/
71
    Replace:"
72
73
    Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
74
75
10. Copia el texto al archivo
76
     sw.js
77
    en el contenido del arreglo llamado ARCHIVOS, pero recuerda
78
    mantener el último elemento, que dice:
79
     "/"

K. archivos.txt

1
"agrega.html",
2
"index.html",
3
"modifica.html",
4
"errors/datosnojson.html",
5
"errors/eliminadoincorrecto.html",
6
"errors/errorinterno.html",
7
"errors/idincorrecto.html",
8
"errors/modificacionincorrecta.html",
9
"errors/nombreincorrecto.html",
10
"errors/resultadonojson.html",
11
"js/Bd.js",
12
"js/esperaUnPocoYSincroniza.js",
13
"js/PASATIEMPO.js",
14
"js/pasatiempoAgrega.js",
15
"js/pasatiempoBusca.js",
16
"js/pasatiempoConsultaNoEliminados.js",
17
"js/pasatiempoConsultaTodos.js",
18
"js/pasatiempoElimina.js",
19
"js/pasatiempoModifica.js",
20
"js/pasatiemposReemplaza.js",
21
"js/renderiza.js",
22
"js/sincroniza.js",
23
"js/validaPasatiempo.js",
24
"js/validaPasatiempos.js",
25
"js/lib/bdConsulta.js",
26
"js/lib/bdEjecuta.js",
27
"js/lib/consume.js",
28
"js/lib/creaIdCliente.js",
29
"js/lib/enviaJsonRecibeJson.js",
30
"js/lib/htmlentities.js",
31
"js/lib/manejaErrores.js",
32
"js/lib/muestraError.js",
33
"js/lib/muestraObjeto.js",
34
"js/lib/ProblemDetailsError.js",
35
"js/lib/recibeTexto.js",
36
"js/lib/recibeTextoObligatorio.js",
37
"js/lib/registraServiceWorker.js",
38
"js/lib/validaEntidadObligatoria.js",

L. sw.js

1
/* Este archivo debe estar colocado en la carpeta raíz del sitio.
2
 * 
3
 * Cualquier cambio en el contenido de este archivo hace que el service
4
 * worker se reinstale. */
5
6
/**
7
 * Cambia el número de la versión cuando cambia el contenido de los
8
 * archivos.
9
 * 
10
 * El número a la izquierda del punto (.), en este caso <q>1</q>, se
11
 * conoce como número mayor y se cambia cuando se realizan
12
 * modificaciones grandes o importantes.
13
 * 
14
 * El número a la derecha del punto (.), en este caso <q>00</q>, se
15
 * conoce como número menor y se cambia cuando se realizan
16
 * modificaciones menores.
17
 */
18
const VERSION = "1.00"
19
20
/**
21
 * Nombre de la carpeta de caché.
22
 */
23
const CACHE = "sincro"
24
25
/**
26
 * Archivos requeridos para que la aplicación funcione fuera de línea.
27
 */
28
const ARCHIVOS = [
29
 "agrega.html",
30
 "index.html",
31
 "modifica.html",
32
 "errors/datosnojson.html",
33
 "errors/eliminadoincorrecto.html",
34
 "errors/errorinterno.html",
35
 "errors/idincorrecto.html",
36
 "errors/modificacionincorrecta.html",
37
 "errors/nombreincorrecto.html",
38
 "errors/resultadonojson.html",
39
 "js/Bd.js",
40
 "js/esperaUnPocoYSincroniza.js",
41
 "js/PASATIEMPO.js",
42
 "js/pasatiempoAgrega.js",
43
 "js/pasatiempoBusca.js",
44
 "js/pasatiempoConsultaNoEliminados.js",
45
 "js/pasatiempoConsultaTodos.js",
46
 "js/pasatiempoElimina.js",
47
 "js/pasatiempoModifica.js",
48
 "js/pasatiemposReemplaza.js",
49
 "js/renderiza.js",
50
 "js/sincroniza.js",
51
 "js/validaPasatiempo.js",
52
 "js/validaPasatiempos.js",
53
 "js/lib/bdConsulta.js",
54
 "js/lib/bdEjecuta.js",
55
 "js/lib/consume.js",
56
 "js/lib/creaIdCliente.js",
57
 "js/lib/enviaJsonRecibeJson.js",
58
 "js/lib/htmlentities.js",
59
 "js/lib/manejaErrores.js",
60
 "js/lib/muestraError.js",
61
 "js/lib/muestraObjeto.js",
62
 "js/lib/ProblemDetailsError.js",
63
 "js/lib/recibeTexto.js",
64
 "js/lib/recibeTextoObligatorio.js",
65
 "js/lib/registraServiceWorker.js",
66
 "js/lib/validaEntidadObligatoria.js",
67
 "/"
68
]
69
70
// Verifica si el código corre dentro de un service worker.
71
if (self instanceof ServiceWorkerGlobalScope) {
72
 // Evento al empezar a instalar el servide worker,
73
 self.addEventListener("install",
74
  (/** @type {ExtendableEvent} */ evt) => {
75
   console.log("El service worker se está instalando.")
76
   evt.waitUntil(llenaElCache())
77
  })
78
79
 // Evento al solicitar información a la red.
80
 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
81
  if (evt.request.method === "GET") {
82
   evt.respondWith(buscaLaRespuestaEnElCache(evt))
83
  }
84
 })
85
86
 // Evento cuando el service worker se vuelve activo.
87
 self.addEventListener("activate",
88
  () => console.log("El service worker está activo."))
89
}
90
91
async function llenaElCache() {
92
 console.log("Intentando cargar caché:", CACHE)
93
 // Borra todos los cachés.
94
 const keys = await caches.keys()
95
 for (const key of keys) {
96
  await caches.delete(key)
97
 }
98
 // Abre el caché de este service worker.
99
 const cache = await caches.open(CACHE)
100
 // Carga el listado de ARCHIVOS.
101
 await cache.addAll(ARCHIVOS)
102
 console.log("Cache cargado:", CACHE)
103
 console.log("Versión:", VERSION)
104
}
105
106
/** @param {FetchEvent} evt */
107
async function buscaLaRespuestaEnElCache(evt) {
108
 // Abre el caché.
109
 const cache = await caches.open(CACHE)
110
 const request = evt.request
111
 /* Busca la respuesta a la solicitud en el contenido del caché, sin
112
  * tomar en cuenta la parte después del símbolo "?" en la URL. */
113
 const response = await cache.match(request, { ignoreSearch: true })
114
 if (response === undefined) {
115
  /* Si no la encuentra, empieza a descargar de la red y devuelve
116
   * la promesa. */
117
  return fetch(request)
118
 } else {
119
  // Si la encuentra, devuelve la respuesta encontrada en el caché.
120
  return response
121
 }
122
}

M. Carpeta « js »

A. js / Bd.js

1
export const ALMACEN_PASATIEMPO = "PASATIEMPO"
2
export const PAS_ID = "PAS_ID"
3
export const INDICE_NOMBRE = "INDICE_NOMBRE"
4
export const PAS_NOMBRE = "PAS_NOMBRE"
5
const BD_NOMBRE = "sincro"
6
const BD_VERSION = 1
7
8
/** @type { Promise<IDBDatabase> } */
9
export const Bd = new Promise((resolve, reject) => {
10
11
 /* Se solicita abrir la base de datos, indicando nombre y
12
  * número de versión. */
13
 const solicitud = indexedDB.open(BD_NOMBRE, BD_VERSION)
14
15
 // Si se presenta un error, rechaza la promesa.
16
 solicitud.onerror = () => reject(solicitud.error)
17
18
 // Si se abre con éxito, devuelve una conexión a la base de datos.
19
 solicitud.onsuccess = () => resolve(solicitud.result)
20
21
 // Si es necesario, se inicia una transacción para cambio de versión.
22
 solicitud.onupgradeneeded = () => {
23
24
  const bd = solicitud.result
25
26
  // Como hay cambio de versión, borra el almacén si es que existe.
27
  if (bd.objectStoreNames.contains(ALMACEN_PASATIEMPO)) {
28
   bd.deleteObjectStore(ALMACEN_PASATIEMPO)
29
  }
30
31
  // Crea el almacén "PASATIEMPO" con el campo llave "PAS_ID".
32
  const almacenPasatiempo =
33
   bd.createObjectStore(ALMACEN_PASATIEMPO, { keyPath: PAS_ID })
34
35
  // Crea un índice ordenado por el campo PAS_NOMBRE que no acepta duplicados.
36
  almacenPasatiempo.createIndex(INDICE_NOMBRE, PAS_NOMBRE)
37
 }
38
39
})

B. js / esperaUnPocoYSincroniza.js

1
import { sincroniza } from "./sincroniza.js"
2
3
/**
4
 * Cada 20 segundos (2000 milisegundos) después de la última
5
 * sincronización, los datos se envían al servidor para volver a
6
 * sincronizarse con los datos del servidor.
7
 */
8
const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000
9
10
export function esperaUnPocoYSincroniza() {
11
 setTimeout(() => sincroniza(), MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR)
12
}

C. js / PASATIEMPO.js

1
/**
2
 * @typedef {Object} PASATIEMPO
3
 * @property {string} PAS_ID
4
 * @property {string} PAS_NOMBRE
5
 * @property {number} PAS_MODIFICACION
6
 * @property {number} PAS_ELIMINADO
7
 */

D. js / pasatiempoAgrega.js

1
import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
2
import { bdEjecuta } from "./lib/bdEjecuta.js"
3
import { creaIdCliente } from "./lib/creaIdCliente.js"
4
import { recibeTextoObligatorio } from "./lib/recibeTextoObligatorio.js"
5
6
7
/**
8
 * @param {SubmitEvent} event
9
 */
10
export async function pasatiempoAgrega(event) {
11
12
 event.preventDefault()
13
 const target = event.target
14
15
 if (!(target instanceof HTMLFormElement))
16
  throw new Error("target no es de tipo form.")
17
18
 const formData = new FormData(target)
19
20
 const modelo = {
21
  PAS_ID: creaIdCliente(Date.now().toString()), // Genera id único en internet.
22
  PAS_NOMBRE: recibeTextoObligatorio(formData, "nombre"),
23
  PAS_MODIFICACION: Date.now(),
24
  PAS_ELIMINADO: 0,
25
 }
26
27
 await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => {
28
  const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO)
29
  almacenPasatiempo.add(modelo)
30
 })
31
32
 location.href = "index.html"
33
34
}

E. js / pasatiempoBusca.js

1
import { bdConsulta } from "./lib/bdConsulta.js"
2
import { validaPasatiempo } from "./validaPasatiempo.js"
3
import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
4
5
/**
6
 * @param {string} id
7
 */
8
export async function pasatiempoBusca(id) {
9
10
 return bdConsulta(Bd, [ALMACEN_PASATIEMPO],
11
  /**
12
   * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO|undefined)
13
   *                                                            => any} resolve 
14
   */
15
  (transaccion, resolve) => {
16
17
   /* Pide el primer objeto de ALMACEN_PASATIEMPO que tenga como llave
18
    * primaria el valor del parámetro id. */
19
   const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).get(id)
20
21
   // onsuccess se invoca solo una vez, devolviendo el objeto solicitado.
22
   consulta.onsuccess = () => {
23
    /* Se recupera el objeto solicitado usando
24
     *  consulta.result
25
     * Si el objeto no se encuentra se recupera undefined. */
26
    const objeto = consulta.result
27
    if (objeto !== undefined) {
28
     const modelo = validaPasatiempo(objeto)
29
     if (modelo.PAS_ELIMINADO === 0) {
30
      resolve(modelo)
31
      return
32
     }
33
    }
34
    resolve(undefined)
35
36
   }
37
38
  })
39
40
}

F. js / pasatiempoConsultaNoEliminados.js

1
import { ALMACEN_PASATIEMPO, Bd, INDICE_NOMBRE } from "./Bd.js"
2
import { bdConsulta } from "./lib/bdConsulta.js"
3
import { validaPasatiempo } from "./validaPasatiempo.js"
4
5
export async function pasatiempoConsultaNoEliminados() {
6
7
 return bdConsulta(Bd, [ALMACEN_PASATIEMPO],
8
  /**
9
   * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO[])=>void
10
   *                                                                  } resolve
11
   */
12
  (transaccion, resolve) => {
13
14
   const resultado = []
15
16
   const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO)
17
18
   // Usa el índice INDICE_NOMBRE para recuperar los datos ordenados.
19
   const indiceNombre = almacenPasatiempo.index(INDICE_NOMBRE)
20
21
   // Pide un cursor para recorrer cada objeto que devuelve la consulta.
22
   const consulta = indiceNombre.openCursor()
23
24
   /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez
25
    * cuando se acaban dichos objetos. */
26
   consulta.onsuccess = () => {
27
    /* El cursor correspondiente al objeto se recupera usando
28
     *  consulta.result */
29
    const cursor = consulta.result
30
    if (cursor === null) {
31
     /* Si el cursor vale null, ya no hay más objetos que procesar; por lo
32
      * mismo, se devuelve el resultado con los pasatiempos recuperados, usando
33
      *  resolve(resultado). */
34
     resolve(resultado)
35
    } else {
36
     /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con
37
      *  cursor.value */
38
     const modelo = validaPasatiempo(cursor.value)
39
     if (modelo.PAS_ELIMINADO === 0) {
40
      resultado.push(modelo)
41
     }
42
     /* Busca el siguiente objeto de la consulta, que se recupera la siguiente
43
      * vez que se invoque la función onsuccess. */
44
     cursor.continue()
45
    }
46
   }
47
48
  })
49
50
}

G. js / pasatiempoConsultaTodos.js

1
import { bdConsulta } from "./lib/bdConsulta.js"
2
import { validaPasatiempo } from "./validaPasatiempo.js"
3
import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
4
5
/**
6
 * Lista todos los objetos, incluyendo los que tienen borrado lógico.
7
 */
8
export async function pasatiempoConsultaTodos() {
9
10
 return bdConsulta(Bd, [ALMACEN_PASATIEMPO],
11
  /**
12
   * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO[])=>void
13
   *                                                                  } resolve
14
   */
15
  (transaccion, resolve) => {
16
17
   const resultado = []
18
19
   // Pide un cursor para recorrer cada objeto que devuelve la consulta.
20
   const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).openCursor()
21
22
   /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez
23
    * cuando se acaban dichos objetos. */
24
   consulta.onsuccess = () => {
25
    /* El cursor correspondiente al objeto se recupera usando
26
     *  consulta.result */
27
    const cursor = consulta.result
28
    if (cursor === null) {
29
     /* Si el cursor vale null, ya no hay más objetos que procesar; por lo
30
      * mismo, se devuelve el resultado con los pasatiempos recuperados, usando
31
      *  resolve(resultado). */
32
     resolve(resultado)
33
    } else {
34
     /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con
35
      *  cursor.value*/
36
     resultado.push(validaPasatiempo(cursor.value))
37
     /* Busca el siguiente objeto de la consulta, que se recupera la siguiente
38
      * vez que se invoque la función onsuccess. */
39
     cursor.continue()
40
    }
41
   }
42
43
  })
44
45
}

H. js / pasatiempoElimina.js

1
import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
2
import { bdEjecuta } from "./lib/bdEjecuta.js"
3
import { pasatiempoBusca } from "./pasatiempoBusca.js"
4
5
/**
6
 * @param { string } id
7
 */
8
export async function pasatiempoElimina(id) {
9
10
 if (confirm('Confirma la eliminación')) {
11
12
  const modelo = await pasatiempoBusca(id)
13
14
  if (modelo !== undefined) {
15
16
   modelo.PAS_MODIFICACION = Date.now()
17
   modelo.PAS_ELIMINADO = 1
18
   await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => {
19
    const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO)
20
    almacenPasatiempo.put(modelo)
21
   })
22
23
  }
24
25
  location.href = "index.html"
26
27
 }
28
29
}

I. js / pasatiempoModifica.js

1
import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
2
import { bdEjecuta } from "./lib/bdEjecuta.js"
3
import { recibeTextoObligatorio } from "./lib/recibeTextoObligatorio.js"
4
import { pasatiempoBusca } from "./pasatiempoBusca.js"
5
6
/**
7
 * @param {SubmitEvent} event
8
 * @param {string} id
9
 */
10
export async function pasatiempoModifica(event, id) {
11
12
 event.preventDefault()
13
 const target = event.target
14
15
 if (!(target instanceof HTMLFormElement))
16
  throw new Error("target no es de tipo form.")
17
18
 const formData = new FormData(target)
19
20
 const nombre = recibeTextoObligatorio(formData, "nombre")
21
22
 const anterior = await pasatiempoBusca(id)
23
24
 if (anterior !== undefined) {
25
26
  anterior.PAS_NOMBRE = nombre
27
  anterior.PAS_MODIFICACION = Date.now()
28
29
  await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => {
30
   const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO)
31
   almacenPasatiempo.put(anterior)
32
  })
33
34
 location.href = "index.html"
35
36
 }
37
38
}

J. js / pasatiemposReemplaza.js

1
import { bdEjecuta } from "./lib/bdEjecuta.js"
2
import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
3
4
/**
5
 * Borra el contenido del almacén PASATIEMPO y guarda nuevospasatiempos.
6
 * @param {import("./PASATIEMPO.js").PASATIEMPO[]} nuevosPasatiempos
7
 */
8
export async function pasatiemposReemplaza(nuevosPasatiempos) {
9
 return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => {
10
  const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO)
11
  almacenPasatiempo.clear()
12
  for (const objeto of nuevosPasatiempos) {
13
   almacenPasatiempo.add(objeto)
14
  }
15
 })
16
}

K. js / renderiza.js

1
import { htmlentities } from "./lib/htmlentities.js"
2
import { muestraObjeto } from "./lib/muestraObjeto.js"
3
4
/**
5
 * @param {import("./PASATIEMPO.js").PASATIEMPO[]} pasatiempos
6
 */
7
export function renderiza(pasatiempos) {
8
 let render = ""
9
 for (const modelo of pasatiempos) {
10
  const nombre = htmlentities(modelo.PAS_NOMBRE)
11
  const searchParams = new URLSearchParams([["id", modelo.PAS_ID]])
12
  const params = htmlentities(searchParams.toString())
13
  render += /* html */
14
   `<li>
15
     <p><a href="modifica.html?${params}">${nombre}</a></p>
16
    </li>`
17
 }
18
 muestraObjeto(
19
  document,
20
  {
21
   lista: { innerHTML: render }
22
  }
23
 )
24
}
25

L. js / sincroniza.js

1
import { pasatiempoConsultaTodos } from "./pasatiempoConsultaTodos.js"
2
import { pasatiemposReemplaza } from "./pasatiemposReemplaza.js"
3
import { esperaUnPocoYSincroniza } from "./esperaUnPocoYSincroniza.js"
4
import { consume } from "./lib/consume.js"
5
import { enviaJsonRecibeJson } from "./lib/enviaJsonRecibeJson.js"
6
import { muestraError } from "./lib/muestraError.js"
7
import { renderiza } from "./renderiza.js"
8
import { validaPasatiempos } from "./validaPasatiempos.js"
9
10
export async function sincroniza() {
11
12
 try {
13
14
  if (navigator.onLine) {
15
   const todos = await pasatiempoConsultaTodos()
16
   const respuesta =
17
    await consume(enviaJsonRecibeJson("php/sincroniza.php", todos))
18
   const pasatiempos = validaPasatiempos(await respuesta.json())
19
   await pasatiemposReemplaza(pasatiempos)
20
   renderiza(pasatiempos)
21
  }
22
23
 } catch (error) {
24
25
  muestraError(error)
26
27
 }
28
29
 esperaUnPocoYSincroniza()
30
31
}

M. js / validaPasatiempo.js

1
/**
2
 * @param { any } objeto
3
 * @returns {import("./PASATIEMPO.js").PASATIEMPO}
4
 */
5
export function validaPasatiempo(objeto) {
6
7
 if (typeof objeto.PAS_ELIMINADO !== "number" || isNaN(objeto.PAS_ELIMINADO))
8
  throw new Error("El campo eliminado debe ser número.")
9
10
 if (typeof objeto.PAS_ID !== "string" || objeto.PAS_ID === "")
11
  throw new Error("El id debe ser texto que no esté en blanco.")
12
13
 if (
14
  typeof objeto.PAS_MODIFICACION !== "number" || isNaN(objeto.PAS_MODIFICACION)
15
 )
16
  throw new Error("El campo modificacion debe ser número.")
17
18
 if (typeof objeto.PAS_NOMBRE !== "string" || objeto.PAS_ID === "")
19
  throw new Error("El nombre debe ser texto que no esté en blanco.")
20
21
 return objeto
22
23
}

N. js / validaPasatiempos.js

1
import { validaPasatiempo } from "./validaPasatiempo.js"
2
3
/**
4
 * @param { any } objetos
5
 * @returns {import("./PASATIEMPO.js").PASATIEMPO[]}
6
 */
7
export function validaPasatiempos(objetos) {
8
 if (!Array.isArray(objetos))
9
  throw new Error("no se recibió un arreglo.")
10
 /**
11
  * @type {import("./PASATIEMPO.js").PASATIEMPO[]}
12
  */
13
 const arreglo = []
14
 for (const objeto of objetos) {
15
  arreglo.push(validaPasatiempo(objeto))
16
 }
17
 return arreglo
18
}

O. Carpeta « js / lib »

1. js / lib / bdConsulta.js

1
/**
2
 * @template T
3
 * @param {Promise<IDBDatabase>} bd
4
 * @param {string[]} almacenes
5
 * @param {(transaccion: IDBTransaction, resolve: (resultado:T)=>void) => any
6
 *                                                                   } consulta
7
 * @returns {Promise<T>}
8
 */
9
export async function bdConsulta(bd, almacenes, consulta) {
10
11
 const base = await bd
12
13
 return new Promise((resolve, reject) => {
14
  // Inicia una transacción de solo lectura.
15
  const transaccion = base.transaction(almacenes, "readonly")
16
  // Al terminar con error ejecuta la función reject.
17
  transaccion.onerror = () => reject(transaccion.error)
18
  // Estas son las operaciones para realizar la consulta.
19
  consulta(transaccion, resolve)
20
 })
21
22
}

2. js / lib / bdEjecuta.js

1
/**
2
 * @param {Promise<IDBDatabase>} bd
3
 * @param {string[]} entidades
4
 * @param {(t:IDBTransaction) => void} operaciones
5
 */
6
export async function bdEjecuta(bd, entidades, operaciones) {
7
8
 // Espera que se abra la bd
9
 const base = await bd
10
11
 return new Promise(
12
  (resolve, reject) => {
13
   // Inicia una transacción de lectura y escritura.
14
   const transaccion = base.transaction(entidades, "readwrite")
15
   // Al terminar con éxito, ejecuta la función resolve.
16
   transaccion.oncomplete = resolve
17
   // Al terminar con error, ejecuta la función reject.
18
   transaccion.onerror = () => reject(transaccion.error)
19
   // Estas son las operaciones de la transacción.
20
   operaciones(transaccion)
21
  })
22
23
}

3. js / lib / consume.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Espera a que la promesa de un fetch termine. Si
5
 * hay error, lanza una excepción.
6
 * 
7
 * @param {Promise<Response> } servicio
8
 */
9
export async function consume(servicio) {
10
 const respuesta = await servicio
11
 if (respuesta.ok) {
12
  return respuesta
13
 } else {
14
  const contentType = respuesta.headers.get("Content-Type")
15
  if (contentType === "application/problem+json; charset=utf-8")
16
   throw new ProblemDetailsError(await respuesta.json())
17
  else
18
   throw new Error(respuesta.statusText)
19
 }
20
}

4. js / lib / creaIdCliente.js

1
/**
2
 * Añade caracteres al azar a una raíz, para obtener un clientId único.
3
 * @param {string} raiz 
4
 */
5
export function creaIdCliente(raiz) {
6
 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
7
 for (var i = 0; i < 15; i++) {
8
  raiz += chars.charAt(Math.floor(Math.random() * chars.length))
9
 }
10
 return raiz
11
}

5. js / lib / enviaJsonRecibeJson.js

1
2
/**
3
 * @param { string } url
4
 * @param { Object } body
5
 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
6
 *  | "CONNECT" | "HEAD" } metodoHttp
7
 */
8
export async function enviaJsonRecibeJson(url, body, metodoHttp = "POST") {
9
 return fetch(
10
  url,
11
  {
12
   method: metodoHttp,
13
   headers: {
14
    "Content-Type": "application/json",
15
    "Accept": "application/json, application/problem+json"
16
   },
17
   body: JSON.stringify(body)
18
  }
19
 )
20
}

6. js / lib / htmlentities.js

1
/**
2
 * Codifica un texto para que cambie los caracteres
3
 * especiales y no se pueda interpretar como
4
 * etiiqueta HTML. Esta técnica evita la inyección
5
 * de código.
6
 * @param { string } texto
7
*/
8
export function htmlentities(texto) {
9
 return texto.replace(/[<>"']/g, textoDetectado => {
10
  switch (textoDetectado) {
11
   case "<": return "<"
12
   case ">": return ">"
13
   case '"': return """
14
   case "'": return "'"
15
   default: return textoDetectado
16
  }
17
 })
18
}
19

7. js / lib / manejaErrores.js

1
import { muestraError } from "./muestraError.js"
2
3
/**
4
 * Intercepta Response.prototype.json para capturar errores de parseo
5
 * y asegurar que se reporten correctamente en navegadores Chromium.
6
 */
7
{
8
 const originalJson = Response.prototype.json
9
10
 Response.prototype.json = function () {
11
  // Llamamos al método original usando el contexto (this) de la respuesta
12
  return originalJson.apply(this, arguments)
13
   .catch((/** @type {any} */ error) => {
14
    // Corrige un error de Chrome que evita el manejo correcto de errores.
15
    throw new Error(error)
16
   })
17
 }
18
}
19
20
window.onerror = function (
21
  /** @type {string} */ _message,
22
  /** @type {string} */ _url,
23
  /** @type {number} */ _line,
24
  /** @type {number} */ _column,
25
  /** @type {Error} */ errorObject
26
) {
27
 muestraError(errorObject)
28
 return true
29
}
30
31
window.addEventListener('unhandledrejection', event => {
32
 muestraError(event.reason)
33
 event.preventDefault()
34
})
35

8. js / lib / muestraError.js

1
import { ProblemDetailsError } from "./ProblemDetailsError.js"
2
3
/**
4
 * Muestra los datos de una Error en la consola y en un cuadro de alerta.
5
 * @param { ProblemDetailsError | Error | null } error descripción del error.
6
 */
7
export function muestraError(error) {
8
9
 if (error === null) {
10
11
  console.error("Error")
12
  alert("Error")
13
14
 } else if (error instanceof ProblemDetailsError) {
15
16
  const problemDetails = error.problemDetails
17
18
  let mensaje =
19
   typeof problemDetails["title"] === "string" ? problemDetails["title"] : ""
20
  if (typeof problemDetails["detail"] === "string") {
21
   if (mensaje !== "") {
22
    mensaje += "\n\n"
23
   }
24
   mensaje += problemDetails["detail"]
25
  }
26
  if (mensaje === "") {
27
   mensaje = "Error"
28
  }
29
  console.error(error, problemDetails)
30
  alert(mensaje)
31
32
 } else {
33
34
  console.error(error)
35
  alert(error.message)
36
37
 }
38
39
}

9. js / lib / muestraObjeto.js

1
/**
2
 * @param {Document | HTMLElement | ShadowRoot} raizHtml
3
 * @param { any } objeto
4
 */
5
export function muestraObjeto(raizHtml, objeto) {
6
 for (const [nombre, definiciones] of Object.entries(objeto)) {
7
  if (Array.isArray(definiciones)) {
8
   muestraArray(raizHtml, nombre, definiciones)
9
  } else if (definiciones !== undefined && definiciones !== null) {
10
   muestraElemento(raizHtml, nombre, definiciones)
11
  }
12
 }
13
}
14
15
/**
16
 * @param { string } nombre
17
 */
18
export function selectorDeNombre(nombre) {
19
 return `[id="${nombre}"],[name="${nombre}"],[data-name="${nombre}"]`
20
}
21
22
/**
23
 * @param { Document | HTMLElement | ShadowRoot } raizHtml
24
 * @param { string } propiedad
25
 * @param {any[]} valores
26
 */
27
function muestraArray(raizHtml, propiedad, valores) {
28
 const conjunto = new Set(valores)
29
 const elementos = raizHtml.querySelectorAll(selectorDeNombre(propiedad))
30
 if (elementos.length === 1 && elementos[0] instanceof HTMLSelectElement) {
31
  muestraOptions(elementos[0], conjunto)
32
 } else {
33
  muestraInputs(elementos, conjunto)
34
 }
35
36
}
37
38
/**
39
 * @param {HTMLSelectElement} select
40
 * @param {Set<any>} conjunto
41
 */
42
function muestraOptions(select, conjunto) {
43
 for (let i = 0, options = select.options, len = options.length; i < len; i++) {
44
  const option = options[i]
45
  option.selected = conjunto.has(option.value)
46
 }
47
}
48
49
/**
50
 * @param {NodeListOf<Element>} elementos
51
 * @param {Set<any>} conjunto
52
 */
53
function muestraInputs(elementos, conjunto) {
54
 for (let i = 0, len = elementos.length; i < len; i++) {
55
  const elemento = elementos[i]
56
  if (elemento instanceof HTMLInputElement) {
57
   elemento.checked = conjunto.has(elemento.value)
58
  }
59
 }
60
}
61
62
const data_ = "data-"
63
const data_Length = data_.length
64
65
/**
66
 * @param {Document | HTMLElement | ShadowRoot} raizHtml
67
 * @param {string} nombre
68
 * @param {{ [s: string]: any; } } definiciones
69
 */
70
function muestraElemento(raizHtml, nombre, definiciones) {
71
 const elemento = raizHtml.querySelector(selectorDeNombre(nombre))
72
 if (elemento !== null) {
73
  for (const [propiedad, valor] of Object.entries(definiciones)) {
74
   if (propiedad in elemento) {
75
    elemento[propiedad] = valor
76
   } else if (
77
    propiedad.length > data_Length
78
    && propiedad.startsWith(data_)
79
    && elemento instanceof HTMLElement
80
   ) {
81
    elemento.dataset[propiedad.substring(data_Length)] = valor
82
   }
83
  }
84
 }
85
}

10. js / lib / ProblemDetailsError.js

1
export class ProblemDetailsError extends Error {
2
3
 /**
4
  * Detalle de los errores devueltos por un servicio.
5
  * Crea una instancia de ProblemDetailsError.
6
  * @param {object} problemDetails Objeto con la descripcipon del error.
7
  */
8
 constructor(problemDetails) {
9
10
  super(typeof problemDetails["detail"] === "string"
11
   ? problemDetails["detail"]
12
   : (typeof problemDetails["title"] === "string"
13
    ? problemDetails["title"]
14
    : "Error"))
15
16
  this.problemDetails = problemDetails
17
18
 }
19
20
}

11. js / lib / recibeTexto.js

1
/**
2
 * @param {FormData} formData
3
 * @param {string} parametro
4
 */
5
export function recibeTexto(formData, parametro) {
6
 const valor = formData.get(parametro)
7
 if (valor !== null && typeof valor !== "string")
8
  throw new Error(`El valor de ${parametro} debe ser texto.`)
9
 return valor === null ? undefined : valor
10
}
11

12. js / lib / recibeTextoObligatorio.js

1
import { recibeTexto } from "./recibeTexto.js"
2
3
/**
4
 * @param {FormData} formData
5
 * @param {string} parametro
6
 */
7
export function recibeTextoObligatorio(formData, parametro) {
8
 const texto = recibeTexto(formData, parametro)
9
 if (texto === undefined) throw new Error(`Falta el valor de ${parametro}.`)
10
 const trimTexto = texto.trim()
11
 if (trimTexto === "") throw new Error(`Campo ${parametro} en blanco.`)
12
 return trimTexto
13
}
14

13. js / lib / registraServiceWorker.js

1
const nombreDeServiceWorker = "sw.js"
2
3
try {
4
 navigator.serviceWorker.register(nombreDeServiceWorker)
5
  .then(registro => {
6
   console.log(nombreDeServiceWorker, "registrado.")
7
   console.log(registro)
8
  })
9
  .catch(error => console.log(error))
10
} catch (error) {
11
 console.log(error)
12
}

14. js / lib / validaEntidadObligatoria.js

1
/**
2
 * @template T
3
 * @param {string} nombre
4
 * @param {T | undefined} entidad
5
 */
6
export function validaEntidadObligatoria(nombre, entidad) {
7
8
 if (entidad === undefined)
9
  throw new Error(`Registro de ${nombre} no encontrado.`)
10
11
 return entidad
12
}
13

N. Carpeta « php »

A. php / Bd.php

1
<?php
2
3
class Bd
4
{
5
6
 private static ?PDO $pdo = null;
7
8
 static function pdo(): PDO
9
 {
10
  if (self::$pdo === null) {
11
   self::$pdo = new PDO(
12
    // cadena de conexión
13
    "sqlite:" . __DIR__ . "/sincro.db",
14
    // usuario
15
    null,
16
    // contraseña
17
    null,
18
    // Opciones: pdos no persistentes y lanza excepciones.
19
    [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
20
   );
21
22
   self::$pdo->exec(
23
    'CREATE TABLE IF NOT EXISTS PASATIEMPO (
24
      PAS_ID TEXT NOT NULL,
25
      PAS_NOMBRE TEXT NOT NULL,
26
      PAS_MODIFICACION INTEGER NOT NULL,
27
      PAS_ELIMINADO INTEGER NOT NULL,
28
      CONSTRAINT PAS_PK
29
       PRIMARY KEY(PAS_ID),
30
      CONSTRAINT PAS_ID_NV
31
       CHECK(LENGTH(PAS_ID) > 0),
32
      CONSTRAINT PAS_NOM_NV
33
       CHECK(LENGTH(PAS_NOMBRE) > 0)
34
     )'
35
   );
36
  }
37
38
  return self::$pdo;
39
 }
40
}
41

B. php / pasatiempoAgrega.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @param array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  } $modelo
13
 */
14
function pasatiempoAgrega(array $modelo)
15
{
16
 $bd = Bd::pdo();
17
 $stmt = $bd->prepare(
18
  "INSERT INTO PASATIEMPO (
19
    PAS_ID, PAS_NOMBRE, PAS_MODIFICACION, PAS_ELIMINADO
20
   ) values (
21
    :PAS_ID, :PAS_NOMBRE, :PAS_MODIFICACION, :PAS_ELIMINADO
22
   )"
23
 );
24
 $stmt->execute([
25
  ":PAS_ID" => $modelo[PAS_ID],
26
  ":PAS_NOMBRE" => $modelo[PAS_NOMBRE],
27
  ":PAS_MODIFICACION" => $modelo[PAS_MODIFICACION],
28
  ":PAS_ELIMINADO" => $modelo[PAS_ELIMINADO],
29
 ]);
30
}
31

C. php / pasatiempoBusca.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @return false | array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  }
13
 */
14
function pasatiempoBusca(string $id): false|array
15
{
16
 $bd = Bd::pdo();
17
 $stmt = $bd->prepare("SELECT * FROM PASATIEMPO WHERE PAS_ID = :PAS_ID");
18
 $stmt->execute([":PAS_ID" => $id]);
19
 $modelo = $stmt->fetch(PDO::FETCH_ASSOC);
20
 return $modelo;
21
}
22

D. php / pasatiempoConsultaNoEliminados.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @return array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  }[]
13
 */
14
function pasatiempoConsultaNoEliminados()
15
{
16
 $bd = Bd::pdo();
17
$stmt = $bd->query(
18
 "SELECT * FROM PASATIEMPO WHERE PAS_ELIMINADO = 0 ORDER BY PAS_NOMBRE"
19
);
20
$lista = $stmt->fetchAll(PDO::FETCH_ASSOC);
21
return $lista;
22
}
23

E. php / pasatiempoModifica.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @param array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  } $modelo
13
 */
14
function pasatiempoModifica(array $modelo)
15
{
16
 $bd = Bd::pdo();
17
 $stmt = $bd->prepare(
18
  "UPDATE PASATIEMPO
19
   SET
20
    PAS_NOMBRE = :PAS_NOMBRE,
21
    PAS_MODIFICACION = :PAS_MODIFICACION,
22
    PAS_ELIMINADO = :PAS_ELIMINADO
23
   WHERE
24
    PAS_ID = :PAS_ID"
25
 );
26
 $stmt->execute([
27
  ":PAS_ID" => $modelo[PAS_ID],
28
  ":PAS_NOMBRE" => $modelo[PAS_NOMBRE],
29
  ":PAS_MODIFICACION" => $modelo[PAS_MODIFICACION],
30
  ":PAS_ELIMINADO" => $modelo[PAS_ELIMINADO],
31
 ]);
32
}
33

F. php / sincroniza.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/lib/recibeJson.php";
5
require_once __DIR__ . "/lib/devuelveJson.php";
6
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
7
require_once __DIR__ . "/validaPasatiempo.php";
8
require_once __DIR__ . "/pasatiempoAgrega.php";
9
require_once __DIR__ . "/pasatiempoBusca.php";
10
require_once __DIR__ . "/pasatiempoConsultaNoEliminados.php";
11
require_once __DIR__ . "/pasatiempoModifica.php";
12
13
 $lista = recibeJson();
14
15
 if (!is_array($lista)) {
16
  $lista = [];
17
 }
18
19
 foreach ($lista as $modelo) {
20
  $modeloEnElCliente = validaPasatiempo($modelo);
21
  $modeloEnElServidor = pasatiempoBusca($modeloEnElCliente[PAS_ID]);
22
23
  if ($modeloEnElServidor === false) {
24
25
   /* CONFLICTO: El modelo no ha estado en el servidor.
26
    * AGREGARLO solamente si no está eliminado. */
27
   if ($modeloEnElCliente[PAS_ELIMINADO] === 0) {
28
    pasatiempoAgrega($modeloEnElCliente);
29
   }
30
  } elseif (
31
   $modeloEnElServidor[PAS_ELIMINADO] === 0
32
   && $modeloEnElCliente[PAS_ELIMINADO] === 1
33
  ) {
34
35
   /* CONFLICTO: El registro está en el servidor, donde no se ha eliminado, pero
36
    * ha sido eliminado en el cliente.
37
    * Gana el cliente, porque optamos por no revivir lo eliminado. */
38
   pasatiempoModifica($modeloEnElCliente);
39
  } else if (
40
   $modeloEnElCliente[PAS_ELIMINADO] === 0
41
   && $modeloEnElServidor[PAS_ELIMINADO] === 0
42
  ) {
43
44
   /* CONFLICTO: Registros en el servidor y en el cliente. Pueden ser
45
    * diferentes.
46
    * GANA FECHA MÁS GRANDE. Cuando gana el servidor, no se hace nada. */
47
   if (
48
    $modeloEnElCliente[PAS_MODIFICACION] >
49
    $modeloEnElServidor[PAS_MODIFICACION]
50
   ) {
51
    // La versión del cliente es más nueva y prevalece.
52
    pasatiempoModifica($modeloEnElCliente);
53
   }
54
  }
55
 }
56
57
 $lista = pasatiempoConsultaNoEliminados();
58
59
 devuelveJson($lista);
60

G. php / TABLA_PASATIEMPO.php

1
<?php
2
3
const PASATIEMPO = "PASATIEMPO";
4
const PAS_ID = "PAS_ID";
5
const PAS_NOMBRE = "PAS_NOMBRE";
6
const PAS_MODIFICACION = "PAS_MODIFICACION";
7
const PAS_ELIMINADO = "PAS_ELIMINADO";
8

H. php / validaPasatiempo.php

1
<?php
2
3
require_once __DIR__ . "/lib/BAD_REQUEST.php";
4
require_once __DIR__ . "/lib/ProblemDetailsException.php";
5
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
6
7
function validaPasatiempo($objeto)
8
{
9
 if (!isset($objeto->PAS_ELIMINADO) || !is_int($objeto->PAS_ELIMINADO))
10
  throw new ProblemDetailsException([
11
   "status" => BAD_REQUEST,
12
   "title" => "El campo eliminado debe ser entero.",
13
   "type" => "/errors/eliminadoincorrecto.html",
14
  ]);
15
16
 if (
17
  !isset($objeto->PAS_ID)
18
  || !is_string($objeto->PAS_ID)
19
  || $objeto->PAS_ID === ""
20
 )
21
  throw new ProblemDetailsException([
22
   "status" => BAD_REQUEST,
23
   "title" => "El id debe ser texto que no esté en blanco.",
24
   "type" => "/errors/idincorrecto.html",
25
  ]);
26
27
 if (!isset($objeto->PAS_MODIFICACION) || !is_int($objeto->PAS_MODIFICACION))
28
  throw new ProblemDetailsException([
29
   "status" => BAD_REQUEST,
30
   "title" => "La modificacion debe ser número.",
31
   "type" => "/errors/modificacionincorrecta.html",
32
  ]);
33
34
 if (
35
  !isset($objeto->PAS_NOMBRE)
36
  || !is_string($objeto->PAS_NOMBRE)
37
  || $objeto->PAS_NOMBRE === ""
38
 )
39
  throw new ProblemDetailsException([
40
   "status" => BAD_REQUEST,
41
   "title" => "El nombre debe ser texto que no esté en blanco.",
42
   "type" => "/errors/nombreincorrecto.html",
43
  ]);
44
45
 return [
46
  PAS_ELIMINADO => $objeto->PAS_ELIMINADO,
47
  PAS_ID => $objeto->PAS_ID,
48
  PAS_NOMBRE => $objeto->PAS_NOMBRE,
49
  PAS_MODIFICACION => $objeto->PAS_MODIFICACION,
50
 ];
51
}
52

I. Carpeta « php / lib »

1. php / lib / BAD_REQUEST.php

1
<?php
2
3
const BAD_REQUEST = 400;
4

2. php / lib / devuelveJson.php

1
<?php
2
3
require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5
function devuelveJson($resultado)
6
{
7
 $json = json_encode($resultado);
8
 if ($json === false) {
9
  devuelveResultadoNoJson();
10
 } else {
11
  header("Content-Type: application/json; charset=utf-8");
12
  echo $json;
13
 }
14
 exit();
15
}
16

3. php / lib / devuelveResultadoNoJson.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
function devuelveResultadoNoJson()
6
{
7
 http_response_code(INTERNAL_SERVER_ERROR);
8
 header("Content-Type: application/problem+json; charset=utf-8");
9
10
 echo '{' .
11
  "status: " . INTERNAL_SERVER_ERROR .
12
  '"title": "El resultado no puede representarse como JSON."' .
13
  '"type": "/errors/resultadonojson.html"' .
14
  '}';
15
}
16

4. php / lib / INTERNAL_SERVER_ERROR.php

1
<?php
2
3
const INTERNAL_SERVER_ERROR = 500;

5. php / lib / manejaErrores.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
require_once __DIR__ . "/ProblemDetailsException.php";
5
6
// Hace que se lance una excepción automáticamente cuando se genere un error.
7
set_error_handler(function ($severity, $message, $file, $line) {
8
 throw new ErrorException($message, 0, $severity, $file, $line);
9
});
10
11
// Código cuando una excepción no es atrapada.
12
set_exception_handler(function (Throwable $excepcion) {
13
 if ($excepcion instanceof ProblemDetailsException) {
14
  devuelveProblemDetails($excepcion->problemDetails);
15
 } else {
16
  devuelveProblemDetails([
17
   "status" => INTERNAL_SERVER_ERROR,
18
   "title" => "Error interno del servidor",
19
   "detail" => $excepcion->getMessage(),
20
   "type" => "/errors/errorinterno.html",
21
  ]);
22
 }
23
 exit();
24
});
25
26
function devuelveProblemDetails(array $array)
27
{
28
 $json = json_encode($array);
29
 if ($json === false) {
30
  devuelveResultadoNoJson();
31
 } else {
32
  http_response_code(isset($array["status"]) ? $array["status"] : 500);
33
  header("Content-Type: application/problem+json; charset=utf-8");
34
  echo $json;
35
 }
36
}
37

6. php / lib / ProblemDetailsException.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
/**
6
 * Detalle de los errores devueltos por un servicio.
7
 */
8
class ProblemDetailsException extends Exception
9
{
10
11
 public array $problemDetails;
12
13
 public function __construct(
14
  array $problemDetails,
15
 ) {
16
  
17
  parent::__construct(
18
   isset($problemDetails["detail"])
19
    ? $problemDetails["detail"]
20
    : (isset($problemDetails["title"])
21
     ? $problemDetails["title"]
22
     : "Error"),
23
   $problemDetails["status"]
24
    ? $problemDetails["status"]
25
    : INTERNAL_SERVER_ERROR
26
  );
27
28
  $this->problemDetails = $problemDetails;
29
 }
30
}
31

7. php / lib / recibeJson.php

1
<?php
2
3
require_once __DIR__ . "/BAD_REQUEST.php";
4
5
function recibeJson()
6
{
7
 $json = json_decode(file_get_contents("php://input"));
8
9
 if ($json === null) {
10
11
  http_response_code(BAD_REQUEST);
12
  header("Content-Type: application/problem+json; charset=utf-8");
13
14
  echo '{' .
15
   "status: " . BAD_REQUEST .
16
   '"title": "Los datos recibidos no están en formato JSON."' .
17
   '"type": "/errors/datosnojson.html"' .
18
   '}';
19
20
  exit();
21
 }
22
23
 return $json;
24
}
25

O. Carpeta « errors »

A. errors / datosnojson.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Los datos recibidos no están en formato JSON</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Los datos recibidos no están en formato JSON</h1>
16
17
</body>
18
19
</html>

B. errors / eliminadoincorrecto.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El campo eliminado debe ser entero</title>
10
11
<body>
12
13
 <h1>El campo eliminado debe ser entero</h1>
14
15
</body>
16
17
</html>

C. errors / errorinterno.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>Error interno del servidor</title>
10
11
</head>
12
13
<body>
14
15
 <h1>Error interno del servidor</h1>
16
17
 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19
</body>
20
21
</html>

D. errors / idincorrecto.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El id debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>El id debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

E. errors / modificacionincorrecta.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>La modificacion debe ser número</title>
10
11
<body>
12
13
 <h1>La modificacion debe ser número</h1>
14
15
</body>
16
17
</html>

F. errors / nombreincorrecto.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El nombre debe ser texto que no esté en blanco</title>
10
11
<body>
12
13
 <h1>El nombre debe ser texto que no esté en blanco</h1>
14
15
</body>
16
17
</html>

G. errors / resultadonojson.html

1
<!DOCTYPE html>
2
<html lang="es">
3
4
<head>
5
6
 <meta charset="UTF-8">
7
 <meta name="viewport" content="width=device-width">
8
9
 <title>El resultado no puede representarse como JSON</title>
10
11
</head>
12
13
<body>
14
15
 <h1>El resultado no puede representarse como JSON</h1>
16
17
 <p>
18
  Debido a un error interno del servidor, el resultado generado, no se puede
19
  recuperar.
20
 </p>
21
22
</body>
23
24
</html>

P. .htaccess

  • Este archivo configura las respuestas del servidor..

  • Lo utilizan principalmente servidores como Apache o Nginx.

  • Configura el mime type para el archivo de manifiesto y sedhabilita el uso de la cache general de http..

1
AddType application/manifest+json .webmanifest
2
3
ExpiresActive On
4
5
Header set Cache-Control "max-age=1, must-revalidate"
6

Q. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1
{
2
 "compilerOptions": {
3
  "checkJs": true,
4
  "strictNullChecks": true,
5
  "target": "ES6",
6
  "module": "Node16",
7
  "moduleResolution": "Node16",
8
  "lib": [
9
   "ES2017",
10
   "WebWorker",
11
   "DOM"
12
  ]
18
}

R. Resumen

  • En esta lección se muestra un ejemplo de sincronización de bases de datos.

16. PWA con Material Design (Desactualizado)

Versión para imprimir.

Este ejemplo está desactualizado, pero es un ejemplo de lo que se piede hacer.

1. Introduccion

2. Referencias

Sitios de Material Design
Material.io

Material Design es una guía de diseño para aplicaciones multiplataforma. La encuentras en https://m3.material.io/.

Herramienta para selección de colores
Material Theme Builder

https://m3.material.io/theme-builder/

Adaptación multiplataforma

La forma de adaptar Material Design en distintas plataformas está en https://material.io/design/platform-guidance/cross-platform-adaptation.html

3. Hazlo funcionar

  1. Prueba e instala, de preferencia con Chrome, el sitio https://pwamd.rf.gd/ y en https://gilpgpwamd.github.io/.

  2. Descarga el archivo /src/pwamd.zip y descompáctalo.

  3. Crea tu proyecto en GitHub pages:

    1. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

      • Mantén la selección Public.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

    5. Entra al repositorio y selecciona ⚙ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto.

  4. Importa el proyecto en GitHub:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo, excepto el archivo .htaccess.

  5. Edita los archivos que desees.

  6. Crea los íconos del proyecto con https://www.photopea.com/. Este paso se hizo com Microsoft Edge (versión Chromium), pero no funcionó con Google Chrome.

  7. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  8. Coloca el archivo favicon.ico en la raíz del proyecto.

  9. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  10. Para cambiar los colores, entra al sitio https://material-foundation.github.io/material-theme-builder/, selecciona los colores de tu aplicación, haz clic en Pick your fonts →. Elige font Roboto, haz clic en Export theme →. Haz clic en Export y selecciona Web (CSS) para descargar el zip que contiene los estilos que generan los colores. Descompacta el zip y copia los archivos de la carpeta css a la carpeta css del proyecto, sobreescribiendo los archivos del ejemplo.

  11. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  12. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  13. Si tu proyecto no usa backend, haz clic derecho en index.html, selecciona Open with Live Server y se abre el navegador para que puedas probar localmente el ejemplo.

  14. Si tu proyecto usa PHP, haz clic derecho en index.html, selecciona PHP Server: serve project y se abre el navegador para que puedas probar localmente el ejemplo.

  15. Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.

  16. Coloca las capturas en la carpeta img y asegúrate de que estén declaradas en el archivo site.webmanifest.

  17. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en vez de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

    5. En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error. InfinityFree bloquea el análisis de las imágenes.

    6. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.

  18. Para depurar paso a paso haz lo siguiente:

    1. En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.

    2. Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).

    3. Selecciona el archivo donde vas a empezar a depurar.

    4. Haz clic en el número de la línea donde vas a empezar a depurar.

    5. Recarga la página de manera normal.

    6. Empieza a usar tu sitio.

    7. Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.

  19. Cuando usas GitHub pages, antes de subir los archivos, no debes modificar el valor de VERSION en el archivo sw.js.

  20. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push
  21. Si usas GitHub pages:

    1. Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.

    2. Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.

    3. A partir de este momento, espera al menos 11 minutos para modificar el valor de VERSION en el archivo sw.js y volver a subir el proyecto.

  22. Si no usas GitHub pages:

    1. Sube el proyecto al hosting que elijas sin incluir el archivo .htaccess. En algunos casos puedes usar filezilla (https://filezilla-project.org/)

    2. En algunos host como InfinityFree, tienes que configurar el certificado SSL.

    3. En algunos host, como InfinityFree, debes subir el archivo .htaccess cuando el certificado SSL se ha creado e instalado. Sirve para forzar el uso de https, para eliminar el control de cache, pues ahora lo lleva el service worker y para asignar el mime type correcto para el archivo de manifest.

    4. Abre un navegador y prueba el proyecto en tu hosting.

    5. En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.

  23. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  24. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de instalar que aparece en el menú de extensión de Chrome.

  25. Instala y usa tu PWA en iOS (iPhone y iPad).

4. Archivos

Haz clic en los triángulos para expandir las carpetas

5. index.html - Página principal

  • La página principal de las aplicaciónes móviles incluye una barra de aplicación centrada, cuyo nombre oficial es centered aligned top app bar, que contiene:

    • El título de la aplicación, que se muestra centrado,

    • al inicio, de manera opcional, un botón de ícono navegación y

    • al final, un botón de acción, mostrando un ícono, que puede servir para acceder al perfil del usuario, mostrando su avatar, acceder a la configuración de la app, mostrando un engrane, o alguna otra acción.

  • Para que el contenido se muestra adecuadamente, se usa el elemento personalizado md-top-app-bar, cuyo comportamiento está definido en la clase MdTopAppBar. Hay que añadirle class="center-aligned"

  • Para el título, dentro de la la barra de aplicación se usa un elemento de tipo h1.

  • El botón opcional de navegación debe llevar al atributo slot="navigation".

  • El botón opcional de acción debe llevar al atributo slot="action". En los ejemplos con pestañas se muestra su uso.

  • El botón de perfil debe llevar la clase avatar.

  • Para añadir elementos debajo del título, indica su id con el atributo adicional. Los ejemplos sobre pestañas indican como.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>PWA con MD</title>
8
9 <!-- Resumen para los motores de búsqueda. -->
10 <meta name="description" content="Ejemplo de PWA con Material Design">
11
12 <script src="js/registraServiceWorker.js"></script>
13
14 <meta name="viewport" content="width=device-width">
15
16 <!-- Color de la barra de navegación de Chrome en dispositivos móviles. -->
17 <meta name="theme-color" content="#fffbfe">
18
19 <!-- Ícono para la página web, que normalmente se pone en la raíz del sitio.
20 Puede ser diferente para cada página. -->
21 <link rel="icon" sizes="32x32" href="favicon.ico">
22
23 <!-- Configuración de la PWA para Chrome, Edge y Safari.
24 Debe ponerse en todas las páginas. -->
25 <link rel="manifest" href="site.webmanifest">
26
27 <!-- Permite a los navegadores, que como Safari no soportan el estándar
28 completo de custom elementsm, lo cumplan totalmente. Debe ponerse en todas
29 las páginas. -->
30 <script src="ungap/custom-elements.js"></script>
31
32 <!-- Configuración de todas las páginas con JavaSript. -->
33 <script type="module" src="js/configura.js"></script>
34 <link rel="stylesheet" href="css/estilos.css">
35 <link rel="stylesheet" href="css/transicion_completa.css">
36
37</head>
38
39<body>
40
41 <md-top-app-bar class="center-aligned">
42
43 <h1>PWA con MD</h1>
44
45 <button is="md-menu-button" slot="navigation"></button>
46
47 <button type="button" class="md-standard-icon-button avatar" title="Perfil"
48 slot="action">
49 <span class="material-symbols-outlined">account_circle</span>
50 </button>
51
52 </md-top-app-bar>
53
54 <main>
55 <p>
56 Esta es la página principal de la app. Las X que siguen son para que veas
57 como se comporta cuando se hace scroll.
58 </p>
59 <p>x</p>
60 <p>x</p>
61 <p>x</p>
62 <p>x</p>
63 <p>x</p>
64 <p>x</p>
65 <p>x</p>
66 <p>x</p>
67 <p>x</p>
68 <p>x</p>
69 <p>x</p>
70 <p>x</p>
71 <p>x</p>
72 <p>x</p>
73 <p>x</p>
74 <p>x</p>
75 <p>x</p>
76 <p>x</p>
77 <p>x</p>
78 <p>x</p>
79 <p>x</p>
80 <p>x</p>
81 <p>x</p>
82 </main>
83
84 <nav-drw></nav-drw>
85
86</body>
87
88</html>

Solo para los más rudos

6. secundaria.html - Vista secundaria

  • Cuando la barra de aplicación no está en la página principal, puede mostrar hasta 3 botones de ícono a la derecha. Deben ser las acciones más usadas.

  • Cuando hay más de 3 acciones, coloca las menos usadas en el overflow menu, que se despliega cliqueando el botón con el ícono en iOS, y el ícono en otros sistemas operativos.

  • Cuando en iOS no hay íconos de acción, el título debe ir centrado.

  • Para que el contenido se muestra adecuadamente, se usa el elemento personalizado md-top-app-bar, cuyo comportamiento está definido en la clase MdTopAppBar.

  • El boton de overflow debe ser el último con slot="action". Usa el atributo is="md-overflow-button". Su atributo onclick debe alternar el overflow menu.

  • El overflow menu, se define con el elemento personalizado mdoverflow-menu, cuyo comportamiento está definido en la clase MdOverflowMenu.

  • Para el título, dentro de la la barra de aplicación se usa un elemento de tipo h1. El texto debe ser corto y caber en un solo renglón, o de lo conrario se cortará.

  • Para mostrar un título más largo, hay que poner después de la barra de aplicación otro h1 y ponerle un id. Ese id se pone como valor del atributo headline dentro de la md-top-app-bar. Cuando la página tenga el scroll hasta arriba, se despliega el h1 que está dentro del md-top-app-bar.. Al hacer scroll, este título se compacta y se muestra el título definido con el atributo headline.

  • Si deseas que la letra del encabezado largo sea más pequeña, usa la clase medium dentro de la md-top-app-bar.

  • El botón de navegación opcional debe llevar el atributo slot="navigation".

  • Los botones de acción deben llevar el atributo slot="action".

  • El overflow menú debe llevar el atributo slot="overflow".

  • Para añadir elementos debajo del título, usa el atributo slot="adicional".

  • El botón de ícono para abrir el overflow menu, se coloca dentro de este menú usando el atributo slot="icon".

  • Los botones de acción se colocan dentro del overflow menu sin ningún atributo slot.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Vista secundaria - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar class="medium" headline="h1Headline">
26
27 <h1>Secundaria</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 <button type="button" class="md-standard-icon-button" title="Agregar"
32 slot="action">
33 <span class="material-symbols-outlined">add</span>
34 </button>
35
36 <button type="button" class="md-standard-icon-button" title="Editar"
37 slot="action">
38 <span class="material-symbols-outlined">edit</span>
39 </button>
40
41 <button is="md-overflow-button" slot="action"
42 onclick="overflow.alterna(this)"></button>
43
44 </md-top-app-bar>
45
46 <h1 id="h1Headline">Página secundaria</h1>
47
48 <main>
49 <p>
50 Esta es una página secundaria de la app. Las X que siguen son para que veas
51 como se comporta cuando se hace scroll.
52 </p>
53 <p>x</p>
54 <p>x</p>
55 <p>x</p>
56 <p>x</p>
57 <p>x</p>
58 <p>x</p>
59 <p>x</p>
60 <p>x</p>
61 <p>x</p>
62 <p>x</p>
63 <p>x</p>
64 <p>x</p>
65 <p>x</p>
66 <p>x</p>
67 <p>x</p>
68 <p>x</p>
69 <p>x</p>
70 <p>x</p>
71 <p>x</p>
72 <p>x</p>
73 <p>x</p>
74 <p>x</p>
75 <p>x</p>
76 </main>
77
78 <md-overflow-menu id="overflow">
79
80 <button type="button">
81 <span class="material-symbols-outlined">star</span>
82 Marcar favorito
83 </button>
84
85 <button type="button">
86 <span class="material-symbols-outlined"> delete</span>
87 Eliminar
88 </button>
89
90 </md-overflow-menu>
91
92 <nav-drw></nav-drw>
93
94</body>
95
96</html>

Solo para los más rudos

7. iconos.html - Íconos oficiales

  • El sitio oficial de los íconos de Material Design es https://fonts.google.com/icons.

  • En el sitio oficial te aparecen los íconos en diferentes estilos.

  • Si seleccionas un ícono y su estilo, te aparecen las instrucciones de como añadirlo a tu código.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Íconos - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar>
26
27 <h1>Íconos</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <main>
34
35 <button type="button" class="md-standard-icon-button">
36 <span class="material-symbols-outlined">favorite</span>
37 </button>
38
39 <button type="button" class="md-standard-icon-button" disabled>
40 <span class="material-symbols-outlined">bolt</span>
41 </button>
42
43 <a class="md-standard-icon-button" target="_blank" rel="noopener noreferrer"
44 href="https://google.com">
45 <span class="material-symbols-outlined">star</span></a>
46
47 <span class="material-symbols-outlined">thumb_up</span>
48
49 <button type="button" class="md-fab-primary"
50 style="position: fixed; bottom: 1rem; right: 1rem;">
51 <span class="material-symbols-outlined">add</span>
52 </button>
53
54 </main>
55
56 <nav-drw></nav-drw>
57
58</body>
59
60</html>

Solo para los más rudos

8. botones.html - Botones

  • Los botones principales deben usar la clase md-filled-button. Solo se pone un botón principal por pagina.

  • Los botones secundarios deben usar la clase md-outline-button.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Botones - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar>
26
27 <h1>Botones</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <main>
34
35 <p>
36
37 <button class="md-filled-button">
38 Primario
39 </button>
40
41 <button class="md-outline-button">
42 Secundario
43 </button>
44
45 </p>
46
47 </main>
48
49 <nav-drw></nav-drw>
50
51</body>
52
53</html>

Solo para los más rudos

9. campos.html - Campos de texto

  • Se usa una etiqueta flotante que se muestra grande cuando el contenido de campo es una cadena vacía y se muestra pequeña cuando el elemento se está capturando o cuando su contenido no está vacío.

  • Añade la class md-filled-text-field a un elemento de tipo label, span, p o div

  • Usa la class float junto a la class md-filled-text-field cuando quieras que la etiqueta esté arriba todo el tiempo. Esto sirve para los output, input type="date" e input type="file" entre otros tipos de elemento.

  • El rótulo de las etiquetas deben ser span o label y deben colocarse inmediatamente después del elemento que realiza la captura.

  • Para que la etiquta flote correctamente, el elemento de captura debe repetir el rótulo de la etiqueta en el atributo placeholder.

  • Los textos de ayuda deben ser elementos de tipo small que se colocan después de la etiqueta.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Campos de texto - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar headline="headline">
26
27 <h1>Texto</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <h1 id="headline">Campos de texto</h1>
34
35 <main>
36
37 <!-- Usa
38 class="float"
39 cuando quieras que la etiqueta esté arriba todo el tiempo. -->
40 <p>
41 <label class="md-filled-text-field float">
42 <output>Saludo</output>
43 <span>Saludo</span>
44 </label>
45 </p>
46
47 <p>
48 <label class="md-filled-text-field">
49 <input required placeholder="Nombre*">
50 <span>Nombre *</span>
51 <small>Obligatorio</small>
52 </label>
53 </p>
54
55 <p>
56 <label class="md-filled-text-field">
57 <input type="email" placeholder="Email">
58 <span accesskey="M">Email</span>
59 </label>
60 </p>
61
62 <p>
63 <label class="md-filled-text-field float">
64 <input type="date" placeholder="Fecha">
65 <span>Fecha</span>
66 </label>
67 </p>
68
69 <p>
70 <label class="md-filled-text-field">
71 <textarea rows="3" placeholder="Dirección"></textarea>
72 <span>Dirección</span>
73 </label>
74 </p>
75
76 </main>
77
78 <nav-drw></nav-drw>
79
80</body>
81
82</html>

Solo para los más rudos

10. select.html - Selects

  • Este control se adapta mejor a Material Designe que el select de HTML.

  • Usa el elemento personalizado md-select-menu, definido con la clase MdSelectMenu.

  • Coloca el elemento md-select-menu con slot="input-text", dentro de un <p class="md-filled-text-field">. Usa un span cono etiqueta.

  • Para asignar un valor inicial, usa el atributo value al md-select-menu.

  • Para que forzosamente se deba seleccionar una opción, agrega el atributo booleano required al md-select-menu.

  • Para las opciones usa el elemento personalizado md-options-menu, definido con la clase MdOptionMenu. Dentro de él coloca elementos tipo span que indican el value de la opción con el atributo data-value y cuyo contenido es el texto de la opción.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Select - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <form>
26
27 <md-top-app-bar>
28
29 <h1>Select</h1>
30
31 <button is="md-menu-button" slot="navigation"></button>
32
33 </md-top-app-bar>
34
35 <main>
36
37 <p>
38 <span id="etiquetaGrupo" class="md-filled-text-field" accesskey="G">
39 <md-select-menu name="grupo" required value="IC21"
40 aria-labelledby="etiquetaGrupo"
41 options="opcionesDeGrupo"></md-select-menu>
42 <span>Grupo *</span>
43 <small>Obligatorio</small>
44 </span>
45 </p>
46
47 <p>
48 <button class="md-filled-button" style="width: 100%;">Enviar</button>
49 </p>
50
51 </main>
52
53 <md-options-menu id="opcionesDeGrupo" aria-label="Opciones de grupo">
54 <span data-value="" title="Selecciona opción"></span>
55 <span data-value="IC21">IC-21</span>
56 <span data-value="IC22">IC-22</span>
57 <span data-value="IC23">IC-23</span>
58 </md-options-menu>
59
60 <nav-drw></nav-drw>
61
62 </form>
63
64</body>
65
66</html>

Solo para los más rudos

11. interruptor.html - Interruptores

  • Usa un elemento input con el atribto type="checkbox" y class="md-switch".

Salida

Ábrelo en otra pestaña.

Interruptor en Android
Interruptor en Android
Interruptor en iOS
Interruptor en iOS
1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Interruptores - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar>
26
27 <h1>Interruptores</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <main>
34 <p>
35 <label accesskey="R">
36 Muestra resultados
37 <input type="checkbox" class="md-switch">
38 </label>
39 </p>
40 </main>
41
42 <nav-drw></nav-drw>
43
44</body>
45
46</html>

Solo para los más rudos

12. slider.html

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Sliders - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar>
26
27 <h1>Sliders</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <main>
34
35 <md-slider-field>
36 <label>Rango</label>
37 <input type="range" slot="slider" min="1" max="10">
38 <small slot="supporting">Calcúlalo bien</small>
39 </md-slider-field>
40
41 </main>
42
43 <nav-drw></nav-drw>
44
45</body>
46
47</html>

Solo para los más rudos

13. segmentado.html - Botones segmentados

  • Permiten selecionar de entre 2 a 5 opciones.

  • La base es un elemento con class="md-segmented-button".

  • Puedes usar type="radio" para seleccionar una sola opción para un mismo valor del atributo name.

  • Puedes usar type="checkbox" para seleccionar varias opciones con un mismo valor del atributo name.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Botón segmentado - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar headline="headline">
26
27 <h1>Segmentado</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <h1 id="headline">Botón segmentado</h1>
34
35 <main>
36
37 <p class="md-segmented-button">
38
39 <input id="bueno" type="radio" name="estado" checked value="2">
40 <label for="bueno">
41 <span class="material-symbols-outlined">done</span>
42 Bueno
43 </label>
44
45 <input id="intermedio" type="radio" name="estado" value="1">
46 <label for="intermedio">
47 <span class="material-symbols-outlined">done</span>
48 Intermedio
49 </label>
50
51 <input id="malo" type="radio" name="estado" value="0">
52 <label for="malo">
53 <span class="material-symbols-outlined">done</span>
54 Malo
55 </label>
56
57 </p>
58
59 </main>
60
61 <nav-drw></nav-drw>
62
63</body>
64
65</html>

Solo para los más rudos

14. one-line.html - Listas con elementos one-line

  • Las listas de Material Design usan un ul con la clase md-list.

  • Las listas pueden usar li con las clases one-line, two-line o three-line

  • Si el elemento es un hipervínculo, el elemento li no lleva clases y en su interior se pone una a con la clase one-line, two-line o three-line

  • Los elementos de la clase one-line, tienen el siguiente contenido:

    • Un encabezado con un elemento span class="headline" que muestra texto resaltado truncado a un renglón.

    • Opcionalmente muestran una imagen a la izquierda, que puede ser:

      Un pequeño ícono cuadrado
      • Se añade la clase icon junto a la clase one-line.

      • Se añade un img o un span class="material-symbols-outlined" dentro del elemento con la clase one-line.

      Una imagen cuadrada
      • Se añade la clase image junto a la clase one-line.

      • Se añade un img o un span class="material-symbols-outlined" dentro del elemento con la clase one-line.

      Una imagen rectangular
      • Se añade la clase video junto a la clase one-line.

      • Se añade un img dentro del elemento con la clase one-line.

      El avatar de un usuario (se muestra dentro de un círculo)
      • Se añade la clase avatar junto a la clase one-line.

      • Se añade un img, un label con las iniciales del usuario o un span class="material-symbols-outlined" dentro del elemento con la clase one-line.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Listas one-line - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar headline="headline">
26
27 <h1>one-line</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <h1 id="headline">Listas one-line</h1>
34
35 <main>
36
37 <ul class="md-list">
38
39 <li class="md-one-line">
40 <span class="headline">
41 Ciudad de México
42 </span>
43 </li>
44
45 <li class="md-one-line">
46 <span class="headline">
47 Lorem ipsum dolor sit amet consectetur adipisicing elit.
48 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
49 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
50 alias!
51 </span>
52 </li>
53
54 <li class="md-one-line icon">
55 <span class="material-symbols-outlined">account_balance</span>
56 <span class="headline">
57 Atenas
58 </span>
59 </li>
60
61 <li class="md-one-line avatar">
62 <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg">
63 <span class="headline">
64 Ana
65 </span>
66 </li>
67
68 <li class="md-one-line image">
69 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
70 <span class="headline">
71 Neza
72 </span>
73 </li>
74
75 <li class="md-one-line video">
76 <img alt="Ciudad de San Francisco"
77 src="img/pexels-craig-dennis-3701822.jpg">
78 <span class="headline">
79 San Francisco
80 </span>
81 </li>
82
83 <li class="md-one-line video">
84 <img alt="Ciudad de San Francisco"
85 src="img/pexels-craig-dennis-3701822.jpg">
86 <span class="headline">
87 Lorem ipsum dolor sit amet consectetur adipisicing elit.
88 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
89 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
90 </span>
91 </li>
92
93 <li>
94 <a class="md-one-line image" target="_blank" rel=”noreferrer”
95 href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/">
96 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
97 <span class="headline">
98 Neza Link
99 </span>
100 </a>
101 </li>
102
103 </ul>
104
105 <footer>
106 <ul>
107 <li>
108 <p>
109 <small>
110 <a target="_blank" rel=”noreferrer”
111 href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/">
112 La foto de la niña es de Moises Patrício, publicada en el sitio Pexels.
113 Haz clic en este hipervínculo para más información.
114 </a>
115 </small>
116 </p>
117 </li>
118 <li>
119 <p>
120 <small>
121 <a target="_blank" rel=”noreferrer”
122 href="https://www.pinterest.com.mx/ludresi/">
123 La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio
124 Pinterest. Haz clic en este hipervínculo para más información.
125 </a>
126 </small>
127 </p>
128 </li>
129 <li>
130 <p>
131 <small>
132 <a target="_blank" rel=”noreferrer”
133 href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/">
134 La foto del puente de San Francisco es de Craig Dennis, publicada en el
135 sitio Pexels. Haz clic en este hipervínculo para más información.
136 </a>
137 </small>
138 </p>
139 </li>
140 </ul>
141 </footer>
142
143 </main>
144
145 <nav-drw></nav-drw>
146
147</body>
148
149</html>

Solo para los más rudos

15. two-line.html - Listas con elementos two-line

  • Las listas de Material Design usan un ul con la clase md-list.

  • Las listas pueden usar li con las clases one-line, two-line o three-line

  • Si el elemento es un hipervínculo, el elemento li no lleva clases y en su interior se pone una a con la clase one-line, two-line o three-line

  • Los elementos de la clase two-line, tienen el siguiente contenido:

    • Un encabezado con un elemento span class="headline" que muestra texto resaltado truncado a un renglón.

    • Un elemento span class="supporting" que muestra texto mas pequeño y truncado a un renglón.

    • Opcionalmente muestran una imagen a la izquierda, que puede ser:

      Un pequeño ícono cuadrado
      • Se añade la clase icon junto a la clase two-line.

      • Se añade un img o un span class="material-symbols-outlined" dentro del elemento con la clase two-line.

      Una imagen cuadrada
      • Se añade la clase image junto a la clase two-line.

      • Se añade un img o un span class="material-symbols-outlined" dentro del elemento con la clase two-line.

      Una imagen rectangular
      • Se añade la clase video junto a la clase two-line.

      • Se añade un img dentro del elemento con la clase two-line.

      El avatar de un usuario (se muestra dentro de un círculo)
      • Se añade la clase avatar junto a la clase two-line.

      • Se añade un img, un label con las iniciales del usuario o un span class="material-symbols-outlined" dentro del elemento con la clase two-line.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Listas two-line - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar headline="headline">
26
27 <h1>two-line</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <h1 id="headline">Listas two-line</h1>
34
35 <main>
36
37 <ul class="md-list">
38
39 <li class="md-two-line">
40 <span class="headline">
41 Ciudad de México 2
42 </span>
43 <span class="supporting">
44 Lorem ipsum dolor sit amet consectetur adipisicing elit.
45 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
46 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
47 alias!
48 </span>
49 </li>
50
51 <li class="md-two-line icon">
52 <span class="material-symbols-outlined">account_balance</span>
53 <span class="headline">
54 Atenas 2
55 </span>
56 <span class="supporting">
57 Lorem ipsum dolor sit amet consectetur adipisicing elit.
58 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
59 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
60 alias!
61 </span>
62 </li>
63
64 <li class="md-two-line avatar">
65 <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg">
66 <span class="headline">
67 Ana 2
68 </span>
69 <span class="supporting">
70 Lorem ipsum dolor sit amet consectetur adipisicing elit.
71 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
72 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
73 alias!
74 </span>
75 </li>
76
77 <li class="md-two-line image">
78 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
79 <span class="headline">
80 Neza 2
81 </span>
82 <span class="supporting">
83 Lorem ipsum dolor sit amet consectetur adipisicing elit.
84 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
85 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
86 alias!
87 </span>
88 </li>
89
90 <li class="md-two-line video">
91 <img alt="Ciudad de San Francisco"
92 src="img/pexels-craig-dennis-3701822.jpg">
93 <span class="headline">
94 San Francisco 2
95 </span>
96 <span class="supporting">
97 Lorem ipsum dolor sit amet consectetur adipisicing elit.
98 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
99 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
100 alias!
101 </span>
102 </li>
103
104 <li>
105 <a class="md-two-line image" target="_blank" rel="noopener noreferrer"
106 href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/">
107 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
108 <span class="headline">
109 Neza Link 2
110 </span>
111 <span class="supporting">
112 Lorem ipsum dolor sit amet consectetur adipisicing elit.
113 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
114 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
115 alias!
116 </span>
117 </a>
118 </li>
119
120 </ul>
121
122 <footer>
123 <ul>
124 <li>
125 <p>
126 <small>
127 <a target="_blank" rel="noopener noreferrer"
128 href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/">
129 La foto de la niña es de Moises Patrício, publicada en el sitio Pexels.
130 Haz clic en este hipervínculo para más información.
131 </a>
132 </small>
133 </p>
134 </li>
135 <li>
136 <p>
137 <small>
138 <a target="_blank" rel=”noreferrer”
139 href="https://www.pinterest.com.mx/ludresi/">
140 La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio
141 Pinterest. Haz clic en este hipervínculo para más información.
142 </a>
143 </small>
144 </p>
145 </li>
146 <li>
147 <p>
148 <small>
149 <a target="_blank" rel=”noreferrer”
150 href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/">
151 La foto del puente de San Francisco es de Craig Dennis, publicada en el
152 sitio Pexels. Haz clic en este hipervínculo para más información.
153 </a>
154 </small>
155 </p>
156 </li>
157 </ul>
158 </footer>
159
160 </main>
161
162 <nav-drw></nav-drw>
163
164</body>
165
166</html>

Solo para los más rudos

16. three-line.html - Listas con elementos three-line

  • Las listas de Material Design usan un ul con la clase md-list.

  • Las listas pueden usar li con las clases one-line, two-line o three-line

  • Si el elemento es un hipervínculo, el elemento li no lleva clases y en su interior se pone una a con la clase one-line, two-line o three-line

  • Los elementos de la clase three-line, tienen el siguiente contenido:

    • Un encabezado con un elemento span class="headline" que muestra texto resaltado truncado a un renglón.

    • Un elemento span class="supporting" que muestra texto mas pequeño y truncado 2 renglones.

    • Opcionalmente muestran una imagen a la izquierda, que puede ser:

      Un pequeño ícono cuadrado
      • Se añade la clase icon junto a la clase three-line.

      • Se añade un img o un span class="material-symbols-outlined" dentro del elemento con la clase three-line.

      Una imagen cuadrada
      • Se añade la clase image junto a la clase three-line.

      • Se añade un img o un span class="material-symbols-outlined" dentro del elemento con la clase three-line.

      Una imagen rectangular
      • Se añade la clase video junto a la clase three-line.

      • Se añade un img dentro del elemento con la clase three-line.

      El avatar de un usuario (se muestra dentro de un círculo)
      • Se añade la clase avatar junto a la clase three-line.

      • Se añade un img, un label con las iniciales del usuario o un span class="material-symbols-outlined" dentro del elemento con la clase three-line.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Listas tree-line - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar headline="headline">
26
27 <h1>tree-line</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <h1 id="headline">Listas tree-line</h1>
34
35 <main>
36
37 <ul class="md-list">
38
39 <li class="md-three-line">
40 <span class="headline">
41 Ciudad de México 3
42 </span>
43 <span class="supporting">
44 Lorem ipsum dolor sit amet consectetur adipisicing elit.
45 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
46 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
47 alias!
48 </span>
49 </li>
50
51 <li class="md-three-line icon">
52 <span class="material-symbols-outlined">account_balance</span>
53 <span class="headline">
54 Atenas 3
55 </span>
56 <span class="supporting">
57 Lorem ipsum dolor sit amet consectetur adipisicing elit.
58 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
59 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
60 alias!
61 </span>
62 </li>
63
64 <li class="md-three-line avatar">
65 <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg">
66 <span class="headline">
67 Ana 3
68 </span>
69 <span class="supporting">
70 Lorem ipsum dolor sit amet consectetur adipisicing elit.
71 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
72 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
73 alias!
74 </span>
75 </li>
76
77 <li class="md-three-line image">
78 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
79 <span class="headline">
80 Neza 3
81 </span>
82 <span class="supporting">
83 Lorem ipsum dolor sit amet consectetur adipisicing elit.
84 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
85 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
86 alias!
87 </span>
88 </li>
89
90 <li class="md-three-line video">
91 <img alt="Ciudad de San Francisco"
92 src="img/pexels-craig-dennis-3701822.jpg">
93 <span class="headline">
94 San Francisco 3
95 </span>
96 <span class="supporting">
97 Lorem ipsum dolor sit amet consectetur adipisicing elit.
98 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
99 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
100 alias!
101 </span>
102 </li>
103
104 <li>
105 <a class="md-three-line image" target="_blank" rel=”noreferrer”
106 href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/">
107 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
108 <span class="headline">
109 Neza Link 3
110 </span>
111 <span class="supporting">
112 Lorem ipsum dolor sit amet consectetur adipisicing elit.
113 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
114 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
115 alias!
116 </span>
117 </a>
118 </li>
119
120 </ul>
121
122 <footer>
123 <ul>
124 <li>
125 <p>
126 <small>
127 <a target="_blank" rel=”noreferrer”
128 href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/">
129 La foto de la niña es de Moises Patrício, publicada en el sitio Pexels.
130 Haz clic en este hipervínculo para más información.
131 </a>
132 </small>
133 </p>
134 </li>
135 <li>
136 <p>
137 <small>
138 <a target="_blank" rel=”noreferrer”
139 href="https://www.pinterest.com.mx/ludresi/">
140 La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio
141 Pinterest. Haz clic en este hipervínculo para más información.
142 </a>
143 </small>
144 </p>
145 </li>
146 <li>
147 <p>
148 <small>
149 <a target="_blank" rel=”noreferrer”
150 href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/">
151 La foto del puente de San Francisco es de Craig Dennis, publicada en el
152 sitio Pexels. Haz clic en este hipervínculo para más información.
153 </a>
154 </small>
155 </p>
156 </li>
157 </ul>
158 </footer>
159
160 </main>
161
162 <nav-drw></nav-drw>
163
164</body>
165
166</html>

Solo para los más rudos

17. tarjetas.html - Tarjetas

  • Las listas de tarjetas deben usar un div class="md-cards".

  • Los elementos de la lista de tarjetas pueden ser div, span o a

  • Pueden llevar multimedia, que debe colocarse dentro de un elemento de tipo figure.

  • El texto de encabezado se marca con la class headline.

  • El texto de soporte se marca con la class supporting.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Tarjetas - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar>
26 <h1>Tarjetas</h1>
27
28 <button is="md-menu-button" slot="navigation"></button>
29
30 </md-top-app-bar>
31
32 <div class="md-cards">
33
34 <a target="_blank" rel=”noreferrer”
35 href="https://www.pexels.com/es-es/foto/lobo-blanco-y-negro-397857/">
36 <figure>
37 <img alt="Lobo" src="img/pexels-steve-397857.jpg">
38 </figure>
39 <span class="headline">
40 Lobo
41 </span>
42 <span class="supporting">
43 Foto de Steve en Pexels.
44 </span>
45 </a>
46
47 <a target="_blank" rel=”noreferrer”
48 href="https://www.pexels.com/es-es/foto/foto-de-buho-ural-3732453/">
49 <figure>
50 <img alt="Buho" src="img/pexels-erik-karits-3732453.jpg">
51 </figure>
52 <span class="headline">
53 Buho
54 </span>
55 <span class="supporting">
56 Foto de Erik Karits en Pexels
57 </span>
58 </a>
59
60 <a target="_blank" rel=”noreferrer”
61 href="https://www.pexels.com/es-es/foto/perro-de-pelo-corto-marron-y-blanco-acostado-3978352/">
62 <figure>
63 <img alt="Perro" src="img/pexels-creative-workshop-3978352.jpg">
64 </figure>
65 <span class="headline">
66 Perro
67 </span>
68 <span class="supporting">
69 Foto de Creative Workshop en Pexels
70 </span>
71 </a>
72
73 <a target="_blank" rel=”noreferrer”
74 href="https://www.pexels.com/es-es/foto/gatito-gris-en-bolsa-de-papel-plateada-141496/">
75 <figure>
76 <img alt="Gato" src="img/pexels-vadim-b-141496.jpg">
77 </figure>
78 <span class="headline">
79 Gato
80 </span>
81 <span class="supporting">
82 Foto de Vadim B en Pexels
83 </span>
84 </a>
85
86 <a target="_blank" rel=”noreferrer”
87 href="https://www.pexels.com/es-es/foto/leon-marron-2270848/">
88 <figure>
89 <img alt="León" src="img/pexels-ralph-2270848.jpg">
90 </figure>
91 <span class="headline">
92 León
93 </span>
94 <span class="supporting">
95 Foto de Ralph en Pexels
96 </span>
97 </a>
98
99 <a target="_blank" rel=”noreferrer”
100 href="https://www.pexels.com/es-es/foto/oso-cafe-35435/">
101 <figure>
102 <img alt="Oso" src="img/pexels-rasmus-svinding-35435.jpg">
103 </figure>
104 <span class="headline">
105 Oso
106 </span>
107 <span class="supporting">
108 Foto de Rasmus Svinding en Pexels
109 </span>
110 </a>
111
112 <a target="_blank" rel=”noreferrer”
113 href="https://www.pexels.com/es-es/foto/animal-perro-mono-hierba-10226903/">
114 <figure>
115 <img alt="Coyote" src="img/pexels-esteban-arango-10226903.jpg">
116 </figure>
117 <span class="headline">
118 Coyote
119 </span>
120 <span class="supporting">
121 Foto de Esteban Arango en Pexels
122 </span>
123 </a>
124
125 </div>
126
127 <nav-drw></nav-drw>
128
129</body>
130
131</html>

Solo para los más rudos

18. Cajón de navegación

  • Se usa para seleccionar entre 5 o más vistas.

  • Se usa en:

    • Móvil

    • Tablet

    • Escritorio

  • Algunos diseñadores prefieren usar pestañas, barras de navegación o navigation rail.

  • Se crea el elemento personalizado nav-drw. Puedes ver su código en m-js/m-nav-drw-js.html

  • Coloca el botón is="md-menu-button", dentro del md-top-app-bar con slot="navigation".

  • Coloca nav-drw hasta el final.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Vista secundaria - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar class="medium" headline="h1Headline">
26
27 <h1>Secundaria</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 <button type="button" class="md-standard-icon-button" title="Agregar"
32 slot="action">
33 <span class="material-symbols-outlined">add</span>
34 </button>
35
36 <button type="button" class="md-standard-icon-button" title="Editar"
37 slot="action">
38 <span class="material-symbols-outlined">edit</span>
39 </button>
40
41 <button is="md-overflow-button" slot="action"
42 onclick="overflow.alterna(this)"></button>
43
44 </md-top-app-bar>
45
46 <h1 id="h1Headline">Página secundaria</h1>
47
48 <main>
49 <p>
50 Esta es una página secundaria de la app. Las X que siguen son para que veas
51 como se comporta cuando se hace scroll.
52 </p>
53 <p>x</p>
54 <p>x</p>
55 <p>x</p>
56 <p>x</p>
57 <p>x</p>
58 <p>x</p>
59 <p>x</p>
60 <p>x</p>
61 <p>x</p>
62 <p>x</p>
63 <p>x</p>
64 <p>x</p>
65 <p>x</p>
66 <p>x</p>
67 <p>x</p>
68 <p>x</p>
69 <p>x</p>
70 <p>x</p>
71 <p>x</p>
72 <p>x</p>
73 <p>x</p>
74 <p>x</p>
75 <p>x</p>
76 </main>
77
78 <md-overflow-menu id="overflow">
79
80 <button type="button">
81 <span class="material-symbols-outlined">star</span>
82 Marcar favorito
83 </button>
84
85 <button type="button">
86 <span class="material-symbols-outlined"> delete</span>
87 Eliminar
88 </button>
89
90 </md-overflow-menu>
91
92 <nav-drw></nav-drw>
93
94</body>
95
96</html>

Solo para los más rudos

19. navtab.html - Navegación con pestañas scrollable

  • Se usa para seleccionar entre 2 o más vistas.

  • Se usa en:

    • Móvil

    • Tablet

    • Escritorio

  • Se crea el elemento personalizado nav-tab-scrollable. Puedes ver su código en m-js/m-nav-tab-scrollable-js.html

  • Para reducir los problemas de despliegues transitorios conocidos como fuoc, se añade la siguiente línea después de los link para estilos: <link rel="expect" blocking="render" href="#navtab">, donde navtab es el id de cualquier hiperenlace del elemento personalizado nav-tab-scrollable.

  • Añade al top-app-bar el atributo adicional con el id para nav-tab-scrollable.

  • Coloca nav-tab-scrollable después de top-app-bar y de h1. Ponle su id.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Pestañas scrollable - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_pestanas.css">
20 <link rel="expect" blocking="render" href="#navtab">
21
22</head>
23
24<body>
25
26 <md-top-app-bar adicional="tab" headline="headline">
27
28 <h1>scrollable</h1>
29
30 <button type="button" class="md-standard-icon-button" title="Agregar"
31 slot="action">
32 <span class="material-symbols-outlined">add</span>
33 </button>
34 <button type="button" class="md-standard-icon-button" title="Editar"
35 slot="action">
36 <span class="material-symbols-outlined">edit</span>
37 </button>
38
39 </md-top-app-bar>
40
41 <h1 id="headline">Pestañas scrollable</h1>
42
43 <nav-tab-scrollable id="tab"></nav-tab-scrollable>
44
45 <main>
46 <p>
47 Esta página usa navegación por pestañas fijas. Las X que siguen son para que
48 veas como se comporta cuando se hace scroll.
49 </p>
50 <p>x</p>
51 <p>x</p>
52 <p>x</p>
53 <p>x</p>
54 <p>x</p>
55 <p>x</p>
56 <p>x</p>
57 <p>x</p>
58 <p>x</p>
59 <p>x</p>
60 <p>x</p>
61 <p>x</p>
62 <p>x</p>
63 <p>x</p>
64 <p>x</p>
65 <p>x</p>
66 <p>x</p>
67 <p>x</p>
68 <p>x</p>
69 <p>x</p>
70 <p>x</p>
71 <p>x</p>
72 <p>x</p>
73 </main>
74
75</body>
76
77</html>

Solo para los más rudos

20. navTabFixed.html - Navegación con pestañas fijas

  • Cuando son pocas pestañas, pueden mantenerse fijas.

  • Se crea el elemento personalizado nav-tab-fixed. Puedes ver su código en m-js/m-nav-tab-fixed-js.html

  • Para reducir los problemas de despliegues transitorios conocidos como fuoc, se añade la siguiente línea después de los link para estilos: <link rel="expect" blocking="render" href="#navtabfixed">, donde navtabfixed es el id de cualquier hiperenlace del elemento personalizado nav-tab-fixed.

  • Añade al top-app-bar el atributo adicional con el id para nav-tab-fixed.

  • Coloca nav-tab-fixed después de top-app-bar y de h1. Ponle su id.

  • Puedes cambiar el ancho de las pestañas cambiando la definición de --tabWidth en estilos.css. o redefine el valor de --tabWidth en el atributo style de la etiquta nav-tab-fixed.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Pestañas fijas - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_pestanas.css">
20 <link rel="expect" blocking="render" href="#navtabfixed">
21
22</head>
23
24<body>
25
26 <md-top-app-bar adicional="tab" headline="headline">
27
28 <h1>fijas</h1>
29
30 <button type="button" class="md-standard-icon-button" title="Agregar"
31 slot="action">
32 <span class="material-symbols-outlined">add</span>
33 </button>
34 <button type="button" class="md-standard-icon-button" title="Editar"
35 slot="action">
36 <span class="material-symbols-outlined">edit</span>
37 </button>
38
39 </md-top-app-bar>
40
41 <h1 id="headline">Pestañas fijas</h1>
42
43 <nav-tab-fixed id="tab"></nav-tab-fixed>
44
45 <main>
46 <p>
47 Esta página usa navegación por pestañas filas. Las X que siguen son para que
48 veas como se comporta cuando se hace scroll.
49 </p>
50 <p>x</p>
51 <p>x</p>
52 <p>x</p>
53 <p>x</p>
54 <p>x</p>
55 <p>x</p>
56 <p>x</p>
57 <p>x</p>
58 <p>x</p>
59 <p>x</p>
60 <p>x</p>
61 <p>x</p>
62 <p>x</p>
63 <p>x</p>
64 <p>x</p>
65 <p>x</p>
66 <p>x</p>
67 <p>x</p>
68 <p>x</p>
69 <p>x</p>
70 <p>x</p>
71 <p>x</p>
72 <p>x</p>
73 </main>
74
75</body>
76
77</html>

21. navbar.html - Barra de navegación

  • Se usa para seleccionar entre 3 a 5 vistas.

  • Se usa en:

    • Móvil

  • Aunque no lo especifica Material Design, cuando el número de vistas es mayor a 5, algunos diseñadores prefieren seguir usando una barra de navegación y convertir el último botón en un cajón de navegación para las vistas restantes.

  • En tablet y móvil se convierte en un menú lateral o en un navigation reel.

  • Se crea el elemento personalizado nav-bar. Usa textos cortos. Puedes ver su código en m-js/m-nav-bar-js.html

  • Para reducir los problemas de despliegues transitorios conocidos como fuoc, se añade la siguiente línea después de los link para estilos: <link rel="expect" blocking="render" href="#navbar">, donde navbar es el id de cualquier hiperenlace del elemento personalizado nav-bar.

  • Coloca nav-bar al final de tu forma.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Barra de Navegación - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_pestanas.css">
20 <link rel="expect" blocking="render" href="#navbar">
21
22</head>
23
24<body
25 style="padding-bottom: 5rem;">
26
27 <md-top-app-bar headline="headline">
28
29 <h1>navegación</h1>
30
31 <button type="button" class="md-standard-icon-button" title="Agregar"
32 slot="action">
33 <span class="material-symbols-outlined">add</span>
34 </button>
35 <button type="button" class="md-standard-icon-button" title="Editar"
36 slot="action">
37 <span class="material-symbols-outlined">edit</span>
38 </button>
39
40 </md-top-app-bar>
41
42 <h1 id="headline">Barra de navegación</h1>
43
44 <main>
45 <p>
46 Esta página usa barra de navegación. Las X que siguen son para que veas como
47 se comporta cuando se hace scroll.
48 </p>
49 <p>x</p>
50 <p>x</p>
51 <p>x</p>
52 <p>x</p>
53 <p>x</p>
54 <p>x</p>
55 <p>x</p>
56 <p>x</p>
57 <p>x</p>
58 <p>x</p>
59 <p>x</p>
60 <p>x</p>
61 <p>x</p>
62 <p>x</p>
63 <p>x</p>
64 <p>x</p>
65 <p>x</p>
66 <p>x</p>
67 <p>x</p>
68 <p>x</p>
69 <p>x</p>
70 <p>x</p>
71 <p>x</p>
72 </main>
73
74 <nav-bar></nav-bar>
75
76</body>
77
78</html>

Solo para los más rudos

22. formulario.html - Formulario típico

  • Los mensajes de error de un componente, se muestran en otro conocido como texto de ayuda, que normalmente usa un elemento small con el atributo slot="supporting".

  • La forma usa el atributo novalidate.

  • En este ejemplo, el campo select-menu con el id selectGenero usa el evento input. para invocar la función copiaMensajes. y copiar los mensajes de error del campo de captura al texto de ayuda.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Formulario - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <form id="form" novalidate onsubmit="procesa(event)">
26
27 <md-top-app-bar>
28
29 <h1>Formulario</h1>
30
31 <button is="md-menu-button" slot="navigation"></button>
32
33 </md-top-app-bar>
34
35 <main>
36
37 <p>
38 <span class="md-filled-text-field" accesskey="G">
39 <md-select-menu id="selectGenero" required options="opcionesDeGenero"
40 oninput="copiaMensajes()"></md-select-menu>
41 <span>Género *</span>
42 <small id="supportingGenero">Obligatorio</small>
43 </span>
44 </p>
45
46 <p>
47 <button class="md-filled-button" style="width: 100%;">Recomendar</button>
48 </p>
49
50 </main>
51
52 <md-options-menu id="opcionesDeGenero" aria-label="Opciones de género">
53 <span data-value="" title="Selecciona género"></span>
54 <span data-value="pop">Pop</span>
55 <span data-value="reg">Reguetón</span>
56 </md-options-menu>
57
58 <nav-drw></nav-drw>
59
60 </form>
61
62 <script type="module">
63 import { muestraTextoDeAyuda } from "./lib/js/muestraTextoDeAyuda.js"
64 import { exportaAHtml } from "./lib/js/exportaAHtml.js"
65 import { muestraError } from "./lib/js/muestraError.js"
66
67 function copiaMensajes() {
68 muestraTextoDeAyuda(selectGenero, supportingGenero, "Obligatorio")
69 }
70 exportaAHtml(copiaMensajes)
71
72 /**
73 * @param {SubmitEvent} evt
74 */
75 function procesa(evt) {
76 evt.preventDefault()
77 try {
78 copiaMensajes()
79 if (
80 selectGenero.validity.valid) {
81 const genero = selectGenero.value
82 const resultado = recomienda(genero)
83 alert(resultado)
84 }
85 } catch (e) {
86 muestraError(e)
87 }
88 }
89 exportaAHtml(procesa)
90
91 /** @param {string} genero */
92 function recomienda(genero) {
93 if (genero === "pop") {
94 return "Para el pop te recomiendo a Dua Lipa."
95 } else if (genero === "reg") {
96 return "Para el reguetón te recomiendo a Bad Bunny."
97 }
98 }
99 </script>
100
101</body>
102
103</html>

23. ayuda.html - Vista de ayuda

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es" class="light dark">
3
4<head>
5
6 <meta charset="UTF-8">
7 <title>Ayuda - PWA con MD</title>
8
9 <script src="js/registraServiceWorker.js"></script>
10
11 <meta name="viewport" content="width=device-width">
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15 <script src="ungap/custom-elements.js"></script>
16
17 <script type="module" src="js/configura.js"></script>
18 <link rel="stylesheet" href="css/estilos.css">
19 <link rel="stylesheet" href="css/transicion_completa.css">
20
21</head>
22
23<body>
24
25 <md-top-app-bar>
26
27 <h1>Ayuda</h1>
28
29 <button is="md-menu-button" slot="navigation"></button>
30
31 </md-top-app-bar>
32
33 <main>
34
35 <ul class="md-list">
36 <li class="md-two-line">
37 <span class="headline">
38 Título
39 </span>
40 <span class="supporting">
41 PWA con Material Design
42 </span>
43 </li>
44 <li class="md-two-line">
45 <span class="headline">
46 Descripción
47 </span>
48 <span class="supporting">
49 Ejemplos de vistas móviles.
50 </span>
51 </li>
52 <li class="md-two-line">
53 <span class="headline">
54 Autor
55 </span>
56 <span class="supporting">
57 Gilberto Pacheco Gallegos
58 </span>
59 </li>
60 <li class="md-two-line">
61 <span class="headline">
62 Derechos de autor
63 </span>
64 <span class="supporting">
65 © 2024 Gilberto Pacheco Gallegos
66 </span>
67 </li>
68 <li class="md-three-line">
69 <span class="headline">
70 Este software usa la librería para PWA
71 </span>
72 <span class="supporting">
73 Esta obra de Gilberto Pacheco Gallegos está bajo una
74 <a target="_blank" rel="license noreferrer"
75 href="http://creativecommons.org/licenses/by/4.0/">
76 Licencia Creative Commons Atribución 4.0 Internacional</a></span>
77 </li>
78 <li>
79 <a class="md-three-line" target="_blank" rel=”noreferrer”
80 href="https://fonts.google.com/icons">
81 <span class="headline">
82 También usa Material Symbols
83 </span>
84 <span class="supporting">
85 Desarrollada por Google bajo licencia Apache 2.0
86 </span>
87 </a>
88 </li>
89 </ul>
90
91 </main>
92
93 <nav-drw></nav-drw>
94
95</body>
96
97</html>

24. Carpeta « js »

A. js / configura.js

1/** Barra de navegación. */
2import "./nav-drw.js"
3import "./nav-tab-scrollable.js"
4import "./nav-tab-fixed.js"
5import "./nav-bar.js"
6/** Elementos utilizados */
7import "../lib/js/custom/md-menu-button.js"
8import "../lib/js/custom/md-options-menu.js"
9import "../lib/js/custom/md-overflow-button.js"
10import "../lib/js/custom/md-overflow-menu.js"
11import "../lib/js/custom/md-select-menu.js"
12import "../lib/js/custom/md-top-app-bar.js"
13import "../lib/js/custom/md-slider-field.js"

B. js / nav-bar.js

1import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
2
3export class NavBar extends HTMLElement {
4
5 connectedCallback() {
6 this.classList.add("md-navigation-bar")
7
8 this.innerHTML = /* HTML */`
9 <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html">
10 <span class="material-symbols-outlined">home</span>
11 Inicio
12 </a>
13
14 <a ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html">
15 <span class="material-symbols-outlined">tabs</span>
16 Pestañas
17 </a>
18
19 <a id="navbar" ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html">
20 <span class="material-symbols-outlined">bottom_navigation</span>
21 Barra
22 </a>
23
24 <a ${resaltaSiEstasEn(["/formulario.html"])} href="formulario.html">
25 <span class="material-symbols-outlined">newspaper</span>
26 Forma
27 </a>`
28
29 }
30
31}
32
33customElements.define("nav-bar", NavBar)

C. js / nav-drw.js

1import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
2import { MdNavigationDrawer } from "../lib/js/custom/MdNavigationDrawer.js"
3
4export class NavDrw extends MdNavigationDrawer {
5
6 /**
7 * @override
8 */
9 getHipervinculos() {
10 return /* HTML */`
11 <h1>PWA con MD</h1>
12
13 <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html">
14 <span class="material-symbols-outlined">home</span>
15 Inicio
16 </a>
17
18 <a ${resaltaSiEstasEn(["/secundaria.html"])} href="secundaria.html">
19 <span class="material-symbols-outlined">scrollable_header</span>
20 Página secundaria
21 </a>
22
23 <a ${resaltaSiEstasEn(["/iconos.html"])} href="iconos.html">
24 <span class="material-symbols-outlined">sentiment_satisfied</span>
25 Íconos
26 </a>
27
28 <a ${resaltaSiEstasEn(["/botones.html"])} href="botones.html">
29 <span class="material-symbols-outlined">right_click</span>
30 Botones
31 </a>
32
33 <a ${resaltaSiEstasEn(["/campos.html"])} href="campos.html">
34 <span class="material-symbols-outlined">password</span>
35 Campos de texto
36 </a>
37
38 <a ${resaltaSiEstasEn(["/select.html"])} href="select.html">
39 <span class="material-symbols-outlined">bottom_panel_close</span>
40 Select
41 </a>
42
43 <a ${resaltaSiEstasEn(["/interruptor.html"])} href="interruptor.html">
44 <span class="material-symbols-outlined">toggle_on</span>
45 Interruptores
46 </a>
47
48 <a ${resaltaSiEstasEn(["/slider.html"])} href="slider.html">
49 <span class="material-symbols-outlined">linear_scale</span>
50 Sliders
51 </a>
52
53 <a ${resaltaSiEstasEn(["/segmentado.html"])} href="segmentado.html">
54 <span class="material-symbols-outlined">splitscreen_left</span>
55 Botón segmentado
56 </a>
57
58 <a ${resaltaSiEstasEn(["/one-line.html"])} href="one-line.html">
59 <span class="material-symbols-outlined">list</span>
60 Listas one-line
61 </a>
62
63 <a ${resaltaSiEstasEn(["/two-line.html"])} href="two-line.html">
64 <span class="material-symbols-outlined">lists</span>
65 Listas two-line
66 </a>
67
68 <a ${resaltaSiEstasEn(["/three-line.html"])} href="three-line.html">
69 <span class="material-symbols-outlined">receipt_long</span>
70 Listas three-line
71 </a>
72
73 <a ${resaltaSiEstasEn(["/tarjetas.html"])} href="tarjetas.html">
74 <span class="material-symbols-outlined">cards</span>
75 Tarjetas
76 </a>
77
78 <a ${resaltaSiEstasEn(["/navtab.html"])} href="navtab.html">
79 <span class="material-symbols-outlined">swipe_left</span>
80 Pestañas scrollable
81 </a>
82
83 <a ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html">
84 <span class="material-symbols-outlined">tabs</span>
85 Pestañas fijas
86 </a>
87
88 <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html">
89 <span class="material-symbols-outlined">bottom_navigation</span>
90 Barra de navegación
91 </a>
92
93 <a ${resaltaSiEstasEn(["/formulario.html"])} href="formulario.html">
94 <span class="material-symbols-outlined">newspaper</span>
95 Formulario
96 </a>
97
98 <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html">
99 <span class="material-symbols-outlined">help</span>
100 Ayuda
101 </a>`
102 }
103
104}
105
106customElements.define("nav-drw", NavDrw)

D. js / nav-tab-fixed.js

1import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
2
3export class NavTabFixed extends HTMLElement {
4
5 connectedCallback() {
6 this.classList.add("md-tab", "fixed")
7
8 this.innerHTML = /* HTML */`
9 <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html">
10 <span class="material-symbols-outlined">home</span>
11 Inicio
12 </a>
13
14 <a ${resaltaSiEstasEn(["/navtab.html"])} href="navtab.html">
15 <span class="material-symbols-outlined">swipe_left</span>
16 Pestañas scrollable
17 </a>
18
19 <a id="navtabfixed" ${resaltaSiEstasEn(["/navTabFixed.html"])}
20 href="navTabFixed.html">
21 <span class="material-symbols-outlined">tabs</span>
22 Pestañas fijas
23 </a>
24
25 <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html">
26 <span class="material-symbols-outlined">bottom_navigation</span>
27 Barra de navegación
28 </a>`
29 }
30
31}
32
33customElements.define("nav-tab-fixed", NavTabFixed)

E. js / nav-tab-scrollable.js

1import { querySelector } from "../lib/js/querySelector.js"
2import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
3
4export class NavTabScrollable extends HTMLElement {
5
6 connectedCallback() {
7 this.classList.add("md-tab", "scrollable")
8
9 this.innerHTML = /* HTML */`
10 <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html">
11 <span class="material-symbols-outlined">home</span>
12 Inicio
13 </a>
14
15 <a ${resaltaSiEstasEn(["/secundaria.html"])} href="secundaria.html">
16 <span class="material-symbols-outlined">scrollable_header</span>
17 Página secundaria
18 </a>
19
20 <a ${resaltaSiEstasEn(["/iconos.html"])} href="iconos.html">
21 <span class="material-symbols-outlined">sentiment_satisfied</span>
22 Íconos
23 </a>
24
25 <a ${resaltaSiEstasEn(["/botones.html"])} href="botones.html">
26 <span class="material-symbols-outlined">right_click</span>
27 Botones
28 </a>
29
30 <a ${resaltaSiEstasEn(["/campos.html"])} href="campos.html">
31 <span class="material-symbols-outlined">password</span>
32 Campos de texto
33 </a>
34
35 <a ${resaltaSiEstasEn(["/select.html"])} href="select.html">
36 <span class="material-symbols-outlined">bottom_panel_close</span>
37 Select
38 </a>
39
40 <a ${resaltaSiEstasEn(["/interruptor.html"])} href="interruptor.html">
41 <span class="material-symbols-outlined">toggle_on</span>
42 Interruptores
43 </a>
44
45 <a ${resaltaSiEstasEn(["/slider.html"])} href="slider.html">
46 <span class="material-symbols-outlined">linear_scale</span>
47 Sliders
48 </a>
49
50 <a ${resaltaSiEstasEn(["/segmentado.html"])} href="segmentado.html">
51 <span class="material-symbols-outlined">splitscreen_left</span>
52 Botón segmentado
53 </a>
54
55 <a ${resaltaSiEstasEn(["/one-line.html"])} href="one-line.html">
56 <span class="material-symbols-outlined">list</span>
57 Listas one-line
58 </a>
59
60 <a ${resaltaSiEstasEn(["/two-line.html"])} href="two-line.html">
61 <span class="material-symbols-outlined">lists</span>
62 Listas two-line
63 </a>
64
65 <a ${resaltaSiEstasEn(["/three-line.html"])} href="three-line.html">
66 <span class="material-symbols-outlined">receipt_long</span>
67 Listas three-line
68 </a>
69
70 <a ${resaltaSiEstasEn(["/tarjetas.html"])} href="tarjetas.html">
71 <span class="material-symbols-outlined">cards</span>
72 Tarjetas
73 </a>
74
75 <a id="navtab" ${resaltaSiEstasEn(["/navtab.html"])} href="navtab.html">
76 <span class="material-symbols-outlined">swipe_left</span>
77 Pestañas scrollable
78 </a>
79
80 <a ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html">
81 <span class="material-symbols-outlined">tabs</span>
82 Pestañas fijas
83 </a>
84
85 <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html">
86 <span class="material-symbols-outlined">bottom_navigation</span>
87 Barra de navegación
88 </a>
89
90 <a ${resaltaSiEstasEn(["/formulario.html"])} href="formulario.html">
91 <span class="material-symbols-outlined">newspaper</span>
92 Formulario
93 </a>
94
95 <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html">
96 <span class="material-symbols-outlined">help</span>
97 Ayuda
98 </a>`
99
100 }
101
102}
103
104customElements.define("nav-tab-scrollable", NavTabScrollable)

F. js / registraServiceWorker.js

1"use strict" // usa JavaScript en modo estricto.
2
3const nombreDeServiceWorker = "sw.js"
4
5try {
6 navigator.serviceWorker.register(nombreDeServiceWorker)
7 .then(registro => {
8 console.log(nombreDeServiceWorker, "registrado.")
9 console.log(registro)
10 })
11 .catch(error => console.log(error))
12} catch (error) {
13 console.log(error)
14}

25. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

short_name

Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.

name

Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.

id

Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.

start_url

Ruta del archivo inicial de la app.

display

Forma de mostrar la app. El término standalone significa que no se muestra la barra de navegación del navegador web.

theme_color

Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.

background_color

Color de fondo de la pantalla desplah en dispositivos móviles.

description

Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

screenshots

Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo que muestra el navegador al instalar la app. Debes incluir al menos una con "form_factor": "wide" y otra con "form_factor": "narrow".

icons

Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.

src

Url de la imagen dentro de la app.

sizes

Dimensiones en pixeles de la imagen, anchoxalto.

type

Tipo mime de la imagen.

purpose

Forma en que se usa la imagen.

maskable

La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.

any

No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.

Normalmente debe proporcionarse un juego de íconos con purpose any y otro juego de íconos con purpose maskable.

form_factor

Orientación de una screenshot.

wide

La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.

narrow

La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.

Debes incluir al menos una screenshot con "form_factor": "wide" y otra con "form_factor": "narrow".

label

Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

1{
2 "short_name": "PWA con MD",
3 "name": "PWA con Material Design",
4 "id": "/index.html",
5 "start_url": "/index.html",
6 "display": "standalone",
7 "theme_color": "#fffbfe",
8 "background_color": "#fffbfe",
9 "display_override": [
10 "window-controls-overlay"
11 ],
12 "description": "PWA con componentes de Material Design.",
13 "screenshots": [
14 {
15 "src": "/img/screenshot_horizontal.png",
16 "sizes": "1257x646",
17 "type": "image/png",
18 "form_factor": "wide",
19 "label": "PWA con Material Design"
20 },
21 {
22 "src": "/img/screenshot_vertical.png",
23 "sizes": "595x644",
24 "type": "image/png",
25 "form_factor": "narrow",
26 "label": "PWA con Material Design (2)"
27 }
28 ],
29 "icons": [
30 {
31 "src": "/img/maskable_icon_x48.png",
32 "sizes": "48x48",
33 "type": "image/png",
34 "purpose": "any"
35 },
36 {
37 "src": "/img/maskable_icon_x72.png",
38 "sizes": "72x72",
39 "type": "image/png",
40 "purpose": "any"
41 },
42 {
43 "src": "/img/maskable_icon_x96.png",
44 "sizes": "96x96",
45 "type": "image/png",
46 "purpose": "maskable"
47 },
48 {
49 "src": "/img/maskable_icon_x128.png",
50 "sizes": "128x128",
51 "type": "image/png",
52 "purpose": "any"
53 },
54 {
55 "src": "/img/maskable_icon_x192.png",
56 "sizes": "192x192",
57 "type": "image/png",
58 "purpose": "any"
59 },
60 {
61 "src": "/img/maskable_icon_x384.png",
62 "sizes": "384x384",
63 "type": "image/png",
64 "purpose": "any"
65 },
66 {
67 "src": "/img/maskable_icon_x512.png",
68 "sizes": "512x512",
69 "type": "image/png",
70 "purpose": "any"
71 },
72 {
73 "src": "/img/maskable_icon.png",
74 "sizes": "3413x3413",
75 "type": "image/png",
76 "purpose": "any"
77 },
78 {
79 "src": "/img/icono2048.png",
80 "sizes": "2048x2048",
81 "type": "image/png",
82 "purpose": "any"
83 },
84 {
85 "src": "/img/maskable_icon_x48.png",
86 "sizes": "48x48",
87 "type": "image/png",
88 "purpose": "maskable"
89 },
90 {
91 "src": "/img/maskable_icon_x72.png",
92 "sizes": "72x72",
93 "type": "image/png",
94 "purpose": "maskable"
95 },
96 {
97 "src": "/img/maskable_icon_x96.png",
98 "sizes": "96x96",
99 "type": "image/png",
100 "purpose": "maskable"
101 },
102 {
103 "src": "/img/maskable_icon_x128.png",
104 "sizes": "128x128",
105 "type": "image/png",
106 "purpose": "maskable"
107 },
108 {
109 "src": "/img/maskable_icon_x192.png",
110 "sizes": "192x192",
111 "type": "image/png",
112 "purpose": "maskable"
113 },
114 {
115 "src": "/img/maskable_icon_x384.png",
116 "sizes": "384x384",
117 "type": "image/png",
118 "purpose": "maskable"
119 },
120 {
121 "src": "/img/maskable_icon_x512.png",
122 "sizes": "512x512",
123 "type": "image/png",
124 "purpose": "maskable"
125 },
126 {
127 "src": "/img/maskable_icon.png",
128 "sizes": "3413x3413",
129 "type": "image/png",
130 "purpose": "maskable"
131 }
132 ]
133}

26. instruccionesListadoSw.txt

1Generar el listado de archivos del sw.js desde Visual Studio Code.
21. Abrir una terminal desde el menú
3 Terminal > New Terminal
4
52. Desde la terminal introducir la orden:
6 Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
83. Abrir el archivo generado, que se llama
9 archivos.txt
10 y sobre este, realizar los pasos que siguen:
11
124. Quita del archivo archivos.txt:
13 * el encabezado,
14 * todas las carpetas,
15 * el archivo .vscode/settings.json,
16 * el archivo .htaccess,
17 * el archivo archivos.txt,
18 * este archivo (instruccionesListadoSw.txt),
19 * el archivo jsconfig.json,
20 * el archivo sw.js,
21 * el archivo de la base de datos, que termina en ".bd" y
22 está en la carpeta srv,
23 * todos los archivos de php y
24 * las líneas en blanco del final
25
265. Cambia los \ por / desde Visual Studio Code con las siguientes
27 combinaciones de teclas:
28
29 Ctrl+H En el diálogo que aparece introduce lo siguiente:
30 Find:\
31 Replace:/
32
33 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
34
356. Coloca las comillas y coma del final de cada línea desde Visual
36 Studio Code con las siguientes combinaciones de teclas:
37
38 Ctrl+H En el diálogo que aparece, selecciona el botón
39 ".*"
40 e introduce lo siguiente:
41 Find:\s*$
42 Replace:",
43
44 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
45
467. Marca la carpeta inicial, presiona la combinación de teclas:
47
48 Shift+Ctrl+L
49
50 borra la selección, teclea " y luego ESC
51
528. Cambia las secuencias de espacios por / con las siguientes
53 combinaciones de teclas:
54
55 Ctrl+H En el diálogo que aparece, selecciona el botón
56 ".*"
57 e introduce lo siguiente:
58 Find:\s+
59 Replace:/
60
61 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
62
639. Cambia las "/ por " con las siguientes combinaciones de teclas:
64
65 Ctrl+H En el diálogo que aparece, quita la selección del botón
66 ".*"
67 e introduce lo siguiente:
68 Find:"/
69 Replace:"
70
71 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
72
7310. Copia el texto al archivo
74 sw.js
75 en el contenido del arreglo llamado ARCHIVOS, pero recuerda
76 mantener el último elemento, que dice:
77 "/"

27. archivos.txt

1"ayuda.html",
2"botones.html",
3"campos.html",
4"favicon.ico",
5"formulario.html",
6"iconos.html",
7"index.html",
8"interruptor.html",
9"navbar.html",
10"navtab.html",
11"navTabFixed.html",
12"one-line.html",
13"secundaria.html",
14"segmentado.html",
15"select.html",
16"site.webmanifest",
17"slider.html",
18"tarjetas.html",
19"three-line.html",
20"two-line.html",
21"css/dark-hc.css",
22"css/dark-mc.css",
23"css/dark.css",
24"css/estilos.css",
25"css/light-hc.css",
26"css/light-mc.css",
27"css/light.css",
28"css/transicion_completa.css",
29"css/transicion_pestanas.css",
30"img/Escultura_de_coyote.jpeg",
31"img/icono2048.png",
32"img/maskable_icon.png",
33"img/maskable_icon_x128.png",
34"img/maskable_icon_x192.png",
35"img/maskable_icon_x384.png",
36"img/maskable_icon_x48.png",
37"img/maskable_icon_x512.png",
38"img/maskable_icon_x72.png",
39"img/maskable_icon_x96.png",
40"img/pexels-craig-dennis-3701822.jpg",
41"img/pexels-creative-workshop-3978352.jpg",
42"img/pexels-erik-karits-3732453.jpg",
43"img/pexels-esteban-arango-10226903.jpg",
44"img/pexels-moises-patrício-10961948.jpg",
45"img/pexels-ralph-2270848.jpg",
46"img/pexels-rasmus-svinding-35435.jpg",
47"img/pexels-steve-397857.jpg",
48"img/pexels-vadim-b-141496.jpg",
49"img/screenshot_horizontal.png",
50"img/screenshot_vertical.png",
51"js/configura.js",
52"js/nav-bar.js",
53"js/nav-drw.js",
54"js/nav-tab-fixed.js",
55"js/nav-tab-scrollable.js",
56"js/registraServiceWorker.js",
57"lib/css/material-symbols-outlined.css",
58"lib/css/md-cards.css",
59"lib/css/md-fab-primary.css",
60"lib/css/md-filled-button.css",
61"lib/css/md-filled-text-field.css",
62"lib/css/md-list.css",
63"lib/css/md-menu.css",
64"lib/css/md-navigation-bar.css",
65"lib/css/md-outline-button.css",
66"lib/css/md-ripple.css",
67"lib/css/md-segmented-button.css",
68"lib/css/md-slider-field.css",
69"lib/css/md-standard-icon-button.css",
70"lib/css/md-switch.css",
71"lib/css/md-tab.css",
72"lib/css/md-top-app-bar.css",
73"lib/css/roboto.css",
74"lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
75"lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
76"lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
77"lib/fonts/roboto-v32-latin-regular.woff2",
78"lib/js/abreElementoHtml.js",
79"lib/js/cierraElementoHtmo.js",
80"lib/js/exportaAHtml.js",
81"lib/js/getAttribute.js",
82"lib/js/htmlentities.js",
83"lib/js/muestraError.js",
84"lib/js/muestraTextoDeAyuda.js",
85"lib/js/ProblemDetails.js",
86"lib/js/querySelector.js",
87"lib/js/resaltaSiEstasEn.js",
88"lib/js/const/ES_APPLE.js",
89"lib/js/custom/md-menu-button.js",
90"lib/js/custom/md-options-menu.js",
91"lib/js/custom/md-overflow-button.js",
92"lib/js/custom/md-overflow-menu.js",
93"lib/js/custom/md-select-menu.js",
94"lib/js/custom/md-slider-field.js",
95"lib/js/custom/md-top-app-bar.js",
96"lib/js/custom/MdNavigationDrawer.js",
97"material-tokens/css/baseline.css",
98"material-tokens/css/colors.css",
99"material-tokens/css/elevation.css",
100"material-tokens/css/motion.css",
101"material-tokens/css/palette.css",
102"material-tokens/css/shape.css",
103"material-tokens/css/state.css",
104"material-tokens/css/typography.css",
105"material-tokens/css/theme/dark.css",
106"material-tokens/css/theme/light.css",
107"ungap/custom-elements.js",

28. sw.js

1/* Este archivo debe estar colocado en la carpeta raíz del sitio.
2 *
3 * Cualquier cambio en el contenido de este archivo hace que el service
4 * worker se reinstale. */
5
6/**
7 * Cambia el número de la versión cuando cambia el contenido de los
8 * archivos.
9 *
10 * El número a la izquierda del punto (.), en este caso <q>1</q>, se
11 * conoce como número mayor y se cambia cuando se realizan
12 * modificaciones grandes o importantes.
13 *
14 * El número a la derecha del punto (.), en este caso <q>00</q>, se
15 * conoce como número menor y se cambia cuando se realizan
16 * modificaciones menores.
17 */
18const VERSION = "1.00"
19
20/**
21 * Nombre de la carpeta de caché.
22 */
23const CACHE = "pwamd"
24
25/**
26 * Archivos requeridos para que la aplicación funcione fuera de
27 * línea.
28 */
29const ARCHIVOS = [
30 "ayuda.html",
31 "botones.html",
32 "campos.html",
33 "favicon.ico",
34 "formulario.html",
35 "iconos.html",
36 "index.html",
37 "interruptor.html",
38 "navbar.html",
39 "navtab.html",
40 "navTabFixed.html",
41 "one-line.html",
42 "secundaria.html",
43 "segmentado.html",
44 "select.html",
45 "site.webmanifest",
46 "slider.html",
47 "tarjetas.html",
48 "three-line.html",
49 "two-line.html",
50 "css/dark-hc.css",
51 "css/dark-mc.css",
52 "css/dark.css",
53 "css/estilos.css",
54 "css/light-hc.css",
55 "css/light-mc.css",
56 "css/light.css",
57 "css/transicion_completa.css",
58 "css/transicion_pestanas.css",
59 "img/Escultura_de_coyote.jpeg",
60 "img/icono2048.png",
61 "img/maskable_icon.png",
62 "img/maskable_icon_x128.png",
63 "img/maskable_icon_x192.png",
64 "img/maskable_icon_x384.png",
65 "img/maskable_icon_x48.png",
66 "img/maskable_icon_x512.png",
67 "img/maskable_icon_x72.png",
68 "img/maskable_icon_x96.png",
69 "img/pexels-craig-dennis-3701822.jpg",
70 "img/pexels-creative-workshop-3978352.jpg",
71 "img/pexels-erik-karits-3732453.jpg",
72 "img/pexels-esteban-arango-10226903.jpg",
73 "img/pexels-moises-patrício-10961948.jpg",
74 "img/pexels-ralph-2270848.jpg",
75 "img/pexels-rasmus-svinding-35435.jpg",
76 "img/pexels-steve-397857.jpg",
77 "img/pexels-vadim-b-141496.jpg",
78 "img/screenshot_horizontal.png",
79 "img/screenshot_vertical.png",
80 "js/configura.js",
81 "js/nav-bar.js",
82 "js/nav-drw.js",
83 "js/nav-tab-fixed.js",
84 "js/nav-tab-scrollable.js",
85 "js/registraServiceWorker.js",
86 "lib/css/material-symbols-outlined.css",
87 "lib/css/md-cards.css",
88 "lib/css/md-fab-primary.css",
89 "lib/css/md-filled-button.css",
90 "lib/css/md-filled-text-field.css",
91 "lib/css/md-list.css",
92 "lib/css/md-menu.css",
93 "lib/css/md-navigation-bar.css",
94 "lib/css/md-outline-button.css",
95 "lib/css/md-ripple.css",
96 "lib/css/md-segmented-button.css",
97 "lib/css/md-slider-field.css",
98 "lib/css/md-standard-icon-button.css",
99 "lib/css/md-switch.css",
100 "lib/css/md-tab.css",
101 "lib/css/md-top-app-bar.css",
102 "lib/css/roboto.css",
103 "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
104 "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
105 "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
106 "lib/fonts/roboto-v32-latin-regular.woff2",
107 "lib/js/abreElementoHtml.js",
108 "lib/js/cierraElementoHtmo.js",
109 "lib/js/exportaAHtml.js",
110 "lib/js/getAttribute.js",
111 "lib/js/htmlentities.js",
112 "lib/js/muestraError.js",
113 "lib/js/muestraTextoDeAyuda.js",
114 "lib/js/ProblemDetails.js",
115 "lib/js/querySelector.js",
116 "lib/js/resaltaSiEstasEn.js",
117 "lib/js/const/ES_APPLE.js",
118 "lib/js/custom/md-menu-button.js",
119 "lib/js/custom/md-options-menu.js",
120 "lib/js/custom/md-overflow-button.js",
121 "lib/js/custom/md-overflow-menu.js",
122 "lib/js/custom/md-select-menu.js",
123 "lib/js/custom/md-slider-field.js",
124 "lib/js/custom/md-top-app-bar.js",
125 "lib/js/custom/MdNavigationDrawer.js",
126 "material-tokens/css/baseline.css",
127 "material-tokens/css/colors.css",
128 "material-tokens/css/elevation.css",
129 "material-tokens/css/motion.css",
130 "material-tokens/css/palette.css",
131 "material-tokens/css/shape.css",
132 "material-tokens/css/state.css",
133 "material-tokens/css/typography.css",
134 "material-tokens/css/theme/dark.css",
135 "material-tokens/css/theme/light.css",
136 "ungap/custom-elements.js",
137 "/"
138]
139
140// Verifica si el código corre dentro de un service worker.
141if (self instanceof ServiceWorkerGlobalScope) {
142 // Evento al empezar a instalar el servide worker,
143 self.addEventListener("install",
144 (/** @type {ExtendableEvent} */ evt) => {
145 console.log("El service worker se está instalando.")
146 evt.waitUntil(llenaElCache())
147 })
148
149 // Evento al solicitar información a la red.
150 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
151 if (evt.request.method === "GET") {
152 evt.respondWith(buscaLaRespuestaEnElCache(evt))
153 }
154 })
155
156 // Evento cuando el service worker se vuelve activo.
157 self.addEventListener("activate",
158 () => console.log("El service worker está activo."))
159}
160
161async function llenaElCache() {
162 console.log("Intentando cargar caché:", CACHE)
163 // Borra todos los cachés.
164 const keys = await caches.keys()
165 for (const key of keys) {
166 await caches.delete(key)
167 }
168 // Abre el caché de este service worker.
169 const cache = await caches.open(CACHE)
170 // Carga el listado de ARCHIVOS.
171 await cache.addAll(ARCHIVOS)
172 console.log("Cache cargado:", CACHE)
173 console.log("Versión:", VERSION)
174}
175
176/** @param {FetchEvent} evt */
177async function buscaLaRespuestaEnElCache(evt) {
178 // Abre el caché.
179 const cache = await caches.open(CACHE)
180 const request = evt.request
181 /* Busca la respuesta a la solicitud en el contenido del caché, sin
182 * tomar en cuenta la parte después del símbolo "?" en la URL. */
183 const response = await cache.match(request, { ignoreSearch: true })
184 if (response === undefined) {
185 /* Si no la encuentra, empieza a descargar de la red y devuelve
186 * la promesa. */
187 return fetch(request)
188 } else {
189 // Si la encuentra, devuelve la respuesta encontrada en el caché.
190 return response
191 }
192}

29. Carpeta « css »

A. css / dark-hc.css

1.dark-high-contrast {
2 --md-sys-color-primary: rgb(251 250 255);
3 --md-sys-color-surface-tint: rgb(170 199 255);
4 --md-sys-color-on-primary: rgb(0 0 0);
5 --md-sys-color-primary-container: rgb(177 203 255);
6 --md-sys-color-on-primary-container: rgb(0 0 0);
7 --md-sys-color-secondary: rgb(251 250 255);
8 --md-sys-color-on-secondary: rgb(0 0 0);
9 --md-sys-color-secondary-container: rgb(194 203 224);
10 --md-sys-color-on-secondary-container: rgb(0 0 0);
11 --md-sys-color-tertiary: rgb(255 249 250);
12 --md-sys-color-on-tertiary: rgb(0 0 0);
13 --md-sys-color-tertiary-container: rgb(225 192 229);
14 --md-sys-color-on-tertiary-container: rgb(0 0 0);
15 --md-sys-color-error: rgb(255 249 249);
16 --md-sys-color-on-error: rgb(0 0 0);
17 --md-sys-color-error-container: rgb(255 186 177);
18 --md-sys-color-on-error-container: rgb(0 0 0);
19 --md-sys-color-background: rgb(17 19 24);
20 --md-sys-color-on-background: rgb(226 226 233);
21 --md-sys-color-surface: rgb(17 19 24);
22 --md-sys-color-on-surface: rgb(255 255 255);
23 --md-sys-color-surface-variant: rgb(68 71 78);
24 --md-sys-color-on-surface-variant: rgb(251 250 255);
25 --md-sys-color-outline: rgb(200 202 212);
26 --md-sys-color-outline-variant: rgb(200 202 212);
27 --md-sys-color-shadow: rgb(0 0 0);
28 --md-sys-color-scrim: rgb(0 0 0);
29 --md-sys-color-inverse-surface: rgb(226 226 233);
30 --md-sys-color-inverse-on-surface: rgb(0 0 0);
31 --md-sys-color-inverse-primary: rgb(0 41 89);
32 --md-sys-color-primary-fixed: rgb(221 231 255);
33 --md-sys-color-on-primary-fixed: rgb(0 0 0);
34 --md-sys-color-primary-fixed-dim: rgb(177 203 255);
35 --md-sys-color-on-primary-fixed-variant: rgb(0 22 52);
36 --md-sys-color-secondary-fixed: rgb(222 231 253);
37 --md-sys-color-on-secondary-fixed: rgb(0 0 0);
38 --md-sys-color-secondary-fixed-dim: rgb(194 203 224);
39 --md-sys-color-on-secondary-fixed-variant: rgb(13 22 38);
40 --md-sys-color-tertiary-fixed: rgb(252 221 255);
41 --md-sys-color-on-tertiary-fixed: rgb(0 0 0);
42 --md-sys-color-tertiary-fixed-dim: rgb(225 192 229);
43 --md-sys-color-on-tertiary-fixed-variant: rgb(35 14 41);
44 --md-sys-color-surface-dim: rgb(17 19 24);
45 --md-sys-color-surface-bright: rgb(55 57 62);
46 --md-sys-color-surface-container-lowest: rgb(12 14 19);
47 --md-sys-color-surface-container-low: rgb(25 28 32);
48 --md-sys-color-surface-container: rgb(29 32 36);
49 --md-sys-color-surface-container-high: rgb(40 42 47);
50 --md-sys-color-surface-container-highest: rgb(51 53 58);
51}
52

B. css / dark-mc.css

1.dark-medium-contrast {
2 --md-sys-color-primary: rgb(177 203 255);
3 --md-sys-color-surface-tint: rgb(170 199 255);
4 --md-sys-color-on-primary: rgb(0 22 52);
5 --md-sys-color-primary-container: rgb(116 145 199);
6 --md-sys-color-on-primary-container: rgb(0 0 0);
7 --md-sys-color-secondary: rgb(194 203 224);
8 --md-sys-color-on-secondary: rgb(13 22 38);
9 --md-sys-color-secondary-container: rgb(136 145 165);
10 --md-sys-color-on-secondary-container: rgb(0 0 0);
11 --md-sys-color-tertiary: rgb(225 192 229);
12 --md-sys-color-on-tertiary: rgb(35 14 41);
13 --md-sys-color-tertiary-container: rgb(164 135 169);
14 --md-sys-color-on-tertiary-container: rgb(0 0 0);
15 --md-sys-color-error: rgb(255 186 177);
16 --md-sys-color-on-error: rgb(55 0 1);
17 --md-sys-color-error-container: rgb(255 84 73);
18 --md-sys-color-on-error-container: rgb(0 0 0);
19 --md-sys-color-background: rgb(17 19 24);
20 --md-sys-color-on-background: rgb(226 226 233);
21 --md-sys-color-surface: rgb(17 19 24);
22 --md-sys-color-on-surface: rgb(251 250 255);
23 --md-sys-color-surface-variant: rgb(68 71 78);
24 --md-sys-color-on-surface-variant: rgb(200 202 212);
25 --md-sys-color-outline: rgb(160 163 172);
26 --md-sys-color-outline-variant: rgb(128 131 140);
27 --md-sys-color-shadow: rgb(0 0 0);
28 --md-sys-color-scrim: rgb(0 0 0);
29 --md-sys-color-inverse-surface: rgb(226 226 233);
30 --md-sys-color-inverse-on-surface: rgb(40 42 47);
31 --md-sys-color-inverse-primary: rgb(41 72 120);
32 --md-sys-color-primary-fixed: rgb(214 227 255);
33 --md-sys-color-on-primary-fixed: rgb(0 17 43);
34 --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35 --md-sys-color-on-primary-fixed-variant: rgb(19 54 101);
36 --md-sys-color-secondary-fixed: rgb(218 226 249);
37 --md-sys-color-on-secondary-fixed: rgb(8 17 33);
38 --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39 --md-sys-color-on-secondary-fixed-variant: rgb(46 54 71);
40 --md-sys-color-tertiary-fixed: rgb(250 216 253);
41 --md-sys-color-on-tertiary-fixed: rgb(29 8 35);
42 --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43 --md-sys-color-on-tertiary-fixed-variant: rgb(69 46 74);
44 --md-sys-color-surface-dim: rgb(17 19 24);
45 --md-sys-color-surface-bright: rgb(55 57 62);
46 --md-sys-color-surface-container-lowest: rgb(12 14 19);
47 --md-sys-color-surface-container-low: rgb(25 28 32);
48 --md-sys-color-surface-container: rgb(29 32 36);
49 --md-sys-color-surface-container-high: rgb(40 42 47);
50 --md-sys-color-surface-container-highest: rgb(51 53 58);
51}
52

C. css / dark.css

1.dark {
2 --md-sys-color-primary: rgb(170 199 255);
3 --md-sys-color-surface-tint: rgb(170 199 255);
4 --md-sys-color-on-primary: rgb(10 48 95);
5 --md-sys-color-primary-container: rgb(40 71 119);
6 --md-sys-color-on-primary-container: rgb(214 227 255);
7 --md-sys-color-secondary: rgb(190 198 220);
8 --md-sys-color-on-secondary: rgb(40 49 65);
9 --md-sys-color-secondary-container: rgb(62 71 89);
10 --md-sys-color-on-secondary-container: rgb(218 226 249);
11 --md-sys-color-tertiary: rgb(221 188 224);
12 --md-sys-color-on-tertiary: rgb(63 40 68);
13 --md-sys-color-tertiary-container: rgb(87 62 92);
14 --md-sys-color-on-tertiary-container: rgb(250 216 253);
15 --md-sys-color-error: rgb(255 180 171);
16 --md-sys-color-on-error: rgb(105 0 5);
17 --md-sys-color-error-container: rgb(147 0 10);
18 --md-sys-color-on-error-container: rgb(255 218 214);
19 --md-sys-color-background: rgb(17 19 24);
20 --md-sys-color-on-background: rgb(226 226 233);
21 --md-sys-color-surface: rgb(17 19 24);
22 --md-sys-color-on-surface: rgb(226 226 233);
23 --md-sys-color-surface-variant: rgb(68 71 78);
24 --md-sys-color-on-surface-variant: rgb(196 198 208);
25 --md-sys-color-outline: rgb(142 144 153);
26 --md-sys-color-outline-variant: rgb(68 71 78);
27 --md-sys-color-shadow: rgb(0 0 0);
28 --md-sys-color-scrim: rgb(0 0 0);
29 --md-sys-color-inverse-surface: rgb(226 226 233);
30 --md-sys-color-inverse-on-surface: rgb(46 48 54);
31 --md-sys-color-inverse-primary: rgb(65 95 145);
32 --md-sys-color-primary-fixed: rgb(214 227 255);
33 --md-sys-color-on-primary-fixed: rgb(0 27 62);
34 --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35 --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36 --md-sys-color-secondary-fixed: rgb(218 226 249);
37 --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38 --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39 --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40 --md-sys-color-tertiary-fixed: rgb(250 216 253);
41 --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42 --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43 --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44 --md-sys-color-surface-dim: rgb(17 19 24);
45 --md-sys-color-surface-bright: rgb(55 57 62);
46 --md-sys-color-surface-container-lowest: rgb(12 14 19);
47 --md-sys-color-surface-container-low: rgb(25 28 32);
48 --md-sys-color-surface-container: rgb(29 32 36);
49 --md-sys-color-surface-container-high: rgb(40 42 47);
50 --md-sys-color-surface-container-highest: rgb(51 53 58);
51}
52

D. css / estilos.css

1/* Selecciona el tema claro para Material Design 3. Puedes elegir
2 * light.css, light-mc.css o light-hc.css */
3@import url(light.css) screen and (prefers-color-scheme: light);
4/* Selecciona el tema oscuro para Material Design 3. Puedes elegir
5 * dark.css, dark-mc.css o dark-hc.css */
6@import url(dark.css) screen and (prefers-color-scheme: dark);
7/* Definiciones para Material Design 3 */
8@import url(../material-tokens/css/baseline.css);
9/* Fonts utilizados */
10@import url(../lib/css/roboto.css);
11@import url(../lib/css/material-symbols-outlined.css);
12/* CSS de elementos utilizados */
13@import url(../lib/css/md-ripple.css);
14@import url(../lib/css/md-top-app-bar.css);
15@import url(../lib/css/md-menu.css);
16@import url(../lib/css/md-standard-icon-button.css);
17@import url(../lib/css/md-fab-primary.css);
18@import url(../lib/css/md-filled-button.css);
19@import url(../lib/css/md-filled-text-field.css);
20@import url(../lib/css/md-outline-button.css);
21@import url(../lib/css/md-switch.css);
22@import url(../lib/css/md-slider-field.css);
23@import url(../lib/css/md-segmented-button.css);
24@import url(../lib/css/md-list.css);
25@import url(../lib/css/md-cards.css);
26@import url(../lib/css/md-tab.css);
27@import url(../lib/css/md-navigation-bar.css);
28
29html {
30 /* Indica los temas del sistema operativo que son soportados. */
31 color-scheme: light dark;
32 --tabWidth: 3.75rem;
33 --anchoNav: 22.5rem;
34}
35
36main {
37 max-width: 600px;
38 margin-left: auto;
39 margin-right: auto;
40}
41
42/* Quita un borde rojo que coloca Firefox. */
43:-moz-ui-invalid {
44 box-shadow: none;
45}
46
47body {
48 margin: 0;
49 font-family: var(--md-sys-typescale-body-large-font);
50 font-weight: var(--md-sys-typescale-body-large-weight);
51 font-size: var(--md-sys-typescale-body-large-size);
52 font-style: var(--md-sys-typescale-body-large-font-style);
53 letter-spacing: var(--md-sys-typescale-body-large-tracking);
54 line-height: var(--md-sys-typescale-body-large-line-height);
55 text-transform: var(--md-sys-typescale-body-large-text-transform);
56 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
57 color: var(--md-sys-color-on-background);
58 background-color: var(--md-sys-color-background);
59 /* Las siguientes líneas Evita los cambios de apariencia al cargar estilos y
60 + custom elements, que son conocidos como Flash Of Unstyled Content (fouc). */
61 opacity: 0;
62 animation-name: fouc;
63 animation-fill-mode: forwards;
64 animation-duration: 1.5s;
65}
66
67@keyframes fouc {
68 to {
69 opacity: 1;
70 }
71}
72
73html {
74 --Font: -apple-system, BlinkMacSystemFont, roboto, sans-serif;
75 --colIntIos: white;
76 --colIntIosOnBk: #2acc2a;
77 --colIntIosOnBkFc: #1bbb1b;
78 --colIntIosOffBk: #dbdbdb;
79 --colIntIosOffBkFc: #BDBDBD;
80 /* Plain typeface */
81 --md-ref-typeface-plain: var(--Font);
82 /* Brand typeface */
83 --md-ref-typeface-brand: var(--Font);
84 --md-sys-typescale-label-large-weight-prominent:
85 var(--md-ref-typeface-weight-bold);
86 --md-box_shadow_level4:
87 0 var(--md-sys-elevation-level4) var(--md-sys-elevation-level4) var(--md-sys-color-shadow);
88 --md-box_shadow_level3:
89 0 var(--md-sys-elevation-level3) var(--md-sys-elevation-level3) var(--md-sys-color-shadow);
90 --md-box_shadow_level2:
91 0 var(--md-sys-elevation-level2) var(--md-sys-elevation-level2) var(--md-sys-color-shadow);
92 --md-box_shadow_level1:
93 0 var(--md-sys-elevation-level1) var(--md-sys-elevation-level1) var(--md-sys-color-shadow);
94 --md-box_shadow_level0: none;
95 --iconSize: 1.5rem;
96 --avatarSize: 2.5rem;
97 --imageSize: 3.5rem;
98 --videoWidth: 7.125rem;
99 --videoHeight: 4rem;
100 --scroll-headline-duracion: 2s;
101 --md-sys-state-focus-indicator-outer-offset: 0.125rem;
102 --md-sys-state-focus-indicator-thickness: 0.1875rem;
103 /* Pressed state layer opacity */
104 --state-pressed-transparency-percentage: 84%;
105 /* Focus state layer opacity */
106 --state-focus-transparency-percentage: 88%;
107 /* Hover state layer opacity */
108 --state-hover-transparency-percentage: 92%;
109}
110
111p {
112 margin: 1rem;
113}
114
115a {
116 color: var(--md-sys-color-on-background);
117}
118
119@media (prefers-color-scheme: light) {
120 html {
121 --md-riple-color: #00000020;
122 }
123}
124
125@media (prefers-color-scheme: dark) {
126 html {
127 --md-riple-color: #ffffff40;
128 }
129}
130
131@keyframes salePorLaIzquierda {
132 to {
133 translate: -100vw 0;
134 }
135}
136
137@keyframes entraPorLaDerecha {
138 from {
139 translate: 100vw 0;
140 }
141}
142
143@keyframes aparece {
144 from {
145 opacity: 0;
146 }
147}
148
149@keyframes desvanece {
150 to {
151 opacity: 0;
152 }
153}

E. css / light-hc.css

1.light-high-contrast {
2 --md-sys-color-primary: rgb(0 33 74);
3 --md-sys-color-surface-tint: rgb(65 95 145);
4 --md-sys-color-on-primary: rgb(255 255 255);
5 --md-sys-color-primary-container: rgb(35 67 115);
6 --md-sys-color-on-primary-container: rgb(255 255 255);
7 --md-sys-color-secondary: rgb(25 34 50);
8 --md-sys-color-on-secondary: rgb(255 255 255);
9 --md-sys-color-secondary-container: rgb(58 67 84);
10 --md-sys-color-on-secondary-container: rgb(255 255 255);
11 --md-sys-color-tertiary: rgb(48 26 53);
12 --md-sys-color-on-tertiary: rgb(255 255 255);
13 --md-sys-color-tertiary-container: rgb(82 58 88);
14 --md-sys-color-on-tertiary-container: rgb(255 255 255);
15 --md-sys-color-error: rgb(78 0 2);
16 --md-sys-color-on-error: rgb(255 255 255);
17 --md-sys-color-error-container: rgb(140 0 9);
18 --md-sys-color-on-error-container: rgb(255 255 255);
19 --md-sys-color-background: rgb(249 249 255);
20 --md-sys-color-on-background: rgb(25 28 32);
21 --md-sys-color-surface: rgb(249 249 255);
22 --md-sys-color-on-surface: rgb(0 0 0);
23 --md-sys-color-surface-variant: rgb(224 226 236);
24 --md-sys-color-on-surface-variant: rgb(33 36 43);
25 --md-sys-color-outline: rgb(64 67 74);
26 --md-sys-color-outline-variant: rgb(64 67 74);
27 --md-sys-color-shadow: rgb(0 0 0);
28 --md-sys-color-scrim: rgb(0 0 0);
29 --md-sys-color-inverse-surface: rgb(46 48 54);
30 --md-sys-color-inverse-on-surface: rgb(255 255 255);
31 --md-sys-color-inverse-primary: rgb(229 236 255);
32 --md-sys-color-primary-fixed: rgb(35 67 115);
33 --md-sys-color-on-primary-fixed: rgb(255 255 255);
34 --md-sys-color-primary-fixed-dim: rgb(4 44 91);
35 --md-sys-color-on-primary-fixed-variant: rgb(255 255 255);
36 --md-sys-color-secondary-fixed: rgb(58 67 84);
37 --md-sys-color-on-secondary-fixed: rgb(255 255 255);
38 --md-sys-color-secondary-fixed-dim: rgb(36 45 61);
39 --md-sys-color-on-secondary-fixed-variant: rgb(255 255 255);
40 --md-sys-color-tertiary-fixed: rgb(82 58 88);
41 --md-sys-color-on-tertiary-fixed: rgb(255 255 255);
42 --md-sys-color-tertiary-fixed-dim: rgb(59 36 64);
43 --md-sys-color-on-tertiary-fixed-variant: rgb(255 255 255);
44 --md-sys-color-surface-dim: rgb(217 217 224);
45 --md-sys-color-surface-bright: rgb(249 249 255);
46 --md-sys-color-surface-container-lowest: rgb(255 255 255);
47 --md-sys-color-surface-container-low: rgb(243 243 250);
48 --md-sys-color-surface-container: rgb(237 237 244);
49 --md-sys-color-surface-container-high: rgb(231 232 238);
50 --md-sys-color-surface-container-highest: rgb(226 226 233);
51}
52

F. css / light-mc.css

1.light-medium-contrast {
2 --md-sys-color-primary: rgb(35 67 115);
3 --md-sys-color-surface-tint: rgb(65 95 145);
4 --md-sys-color-on-primary: rgb(255 255 255);
5 --md-sys-color-primary-container: rgb(88 117 168);
6 --md-sys-color-on-primary-container: rgb(255 255 255);
7 --md-sys-color-secondary: rgb(58 67 84);
8 --md-sys-color-on-secondary: rgb(255 255 255);
9 --md-sys-color-secondary-container: rgb(108 117 136);
10 --md-sys-color-on-secondary-container: rgb(255 255 255);
11 --md-sys-color-tertiary: rgb(82 58 88);
12 --md-sys-color-on-tertiary: rgb(255 255 255);
13 --md-sys-color-tertiary-container: rgb(135 107 140);
14 --md-sys-color-on-tertiary-container: rgb(255 255 255);
15 --md-sys-color-error: rgb(140 0 9);
16 --md-sys-color-on-error: rgb(255 255 255);
17 --md-sys-color-error-container: rgb(218 52 46);
18 --md-sys-color-on-error-container: rgb(255 255 255);
19 --md-sys-color-background: rgb(249 249 255);
20 --md-sys-color-on-background: rgb(25 28 32);
21 --md-sys-color-surface: rgb(249 249 255);
22 --md-sys-color-on-surface: rgb(25 28 32);
23 --md-sys-color-surface-variant: rgb(224 226 236);
24 --md-sys-color-on-surface-variant: rgb(64 67 74);
25 --md-sys-color-outline: rgb(92 95 103);
26 --md-sys-color-outline-variant: rgb(120 122 131);
27 --md-sys-color-shadow: rgb(0 0 0);
28 --md-sys-color-scrim: rgb(0 0 0);
29 --md-sys-color-inverse-surface: rgb(46 48 54);
30 --md-sys-color-inverse-on-surface: rgb(240 240 247);
31 --md-sys-color-inverse-primary: rgb(170 199 255);
32 --md-sys-color-primary-fixed: rgb(88 117 168);
33 --md-sys-color-on-primary-fixed: rgb(255 255 255);
34 --md-sys-color-primary-fixed-dim: rgb(62 92 142);
35 --md-sys-color-on-primary-fixed-variant: rgb(255 255 255);
36 --md-sys-color-secondary-fixed: rgb(108 117 136);
37 --md-sys-color-on-secondary-fixed: rgb(255 255 255);
38 --md-sys-color-secondary-fixed-dim: rgb(83 92 111);
39 --md-sys-color-on-secondary-fixed-variant: rgb(255 255 255);
40 --md-sys-color-tertiary-fixed: rgb(135 107 140);
41 --md-sys-color-on-tertiary-fixed: rgb(255 255 255);
42 --md-sys-color-tertiary-fixed-dim: rgb(109 83 114);
43 --md-sys-color-on-tertiary-fixed-variant: rgb(255 255 255);
44 --md-sys-color-surface-dim: rgb(217 217 224);
45 --md-sys-color-surface-bright: rgb(249 249 255);
46 --md-sys-color-surface-container-lowest: rgb(255 255 255);
47 --md-sys-color-surface-container-low: rgb(243 243 250);
48 --md-sys-color-surface-container: rgb(237 237 244);
49 --md-sys-color-surface-container-high: rgb(231 232 238);
50 --md-sys-color-surface-container-highest: rgb(226 226 233);
51}
52

G. css / light.css

1.light {
2 --md-sys-color-primary: rgb(65 95 145);
3 --md-sys-color-surface-tint: rgb(65 95 145);
4 --md-sys-color-on-primary: rgb(255 255 255);
5 --md-sys-color-primary-container: rgb(214 227 255);
6 --md-sys-color-on-primary-container: rgb(0 27 62);
7 --md-sys-color-secondary: rgb(86 95 113);
8 --md-sys-color-on-secondary: rgb(255 255 255);
9 --md-sys-color-secondary-container: rgb(218 226 249);
10 --md-sys-color-on-secondary-container: rgb(19 28 43);
11 --md-sys-color-tertiary: rgb(112 85 117);
12 --md-sys-color-on-tertiary: rgb(255 255 255);
13 --md-sys-color-tertiary-container: rgb(250 216 253);
14 --md-sys-color-on-tertiary-container: rgb(40 19 46);
15 --md-sys-color-error: rgb(186 26 26);
16 --md-sys-color-on-error: rgb(255 255 255);
17 --md-sys-color-error-container: rgb(255 218 214);
18 --md-sys-color-on-error-container: rgb(65 0 2);
19 --md-sys-color-background: rgb(249 249 255);
20 --md-sys-color-on-background: rgb(25 28 32);
21 --md-sys-color-surface: rgb(249 249 255);
22 --md-sys-color-on-surface: rgb(25 28 32);
23 --md-sys-color-surface-variant: rgb(224 226 236);
24 --md-sys-color-on-surface-variant: rgb(68 71 78);
25 --md-sys-color-outline: rgb(116 119 127);
26 --md-sys-color-outline-variant: rgb(196 198 208);
27 --md-sys-color-shadow: rgb(0 0 0);
28 --md-sys-color-scrim: rgb(0 0 0);
29 --md-sys-color-inverse-surface: rgb(46 48 54);
30 --md-sys-color-inverse-on-surface: rgb(240 240 247);
31 --md-sys-color-inverse-primary: rgb(170 199 255);
32 --md-sys-color-primary-fixed: rgb(214 227 255);
33 --md-sys-color-on-primary-fixed: rgb(0 27 62);
34 --md-sys-color-primary-fixed-dim: rgb(170 199 255);
35 --md-sys-color-on-primary-fixed-variant: rgb(40 71 119);
36 --md-sys-color-secondary-fixed: rgb(218 226 249);
37 --md-sys-color-on-secondary-fixed: rgb(19 28 43);
38 --md-sys-color-secondary-fixed-dim: rgb(190 198 220);
39 --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89);
40 --md-sys-color-tertiary-fixed: rgb(250 216 253);
41 --md-sys-color-on-tertiary-fixed: rgb(40 19 46);
42 --md-sys-color-tertiary-fixed-dim: rgb(221 188 224);
43 --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92);
44 --md-sys-color-surface-dim: rgb(217 217 224);
45 --md-sys-color-surface-bright: rgb(249 249 255);
46 --md-sys-color-surface-container-lowest: rgb(255 255 255);
47 --md-sys-color-surface-container-low: rgb(243 243 250);
48 --md-sys-color-surface-container: rgb(237 237 244);
49 --md-sys-color-surface-container-high: rgb(231 232 238);
50 --md-sys-color-surface-container-highest: rgb(226 226 233);
51}
52

H. css / transicion_completa.css

1
2@view-transition {
3 navigation: auto;
4}
5
6::view-transition-group(root) {
7 animation-duration: var(--md-sys-motion-duration-700);
8}
9
10html::view-transition-old(root) {
11 animation-name: desvanece;
12}
13
14html::view-transition-new(root) {
15 animation-delay: var(--md-sys-motion-duration-700);
16 animation-name: aparece;
17}

I. css / transicion_pestanas.css

1@view-transition {
2 navigation: auto;
3}
4
5#headline {
6 view-transition-name: encabezado;
7}
8
9main {
10 view-transition-name: contenido;
11}
12
13::view-transition-group(encabezado) {
14 animation-duration: var(--md-sys-motion-duration-1000);
15}
16
17::view-transition-group(contenido) {
18 animation-duration: var(--md-sys-motion-duration-1000);
19}
20
21html::view-transition-old(encabezado) {
22 animation-name: salePorLaIzquierda;
23}
24
25html::view-transition-new(encabezado) {
26 animation-name: entraPorLaDerecha;
27}
28
29html::view-transition-old(contenido) {
30 animation-name: salePorLaIzquierda;
31}
32
33html::view-transition-new(contenido) {
34 animation-duration: var(--md-sys-motion-duration-700);
35 animation-name: entraPorLaDerecha;
36}

30. favicon.ico

  • Es el ícono que se muestra en las pestañas del navegador. Normalmente debe ser de 32x32 pixeles y debe colocarse en la carpeta raíz del proyecto.

favicon.ico

31. Carpeta « img »

A. img / Escultura_de_coyote.jpeg

Coyote de Neza

B. img / icono2048.png

icono2048.png

C. img / maskable_icon.png

maskable_icon.png

D. img / maskable_icon_x128.png

maskable_icon_x128.png

E. img / maskable_icon_x192.png

maskable_icon_x192.png

F. img / maskable_icon_x384.png

maskable_icon_x384.png

G. img / maskable_icon_x48.png

maskable_icon_x48.png

H. img / maskable_icon_x512.png

maskable_icon_x512.png

I. img / maskable_icon_x72.png

maskable_icon_x72.png

J. img / maskable_icon_x96.png

maskable_icon_x96.png

K. img / pexels-craig-dennis-3701822.jpg

Ciudad de San Francisco

L. img / pexels-creative-workshop-3978352.jpg

Perro

M. img / pexels-erik-karits-3732453.jpg

Buho

N. img / pexels-esteban-arango-10226903.jpg

Coyote

O. img / pexels-moises-patrício-10961948.jpg

Avatar de Ana

P. img / pexels-ralph-2270848.jpg

León

Q. img / pexels-rasmus-svinding-35435.jpg

Oso

R. img / pexels-steve-397857.jpg

Lobo

S. img / pexels-vadim-b-141496.jpg

Gato

T. img / screenshot_horizontal.png

screenshot_horizontal.png

U. img / screenshot_vertical.png

screenshot_vertical.png

32. Carpeta « lib »

A. Carpeta « lib / css »

1. lib / css / material-symbols-outlined.css

1@font-face {
2 font-family: 'Material Symbols Outlined';
3 font-style: normal;
4 src:
5 url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].woff2) format('woff2'),
6 url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].ttf) format('truetype');
7}
8
9.material-symbols-outlined {
10 font-family: 'Material Symbols Outlined';
11 font-weight: normal;
12 font-style: normal;
13 font-size: 1.5rem;
14 width: 1.5rem;
15 height: 1.5rem;
16 /* Preferred icon size */
17 display: inline-block;
18 line-height: 1;
19 text-transform: none;
20 letter-spacing: normal;
21 word-wrap: normal;
22 white-space: nowrap;
23 direction: ltr;
24}

2. lib / css / md-cards.css

1.md-cards {
2 margin: 0.5rem;
3 gap: 0.5rem;
4 display: grid;
5 grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
6}
7
8/* container */
9.md-cards>*::before {
10 content: "";
11 position: absolute;
12 z-index: -2;
13 top: 0;
14 right: 0;
15 left: 0;
16 bottom: 0;
17 background-color: var(--md-sys-color-surface-variant);
18}
19
20/* state layer */
21.md-cards>*::after {
22 content: "";
23 position: absolute;
24 z-index: -1;
25 top: 0;
26 right: 0;
27 left: 0;
28 bottom: 0;
29 background-color: transparent;
30}
31
32.md-cards>* {
33 position: relative;
34 display: block;
35 text-decoration: none;
36 color: var(--md-sys-color-on-surface-variant);
37 border-radius: 0.75rem;
38 overflow: hidden;
39 box-shadow: var(--md-box_shadow_level0);
40}
41
42/* state layer */
43.md-cards>:hover::after {
44 background-color: var(--md-sys-color-on-surface-variant);
45 opacity: var(--md-sys-state-hover-state-layer-opacity);
46}
47
48.md-cards>a:hover {
49 box-shadow: var(--md-box_shadow_level1);
50}
51
52/* state layer */
53.md-cards>:focus::after {
54 background-color: var(--md-sys-color-on-surface-variant);
55 opacity: var(--md-sys-state-focus-state-layer-opacity);
56}
57
58.md-cards>a:focus {
59 outline: none;
60}
61
62/* state layer */
63.md-cards>:active::after {
64 background-color: var(--md-sys-color-on-surface-variant);
65 opacity: var(--md-sys-state-pressed-state-layer-opacity);
66}
67
68.md-cards a:active {
69 background-position: center;
70 background-image:
71 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
72 background-size: 100%;
73 animation-name: md-ripple;
74 animation-duration: var(--md-sys-motion-duration-500);
75 box-shadow: var(--md-box_shadow_level0) !important;
76}
77
78.md-cards>*>* {
79 display: block;
80}
81
82.md-cards figure {
83 border-radius: 0.75rem;
84 padding: 0;
85 margin: 0;
86 width: 100%;
87}
88
89.md-cards figure * {
90 border-radius: 0.75rem;
91 width: 100%;
92}
93
94.md-cards .headline {
95 margin: 1rem;
96 font-family: var(--md-sys-typescale-headline-small-font);
97 font-weight: var(--md-sys-typescale-headline-small-weight);
98 font-size: var(--md-sys-typescale-headline-small-size);
99 font-style: var(--md-sys-typescale-headline-small-font-style);
100 letter-spacing: var(--md-sys-typescale-headline-small-tracking);
101 line-height: var(--md-sys-typescale-headline-small-line-height);
102 text-transform: var(--md-sys-typescale-headline-small-text-transform);
103 text-decoration: var(--md-sys-typescale-headline-small-text-decoration);
104}
105
106.md-cards a .headline {
107 text-decoration: underline;
108}
109
110.md-cards .supporting {
111 margin: 1rem;
112 font-family: var(--md-sys-typescale-body-large-font);
113 font-weight: var(--md-sys-typescale-body-large-weight);
114 font-size: var(--md-sys-typescale-body-large-size);
115 font-style: var(--md-sys-typescale-body-large-font-style);
116 letter-spacing: var(--md-sys-typescale-body-large-tracking);
117 line-height: var(--md-sys-typescale-body-large-line-height);
118 text-transform: var(--md-sys-typescale-body-large-text-transform);
119 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
120}

3. lib / css / md-fab-primary.css

1.md-fab-primary {
2 position: relative;
3 display: inline-block;
4 border: none;
5 width: 3.5rem;
6 height: 3.5rem;
7 border-radius: var(--md-sys-shape-corner-large-default-size);
8 overflow: hidden;
9 padding: 0;
10 padding-block: 0;
11 padding-inline: 0;
12 text-decoration: none;
13 background-color: var(--md-sys-color-primary-container);
14 box-shadow: var(--md-box_shadow_level3);
15}
16
17.md-fab-primary[hidden] {
18 display: none;
19}
20
21/* state layer */
22.md-fab-primary::after {
23 content: "";
24 position: absolute;
25 top: 0;
26 right: 0;
27 left: 0;
28 bottom: 0;
29}
30
31.md-fab-primary span {
32 position: relative;
33 color: var(--md-sys-color-on-primary-container);
34}
35
36.md-fab-primary:hover {
37 box-shadow: var(--md-box_shadow_level4);
38}
39
40.md-fab-primary:hover::after {
41 background-color: var(--md-sys-color-on-primary-container);
42 opacity: var(--md-sys-state-hover-state-layer-opacity);
43}
44
45.md-fab-primary:hover span {
46 color: var(--md-sys-color-on-primary-container);
47}
48
49
50.md-fab-primary:focus {
51 box-shadow: var(--md-box_shadow_level3);
52 outline:none;
53 /* outline:
54 var(--md-sys-color-secondary) var(--md-sys-state-focus-indicator-thickness);
55 outline-offset: var(--md-sys-state-focus-indicator-outer-offset); */
56}
57
58.md-fab-primary:focus::after {
59 background-color: var(--md-sys-color-on-primary-container);
60 opacity: var(--md-sys-state-focus-state-layer-opacity);
61}
62
63.md-fab-primary:focus span {
64 color: var(--md-sys-color-on-primary-container);
65}
66
67.md-fab-primary:active {
68 box-shadow: var(--md-box_shadow_level3);
69 background-position: center;
70 background-image:
71 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
72 background-size: 100%;
73 animation-name: md-ripple;
74 animation-duration: var(--md-sys-motion-duration-500);
75}
76
77.md-fab-primary:active::after {
78 background-color: var(--md-sys-color-on-primary-container);
79 opacity: var(--md-sys-state-pressed-state-layer-opacity);
80}
81
82.md-fab-primary:active span {
83 color: var(--md-sys-color-on-primary-container);
84}

4. lib / css / md-filled-button.css

1/* container */
2.md-filled-button::before {
3 content: "";
4 position: absolute;
5 z-index: -2;
6 top: 0;
7 right: 0;
8 left: 0;
9 bottom: 0;
10 background-color: var(--md-sys-color-primary);
11}
12
13/* state layer */
14.md-filled-button::after {
15 content: "";
16 position: absolute;
17 z-index: -1;
18 top: 0;
19 right: 0;
20 left: 0;
21 bottom: 0;
22 background-color: transparent;
23}
24
25/* label, shape */
26.md-filled-button {
27 position: relative;
28 box-sizing: border-box;
29 border-radius: 1.25rem;
30 height: 2.5rem;
31 line-height: 2.5rem;
32 padding: 0 1.5rem;
33 border: none;
34 background-color: transparent;
35 box-shadow: var(--md-box_shadow_level0);
36 font-family: var(--md-sys-typescale-label-large-font);
37 font-weight: var(--md-sys-typescale-label-large-weight);
38 font-size: var(--md-sys-typescale-label-large-size);
39 font-style: var(--md-sys-typescale-label-large-font-style);
40 letter-spacing: var(--md-sys-typescale-label-large-tracking);
41 text-transform: var(--md-sys-typescale-label-large-text-transform);
42 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
43 color: var(--md-sys-color-on-primary);
44 white-space: nowrap;
45 text-overflow: ellipsis;
46 overflow: hidden;
47}
48
49/* label, shape */
50.md-filled-button:hover {
51 color: var(--md-sys-color-on-primary);
52 box-shadow: var(--md-box_shadow_level1);
53}
54
55/* state layer */
56.md-filled-button:hover::after {
57 background-color: var(--md-sys-color-on-primary);
58 opacity: var(--md-sys-state-hover-state-layer-opacity);
59}
60
61/* label, shape */
62.md-filled-button:focus {
63 outline: none;
64 color: var(--md-sys-color-on-primary);
65 box-shadow: var(--md-box_shadow_level0) !important;
66}
67
68/* state layer */
69.md-filled-button:focus::after {
70 background-color: var(--md-sys-color-on-primary);
71 opacity: var(--md-sys-state-focus-state-layer-opacity);
72}
73
74/* label, shape */
75.md-filled-button:active {
76 color: var(--md-sys-color-on-primary);
77 background-position: center;
78 background-image:
79 radial-gradient(circle, var(--md-sys-color-on-primary-container) 1%, transparent 1%);
80 background-size: 100%;
81 animation-name: md-ripple;
82 animation-duration: var(--md-sys-motion-duration-500);
83 box-shadow: var(--md-box_shadow_level0) !important;
84}
85
86/* state layer */
87.md-filled-button:active::after {
88 background-color: var(--md-sys-color-on-primary);
89 opacity: var(--md-sys-state-pressed-state-layer-opacity);
90}
91
92/* label, shape */
93.md-filled-button:disabled {
94 background-color: transparent !important;
95 color: var(--md-sys-color-on-surface) !important;
96 opacity: 0.38 !important;
97 box-shadow: var(--md-box_shadow_level0) !important;
98}
99
100/* container */
101.md-filled-button:disabled::before {
102 background-color: var(--md-sys-color-on-surface) !important;
103 opacity: 0.12 !important;
104}
105
106/* state layer */
107.md-filled-button:disabled::after {
108 background-color: transparent !important;
109 opacity: 1 !important;
110}

5. lib / css / md-filled-text-field.css

1.md-filled-text-field {
2 position: relative;
3 overflow: hidden;
4 display: flex;
5 flex-direction: column;
6 align-items: stretch;
7 padding-top: calc(0.5rem + var(--md-sys-typescale-body-small-line-height));
8 border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
9 border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
10 overflow: hidden;
11}
12
13/* container */
14.md-filled-text-field::before {
15 content: "";
16 position: absolute;
17 z-index: -2;
18 top: 0;
19 right: 0;
20 left: 0;
21 bottom: 0;
22 background-color: var(--md-sys-color-surface-container-highest);
23}
24
25/* state layer */
26.md-filled-text-field::after {
27 content: "";
28 position: absolute;
29 z-index: -1;
30 top: 0;
31 right: 0;
32 left: 0;
33 bottom: 0;
34 background-color: transparent;
35}
36
37.md-filled-text-field span,
38.md-filled-text-field label {
39 position: absolute;
40 top: 0.5rem;
41 left: 1rem;
42 right: 1rem;
43 display: block;
44 transform: translateY(1rem);
45 transition-property: transform;
46 transition-duration: var(--md-sys-motion-duration-300);
47 white-space: nowrap;
48 text-overflow: ellipsis;
49 overflow: hidden;
50 color: var(--md-sys-color-on-surface-variant);
51 font-family: var(--md-sys-typescale-body-large-font);
52 font-weight: var(--md-sys-typescale-body-large-weight);
53 font-size: var(--md-sys-typescale-body-large-size);
54 font-style: var(--md-sys-typescale-body-large-font-style);
55 letter-spacing: var(--md-sys-typescale-body-large-tracking);
56 line-height: var(--md-sys-typescale-body-large-line-height);
57 text-transform: var(--md-sys-typescale-body-large-text-transform);
58 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
59}
60
61.md-filled-text-field :not(:placeholder-shown)+span,
62.md-filled-text-field :not(:placeholder-shown)+label,
63.md-filled-text-field .populated+span,
64.md-filled-text-field .populated+label,
65.md-filled-text-field:focus-within span,
66.md-filled-text-field:focus-within label,
67.md-filled-text-field.float span,
68.md-filled-text-field.float label {
69 transform: translateY(0);
70 font-family: var(--md-sys-typescale-body-small-font);
71 font-weight: var(--md-sys-typescale-body-small-weight);
72 font-size: var(--md-sys-typescale-body-small-size);
73 font-style: var(--md-sys-typescale-body-small-font-style);
74 letter-spacing: var(--md-sys-typescale-body-small-tracking);
75 line-height: var(--md-sys-typescale-body-small-line-height);
76 text-transform: var(--md-sys-typescale-body-small-text-transform);
77 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
78}
79
80.md-filled-text-field :not(label, span, small) {
81 position: relative;
82 caret-color: var(--md-sys-color-primary);
83 min-height: 2rem;
84 box-sizing: border-box;
85 padding-left: 1rem;
86 padding-bottom: 0.5rem;
87 padding-right: 1rem;
88 border: none;
89 resize: none;
90 color: var(--md-sys-color-on-surface);
91 font-family: var(--md-sys-typescale-body-large-font);
92 font-weight: var(--md-sys-typescale-body-large-weight);
93 font-size: var(--md-sys-typescale-body-large-size);
94 font-style: var(--md-sys-typescale-body-large-font-style);
95 letter-spacing: var(--md-sys-typescale-body-large-tracking);
96 line-height: var(--md-sys-typescale-body-large-line-height);
97 text-transform: var(--md-sys-typescale-body-large-text-transform);
98 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
99 background-color: transparent;
100 outline: none;
101 border-bottom-width: 0.0625rem;
102 border-bottom-style: solid;
103 border-bottom-color: var(--md-sys-color-on-surface-variant);
104}
105
106.md-filled-text-field ::placeholder {
107 color: transparent;
108}
109
110.md-filled-text-field small {
111 display: block;
112 color: var(--md-sys-color-on-surface-variant);
113 background-color: var(--md-sys-color-background);
114 font-family: var(--md-sys-typescale-body-small-font);
115 font-weight: var(--md-sys-typescale-body-small-weight);
116 font-size: var(--md-sys-typescale-body-small-size);
117 font-style: var(--md-sys-typescale-body-small-font-style);
118 letter-spacing: var(--md-sys-typescale-body-small-tracking);
119 line-height: var(--md-sys-typescale-body-small-line-height);
120 text-transform: var(--md-sys-typescale-body-small-text-transform);
121 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
122 padding: 0.25rem 1rem 0 1rem;
123 white-space: nowrap;
124 text-overflow: ellipsis;
125 overflow: hidden;
126}
127
128.md-filled-text-field:hover span,
129.md-filled-text-field:hover label {
130 color: var(--md-sys-color-on-surface-variant);
131}
132
133.md-filled-text-field:hover :not(label, span, small) {
134 padding-bottom: 0.5rem;
135 border-bottom-width: 0.0625rem;
136 border-bottom-color: var(--md-sys-color-on-surface);
137}
138
139.md-filled-text-field:hover::after {
140 background-color: var(--md-sys-color-on-surface);
141 opacity: var(--md-sys-state-hover-state-layer-opacity);
142}
143
144.md-filled-text-field:hover small {
145 color: var(--md-sys-color-on-surface-variant);
146}
147
148.md-filled-text-field:focus-within span,
149.md-filled-text-field:focus-within label {
150 color: var(--md-sys-color-primary);
151}
152
153.md-filled-text-field :focus {
154 color: var(--md-sys-color-on-surface);
155 outline: none;
156 padding-bottom: 0.4375rem;
157 border-bottom-width: 0.125rem;
158 border-bottom-color: var(--md-sys-color-primary);
159}
160
161.md-filled-text-field:hover :focus {
162 padding-bottom: 0.4375rem;
163 border-bottom-width: 0.125rem;
164}
165
166.md-filled-text-field:focus-within small {
167 color: var(--md-sys-color-on-surface-variant);
168}
169
170.md-filled-text-field :invalid {
171 color: var(--md-sys-color-on-surface);
172 border-bottom-color: var(--md-sys-color-error);
173}
174
175.md-filled-text-field :invalid+span,
176.md-filled-text-field :invalid+label {
177 color: var(--md-sys-color-error);
178}
179
180.md-filled-text-field :invalid~small,
181.md-filled-text-field:hover :invalid~small,
182.md-filled-text-field:focus-within .input-text:invalid~small {
183 color: var(--md-sys-color-error);
184}
185
186.md-filled-text-field :invalid:focus {
187 caret-color: var(--md-sys-color-error);
188 border-bottom-color: var(--md-sys-color-error);
189}
190
191.md-filled-text-field:hover :invalid {
192 color: var(--md-sys-color-on-surface);
193 border-bottom-color: var(--md-sys-color-error);
194}

6. lib / css / md-list.css

1.md-list {
2 margin: 0.5rem 0;
3 padding: 0;
4 list-style-type: none;
5}
6
7.md-list .md-one-line,
8.md-list .md-two-line,
9.md-list .md-three-line {
10 position: relative;
11 display: flex;
12 box-sizing: border-box;
13}
14
15/* container */
16.md-list .md-one-line::before,
17.md-list .md-two-line::before,
18.md-list .md-three-line::before {
19 content: "";
20 position: absolute;
21 z-index: -2;
22 top: 0;
23 right: 0;
24 left: 0;
25 bottom: 0;
26 background-color: var(--md-sys-color-surface);
27}
28
29/* state layer */
30.md-list .md-one-line::after,
31.md-list .md-two-line::after,
32.md-list .md-three-line::after {
33 content: "";
34 position: absolute;
35 z-index: -1;
36 top: 0;
37 right: 0;
38 left: 0;
39 bottom: 0;
40 background-color: transparent;
41}
42
43.md-list .md-one-line {
44 align-items: center;
45 gap: 1rem;
46 min-height: 3.5rem;
47 padding: 0.5rem 1.5rem 0.5rem 1rem;
48}
49
50.md-list .md-one-line.video,
51.md-list .md-two-line.video {
52 padding: 0.75rem 1.5rem 0.75rem 0;
53}
54
55.md-list .md-two-line,
56.md-list .md-three-line {
57 flex-flow: column;
58}
59
60.md-list .md-two-line {
61 justify-content: center;
62 min-height: 4.5rem;
63 padding: 0.5rem 1.5rem 0.5rem 1rem;
64}
65
66.md-list .md-two-line.icon,
67.md-list .md-two-line.avatar,
68.md-list .md-two-line.image,
69.md-list .md-two-line.video,
70.md-list .md-three-line.icon,
71.md-list .md-three-line.avatar,
72.md-list .md-three-line.image,
73.md-list .md-three-line.video {
74 display: grid;
75 column-gap: 1rem;
76 row-gap: 0;
77 grid-template-areas:
78 "img headline"
79 "img supporting";
80}
81
82.md-list .md-two-line.icon,
83.md-list .md-two-line.avatar,
84.md-list .md-two-line.image,
85.md-list .md-two-line.video {
86 align-content: center;
87 grid-template-rows: 1fr 1fr;
88}
89
90.md-list .md-two-line.icon,
91.md-list .md-three-line.icon {
92 grid-template-columns: var(--iconSize) 1fr;
93}
94
95.md-list .md-two-line.avatar,
96.md-list .md-three-line.avatar {
97 grid-template-columns: var(--avatarSize) 1fr;
98}
99
100.md-list .md-two-line.image,
101.md-list .md-three-line.image {
102 grid-template-columns: var(--imageSize) 1fr;
103}
104
105.md-list .md-two-line.video,
106.md-list .md-three-line.video {
107 grid-template-columns: var(--videoWidth) 1fr;
108}
109
110.md-list .md-three-line {
111 align-content: flex-start;
112 min-height: 5.5rem;
113 padding: 0.75rem 1.5rem 0.75rem 1rem;
114}
115
116.md-list .md-three-line.video {
117 padding: 0.75rem 1.5rem 0.75rem 0;
118}
119
120.md-list .md-three-line.icon,
121.md-list .md-three-line.avatar,
122.md-list .md-three-line.image,
123.md-list .md-three-line.video {
124 align-content: start;
125 grid-template-rows: var(--md-sys-typescale-label-large-line-height) 1fr;
126}
127
128/* state layer */
129.md-list .md-one-line:hover::after,
130.md-list .md-two-line:hover::after,
131.md-list .md-three-line:hover::after {
132 background-color: var(--md-sys-color-on-surface);
133 opacity: var(--md-sys-state-hover-state-layer-opacity);
134}
135
136/* state layer */
137.md-list a.md-one-line:focus::after,
138.md-list a.md-two-line:focus::after,
139.md-list a.md-three-line:focus::after,
140.md-list a.md-one-line:focus-visible::after,
141.md-list a.md-two-line:focus-visible::after,
142.md-list a.md-three-line:focus-visible::after {
143 background-color: var(--md-sys-color-on-surface);
144 opacity: var(--md-sys-state-focus-state-layer-opacity);
145}
146
147.md-list a:focus,
148.md-list a:focus-visible {
149 outline: none;
150}
151
152.md-list a:active {
153 background-position: center;
154 background-image:
155 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
156 background-size: 100%;
157 animation-name: md-ripple;
158 animation-duration: var(--md-sys-motion-duration-500);
159 box-shadow: var(--md-box_shadow_level0) !important;
160}
161
162/* state layer */
163.md-list a.md-one-line:active::after,
164.md-list a.md-two-line:active::after,
165.md-list a.md-three-line:active::after {
166 background-color: var(--md-sys-color-on-surface);
167 opacity: var(--md-sys-state-pressed-state-layer-opacity);
168}
169
170.md-list a.md-two-line,
171.md-list a.md-three-line {
172 text-decoration: none;
173}
174
175.md-list a.md-two-line .headline,
176.md-list a.md-three-line .headline {
177 text-decoration: underline;
178}
179
180.md-list .headline {
181 grid-area: headline;
182 display: block;
183 box-sizing: border-box;
184 color: var(--md-sys-color-on-surface);
185 font-family: var(--md-sys-typescale-body-large-font);
186 font-weight: var(--md-sys-typescale-body-large-weight);
187 font-size: var(--md-sys-typescale-body-large-size);
188 font-style: var(--md-sys-typescale-body-large-font-style);
189 letter-spacing: var(--md-sys-typescale-body-large-tracking);
190 line-height: var(--md-sys-typescale-body-large-line-height);
191 text-transform: var(--md-sys-typescale-body-large-text-transform);
192 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
193 max-height: var(--md-sys-typescale-body-large-line-height);
194 white-space: nowrap;
195 text-overflow: ellipsis;
196 overflow: hidden;
197}
198
199.md-list .md-two-line.icon .headline,
200.md-list .md-two-line.avatar .headline,
201.md-list .md-two-line.image .headline,
202.md-list .md-two-line.video .headline,
203.md-list .md-three-line.icon .headline,
204.md-list .md-three-line.avatar .headline,
205.md-list .md-three-line.image .headline,
206.md-list .md-three-line.video .headline {
207 align-self: end;
208}
209
210.md-list .supporting {
211 grid-area: supporting;
212 display: -webkit-box;
213 -webkit-box-orient: vertical;
214 overflow: hidden;
215 box-sizing: border-box;
216 align-self: start;
217 font-family: var(--md-sys-typescale-body-medium-font);
218 font-weight: var(--md-sys-typescale-body-medium-weight);
219 font-size: var(--md-sys-typescale-body-medium-size);
220 font-style: var(--md-sys-typescale-body-medium-font-style);
221 letter-spacing: var(--md-sys-typescale-body-medium-tracking);
222 line-height: var(--md-sys-typescale-body-medium-line-height);
223 text-transform: var(--md-sys-typescale-body-medium-text-transform);
224 text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
225}
226
227.md-list .md-two-line .supporting {
228 max-height: var(--md-sys-typescale-body-medium-line-height);
229 line-clamp: 1;
230 -webkit-line-clamp: 1;
231}
232
233.md-list .md-three-line .supporting {
234 max-height: calc(2 * var(--md-sys-typescale-body-medium-line-height));
235 line-clamp: 2;
236 -webkit-line-clamp: 2;
237}
238
239.md-list .avatar img,
240.md-list .avatar label,
241.md-list .avatar .material-symbols-outlined:first-child {
242 flex-shrink: 0;
243 background-color: var(--md-sys-color-primary-container);
244 color: var(--md-sys-color-on-primary-container);
245 border-radius: 50%;
246 width: var(--avatarSize);
247 height: var(--avatarSize);
248}
249
250.md-list .avatar label {
251 display: inline-block;
252 font-family: var(--md-sys-typescale-title-medium-font);
253 font-weight: var(--md-sys-typescale-title-medium-weight);
254 font-size: var(--md-sys-typescale-title-medium-size);
255 font-style: var(--md-sys-typescale-title-medium-font-style);
256 letter-spacing: var(--md-sys-typescale-title-medium-tracking);
257 line-height: var(--md-sys-typescale-title-medium-line-height);
258 text-transform: var(--md-sys-typescale-title-medium-text-transform);
259 text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
260 overflow: hidden;
261}
262
263.md-list .avatar .material-symbols-outlined:first-child {
264 font-size: var(--avatarSize);
265}
266
267.md-list .avatar.md-two-line img,
268.md-list .avatar.md-two-line label,
269.md-list .avatar.md-two-line .material-symbols-outlined:first-child {
270 grid-area: img;
271 align-self: center;
272}
273
274.md-list .avatar.md-three-line img,
275.md-list .avatar.md-three-line label,
276.md-list .avatar.md-three-line .material-symbols-outlined:first-child {
277 grid-area: img;
278 align-self: start;
279}
280
281.md-list .icon img,
282.md-list .icon .material-symbols-outlined:first-child {
283 flex-shrink: 0;
284 color: var(--md-sys-color-on-surface-variant);
285 width: var(--iconSize);
286 height: var(--iconSize);
287}
288
289.md-list .icon .material-symbols-outlined:first-child {
290 font-size: var(--iconSize);
291}
292
293.md-list .icon.md-two-line img,
294.md-list .icon.md-two-line .material-symbols-outlined:first-child {
295 grid-area: img;
296 align-self: center;
297}
298
299.md-list .icon.md-three-line img,
300.md-list .icon.md-three-line .material-symbols-outlined:first-child {
301 grid-area: img;
302 align-self: start;
303}
304
305.md-list .video img {
306 flex-shrink: 0;
307 color: var(--md-sys-color-on-surface-variant);
308 width: var(--videoWidth);
309 height: var(--videoHeight);
310}
311
312.md-list .video.md-two-line img {
313 grid-area: img;
314 align-self: center;
315}
316
317.md-list .video.md-three-line img {
318 grid-area: img;
319 align-self: start;
320}
321
322.md-list .image img,
323.md-list .image .material-symbols-outlined:first-child {
324 flex-shrink: 0;
325 color: var(--md-sys-color-on-surface-variant);
326 width: var(--imageSize);
327 height: var(--imageSize);
328}
329
330.md-list .image .material-symbols-outlined:first-child {
331 font-size: var(--imageSize);
332}
333
334.md-list .image.md-two-line img,
335.md-list .image.md-two-line .material-symbols-outlined:first-child {
336 grid-area: img;
337 align-self: center;
338}
339
340.md-list .image.md-three-line img,
341.md-list .image.md-three-line .material-symbols-outlined:first-child {
342 grid-area: img;
343 align-self: start;
344}

7. lib / css / md-menu.css

1.md-menu {
2 display: none;
3 z-index: 2;
4 box-sizing: border-box;
5 cursor: default;
6 padding: 0.25rem 0;
7 border-radius:
8 var(--md-sys-shape-corner-extra-small-default-size);
9 background-color: var(--md-sys-color-surface-container-low);
10 box-shadow: var(--md-box_shadow_level2);
11 transform: translateY(-50%) scaleY(0);
12 transition-timing-function:
13 cubic-bezier(var(--md-sys-motion-easing-standard-x0),
14 var(--md-sys-motion-easing-standard-y0),
15 var(--md-sys-motion-easing-standard-x1),
16 var(--md-sys-motion-easing-standard-y1));
17 transition-property: display, transform;
18 transition-behavior: allow-discrete;
19 transition-duration: var(--md-sys-motion-duration-500);
20}
21
22.md-menu.open {
23 display: block;
24 transform: translateY(0) scaleY(1);
25}
26
27@starting-style {
28 .md-menu.open {
29 display: block;
30 transform: translateY(-50%) scaleY(0);
31 }
32}
33
34/* container */
35.md-menu>*::after {
36 content: "";
37 position: absolute;
38 z-index: -2;
39 top: 0;
40 right: 0;
41 left: 0;
42 bottom: 0;
43}
44
45/* container */
46.md-menu>.selected::after {
47 background-color: var(--md-sys-color-secondary-container);
48}
49
50/* label, shape */
51.md-menu>* {
52 position: relative;
53 display: block;
54 box-sizing: border-box;
55 height: 3rem;
56 line-height: 3rem;
57 padding: 0 0.75rem;
58 color: var(--md-sys-color-on-surface);
59 font-family: var(--md-sys-typescale-label-large-font);
60 font-weight: var(--md-sys-typescale-label-large-weight);
61 font-size: var(--md-sys-typescale-label-large-size);
62 font-style: var(--md-sys-typescale-label-large-font-style);
63 letter-spacing: var(--md-sys-typescale-label-large-tracking);
64 text-transform: var(--md-sys-typescale-label-large-text-transform);
65 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
66 white-space: nowrap;
67 text-overflow: ellipsis;
68 overflow: hidden;
69}
70
71/* label, shape */
72.md-menu>.selected {
73 color: var(--md-sys-color-on-secondary-container);
74}
75
76/* state layer */
77.md-menu>*::before {
78 content: "";
79 position: absolute;
80 z-index: -1;
81 top: 0;
82 right: 0;
83 left: 0;
84 bottom: 0;
85}
86
87/* icon */
88.md-menu>* span {
89 position: relative;
90 margin-right: 0.75rem;
91 vertical-align: middle;
92 color: var(--md-sys-color-on-surface-variant);
93 font-size: 1.5rem;
94 width: 1.5rem;
95 height: 1.5rem;
96}
97
98/* icon */
99.md-menu>.selected span {
100 color: var(--md-sys-color-on-secondary-container);
101}
102
103/* state layer */
104.md-menu>:hover::before {
105 background-color: var(--md-sys-color-on-surface);
106 opacity: var(--md-sys-state-hover-state-layer-opacity);
107}
108
109/* label, shape */
110.md-menu>:hover {
111 color: var(--md-sys-color-on-surface);
112}
113
114/* icon */
115.md-menu>:hover span {
116 color: var(--md-sys-color-on-surface-variant);
117}
118
119/* state layer */
120.md-menu>:focus::before {
121 background-color: var(--md-sys-color-on-surface);
122 opacity: var(--md-sys-state-focus-state-layer-opacity);
123}
124
125/* label, shape */
126.md-menu>:focus {
127 color: var(--md-sys-color-on-surface);
128 outline: none;
129}
130
131/* icon */
132.md-menu>:focus span {
133 color: var(--md-sys-color-on-surface-variant);
134}
135
136/* label, shape */
137.md-menu>:active {
138 background-position: center;
139 background-image:
140 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
141 background-size: 100%;
142 animation-name: md-ripple;
143 animation-duration: var(--md-sys-motion-duration-500);
144 color: var(--md-sys-color-on-surface);
145}
146
147/* state layer */
148.md-menu>:active::before {
149 background-color: var(--md-sys-color-on-surface);
150 opacity: var(--md-sys-state-pressed-state-layer-opacity);
151}
152
153
154/* icon */
155.md-menu>:active span {
156 color: var(--md-sys-color-on-surface-variant);
157}
158
159.md-menu input[type="radio"] {
160 appearance: none;
161 transform: scaleX(0);
162}

8. lib / css / md-navigation-bar.css

1.md-navigation-bar {
2 display: flex;
3 justify-content: center;
4 align-items: stretch;
5 position: fixed;
6 left: 0;
7 right: 0;
8 bottom: 0;
9 background-color: var(--md-sys-color-surface-container-low);
10}
11
12.md-navigation-bar a {
13 position: relative;
14 display: block;
15 flex: 0 1 auto;
16 color: var(--md-sys-color-on-surface-variant);
17 font-family: var(--md-sys-typescale-label-medium-font);
18 font-weight: var(--md-sys-typescale-label-medium-weight);
19 font-size: var(--md-sys-typescale-label-medium-size);
20 font-style: var(--md-sys-typescale-label-medium-font-style);
21 letter-spacing: var(--md-sys-typescale-label-medium-tracking);
22 line-height: var(--md-sys-typescale-label-medium-line-height);
23 text-transform: var(--md-sys-typescale-label-medium-text-transform);
24 text-decoration: var(--md-sys-typescale-label-medium-text-decoration);
25 text-decoration: none;
26 padding-top: 0.75rem;
27 padding-bottom: 1rem;
28 padding-left: 0.25rem;
29 padding-right: 0.25rem;
30 text-align: center;
31 overflow: hidden;
32 box-sizing: border-box;
33}
34
35.md-navigation-bar a.active {
36 color: var(--md-sys-color-on-surface);
37}
38
39/* state layer */
40.md-navigation-bar a::after {
41 content: "";
42 position: absolute;
43 z-index: -2;
44 top: 0;
45 right: 0;
46 left: 0;
47 bottom: 0;
48 background-color: transparent;
49}
50
51/* state layer */
52.md-navigation-bar a:hover::after {
53 background-color: var(--md-sys-color-on-surface-variant);
54 opacity: var(--md-sys-state-hover-state-layer-opacity);
55}
56
57/* state layer */
58.md-navigation-bar a.active:hover::after {
59 background-color: var(--md-sys-color-on-surface);
60}
61
62.md-navigation-bar a:focus {
63 outline: none;
64}
65
66/* state layer */
67.md-navigation-bar a:focus::after {
68 background-color: var(--md-sys-color-on-surface-variant);
69 opacity: var(--md-sys-state-focus-state-layer-opacity);
70}
71
72/* state layer */
73.md-navigation-bar a.active:focus::after {
74 background-color: var(--md-sys-color-on-surface);
75}
76
77.md-navigation-bar a:active {
78 background-position: center;
79 background-image:
80 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
81 background-size: 100%;
82 animation-name: md-ripple;
83 animation-duration: var(--md-sys-motion-duration-500);
84}
85
86/* state layer */
87.md-navigation-bar a:active::after {
88 background-color: var(--md-sys-color-on-surface-variant);
89 opacity: var(--md-sys-state-pressed-state-layer-opacity);
90}
91
92/* state layer */
93.md-navigation-bar a.active:active::after {
94 background-color: var(--md-sys-color-on-surface);
95}
96
97.md-navigation-bar span {
98 color: var(--md-sys-color-on-surface-variant);
99 position: relative;
100 display: block;
101 height: 2rem;
102 width: 4rem;
103 line-height: 2rem;
104 margin-bottom: 0.25rem;
105 box-sizing: border-box;
106 margin-left: auto;
107 margin-right: auto;
108 overflow: hidden;
109}
110
111.md-navigation-bar a.active span {
112 color: var(--md-sys-color-on-secondary-container);
113}
114
115.md-navigation-bar a.active {
116 font-variation-settings: 'FILL'1, 'wght'700, 'GRAD'0, 'opsz'48;
117}
118
119.md-navigation-bar span::before {
120 content: "";
121 background-color: var(--md-sys-color-secondary-container);
122 position: absolute;
123 z-index: -1;
124 top: 0;
125 left: 0;
126 height: 2rem;
127 width: 4rem;
128 border-radius: 1rem;
129 box-sizing: border-box;
130 transform: scaleX(0);
131 transition-property: transform;
132 transition-duration: var(--md-sys-motion-duration-500);
133}
134
135.md-navigation-bar a.active span::before,
136.md-navigation-bar a:active span::before {
137 transform: scaleX(1);
138}

9. lib / css / md-outline-button.css

1.md-outline-button {
2 position: relative;
3 box-sizing: border-box;
4 border-radius: 1.25rem;
5 height: 2.5rem;
6 padding: 0 1.5rem;
7 border: 0.0625rem solid var(--md-sys-color-outline);
8 background-color: transparent;
9 box-shadow: var(--md-box_shadow_level0);
10 font-family: var(--md-sys-typescale-label-large-font);
11 font-weight: var(--md-sys-typescale-label-large-weight);
12 font-size: var(--md-sys-typescale-label-large-size);
13 font-style: var(--md-sys-typescale-label-large-font-style);
14 letter-spacing: var(--md-sys-typescale-label-large-tracking);
15 text-transform: var(--md-sys-typescale-label-large-text-transform);
16 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
17 color: var(--md-sys-color-primary);
18 white-space: nowrap;
19 text-overflow: ellipsis;
20 overflow: hidden;
21}
22
23/* state layer */
24.md-outline-button::after {
25 content: "";
26 position: absolute;
27 z-index: -1;
28 top: 0;
29 right: 0;
30 left: 0;
31 bottom: 0;
32 background-color: transparent;
33}
34
35.md-outline-button:hover {
36 color: var(--md-sys-color-primary);
37 border-color: var(--md-sys-color-outline);
38}
39
40/* state layer */
41.md-outline-button:hover::after {
42 background-color: var(--md-sys-color-primary);
43 opacity: var(--md-sys-state-hover-state-layer-opacity);
44}
45
46.md-outline-button:focus {
47 outline: none;
48 color: var(--md-sys-color-primary);
49 border-color: var(--md-sys-color-outline);
50}
51
52/* state layer */
53.md-outline-button:focus::after {
54 background-color: var(--md-sys-color-primary);
55 opacity: var(--md-sys-state-focus-state-layer-opacity);
56}
57
58.md-outline-button:active {
59 color: var(--md-sys-color-primary);
60 border-color: var(--md-sys-color-outline);
61 background-position: center;
62 background-image:
63 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
64 background-size: 100%;
65 animation-name: md-ripple;
66 animation-duration: var(--md-sys-motion-duration-500);
67 box-shadow: var(--md-box_shadow_level0) !important;
68}
69
70/* state layer */
71.md-outline-button:active::after {
72 background-color: var(--md-sys-color-primary);
73 opacity: var(--md-sys-state-pressed-state-layer-opacity);
74}
75
76.md-outline-button:disabled {
77 background-color: transparent !important;
78 border-color: var(--md-sys-color-on-surface) !important;
79 color: var(--md-sys-color-on-surface) !important;
80 opacity: 0.38 !important;
81}
82
83/* container */
84.md-outline-button:disabled::after {
85 background-color: transparent !important;
86 opacity: 1 !important;
87}

10. lib / css / md-ripple.css

1@keyframes md-ripple {
2
3 from {
4 background-size: 100%;
5 }
6
7 to {
8 background-size: 15000%;
9 }
10
11}

11. lib / css / md-segmented-button.css

1.md-segmented-button {
2 display: flex;
3 align-items: stretch;
4 box-sizing: border-box;
5 border: 0.0625rem solid var(--md-sys-color-outline);
6 height: 2.5rem;
7 border-radius: 1.25rem;
8 overflow: hidden;
9}
10
11.md-segmented-button[hidden] {
12 display: none;
13}
14
15.md-segmented-button input {
16 -webkit-appearance: none;
17 appearance: none;
18 flex: 0 1 0.0625rem;
19 width: 0.0625rem;
20 height: 2.375rem;
21 margin: 0;
22 padding: 0;
23 background-color: var(--md-sys-color-outline);
24}
25
26.md-segmented-button input:first-of-type {
27 transform: scaleX(0);
28}
29
30.md-segmented-button input:focus {
31 outline: none;
32}
33
34.md-segmented-button :checked+label {
35 color: var(--md-sys-color-on-secondary-container);
36}
37
38.md-segmented-button label {
39 position: relative;
40 flex: 1 1 1.5rem;
41 display: block;
42 box-sizing: border-box;
43 height: 2.375rem;
44 line-height: 2.375rem;
45 text-align: center;
46 color: var(--md-sys-color-on-surface);
47 font-family: var(--md-sys-typescale-label-large-font);
48 font-weight: var(--md-sys-typescale-label-large-weight);
49 font-size: var(--md-sys-typescale-label-large-size);
50 font-style: var(--md-sys-typescale-label-large-font-style);
51 letter-spacing: var(--md-sys-typescale-label-large-tracking);
52 text-transform: var(--md-sys-typescale-label-large-text-transform);
53 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
54 padding: 0 0.75rem;
55 overflow: hidden;
56 white-space: nowrap;
57 text-overflow: ellipsis;
58 overflow: hidden;
59}
60
61.md-segmented-button label::before {
62 /* container */
63 content: "";
64 position: absolute;
65 z-index: -2;
66 top: 0;
67 right: 0;
68 left: 0;
69 bottom: 0;
70}
71
72.md-segmented-button label::after {
73 /* state layer */
74 content: "";
75 position: absolute;
76 z-index: -1;
77 top: 0;
78 right: 0;
79 left: 0;
80 bottom: 0;
81 background-color: transparent;
82}
83
84.md-segmented-button :checked+label::before {
85 /* container */
86 background-color: var(--md-sys-color-secondary-container);
87}
88
89.md-segmented-button label span {
90 vertical-align: middle;
91 color: var(--md-sys-color-on-surface);
92 font-size: 1.125rem;
93 width: 1.125rem;
94 height: 1.125rem;
95 margin-right: 0.5rem;
96}
97
98
99.md-segmented-button label span:first-child {
100 display: none;
101}
102
103.md-segmented-button :checked+label span:first-child {
104 display: inline-block;
105 color: var(--md-sys-color-on-secondary-container);
106}
107
108/* state layer */
109.md-segmented-button label:hover::after {
110 background-color: var(--md-sys-color-on-surface);
111 opacity: var(--md-sys-state-hover-state-layer-opacity);
112}
113
114/* state layer */
115.md-segmented-button :checked+label:hover::after {
116 background-color: var(--md-sys-color-on-secondary-container);
117}
118
119/* state layer */
120.md-segmented-button :focus+label::after {
121 background-color: var(--md-sys-color-on-surface);
122 opacity: var(--md-sys-state-focus-state-layer-opacity);
123}
124
125/* state layer */
126.md-segmented-button :focus:checked+label::after {
127 background-color: var(--md-sys-color-on-secondary-container);
128}
129
130.md-segmented-button label:active,
131.md-segmented-button :active+label {
132 background-position: center;
133 background-image:
134 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
135 background-size: 100%;
136 animation-name: md-ripple;
137 animation-duration: var(--md-sys-motion-duration-500);
138}
139
140/* state layer */
141.md-segmented-button :active+label::after {
142 background-color: var(--md-sys-color-on-surface);
143 opacity: var(--md-sys-state-pressed-state-layer-opacity);
144}
145
146/* state layer */
147.md-segmented-button :active:checked+label::after {
148 background-color: var(--md-sys-color-on-secondary-container);
149}

12. lib / css / md-slider-field.css

1md-slider-field input::-webkit-slider-runnable-track {
2 height: 0.25rem;
3 border-radius: 0.125rem;
4}
5
6md-slider-field input::-webkit-slider-thumb {
7 -webkit-appearance: none;
8 background-color: var(--md-sys-color-primary);
9 width: 1.25rem;
10 height: 1.25rem;
11 border-radius: 0.625rem;
12 margin-top: -0.5625rem;
13}
14
15md-slider-field input:hover::-webkit-slider-thumb {
16 box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-hover-transparency-percentage));
17}
18
19md-slider-field input:focus::-webkit-slider-thumb {
20 box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-focus-transparency-percentage));
21}
22
23md-slider-field input:active::-webkit-slider-thumb {
24 box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-pressed-transparency-percentage)) !important;
25 background-position: center;
26 background-image:
27 radial-gradient(circle, var(--md-sys-color-primary-container) 1%, transparent 1%);
28 background-size: 100%;
29 animation-name: md-ripple;
30 animation-duration: var(--md-sys-motion-duration-500);
31}
32
33md-slider-field.material::-moz-range-track {
34 height: 0.25rem;
35 border-radius: 0.125rem;
36}
37
38md-slider-field input::-moz-range-thumb {
39 -webkit-appearance: none;
40 appearance: none;
41 background-color: var(--md-sys-color-primary);
42 width: 1.25rem;
43 height: 1.25rem;
44 border: none;
45 border-radius: 0.625rem;
46}
47
48md-slider-field input:hover::-moz-range-thumb {
49 box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-hover-transparency-percentage));
50}
51
52md-slider-field input:focus::-moz-range-thumb {
53 box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-focus-transparency-percentage));
54}
55
56md-slider-field input:active::-moz-range-thumb {
57 box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-pressed-transparency-percentage)) !important;
58 background-position: center;
59 background-image:
60 radial-gradient(circle, var(--md-sys-color-primary-container) 1%, transparent 1%);
61 background-size: 100%;
62 animation-name: md-ripple;
63 animation-duration: var(--md-sys-motion-duration-500);
64}

13. lib / css / md-standard-icon-button.css

1.md-standard-icon-button {
2 position: relative;
3 display: inline-block;
4 border: none;
5 padding: 0.25rem;
6 background-color: transparent;
7 text-decoration: none;
8 border-radius: 50%;
9 overflow: hidden;
10}
11
12.md-standard-icon-button[hidden] {
13 display: none;
14}
15
16/* state layer */
17.md-standard-icon-button::after {
18 content: "";
19 position: absolute;
20 top: 0.25rem;
21 right: 0.25rem;
22 left: 0.25rem;
23 bottom: 0.25rem;
24 border-radius: 50%;
25}
26
27.md-standard-icon-button span {
28 position: relative;
29 padding: 0.5rem;
30 color: var(--md-sys-color-on-surface-variant);
31 font-size: 1.5rem;
32 width: 1.5rem;
33 height: 1.5rem;
34}
35
36.md-standard-icon-button.avatar span {
37 padding: 0.3125rem;
38 font-size: 1.875rem;
39 width: 1.875rem;
40 height: 1.875rem;
41}
42
43.md-standard-icon-button:hover::after {
44 background-color: var(--md-sys-color-on-surface-variant);
45 opacity: var(--md-sys-state-hover-state-layer-opacity);
46}
47
48.md-standard-icon-button:hover span {
49 color: var(--md-sys-color-on-surface-variant);
50}
51
52.md-standard-icon-button:focus {
53 outline: none;
54}
55
56.md-standard-icon-button:focus::after {
57 background-color: var(--md-sys-color-on-surface-variant);
58 opacity: var(--md-sys-state-focus-state-layer-opacity);
59}
60
61.md-standard-icon-button:focus span {
62 color: var(--md-sys-color-on-surface-variant);
63}
64
65.md-standard-icon-button:active {
66 background-position: center;
67 background-image:
68 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
69 background-size: 100%;
70 animation-name: md-ripple;
71 animation-duration: var(--md-sys-motion-duration-500);
72}
73
74.md-standard-icon-button:active::after {
75 background-color: var(--md-sys-color-on-surface-variant);
76 opacity: var(--md-sys-state-pressed-state-layer-opacity);
77}
78
79.md-standard-icon-button:active span {
80 -color: var(--md-sys-color-on-surface-variant);
81}
82
83.md-standard-icon-button:disabled::after {
84 background-color: transparent !important;
85 opacity: 1;
86}
87
88.md-standard-icon-button:disabled span {
89 color: var(--md-sys-color-on-surface) !important;
90 opacity: 0.38;
91}
92
93.md-standard-icon-button:disabled:active {
94 background-image: none;
95 animation-name: none;
96 animation-duration: unset;
97}

14. lib / css / md-switch.css

1.md-switch {
2 -webkit-appearance: none;
3 appearance: none;
4 position: relative;
5 display: inline-block;
6 vertical-align: middle;
7 box-sizing: content-box;
8 padding: 0;
9 padding-block: 0;
10 padding-inline: 0;
11}
12
13.md-switch:focus {
14 outline: none;
15}
16
17/* Track */
18body.material .md-switch {
19 width: 3rem;
20 height: 1.75rem;
21 border-radius: 1rem;
22 border: 0.125rem solid var(--md-sys-color-outline);
23 background-color: var(--md-sys-color-surface-container-highest);
24}
25
26body.material .md-switch:checked {
27 background-color: var(--md-sys-color-primary);
28}
29
30/* State */
31body.material .md-switch::before {
32 content: "";
33 display: none;
34 position: absolute;
35 height: 2.5rem;
36 width: 2.5rem;
37 border-radius: 1.25rem;
38 top: -0.375rem;
39 left: -0.375rem;
40}
41
42body.material .md-switch:checked:before {
43 left: auto;
44 right: -0.375rem;
45}
46
47/* Handle */
48body.material .md-switch::after {
49 content: "";
50 display: inline-block;
51 position: absolute;
52 transition-property: all;
53 transition-duration: var(--md-sys-motion-duration-700);
54 height: 1rem;
55 width: 1rem;
56 border-radius: 0.5rem;
57 top: 0.375rem;
58 left: 0.375rem;
59 background-color: var(--md-sys-color-outline);
60 box-shadow: var(--md-box_shadow_level1);
61}
62
63body.material .md-switch:checked:after {
64 height: 1.5rem;
65 width: 1.5rem;
66 border-radius: 0.75rem;
67 top: 0.125rem;
68 left: auto;
69 right: 0.125rem;
70 background-color: var(--md-sys-color-on-primary);
71}
72
73body.material .md-switch:hover::before {
74 display: inline-block;
75 background-color: var(--md-sys-color-on-surface);
76 opacity: var(--md-sys-state-hover-state-layer-opacity);
77}
78
79body.material .md-switch:checked:hover::before {
80 background-color: var(--md-sys-color-primary);
81}
82
83body.material .md-switch:hover::after {
84 background-color: var(--md-sys-color-on-surface-variant);
85}
86
87body.material .md-switch:checked:hover::after {
88 background-color: var(--md-sys-color-primary-container);
89}
90
91body.material .md-switch:focus::before {
92 display: inline-block;
93 background-color: var(--md-sys-color-on-surface);
94 opacity: var(--md-sys-state-focus-state-layer-opacity);
95}
96
97body.material .md-switch:checked:focus::before {
98 background-color: var(--md-sys-color-primary);
99}
100
101body.material .md-switch:focus::after {
102 background-color: var(--md-sys-color-on-surface-variant);
103}
104
105body.material .md-switch:checked:focus::after {
106 background-color: var(--md-sys-color-primary-container);
107}
108
109body.material .md-switch:active::before {
110 display: inline-block;
111 background-color: var(--md-sys-color-on-surface);
112 opacity: var(--md-sys-state-focus-state-layer-opacity);
113}
114
115
116body.material .md-switch:checked:active::before {
117 background-color: var(--md-sys-color-primary);
118}
119
120body.material .md-switch:active::after {
121 width: 1.75rem;
122 height: 1.75rem;
123 top: 0;
124 left: 0;
125 border-radius: 0.875rem;
126 background-position: center;
127 animation-name: md-ripple;
128 animation-duration: var(--md-sys-motion-duration-500);
129 background-size: 100%;
130 background-color: var(--md-sys-color-on-surface-variant);
131 background-image:
132 radial-gradient(circle, var(--md-sys-color-primary-container) 1%, transparent 1%);
133 box-shadow: var(--md-box_shadow_level1), 0 0 0 0.375rem color-mix(in srgb, var(--md-sys-color-on-surface), transparent var(--state-pressed-transparency-percentage)) !important;
134}
135
136body.material .md-switch:checked:active::after {
137 left: auto;
138 right: 0;
139 background-color: var(--md-sys-color-primary-container);
140 background-image:
141 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
142 box-shadow: var(--md-box_shadow_level1), 0 0 0 0.375rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-pressed-transparency-percentage)) !important;
143}
144
145body.apple .md-switch {
146 width: 3rem;
147 border-radius: 0.875rem;
148 height: 1.75rem;
149 background-color: var(--colIntIosOffBk);
150}
151
152body.apple .md-switch:checked {
153 background-color: var(--colIntIosOnBk);
154}
155
156body.apple .md-switch:focus {
157 background-color: var(--colIntIosOffBkFc);
158}
159
160body.apple .md-switch:checked:focus {
161 background-color: var(--colIntIosOnBkFc);
162}
163
164body.apple .md-switch::after {
165 content: "";
166 display: inline-block;
167 position: absolute;
168 width: 1.5rem;
169 height: 1.5rem;
170 border-radius: 0.75rem;
171 top: 0.125rem;
172 left: 0.125rem;
173 background-color: var(--colIntIos);
174}
175
176body.apple .md-switch:checked::after {
177 left: auto;
178 right: 0.125rem;
179 border-color: var(--colIntIosOnBk);
180}
181
182body.apple .md-switch:focus::after {
183 border-color: var(--colIntIosOffBkFc);
184}
185
186body.apple .md-switch:checked:focus::after {
187 border-color: var(--colIntIosOnBkFc);
188}

15. lib / css / md-tab.css

1.md-tab {
2 display: flex;
3 background-color: transparent;
4 align-items: stretch;
5 flex-wrap: nowrap;
6 overflow-x: auto;
7 position: sticky;
8 z-index: 1;
9}
10
11.md-tab.fixed {
12 justify-content: center;
13}
14
15.md-tab.scrollable {
16 padding-left: 2rem;
17 gap: 1rem;
18}
19
20.md-tab.scroll {
21 background-color: var(--md-sys-color-surface-container-low);
22}
23
24.md-tab a {
25 position: relative;
26 display: flex;
27 flex-direction: column;
28 justify-content: start;
29 align-items: center;
30 color: var(--md-sys-color-on-surface-variant);
31 font-family: var(--md-sys-typescale-title-small-font);
32 font-weight: var(--md-sys-typescale-title-small-weight);
33 font-size: var(--md-sys-typescale-title-small-size);
34 font-style: var(--md-sys-typescale-title-small-font-style);
35 letter-spacing: var(--md-sys-typescale-title-small-tracking);
36 line-height: var(--md-sys-typescale-title-small-line-height);
37 text-transform: var(--md-sys-typescale-title-small-text-transform);
38 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
39 text-align: center;
40 box-sizing: border-box;
41 border-bottom: 0.1875rem solid var(--md-sys-color-surface);
42}
43
44.md-tab.fixed a {
45 flex: 0 0 var(--tabWidth);
46}
47
48.md-tab.scrollable a {
49 flex: 0 0 auto;
50}
51
52.md-tab a.active {
53 border-bottom-color: var(--md-sys-color-primary);
54}
55
56/* state layer */
57.md-tab a::after {
58 content: "";
59 position: absolute;
60 z-index: -1;
61 top: 0;
62 right: 0;
63 left: 0;
64 bottom: 0;
65 background-color: transparent;
66}
67
68.md-tab span {
69 font-size: var(--iconSize);
70 height: var(--iconSize);
71 width: var(--iconSize);
72 color: var(--md-sys-color-on-surface-variant);
73}
74
75.md-tab .active span {
76 color: var(--md-sys-color-primary);
77}
78
79.md-tab a:hover {
80 color: var(--md-sys-color-on-surface);
81}
82
83/* state layer */
84.md-tab a:hover::after {
85 background-color: var(--md-sys-color-on-surface);
86 opacity: var(--md-sys-state-hover-state-layer-opacity);
87}
88
89.md-tab a.active:hover {
90 color: var(--md-sys-color-primary);
91}
92
93/* state layer */
94.md-tab a.active:hover::after {
95 background-color: var(--md-sys-color-primary);
96 opacity: var(--md-sys-state-hover-state-layer-opacity);
97}
98
99.md-tab a:hover span {
100 color: var(--md-sys-color-on-surface);
101}
102
103.md-tab a.active:hover span {
104 color: var(--md-sys-color-primary);
105}
106
107.md-tab a:focus {
108 outline: none;
109}
110
111/* state layer */
112.md-tab a:focus::after {
113 background-color: var(--md-sys-color-on-surface);
114 opacity: var(--md-sys-state-focus-state-layer-opacity);
115}
116
117/* state layer */
118.md-tab a.active:focus::after {
119 background-color: var(--md-sys-color-primary);
120 opacity: var(--md-sys-state-hover-state-layer-opacity);
121}
122
123.md-tab a:active {
124 background-position: center;
125 background-image:
126 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
127 background-size: 100%;
128 animation-name: md-ripple;
129 animation-duration: var(--md-sys-motion-duration-500);
130}
131
132/* state layer */
133.md-tab a:active::after {
134 background-color: var(--md-sys-color-on-surface);
135 opacity: var(--md-sys-state-pressed-state-layer-opacity);
136}
137
138/* state layer */
139.md-tab a.active:active::after {
140 background-color: var(--md-sys-color-primary);
141 opacity: var(--md-sys-state-pressed-state-layer-opacity);
142}

16. lib / css / md-top-app-bar.css

1.md-headline {
2 box-sizing: border-box;
3 margin: 0;
4 color: var(--md-sys-color-on-surface);
5 background-color: var(--md-sys-color-surface);
6 transition-property: color;
7 transition-duration: var(--md-sys-motion-duration-1000);
8 transition-timing-function: ease-in;
9}
10
11.md-headline.scroll-adicional {
12 color: var(--md-sys-color-surface-container-low);
13 background-color: var(--md-sys-color-surface-container-low);
14}
15
16.md-headline.scroll {
17 color: var(--md-sys-color-surface);
18}
19
20.md-headline.headline-small {
21 padding: 0 1rem 1.5rem 1rem;
22}
23
24.md-headline.headline-medium {
25 padding: 0 1rem 1.75rem 1rem;
26}
27
28md-top-app-bar[headline] h1 {
29 opacity: 0;
30 transition-property: opacity;
31 transition-duration: var(--md-sys-motion-duration-1000);
32}
33
34md-top-app-bar[headline].scroll h1 {
35 opacity: 1;
36 transition-timing-function: ease-in;
37}

17. lib / css / roboto.css

1/* roboto-regular - latin */
2@font-face {
3 /* Revisa
4 * https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
5 * para otras opciones. */
6 font-display: swap;
7 font-family: 'Roboto';
8 font-style: normal;
9 font-weight: 400;
10 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
11 src: url('../fonts/roboto-v32-latin-regular.woff2') format('woff2');
12}

B. Carpeta « lib / fonts »

1. lib / fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints

2. lib / fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf

3. lib / fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2

4. lib / fonts / roboto-v32-latin-regular.woff2

C. Carpeta « lib / js »

1. lib / js / abreElementoHtml.js

1/**
2 * @param { HTMLElement } elementoHtml
3 */
4export function abreElementoHtml(elementoHtml) {
5 elementoHtml.classList.add("open")
6}

2. lib / js / cierraElementoHtmo.js

1/**
2 * @param { HTMLElement } elementoHtml
3 */
4export function cierraElementoHtmo(elementoHtml) {
5 elementoHtml.classList.remove("open")
6}
7

3. lib / js / exportaAHtml.js

1/**
2 * Permite que los eventos de html usen la función.
3 * @param {function} functionInstance
4 */
5export function exportaAHtml(functionInstance) {
6 window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance
7}
8
9/**
10 * @param {function} valor
11 */
12export function nombreDeFuncionParaHtml(valor) {
13 const names = valor.name.split(/\s+/g)
14 return names[names.length - 1]
15}

4. lib / js / getAttribute.js

1/**
2 * @param {HTMLElement} elementoHtml
3 * @param {string} nombre
4 * @returns {string}
5 */
6export function getAttribute(elementoHtml, nombre) {
7 const valor = elementoHtml.getAttribute(nombre)
8 return valor === null ? "" : valor
9}

5. lib / js / htmlentities.js

1/**
2 * Codifica un texto para que cambie los caracteres
3 * especiales y no se pueda interpretar como
4 * etiiqueta HTML. Esta técnica evita la inyección
5 * de código.
6 * @param { string } texto
7 * @returns { string } un texto que no puede
8 * interpretarse como HTML. */
9export function htmlentities(texto) {
10 return texto.replace(/[<>"']/g, textoDetectado => {
11 switch (textoDetectado) {
12 case "<": return "<"
13 case ">": return ">"
14 case '"': return """
15 case "'": return "'"
16 default: return textoDetectado
17 }
18 })
19}

6. lib / js / muestraError.js

1import { exportaAHtml } from "./exportaAHtml.js"
2import { ProblemDetails } from "./ProblemDetails.js"
3
4/**
5 * Muestra un error en la consola y en un cuadro de
6 * alerta el mensaje de una excepción.
7 * @param { ProblemDetails | Error | null } error descripción del error.
8 */
9export function muestraError(error) {
10
11 if (error === null) {
12
13 console.error("Error")
14 alert("Error")
15
16 } else if (error instanceof ProblemDetails) {
17
18 let mensaje = error.title
19 if (error.detail) {
20 mensaje += `\n\n${error.detail}`
21 }
22 mensaje += `\n\nCódigo: ${error.status}`
23 if (error.type) {
24 mensaje += ` ${error.type}`
25 }
26
27 console.error(mensaje)
28 console.error(error)
29 console.error("Headers:")
30 error.headers.forEach((valor, llave) => console.error(llave, "=", valor))
31 alert(mensaje)
32
33 } else {
34
35 console.error(error)
36 alert(error.message)
37
38 }
39
40}
41
42exportaAHtml(muestraError)

7. lib / js / muestraTextoDeAyuda.js

1import { exportaAHtml } from "./exportaAHtml.js"
2
3/**
4 * Si un elemento HTML tiene un mensaje de validación, lo
5 * muestra en su elemento de ayuda; en caso contrario, muestra
6 * un mensaje de ayuda.
7 * @param { {
8 * validity: { valid: boolean };
9 * validationMessage: string
10 * } } elementoHtml elemento que contiene datos de validación.
11 * @param { HTMLElement } elementoDeAyuda elemento fonde
12 * se muestran los elementos de validación para elementoHtml.
13 * @param { string } mensajeDeAyuda mensaje de ayuda cuando el
14 * estado de elementoHtml es válido.
15 */
16export function muestraTextoDeAyuda(elementoHtml, elementoDeAyuda,
17 mensajeDeAyuda) {
18 if (elementoHtml.validity.valid) {
19 elementoDeAyuda.textContent = mensajeDeAyuda
20 } else {
21 elementoDeAyuda.textContent = elementoHtml.validationMessage
22 }
23}
24
25exportaAHtml(muestraTextoDeAyuda)

8. lib / js / ProblemDetails.js

1/**
2 * Detalle de los errores devueltos por un servicio.
3 */
4export class ProblemDetails extends Error {
5
6 /**
7 * @param {number} status
8 * @param {Headers} headers
9 * @param {string} title
10 * @param {string} [type]
11 * @param {string} [detail]
12 */
13 constructor(status, headers, title, type, detail) {
14 super(title)
15 /**
16 * @readonly
17 */
18 this.status = status
19 /**
20 * @readonly
21 */
22 this.headers = headers
23 /**
24 * @readonly
25 */
26 this.type = type
27 /**
28 * @readonly
29 */
30 this.detail = detail
31 /**
32 * @readonly
33 */
34 this.title = title
35 }
36
37}

9. lib / js / querySelector.js

1/**
2 * @template { HTMLElement } T
3 * @param { Document | Element | ShadowRoot } raiz
4 * @param { string } query
5 * @returns { T }
6 */
7export function querySelector(raiz, query) {
8 /** @type { T | null } */
9 const resutado = raiz.querySelector(query)
10 if (resutado === null)
11 throw new Error(`No se encuentra ${query}.`)
12 return resutado
13}

10. lib / js / resaltaSiEstasEn.js

1import { querySelector } from "./querySelector.js"
2
3/**
4 * @param {string[]} paginas
5 */
6export function resaltaSiEstasEn(paginas) {
7
8 const pathname = location.pathname
9
10 for (const pagina of paginas) {
11
12 if (pathname === pagina) {
13 setTimeout(() => {
14 const tab = document.querySelector(".active")
15 if (tab !== null && tab.closest(".scrollable") !== null) {
16 tab.scrollIntoView({ inline: "center", block: "end" })
17 }
18 })
19 return `class="active"`
20 }
21
22 }
23
24 return ""
25
26}

11. Carpeta « lib / js / const »

A. lib / js / const / ES_APPLE.js

1export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent)

12. Carpeta « lib / js / custom »

A. lib / js / custom / md-menu-button.js

1import { MdNavigationDrawer } from "./MdNavigationDrawer.js"
2
3export class MdMenuButton extends HTMLButtonElement {
4
5 constructor() {
6 super()
7 this.abreDrawer = this.abreDrawer.bind(this)
8 }
9
10 connectedCallback() {
11 this.type = "button"
12 this.classList.add("md-standard-icon-button")
13 this.innerHTML = /* HTML */
14 `<span class="material-symbols-outlined">menu</span>`
15 this.addEventListener("click", this.abreDrawer)
16 }
17
18 disconnectedCallback() {
19 this.removeEventListener("click", this.abreDrawer)
20 }
21
22 abreDrawer() {
23 const drawer = document.querySelector(".drawer")
24 if (drawer instanceof MdNavigationDrawer) {
25 drawer.abre()
26 }
27 }
28}
29
30customElements.define("md-menu-button", MdMenuButton, { extends: "button" })

B. lib / js / custom / md-options-menu.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3import { querySelector } from "../querySelector.js"
4
5export class MdOptionsMenu extends HTMLElement {
6
7 getContent() {
8 return /* HTML */`
9
10 <style>
11
12 :host {
13 position: absolute;
14 }
15
16 </style>
17
18 <slot></slot>`
19 }
20
21 constructor() {
22 super()
23 const shadow = this.attachShadow({ mode: "open" })
24 shadow.innerHTML = this.getContent()
25 this._configuraOpciones = this._configuraOpciones.bind(this)
26
27 /**
28 * @private
29 * @type { HTMLSlotElement }
30 */
31 this._slot = querySelector(shadow, "slot")
32 /**
33 * @private
34 * @type { HTMLElement[] }
35 */
36 this._opciones = []
37 this._slot.addEventListener("slotchange", this._configuraOpciones)
38 }
39
40 connectedCallback() {
41 this.classList.add("md-menu")
42 this.role = "listbox"
43 }
44
45 /**
46 * @returns {readonly Readonly<HTMLElement>[]}
47 */
48 get opciones() {
49 return this._opciones
50 }
51
52 get seleccion() {
53 /** @type { HTMLInputElement | null } */
54 const seleccionado = this.querySelector(".selected")
55 return seleccionado === null ? "" : seleccionado.value
56 }
57
58 _configuraOpciones() {
59 /**
60 * @type {HTMLElement[]}
61 */
62 const opciones = []
63 for (const opcion of this._slot.assignedElements()) {
64 opcion.role = "option"
65 if (opcion instanceof HTMLElement) {
66 opciones.push(opcion)
67 }
68 }
69 this._opciones = opciones
70 }
71
72 abre() {
73 abreElementoHtml(this)
74 }
75
76
77 cierra() {
78 cierraElementoHtmo(this)
79 }
80
81 /**
82 * @param {string} value
83 */
84 muestraValue(value) {
85 let texto = ""
86 for (const opcion of this._opciones) {
87 if (opcion.dataset.value === value) {
88 opcion.classList.add("selected")
89 let textContent = opcion.textContent
90 if (texto === "" && textContent !== null) {
91 textContent = textContent.trim()
92 if (textContent !== "") {
93 texto = textContent
94 }
95 }
96 } else {
97 opcion.classList.remove("selected")
98 }
99 }
100 return texto
101 }
102
103}
104
105customElements.define("md-options-menu", MdOptionsMenu)

C. lib / js / custom / md-overflow-button.js

1import { ES_APPLE } from "../const/ES_APPLE.js"
2
3export class MdOverflowButton extends HTMLButtonElement {
4
5 connectedCallback() {
6 this.type = "button"
7 this.classList.add("md-standard-icon-button")
8 this.innerHTML = ES_APPLE
9 ? /* HTML */
10 `<span style="color: var(--md-sys-color-on-surface-variant)"
11 class="material-symbols-outlined">
12 more_horiz
13 </span>`
14 : /* HTML */
15 `<span style="color: var(--md-sys-color-on-surface-variant)"
16 class="material-symbols-outlined">
17 more_vert
18 </span>`
19 }
20
21}
22
23customElements
24 .define("md-overflow-button", MdOverflowButton, { extends: "button" })

D. lib / js / custom / md-overflow-menu.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3
4export class MdOverflowMenu extends HTMLElement {
5
6 getContent() {
7 return /* HTML */`
8
9 <style>
10
11 :host {
12 position: fixed;
13 min-width: 7rem;
14 max-width: 280px;
15 }
16
17 ::slotted(*) {
18 text-align: start;
19 width: 100%;
20 border: none;
21 background-color: transparent;
22 }
23
24 </style>
25
26 <slot></slot>`
27 }
28
29 constructor() {
30 super()
31 const shadow = this.attachShadow({ mode: "open" })
32 shadow.innerHTML = this.getContent()
33 this.clicCierra = this.clicCierra.bind(this)
34 /**
35 * @private
36 * @type {HTMLButtonElement| null}
37 */
38 this._toggleButton = null
39 }
40
41 connectedCallback() {
42 this.classList.add("md-menu")
43 this.role = "menu"
44 }
45
46 /**
47 * @param {HTMLButtonElement} toggleButton
48 */
49 alterna(toggleButton) {
50 this._toggleButton = toggleButton
51 const top = toggleButton.offsetTop + toggleButton.offsetHeight - 4
52 const right =
53 innerWidth - (toggleButton.offsetLeft + toggleButton.offsetWidth) - 3
54 this.style.top = `${top}px`
55 this.style.right = `${right}px`
56 const list = this.classList
57 if (list.contains("open")) {
58 this.cierra()
59 } else {
60 this.abre()
61 }
62 }
63
64 abre() {
65 document.addEventListener("click", this.clicCierra)
66 abreElementoHtml(this)
67 }
68
69 cierra() {
70 document.removeEventListener("click", this.clicCierra)
71 cierraElementoHtmo(this)
72 }
73
74 /**
75 * @param {Event} evt
76 */
77 clicCierra(evt) {
78 const target = evt.target
79 if (this.classList.contains("open")
80 && this._toggleButton !== null
81 && target instanceof HTMLElement
82 && !this._toggleButton.contains(target)) {
83 this.cierra()
84 }
85 }
86}
87
88customElements.define("md-overflow-menu", MdOverflowMenu)

E. lib / js / custom / md-select-menu.js

1import { getAttribute } from "../getAttribute.js"
2import { querySelector } from "../querySelector.js"
3import { MdOptionsMenu } from "./md-options-menu.js"
4
5export class MdSelectMenu extends HTMLElement {
6
7 static get observedAttributes() {
8 return ["options", "value", "required"]
9 }
10
11 getContent() {
12 return /* HTML */ `
13 <link rel="stylesheet" href="/lib/css/material-symbols-outlined.css">
14
15 <style>
16 :host {
17 display: block;
18 cursor: default;
19 }
20
21 output {
22 display: block;
23 padding-right: 2rem;
24 white-space: nowrap;
25 text-overflow: ellipsis;
26 overflow: hidden;
27 }
28
29 #up {
30 position: absolute;
31 bottom: 0.5rem;
32 right: 0.75rem;
33 display: none;
34 color: var(--md-sys-color-on-surface-variant);
35 }
36
37 #down {
38 position: absolute;
39 bottom: 0.5rem;
40 right: 0.75rem;
41 color: var(--md-sys-color-on-surface-variant);
42 }
43
44 :host(.open) #up {
45 display: inline-block;
46 }
47
48 :host(.open) #down {
49 display: none;
50 }
51
52 :host(:invalid) #up,
53 :host(:invalid) #down {
54 color: var(--md-sys-color-error);
55 }
56
57 </style>
58 <output></output>
59 <span id="down" class="material-symbols-outlined">
60 arrow_drop_down
61 </span>
62 <span id="up" class="material-symbols-outlined">
63 arrow_drop_up
64 </span>`
65 }
66
67 constructor() {
68 super()
69
70 const shadow = this.attachShadow({ mode: "open" })
71 shadow.innerHTML = this.getContent()
72
73 this._alterna = this._alterna.bind(this)
74 this._onKeyDown = this._onKeyDown.bind(this)
75 this._cierra = this._cierra.bind(this)
76 this._clicEnDialogo = this._clicEnDialogo.bind(this)
77 this.clicExterno = this.clicExterno.bind(this)
78 this.muestraValue = this.muestraValue.bind(this)
79
80 /**
81 * @private
82 * @type {string}
83 */
84 this._customValidity = ""
85
86 /**
87 * @private
88 * @type { HTMLOutputElement }
89 */
90 this.output = querySelector(shadow, "output")
91 /**
92 * @private
93 * @type { MdOptionsMenu | null }
94 */
95 this._optionsMenu = null
96 /**
97 * @protected
98 * @readonly
99 */
100 this._internals = this.attachInternals()
101 this._internals.role = "select"
102 addEventListener("load", this.muestraValue)
103 }
104
105 connectedCallback() {
106 this.tabIndex = 0
107 this.role = "combobox"
108 this.ariaHasPopup = "listbox"
109 this.ariaExpanded = "false"
110 this["aria-controls"] = this.options
111 this.addEventListener("keydown", this._onKeyDown)
112 const parentElement = this.parentElement
113 if (parentElement !== null) {
114 parentElement.addEventListener("click", this._alterna)
115 }
116 }
117
118 /**
119 * @param {string} nombreDeAtributo
120 * @param {string} _valorAnterior
121 * @param {string} _nuevoValor
122 */
123 attributeChangedCallback(nombreDeAtributo, _valorAnterior, _nuevoValor) {
124 switch (nombreDeAtributo) {
125 case "options":
126 this._cambiaOptions()
127 break
128 case "value":
129 this.muestraValue()
130 break
131 case "required":
132 this.checkValidity()
133 break
134 }
135 }
136
137 get options() {
138 return getAttribute(this, "options")
139 }
140
141 set options(options) {
142 this.setAttribute("options", options)
143 }
144
145 _cambiaOptions() {
146 if (this._optionsMenu !== null) {
147 this._optionsMenu = null
148 }
149 this["aria-controls"] = this.options
150 }
151
152 get required() {
153 return this.hasAttribute("required")
154 }
155
156 set required(required) {
157 this.toggleAttribute("required", Boolean(required))
158 }
159
160 get value() {
161 return getAttribute(this, "value")
162 }
163
164 set value(value) {
165 this.setAttribute("value", value)
166 }
167
168 get name() {
169 return getAttribute(this, "name")
170 }
171
172 set name(name) {
173 this.setAttribute("name", name)
174 }
175
176 muestraValue() {
177 const value = this.value
178 this._internals.setFormValue(value)
179
180 // En un futuro se usará esto en vez de la clase populated.
181 // if (value === "") {
182 // this._internals.states.delete("populated")
183 // } else {
184 // this._internals.states.add("populated")
185 // }
186
187 if (this.isConnected) {
188 if (value === "") {
189 this.classList.remove("populated")
190 } else {
191 this.classList.add("populated")
192 }
193 this._checkValidity()
194 const optionsMenu = this.optionsMenu
195 if (optionsMenu !== null) {
196 this.output.value = optionsMenu.muestraValue(value)
197 }
198 }
199 }
200
201 get form() {
202 return this._internals && this._internals.form
203 }
204
205 get willValidate() {
206 return this._internals ? this._internals.willValidate : true
207 }
208
209 /**
210 * @param {string} message
211 */
212 setCustomValidity(message) {
213 this._customValidity = message
214 this._checkValidity()
215 }
216
217 /**
218 * @returns {ValidityState}
219 */
220 get validity() {
221 return this._internals.validity
222 }
223
224 checkValidity() {
225 return this._internals.checkValidity()
226 }
227
228 reportValidity() {
229 return this._internals.reportValidity()
230 }
231
232 get validationMessage() {
233 return this._internals.validationMessage
234 }
235 /** @returns {boolean} */
236 _checkValidity() {
237 if (this._customValidity !== "") {
238 this._internals.setValidity({ customError: true }, this._customValidity)
239 return false
240 } else if (this.required && this.value === "") {
241 this._internals.setValidity({ valueMissing: true }, "Seleccione una opción.")
242 return false
243 } else {
244 this._internals.setValidity({})
245 return true
246 }
247 }
248
249 /** @private */
250 _alterna() {
251 if (this.classList.contains("open")) {
252 this._cierra()
253 } else {
254 this._abre()
255 }
256 }
257
258 /** @private */
259 _abre() {
260 this.classList.add("open")
261 const parentElement = this.parentElement
262 if (parentElement !== null) {
263 const optionsMenu = this.optionsMenu
264 if (optionsMenu !== null) {
265 optionsMenu.style.top = `${parentElement.offsetTop + 58}px`
266 optionsMenu.style.left = `${parentElement.offsetLeft}px`
267 optionsMenu.style.width = `${parentElement.offsetWidth}px`
268 optionsMenu.abre()
269 this.focus()
270 optionsMenu.addEventListener("click", this._clicEnDialogo)
271 }
272 this.ariaExpanded = "true"
273 document.addEventListener("click", this.clicExterno)
274 }
275 }
276
277 /** @private */
278 _cierra() {
279 this.classList.remove("open")
280 const optionsMenu = this.optionsMenu
281 if (optionsMenu !== null) {
282 optionsMenu.cierra()
283 optionsMenu.removeEventListener("click", this._clicEnDialogo)
284 }
285 this.ariaExpanded = "false"
286 document.removeEventListener("click", this.clicExterno)
287 this.dispatchEvent(new Event("input", { bubbles: true }))
288 }
289
290 get optionsMenu() {
291 if (this._optionsMenu === null) {
292 if (this.options !== "") {
293 const optionsMenu = document.getElementById(this.options)
294 if (optionsMenu instanceof MdOptionsMenu) {
295 this._optionsMenu = optionsMenu
296 } else {
297 throw new Error(`Valor incorrecto para options: "${this.options}".`)
298 }
299 }
300 }
301 return this._optionsMenu
302 }
303
304 /** @private */
305 _avanzaOpcion() {
306 const i = this._valueIndex
307 if (i > -1) {
308 const optionsMenu = this.optionsMenu
309 if (optionsMenu !== null) {
310 const opciones = optionsMenu.opciones
311 if (i < opciones.length - 1) {
312 this.value = getAttribute(opciones[i + 1], "data-value")
313 }
314 }
315 }
316 }
317
318 /** @private */
319 _retrocedeOpcion() {
320 const i = this._valueIndex
321 if (i > -1) {
322 const optionsMenu = this.optionsMenu
323 if (optionsMenu !== null) {
324 const opciones = optionsMenu.opciones
325 if (i > 0) {
326 this.value = getAttribute(opciones[i - 1], "data-value")
327 }
328 }
329 }
330 }
331
332 /**
333 * @private
334 * @returns {number}
335 */
336 get _valueIndex() {
337 const value = this.value
338 const optionsMenu = this.optionsMenu
339 return (optionsMenu === null
340 ? -1
341 : optionsMenu.opciones.findIndex(opcion => opcion.dataset.value === value))
342 }
343
344 /**
345 * @private
346 * @param {Event} event
347 */
348 _clicEnDialogo(event) {
349 const target = event.target
350 const optionsMenu = this.optionsMenu
351 let value = ""
352 if (optionsMenu !== null) {
353 for (const opcion of optionsMenu.opciones) {
354 if (opcion === target) {
355 opcion.classList.add("selected")
356 value = getAttribute(opcion, "data-value")
357 } else {
358 opcion.classList.remove("selected")
359 }
360 }
361 }
362 this.value = value
363 this._cierra()
364 this.focus()
365}
366
367 /**
368 * @param {Event} evt
369 */
370 clicExterno(evt) {
371 const target = evt.target
372 const parentElement = this.parentElement
373 const optionsMenu = this._optionsMenu
374 if (this.classList.contains("open")
375 && target instanceof HTMLElement
376 && parentElement !== null
377 && !parentElement.contains(target)
378 && optionsMenu !== null
379 && !optionsMenu.contains(target)) {
380 this._cierra()
381 }
382 }
383
384 /**
385 * @param { KeyboardEvent } event
386 */
387 _onKeyDown(event) {
388 const key = event.key
389 const optionsMenu = this._optionsMenu
390 if (optionsMenu !== null) {
391 if (optionsMenu.classList.contains("open")) {
392 if (key === "ArrowDown") {
393 event.preventDefault()
394 this._avanzaOpcion()
395 } else if (key === "ArrowUp") {
396 event.preventDefault()
397 this._retrocedeOpcion()
398 } else if (key === "Escape") {
399 event.preventDefault()
400 this._cierra()
401 } else if (key === " ") {
402 event.preventDefault()
403 this._cierra()
404 } else if (key === "Tab") {
405 this._cierra()
406 } else {
407 event.preventDefault()
408 }
409 } else if (key === " ") {
410 event.preventDefault()
411 this._abre()
412 } else if (key === "Tab") {
413 this._cierra()
414 } else {
415 event.preventDefault()
416 }
417 }
418 }
419
420}
421
422MdSelectMenu.formAssociated = true
423
424customElements.define("md-select-menu", MdSelectMenu)

F. lib / js / custom / md-slider-field.js

1import { querySelector } from "../querySelector.js"
2
3export class MdSliderField extends HTMLElement {
4
5 getContent() {
6 return /* HTML */`
7 <style>
8 :host {
9 display: block;
10 margin: 1rem;
11 }
12
13 :host([hidden]) {
14 display: none;
15 }
16
17 #label::slotted(*) {
18 display: block;
19 white-space: nowrap;
20 text-overflow: ellipsis;
21 overflow: hidden;
22 color: var(--md-sys-color-on-surface-variant);
23 font-family: var(--md-sys-typescale-body-small-font);
24 font-weight: var(--md-sys-typescale-body-small-weight);
25 font-size: var(--md-sys-typescale-body-small-size);
26 font-style: var(--md-sys-typescale-body-small-font-style);
27 letter-spacing: var(--md-sys-typescale-body-small-tracking);
28 line-height: var(--md-sys-typescale-body-small-line-height);
29 text-transform: var(--md-sys-typescale-body-small-text-transform);
30 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
31 }
32
33 [name="slider"]::slotted(input) {
34 -webkit-appearance: none;
35 appearance: none;
36 height: 0.25rem;
37 border-radius: 0.125rem;
38 background-image:
39 linear-gradient(to right, var(--md-sys-color-primary) 0%, var(--md-sys-color-primary) 50%, var(--md-sys-color-surface-container-highest) 50%, var(--md-sys-color-surface-container-highest) 100%);
40 }
41
42 [name="slider"]::slotted(input:focus) {
43 outline: none;
44 }
45
46 [name="supporting"]::slotted(*) {
47 display: block;
48 color: var(--md-sys-color-on-surface-variant);
49 font-family: var(--md-sys-typescale-body-small-font);
50 font-weight: var(--md-sys-typescale-body-small-weight);
51 font-size: var(--md-sys-typescale-body-small-size);
52 font-style: var(--md-sys-typescale-body-small-font-style);
53 letter-spacing: var(--md-sys-typescale-body-small-tracking);
54 line-height: var(--md-sys-typescale-body-small-line-height);
55 text-transform: var(--md-sys-typescale-body-small-text-transform);
56 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
57 padding-top: 0.5rem;
58 white-space: nowrap;
59 text-overflow: ellipsis;
60 overflow: hidden;
61 }
62 </style>
63 <slot id="label"></slot>
64 <slot name="slider"></slot>
65 <slot name="supporting"></slot>`
66 }
67
68 constructor() {
69 super()
70 const shadow = this.attachShadow({ mode: "open", delegatesFocus: true })
71 shadow.innerHTML = this.getContent()
72 this._configuraSlider = this._configuraSlider.bind(this)
73 this.analiza = this.analiza.bind(this)
74
75 /**
76 * @private
77 * @type {HTMLSlotElement}
78 */
79 this._slotSlider = querySelector(shadow, '[name="slider"]')
80 /**
81 * @private
82 * @type {HTMLInputElement|null}
83 */
84 this._input = null
85 this._slotSlider.addEventListener("slotchange", this._configuraSlider)
86 }
87
88 /** @private */
89 _configuraSlider() {
90 if (this._input !== null) {
91 this._input.removeEventListener("input", this.analiza)
92 this._input = null
93 }
94 for (const input of this._slotSlider.assignedElements()) {
95 if (input instanceof HTMLInputElement) {
96 this._input = input
97 input.addEventListener("input", this.analiza)
98 this.analiza()
99 }
100 }
101 }
102
103 analiza() {
104 const i = this._input
105 if (i !== null) {
106 const v = i.valueAsNumber
107 const min = parseFloat(i.min)
108 const max = parseFloat(i.max)
109 const value = (v - min) / (max - min) * 100
110 i.title = v.toString()
111 i.style.background =
112 `linear-gradient(to right, var(--md-sys-color-primary) 0%, var(--md-sys-color-primary) ${value
113 }%, var(--md-sys-color-surface-container-highest) ${value
114 }%, var(--md-sys-color-surface-container-highest) 100%)`
115 }
116 }
117
118}
119
120customElements.define("md-slider-field", MdSliderField)

G. lib / js / custom / md-top-app-bar.js

1import { ES_APPLE } from "../const/ES_APPLE.js"
2import { getAttribute } from "../getAttribute.js"
3import { querySelector } from "../querySelector.js"
4
5class MdTopAppBar extends HTMLElement {
6
7 getContent() {
8 return /* HTML */`
9 <style>
10
11 :host {
12 display: flex;
13 box-sizing: border-box;
14 align-items: center;
15 padding: 0 0.25rem;
16 background-color: var(--md-sys-color-surface);
17 position: sticky;
18 z-index: 1;
19 left: env(titlebar-area-x, 0);
20 top: env(titlebar-area-y, 0);
21 height: env(titlebar-area-height, 4rem);
22 width: env(titlebar-area-width, 100%);
23 }
24
25 :host(.apple) {
26 height: env(titlebar-area-height, 3rem);
27 }
28
29 :host(.scroll) {
30 background-color: var(--md-sys-color-surface-container-low);
31 }
32
33 #navigation {
34 flex: 0 0 auto;
35 overflow: hidden
36 }
37
38 #navigation ::slotted(*) {
39 color: var(--md-sys-color-on-surface);
40 }
41
42 #acciones {
43 margin-left: auto;
44 flex: 0 0 auto;
45 overflow: hidden
46 }
47
48 :host(.centrado) #acciones,
49 :host(.center-aligned) #acciones {
50 flex: 0 0 3rem;
51 overflow: hidden
52 }
53
54 #headline::slotted(*) {
55 -webkit-app-region: drag;
56 flex: 1 1 auto;
57 white-space: nowrap;
58 text-overflow: ellipsis;
59 overflow: hidden;
60 font-family: var(--md-sys-typescale-title-large-font);
61 font-weight: var(--md-sys-typescale-title-large-weight);
62 font-size: var(--md-sys-typescale-title-large-size);
63 font-style: var(--md-sys-typescale-title-large-font-style);
64 letter-spacing: var(--md-sys-typescale-title-large-tracking);
65 line-height: var(--md-sys-typescale-title-large-line-height);
66 text-transform: var(--md-sys-typescale-title-large-text-transform);
67 text-decoration: var(--md-sys-typescale-title-large-text-decoration);
68 color: var(--md-sys-color-on-surface);
69 }
70
71 :host(.center-aligned) #headline::slotted(*) {
72 flex: 1 1 auto;
73 text-align: center
74 }
75
76 </style>
77
78 <span id="navigation">
79 <slot name="navigation"></slot>
80 </span>
81 <slot id="headline"></slot>
82 <span id="acciones">
83 <slot name="action"></slot>
84 </span>`
85 }
86
87 constructor() {
88 super()
89 if (ES_APPLE) {
90 document.body.classList.add("apple")
91 document.body.classList.remove("material")
92 } else {
93 document.body.classList.add("material")
94 document.body.classList.remove("apple")
95 }
96
97 /**
98 * @private
99 * @readonly
100 */
101 const shadow = this.attachShadow({ mode: "open" })
102 shadow.innerHTML = this.getContent()
103 this._configuraAction = this._configuraAction.bind(this)
104 /**
105 * @private
106 * @type {number}
107 */
108 this._posY = 0
109 /**
110 * @private
111 * @type {boolean}
112 */
113 this._scrolling = false
114 /**
115 * @private
116 * @type { HTMLSlotElement }
117 */
118 this._navigation = querySelector(shadow, '[name="navigation"]')
119 /**
120 * @private
121 * @type { HTMLSlotElement }
122 */
123 this._action = querySelector(shadow, '[name="action"]')
124 /**
125 * @private
126 * @type { HTMLHeadingElement | null }
127 */
128 this._headline = null
129 /**
130 * @private
131 * @type { HTMLElement | null }
132 */
133 this._adicional = null
134 this._action.addEventListener("slotchange", this._configuraAction)
135 addEventListener("scroll", () => this._onScroll())
136 addEventListener("load", () => this.configurOtros())
137 }
138
139 connectedCallback() {
140 this.role = "toolbar"
141 this._configuraAction()
142 }
143
144 configurOtros() {
145 const idHeadline = getAttribute(this, "headline")
146 if (idHeadline !== "") {
147 const headline = document.getElementById(idHeadline)
148 if (headline instanceof HTMLHeadingElement) {
149 this._headline = headline
150 if (this.classList.contains("apple") || this.classList.contains("medium")) {
151 headline.classList.add("md-headline", "headline-small")
152 } else {
153 headline.classList.add("md-headline", "headline-medium")
154 }
155 }
156 }
157 const idAdicional = getAttribute(this, "adicional")
158 if (idAdicional !== "") {
159 this._adicional = document.getElementById(idAdicional)
160 if (this._adicional !== null) {
161 if (this.classList.contains("apple")) {
162 this._adicional.style.top = "env(titlebar-area-height, 3rem)"
163 } else {
164 this._adicional.style.top = "env(titlebar-area-height, 4rem)"
165 }
166 }
167 }
168 }
169
170 _configuraAction() {
171 const assignedElements = this._action.assignedElements()
172 if (this.isConnected) {
173 if (ES_APPLE) {
174 this.classList.add("apple")
175 this.classList.remove("material")
176 } else {
177 this.classList.add("material")
178 this.classList.remove("apple")
179 }
180 if (this.classList.contains("center-aligned")) {
181 this.classList.remove("centrado")
182 this.classList.remove("justificado")
183 } else {
184 if (ES_APPLE && assignedElements.length <= 1) {
185 this.classList.add("centrado")
186 this.classList.remove("justificado")
187 } else {
188 this.classList.add("justificado")
189 this.classList.remove("centrado")
190 }
191 }
192 }
193 }
194
195 /** @private */
196 _onScroll() {
197 this._posY = scrollY
198 if (!this._scrolling) {
199 requestAnimationFrame(() => this._avanza())
200 }
201 this._scrolling = true
202 }
203
204 /** @private */
205 _avanza() {
206 if (this._posY === 0) {
207 this.classList.remove("scroll")
208 if (this._headline !== null) {
209 if (this._adicional === null) {
210 this._headline.classList.remove("scroll")
211 } else {
212 this._headline.classList.remove("scroll-adicional")
213 }
214 }
215 if (this._adicional !== null) {
216 this._adicional.classList.remove("scroll")
217 }
218 } else {
219 this.classList.add("scroll")
220 if (this._headline !== null) {
221 if (this._adicional === null) {
222 this._headline.classList.add("scroll")
223 } else {
224 this._headline.classList.add("scroll-adicional")
225 }
226 }
227 if (this._adicional !== null) {
228 this._adicional.classList.add("scroll")
229 }
230 }
231 this._scrolling = false
232 }
233
234}
235
236customElements.define("md-top-app-bar", MdTopAppBar)

H. lib / js / custom / MdNavigationDrawer.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3import { querySelector } from "../querySelector.js"
4
5export class MdNavigationDrawer extends HTMLElement {
6
7 /**
8 * @returns {string}
9 */
10 getHipervinculos() { throw new Error("abstract") }
11
12 getContent() {
13 return /* HTML */`
14
15 <link rel="stylesheet" href="/lib/css/material-symbols-outlined.css">
16 <link rel="stylesheet" href="/lib/css/md-ripple.css">
17 <link rel="stylesheet" href="/material-tokens/css/shape.css">
18 <link rel="stylesheet" href="/material-tokens/css/motion.css">
19
20 <style>
21
22 :host {
23 display: block;
24 }
25
26 :host([hidden]) {
27 display: none;
28 }
29
30 nav {
31 display: none;
32 flex-direction: column;
33 position: fixed;
34 z-index: 4;
35 box-sizing: border-box;
36 top: 0;
37 left: 0;
38 bottom: 0;
39 width: var(--anchoNav);
40 max-width: 80vw;
41 overflow: hidden;
42 overscroll-behavior: contain;
43 background-color: var(--md-sys-color-surface-container-low);
44 transform: translateX(-100%);
45 transition-property: display, transform;
46 transition-behavior: allow-discrete;
47 }
48
49 nav.open {
50 display: flex;
51 transform: translateX(0);
52 }
53
54 nav>div {
55 flex-grow: 1;
56 overflow: auto;
57 padding: 0.75rem 1rem;
58 }
59
60 h1 {
61 margin: 0;
62 height: 3.5rem;
63 line-height: 3.5rem;
64 padding: 0 0 0 0.75rem;
65 white-space: nowrap;
66 text-overflow: ellipsis;
67 overflow: hidden;
68 color: var(--md-sys-color-on-surface-variant);
69 font-family: var(--md-sys-typescale-title-small-font);
70 font-weight: var(--md-sys-typescale-title-small-weight);
71 font-size: var(--md-sys-typescale-title-small-size);
72 font-style: var(--md-sys-typescale-title-small-font-style);
73 letter-spacing: var(--md-sys-typescale-title-small-tracking);
74 text-transform: var(--md-sys-typescale-title-small-text-transform);
75 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
76 }
77
78 a::after { /* container inactive */
79 content: "";
80 position: absolute;
81 z-index: -2;
82 top: 0;
83 right: 0;
84 left: 0;
85 bottom: 0;
86 }
87
88 a.active::after { /* container */
89 background-color: var(--md-sys-color-secondary-container);
90 }
91
92 a { /* label, shape inactive */
93 position: relative;
94 display: block;
95 box-sizing: border-box;
96 height: 3.5rem;
97 line-height: 3.5rem;
98 padding: 0 0.75rem;
99 border-radius: 1.75rem;
100 color: var(--md-sys-color-on-surface-variant);
101 font-family: var(--md-sys-typescale-label-large-font);
102 font-weight: var(--md-sys-typescale-label-large-weight);
103 font-size: var(--md-sys-typescale-label-large-size);
104 font-style: var(--md-sys-typescale-label-large-font-style);
105 letter-spacing: var(--md-sys-typescale-label-large-tracking);
106 text-transform: var(--md-sys-typescale-label-large-text-transform);
107 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
108 overflow: hidden;
109 white-space: nowrap;
110 text-overflow: ellipsis;
111 }
112
113 a.active { /* label, shape */
114 font-weight: var(--md-sys-typescale-label-large-weight-prominent);
115 color: var(--md-sys-color-on-secondary-container);
116 }
117
118 a::before { /* state layer */
119 content: "";
120 position: absolute;
121 z-index: -1;
122 top: 0;
123 right: 0;
124 left: 0;
125 bottom: 0;
126 }
127
128 a span { /* inactive icon */
129 position: relative;
130 margin-right: 0.75rem;
131 vertical-align: middle;
132 color: var(--md-sys-color-on-surface-variant);
133 font-size: 1.5rem;
134 width: 1.5rem;
135 height: 1.5rem;
136 }
137
138 a.active span { /* icon */
139 color: var(--md-sys-color-on-secondary-container);
140 }
141
142 #scrim {
143 display: none;
144 position: fixed;
145 z-index: 3;
146 top: 0;
147 left: 0;
148 bottom: 0;
149 right: 0;
150 opacity: 0.4;
151 background-color: var(--md-ref-palette-neutral-variant20);
152 transform: translateX(-100%);
153 transition-property: display, transform;
154 transition-behavior: allow-discrete;
155 }
156
157 #scrim.open {
158 display: block;
159 transform: translateX(0);
160 }
161
162 @starting-style {
163 nav.open{
164 display: flex;
165 transform: translateX(-100%);
166 }
167 #scrim.open {
168 display: block;
169 transform: translateX(-100%);
170 }
171 }
172
173 a:hover { /* inactive label, shape */
174 color: var(--md-sys-color-on-surface);
175 }
176
177 a.active:hover { /* active label, shape */
178 color: var(--md-sys-color-on-secondary-container);
179 }
180
181 a:hover::before { /* inactive state layer */
182 background-color: var(--md-sys-color-on-surface);
183 opacity: var(--md-sys-state-hover-state-layer-opacity);
184 }
185
186 a.active:hover::before { /* state layer */
187 background-color: var(--md-sys-color-on-secondary-container);
188 }
189
190 a:hover span { /* inactive icon */
191 color: var(--md-sys-color-on-surface);
192 }
193
194 a.active:hover span { /* icon */
195 color: var(--md-sys-color-on-secondary-container);
196 }
197
198 a:focus { /* inactive label, shape */
199 outline: none;
200 color: var(--md-sys-color-on-surface);
201 }
202
203 a.active:focus { /* label, shape */
204 color: var(--md-sys-color-on-secondary-container);
205 }
206
207 a:focus::before { /* inactive state layer */
208 background-color: var(--md-sys-color-on-surface);
209 opacity: var(--md-sys-state-focus-state-layer-opacity);
210 }
211
212 a.active:focus::before { /* state layer */
213 background-color: var(--md-sys-color-on-secondary-container);
214 }
215
216 a:focus span { /* inactive icon */
217 color: var(--md-sys-color-on-surface);
218 }
219
220 a.active:focus span { /* icon */
221 color: var(--md-sys-color-on-secondary-container);
222 }
223
224 a:active { /* inactive pressed label, shape */
225 background-position: center;
226 background-image:
227 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
228 background-size: 100%;
229 animation-name: md-ripple;
230 animation-duration: var(--md-sys-motion-duration-500);
231 color: var(--md-sys-color-on-surface);
232 }
233
234 a.active:active { /* active pressed label, shape */
235 color: var(--md-sys-color-on-secondary-container);
236 }
237
238 a:active::before { /* inactive pressed state layer */
239 background-color: var(--md-sys-color-on-surface);
240 opacity: var(--md-sys-state-pressed-state-layer-opacity);
241 }
242
243 a.active:active::before { /* active pressed state layer */
244 background-color: var(--md-sys-color-on-secondary-container);
245 }
246
247 a:active span { /* inactive pressed icon */
248 color: var(--md-sys-color-on-surface);
249 }
250
251 a.active:focus span { /* active pressed icon */
252 color: var(--md-sys-color-on-secondary-container);
253 }
254
255 </style>
256
257 <div id="scrim"class="duration-700 easing-standard"></div>
258 <nav class="large-end duration-700 easing-standard"><div></div></nav>`
259 }
260
261 constructor() {
262 super()
263 const shadow = this.attachShadow({ mode: "open", delegatesFocus: true })
264 shadow.innerHTML = this.getContent()
265 this.cierra = this.cierra.bind(this)
266
267 /** @type {HTMLElement} */
268 this._nav = querySelector(shadow, "nav")
269
270 /** @type {HTMLUListElement} */
271 this._div = querySelector(this._nav, "div")
272
273 /** @type {HTMLUListElement} */
274 this._scrim = querySelector(shadow, "#scrim")
275 this._scrim.addEventListener("click", this.cierra)
276 }
277
278 connectedCallback() {
279 this.classList.add("drawer")
280 this._div.innerHTML = this.getHipervinculos()
281 }
282
283 abre() {
284 abreElementoHtml(this._nav)
285 abreElementoHtml(this._scrim)
286 }
287
288 cierra() {
289 cierraElementoHtmo(this._nav)
290 cierraElementoHtmo(this._scrim)
291 }
292
293}

33. Carpeta « material-tokens »

A. Carpeta « material-tokens / css »

1. material-tokens / css / baseline.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17@import url(palette.css);
18@import url(typography.css);
19@import url(colors.css);
20@import url(shape.css);
21@import url(motion.css);
22@import url(state.css);
23@import url(elevation.css);
24@import url(theme/light.css) screen and (prefers-color-scheme: light);
25@import url(theme/dark.css) screen and (prefers-color-scheme: dark);
26

2. material-tokens / css / colors.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17.primary {
18 color: var(--md-sys-color-on-primary);
19 background-color: var(--md-sys-color-primary);
20}
21.on-primary {
22 color: var(--md-sys-color-primary);
23 background-color: var(--md-sys-color-on-primary);
24}
25.primary-container {
26 color: var(--md-sys-color-on-primary-container);
27 background-color: var(--md-sys-color-primary-container);
28}
29.on-primary-container {
30 color: var(--md-sys-color-primary-container);
31 background-color: var(--md-sys-color-on-primary-container);
32}
33.secondary {
34 color: var(--md-sys-color-on-secondary);
35 background-color: var(--md-sys-color-secondary);
36}
37.on-secondary {
38 color: var(--md-sys-color-secondary);
39 background-color: var(--md-sys-color-on-secondary);
40}
41.secondary-container {
42 color: var(--md-sys-color-on-secondary-container);
43 background-color: var(--md-sys-color-secondary-container);
44}
45.on-secondary-container {
46 color: var(--md-sys-color-secondary-container);
47 background-color: var(--md-sys-color-on-secondary-container);
48}
49.tertiary {
50 color: var(--md-sys-color-on-tertiary);
51 background-color: var(--md-sys-color-tertiary);
52}
53.on-tertiary {
54 color: var(--md-sys-color-tertiary);
55 background-color: var(--md-sys-color-on-tertiary);
56}
57.tertiary-container {
58 color: var(--md-sys-color-on-tertiary-container);
59 background-color: var(--md-sys-color-tertiary-container);
60}
61.on-tertiary-container {
62 color: var(--md-sys-color-tertiary-container);
63 background-color: var(--md-sys-color-on-tertiary-container);
64}
65.background {
66 color: var(--md-sys-color-on-background);
67 background-color: var(--md-sys-color-background);
68}
69.surface {
70 color: var(--md-sys-color-on-surface);
71 background-color: var(--md-sys-color-surface);
72}
73.surface-variant {
74 color: var(--md-sys-color-on-surface-variant);
75 background-color: var(--md-sys-color-surface-variant);
76}
77.on-surface-variant {
78 color: var(--md-sys-color-surface-variant);
79 background-color: var(--md-sys-color-on-surface-variant);
80}
81.outline {
82 border: 1px solid var(--md-sys-color-outline);
83}
84.inverse-surface {
85 color: var(--md-sys-color-on-inverse-surface);
86 background-color: var(--md-sys-color-inverse-surface);
87}
88.on-inverse-surface {
89 color: var(--md-sys-color-inverse-surface);
90 background-color: var(--md-sys-color-on-inverse-surface);
91}
92.inverse-primary {
93 color: var(--md-sys-color-on-inverse-primary);
94 background-color: var(--md-sys-color-inverse-primary);
95}
96.on-inverse-primary {
97 color: var(--md-sys-color-inverse-primary);
98 background-color: var(--md-sys-color-on-inverse-primary);
99}
100.surface-tint {
101 background-color: var(--md-sys-color-on-surface-tint);
102}
103.error {
104 color: var(--md-sys-color-on-error);
105 background-color: var(--md-sys-color-error);
106}
107.on-error {
108 color: var(--md-sys-color-error);
109 background-color: var(--md-sys-color-on-error);
110}
111.error-container {
112 color: var(--md-sys-color-on-error-container);
113 background-color: var(--md-sys-color-error-container);
114}
115.on-error-container {
116 color: var(--md-sys-color-error-container);
117 background-color: var(--md-sys-color-on-error-container);
118}
119.black {
120 background-color: var(--md-ref-palette-black);
121}
122.black-text {
123 color: var(--md-ref-palette-black);
124}
125.white {
126 background-color: var(--md-ref-palette-white);
127}
128.white-text {
129 color: var(--md-ref-palette-white);
130}
131

3. material-tokens / css / elevation.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17:root {
18 /* Surface tint color */
19 --md-sys-elevation-surface-tint-color: var(--md-sys-color-primary);
20 /* +5 */
21 --md-sys-elevation-level5-value: 12px;
22 --md-sys-elevation-level5-unit: 1px;
23 --md-sys-elevation-level5: 12px;
24 /* +4 */
25 --md-sys-elevation-level4-value: 8px;
26 --md-sys-elevation-level4-unit: 1px;
27 --md-sys-elevation-level4: 8px;
28 /* +3 */
29 --md-sys-elevation-level3-value: 6px;
30 --md-sys-elevation-level3-unit: 1px;
31 --md-sys-elevation-level3: 6px;
32 /* +2 */
33 --md-sys-elevation-level2-value: 3px;
34 --md-sys-elevation-level2-unit: 1px;
35 --md-sys-elevation-level2: 3px;
36 /* +1 */
37 --md-sys-elevation-level1-value: 1px;
38 --md-sys-elevation-level1-unit: 1px;
39 --md-sys-elevation-level1: 1px;
40 /* 0 */
41 --md-sys-elevation-level0-value: 0px;
42 --md-sys-elevation-level0-unit: 1px;
43 --md-sys-elevation-level0: 0px;
44}
45.elevation-0 {
46 box-shadow: var(--md-sys-elevation-level0);
47}
48.elevation-1 {
49 box-shadow: var(--md-sys-elevation-level1);
50}
51.elevation-2 {
52 box-shadow: var(--md-sys-elevation-level2);
53}
54.elevation-3 {
55 box-shadow: var(--md-sys-elevation-level3);
56}
57.elevation-4 {
58 box-shadow: var(--md-sys-elevation-level4);
59}
60.elevation-5 {
61 box-shadow: var(--md-sys-elevation-level5);
62}
63

4. material-tokens / css / motion.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17:root {
18 /* Emphasized decelerate easing (out) */
19 --md-sys-motion-easing-emphasized-decelerate-x0: 0.05000000074505806;
20 --md-sys-motion-easing-emphasized-decelerate-y0: 0.699999988079071;
21 --md-sys-motion-easing-emphasized-decelerate-x1: 0.10000000149011612;
22 --md-sys-motion-easing-emphasized-decelerate-y1: 1;
23 /* Emphasized accelerate easing (in) */
24 --md-sys-motion-easing-emphasized-accelerate-x0: 0.30000001192092896;
25 --md-sys-motion-easing-emphasized-accelerate-y0: 0;
26 --md-sys-motion-easing-emphasized-accelerate-x1: 0.800000011920929;
27 --md-sys-motion-easing-emphasized-accelerate-y1: 0.15000000596046448;
28 /* Standard decelerate easing (out) */
29 --md-sys-motion-easing-standard-decelerate-x0: 0;
30 --md-sys-motion-easing-standard-decelerate-y0: 0;
31 --md-sys-motion-easing-standard-decelerate-x1: 0;
32 --md-sys-motion-easing-standard-decelerate-y1: 1;
33 /* Standard accelerate easing (in) */
34 --md-sys-motion-easing-standard-accelerate-x0: 0.30000001192092896;
35 --md-sys-motion-easing-standard-accelerate-y0: 0;
36 --md-sys-motion-easing-standard-accelerate-x1: 1;
37 --md-sys-motion-easing-standard-accelerate-y1: 1;
38 /* Duration 1000ms */
39 --md-sys-motion-duration-1000: 1000ms;
40 /* Duration 900ms */
41 --md-sys-motion-duration-900: 900ms;
42 /* Duration 800ms */
43 --md-sys-motion-duration-800: 800ms;
44 /* Duration 700ms */
45 --md-sys-motion-duration-700: 700ms;
46 /* Duration 600ms */
47 --md-sys-motion-duration-600: 600ms;
48 /* Duration 550ms */
49 --md-sys-motion-duration-550: 550ms;
50 /* Duration 500ms */
51 --md-sys-motion-duration-500: 500ms;
52 /* Duration 450ms */
53 --md-sys-motion-duration-450: 450ms;
54 /* Duration 400ms */
55 --md-sys-motion-duration-400: 400ms;
56 /* Duration 350ms */
57 --md-sys-motion-duration-350: 350ms;
58 /* Duration 300ms */
59 --md-sys-motion-duration-300: 300ms;
60 /* Duration 250ms */
61 --md-sys-motion-duration-250: 250ms;
62 /* Duration 200ms */
63 --md-sys-motion-duration-200: 200ms;
64 /* Duration 150ms */
65 --md-sys-motion-duration-150: 150ms;
66 /* Duration 100ms */
67 --md-sys-motion-duration-100: 100ms;
68 /* Duration 50ms */
69 --md-sys-motion-duration-50: 50ms;
70 /* Standard easing (in and out) */
71 --md-sys-motion-easing-standard-x0: 0.20000000298023224;
72 --md-sys-motion-easing-standard-y0: 0;
73 --md-sys-motion-easing-standard-x1: 0;
74 --md-sys-motion-easing-standard-y1: 1;
75 /* Linear easing */
76 --md-sys-motion-easing-linear-x0: 0;
77 --md-sys-motion-easing-linear-y0: 0;
78 --md-sys-motion-easing-linear-x1: 1;
79 --md-sys-motion-easing-linear-y1: 1;
80 /* Emphasized */
81 --md-sys-motion-easing-emphasized-x0: 0.20000000298023224;
82 --md-sys-motion-easing-emphasized-y0: 0;
83 --md-sys-motion-easing-emphasized-x1: 0;
84 --md-sys-motion-easing-emphasized-y1: 1;
85 /* Motion path */
86 --md-sys-motion-path-standard-path: 1;
87}
88.duration-50 {
89 transition-duration: var(--md-sys-motion-duration-50);
90}
91.duration-100 {
92 transition-duration: var(--md-sys-motion-duration-100);
93}
94.duration-150 {
95 transition-duration: var(--md-sys-motion-duration-150);
96}
97.duration-200 {
98 transition-duration: var(--md-sys-motion-duration-200);
99}
100.duration-250 {
101 transition-duration: var(--md-sys-motion-duration-250);
102}
103.duration-300 {
104 transition-duration: var(--md-sys-motion-duration-300);
105}
106.duration-350 {
107 transition-duration: var(--md-sys-motion-duration-350);
108}
109.duration-400 {
110 transition-duration: var(--md-sys-motion-duration-400);
111}
112.duration-450 {
113 transition-duration: var(--md-sys-motion-duration-450);
114}
115.duration-500 {
116 transition-duration: var(--md-sys-motion-duration-500);
117}
118.duration-550 {
119 transition-duration: var(--md-sys-motion-duration-550);
120}
121.duration-600 {
122 transition-duration: var(--md-sys-motion-duration-600);
123}
124.duration-700 {
125 transition-duration: var(--md-sys-motion-duration-700);
126}
127.duration-800 {
128 transition-duration: var(--md-sys-motion-duration-800);
129}
130.duration-900 {
131 transition-duration: var(--md-sys-motion-duration-900);
132}
133.duration-1000 {
134 transition-duration: var(--md-sys-motion-duration-1000);
135}
136.easing-standard {
137 transition-timing-function: cubic-bezier(
138 var(--md-sys-motion-easing-standard-x0),
139 var(--md-sys-motion-easing-standard-y0),
140 var(--md-sys-motion-easing-standard-x1),
141 var(--md-sys-motion-easing-standard-y1)
142 );
143}
144.easing-linear {
145 transition-timing-function: cubic-bezier(
146 var(--md-sys-motion-easing-linear-x0),
147 var(--md-sys-motion-easing-linear-y0),
148 var(--md-sys-motion-easing-linear-x1),
149 var(--md-sys-motion-easing-linear-y1)
150 );
151}
152.easing-standard-accelerate {
153 transition-timing-function: cubic-bezier(
154 var(--md-sys-motion-easing-standard-accelerate-x0),
155 var(--md-sys-motion-easing-standard-accelerate-y0),
156 var(--md-sys-motion-easing-standard-accelerate-x1),
157 var(--md-sys-motion-easing-standard-accelerate-y1)
158 );
159}
160.easing-standard-decelerate {
161 transition-timing-function: cubic-bezier(
162 var(--md-sys-motion-easing-standard-decelerate-x0),
163 var(--md-sys-motion-easing-standard-decelerate-y0),
164 var(--md-sys-motion-easing-standard-decelerate-x1),
165 var(--md-sys-motion-easing-standard-decelerate-y1)
166 );
167}
168.easing-emphasized {
169 transition-timing-function: cubic-bezier(
170 var(--md-sys-motion-easing-emphasized-x0),
171 var(--md-sys-motion-easing-emphasized-y0),
172 var(--md-sys-motion-easing-emphasized-x1),
173 var(--md-sys-motion-easing-emphasized-y1)
174 );
175}
176

5. material-tokens / css / palette.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17:root {
18 /* Error 0 */
19 --md-ref-palette-error0: #000000ff;
20 /* Error 10 */
21 --md-ref-palette-error10: #410e0bff;
22 /* Error 20 */
23 --md-ref-palette-error20: #601410ff;
24 /* Error 30 */
25 --md-ref-palette-error30: #8c1d18ff;
26 /* Error 40 */
27 --md-ref-palette-error40: #b3261eff;
28 /* Error 50 */
29 --md-ref-palette-error50: #dc362eff;
30 /* Error 60 */
31 --md-ref-palette-error60: #e46962ff;
32 /* Error 70 */
33 --md-ref-palette-error70: #ec928eff;
34 /* Error 80 */
35 --md-ref-palette-error80: #f2b8b5ff;
36 /* Error 90 */
37 --md-ref-palette-error90: #f9dedcff;
38 /* Error 95 */
39 --md-ref-palette-error95: #fceeeeff;
40 /* Error 99 */
41 --md-ref-palette-error99: #fffbf9ff;
42 /* Error 100 */
43 --md-ref-palette-error100: #ffffffff;
44 /* Tertiary 0 */
45 --md-ref-palette-tertiary0: #000000ff;
46 /* Tertiary 10 */
47 --md-ref-palette-tertiary10: #31111dff;
48 /* Tertiary 20 */
49 --md-ref-palette-tertiary20: #492532ff;
50 /* Tertiary 30 */
51 --md-ref-palette-tertiary30: #633b48ff;
52 /* Tertiary 40 */
53 --md-ref-palette-tertiary40: #7d5260ff;
54 /* Tertiary 50 */
55 --md-ref-palette-tertiary50: #986977ff;
56 /* Tertiary 60 */
57 --md-ref-palette-tertiary60: #b58392ff;
58 /* Tertiary 70 */
59 --md-ref-palette-tertiary70: #d29dacff;
60 /* Tertiary 80 */
61 --md-ref-palette-tertiary80: #efb8c8ff;
62 /* Tertiary 90 */
63 --md-ref-palette-tertiary90: #ffd8e4ff;
64 /* Tertiary 95 */
65 --md-ref-palette-tertiary95: #ffecf1ff;
66 /* Tertiary 99 */
67 --md-ref-palette-tertiary99: #fffbfaff;
68 /* Tertiary 100 */
69 --md-ref-palette-tertiary100: #ffffffff;
70 /* Secondary 0 */
71 --md-ref-palette-secondary0: #000000ff;
72 /* Secondary 10 */
73 --md-ref-palette-secondary10: #1d192bff;
74 /* Secondary 20 */
75 --md-ref-palette-secondary20: #332d41ff;
76 /* Secondary 30 */
77 --md-ref-palette-secondary30: #4a4458ff;
78 /* Secondary 40 */
79 --md-ref-palette-secondary40: #625b71ff;
80 /* Secondary 50 */
81 --md-ref-palette-secondary50: #7a7289ff;
82 /* Secondary 60 */
83 --md-ref-palette-secondary60: #958da5ff;
84 /* Secondary 70 */
85 --md-ref-palette-secondary70: #b0a7c0ff;
86 /* Secondary 80 */
87 --md-ref-palette-secondary80: #ccc2dcff;
88 /* Secondary 90 */
89 --md-ref-palette-secondary90: #e8def8ff;
90 /* Secondary 95 */
91 --md-ref-palette-secondary95: #f6edffff;
92 /* Secondary 99 */
93 --md-ref-palette-secondary99: #fffbfeff;
94 /* Secondary 100 */
95 --md-ref-palette-secondary100: #ffffffff;
96 /* Primary 0 */
97 --md-ref-palette-primary0: #000000ff;
98 /* Primary 10 */
99 --md-ref-palette-primary10: #21005dff;
100 /* Primary 20 */
101 --md-ref-palette-primary20: #381e72ff;
102 /* Primary 30 */
103 --md-ref-palette-primary30: #4f378bff;
104 /* Primary 40 */
105 --md-ref-palette-primary40: #6750a4ff;
106 /* Primary 50 */
107 --md-ref-palette-primary50: #7f67beff;
108 /* Primary 60 */
109 --md-ref-palette-primary60: #9a82dbff;
110 /* Primary 70 */
111 --md-ref-palette-primary70: #b69df8ff;
112 /* Primary 80 */
113 --md-ref-palette-primary80: #d0bcffff;
114 /* Primary 90 */
115 --md-ref-palette-primary90: #eaddffff;
116 /* Primary 95 */
117 --md-ref-palette-primary95: #f6edffff;
118 /* Primary 99 */
119 --md-ref-palette-primary99: #fffbfeff;
120 /* Primary 100 */
121 --md-ref-palette-primary100: #ffffffff;
122 /* Neutral Variant 0 */
123 --md-ref-palette-neutral-variant0: #000000ff;
124 /* Neutral Variant 10 */
125 --md-ref-palette-neutral-variant10: #1d1a22ff;
126 /* Neutral Variant 20 */
127 --md-ref-palette-neutral-variant20: #322f37ff;
128 /* Neutral Variant 30 */
129 --md-ref-palette-neutral-variant30: #49454fff;
130 /* Neutral Variant 40 */
131 --md-ref-palette-neutral-variant40: #605d66ff;
132 /* Neutral Variant 50 */
133 --md-ref-palette-neutral-variant50: #79747eff;
134 /* Neutral Variant 60 */
135 --md-ref-palette-neutral-variant60: #938f99ff;
136 /* Neutral Variant 70 */
137 --md-ref-palette-neutral-variant70: #aea9b4ff;
138 /* Neutral Variant 80 */
139 --md-ref-palette-neutral-variant80: #cac4d0ff;
140 /* Neutral Variant 90 */
141 --md-ref-palette-neutral-variant90: #e7e0ecff;
142 /* Neutral Variant 95 */
143 --md-ref-palette-neutral-variant95: #f5eefaff;
144 /* Neutral Variant 99 */
145 --md-ref-palette-neutral-variant99: #fffbfeff;
146 /* Neutral Variant 100 */
147 --md-ref-palette-neutral-variant100: #ffffffff;
148 /* Neutral 0 */
149 --md-ref-palette-neutral0: #000000ff;
150 /* Neutral 10 */
151 --md-ref-palette-neutral10: #1c1b1fff;
152 /* Neutral 20 */
153 --md-ref-palette-neutral20: #313033ff;
154 /* Neutral 30 */
155 --md-ref-palette-neutral30: #484649ff;
156 /* Neutral 40 */
157 --md-ref-palette-neutral40: #605d62ff;
158 /* Neutral 50 */
159 --md-ref-palette-neutral50: #787579ff;
160 /* Neutral 60 */
161 --md-ref-palette-neutral60: #939094ff;
162 /* Neutral 70 */
163 --md-ref-palette-neutral70: #aeaaaeff;
164 /* Neutral 80 */
165 --md-ref-palette-neutral80: #c9c5caff;
166 /* Neutral 90 */
167 --md-ref-palette-neutral90: #e6e1e5ff;
168 /* Neutral 95 */
169 --md-ref-palette-neutral95: #f4eff4ff;
170 /* Neutral 99 */
171 --md-ref-palette-neutral99: #fffbfeff;
172 /* Neutral 100 */
173 --md-ref-palette-neutral100: #ffffffff;
174 /* Black */
175 --md-ref-palette-black: #000000ff;
176 /* White */
177 --md-ref-palette-white: #ffffffff;
178}
179

6. material-tokens / css / shape.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17:root {
18 /* Fully rounded */
19 --md-sys-shape-corner-full-family: 3px;
20 /* Extra large top rounding */
21 --md-sys-shape-corner-extra-large-top-family: 1px;
22 --md-sys-shape-corner-extra-large-top-default-size: 0px;
23 --md-sys-shape-corner-extra-large-top-top-left: 28px;
24 --md-sys-shape-corner-extra-large-top-top-right-unit: 1px;
25 --md-sys-shape-corner-extra-large-top-top-right: 28px;
26 /* Extra large rounding */
27 --md-sys-shape-corner-extra-large-family: 1px;
28 --md-sys-shape-corner-extra-large-default-size-unit: 1px;
29 --md-sys-shape-corner-extra-large-default-size: 28px;
30 /* Large top rounding */
31 --md-sys-shape-corner-large-top-family: 1px;
32 --md-sys-shape-corner-large-top-default-size-unit: 1px;
33 --md-sys-shape-corner-large-top-default-size: 0px;
34 --md-sys-shape-corner-large-top-top-left-unit: 1px;
35 --md-sys-shape-corner-large-top-top-left: 16px;
36 --md-sys-shape-corner-large-top-top-right-unit: 1px;
37 --md-sys-shape-corner-large-top-top-right: 16px;
38 /* Large end rounding */
39 --md-sys-shape-corner-large-end-family: 1px;
40 --md-sys-shape-corner-large-end-default-size-unit: 1px;
41 --md-sys-shape-corner-large-end-default-size: 0px;
42 --md-sys-shape-corner-large-end-top-right-unit: 1px;
43 --md-sys-shape-corner-large-end-top-right: 16px;
44 --md-sys-shape-corner-large-end-bottom-right-unit: 1px;
45 --md-sys-shape-corner-large-end-bottom-right: 16px;
46 /* Large rounding */
47 --md-sys-shape-corner-large-family: 1px;
48 --md-sys-shape-corner-large-default-size-unit: 1px;
49 --md-sys-shape-corner-large-default-size: 16px;
50 /* Medium rounding */
51 --md-sys-shape-corner-medium-family: 1px;
52 --md-sys-shape-corner-medium-default-size-unit: 1px;
53 --md-sys-shape-corner-medium-default-size: 12px;
54 /* Small rounding */
55 --md-sys-shape-corner-small-family: 1px;
56 --md-sys-shape-corner-small-default-size-unit: 1px;
57 --md-sys-shape-corner-small-default-size: 8px;
58 /* Extra small top rounding */
59 --md-sys-shape-corner-extra-small-top-family: 1px;
60 --md-sys-shape-corner-extra-small-top-default-size-unit: 1px;
61 --md-sys-shape-corner-extra-small-top-default-size: 0px;
62 --md-sys-shape-corner-extra-small-top-top-left-unit: 1px;
63 --md-sys-shape-corner-extra-small-top-top-left: 4px;
64 --md-sys-shape-corner-extra-small-top-top-right-unit: 1px;
65 --md-sys-shape-corner-extra-small-top-top-right: 4px;
66 /* Extra small rounding */
67 --md-sys-shape-corner-extra-small-family: 1px;
68 --md-sys-shape-corner-extra-small-default-size-unit: 1px;
69 --md-sys-shape-corner-extra-small-default-size: 4px;
70 /* No rounding */
71 --md-sys-shape-corner-none-family: 1px;
72 --md-sys-shape-corner-none-default-size-unit: 1px;
73 --md-sys-shape-corner-none-default-size: 0px;
74
75 --md-sys-shape-small: var(--md-sys-shape-corner-small-default-size);
76 --md-sys-shape-medium: var(--md-sys-shape-corner-medium-default-size);
77 --md-sys-shape-large: var(--md-sys-shape-corner-large-default-size);
78}
79
80.shape-none {
81 border-radius: var(--md-sys-shape-corner-none-default-size);
82}
83.shape-extra-small {
84 border-radius: var(--md-sys-shape-corner-extra-small-default-size);
85}
86.shape-small {
87 border-radius: var(--md-sys-shape-corner-small-default-size);
88}
89.shape-medium {
90 border-radius: var(--md-sys-shape-corner-medium-default-size);
91}
92.shape-large {
93 border-radius: var(--md-sys-shape-corner-large-default-size);
94}
95.shape-extra-large {
96 border-radius: var(--md-sys-shape-corner-extra-large-default-size);
97}
98.extra-small-top {
99 border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
100 border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
101}
102.large-end {
103 border-top-right-radius: var(--md-sys-shape-corner-large-end-top-right);
104 border-bottom-right-radius: var(--md-sys-shape-corner-large-end-bottom-right);
105}
106.large-top {
107 border-top-left-radius: var(--md-sys-shape-corner-large-top-top-left);
108 border-top-right-radius: var(--md-sys-shape-corner-large-top-top-right);
109}
110.extra-large-top {
111 border-top-left-radius: var(--md-sys-shape-corner-extra-large-top-top-left);
112 border-top-right-radius: var(--md-sys-shape-corner-extra-large-top-top-right);
113}
114

7. material-tokens / css / state.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17:root {
18 /* Dragged state layer opacity */
19 --md-sys-state-dragged-state-layer-opacity: 0.1599999964237213;
20 /* Pressed state layer opacity */
21 --md-sys-state-pressed-state-layer-opacity: 0.11999999731779099;
22 /* Focus state layer opacity */
23 --md-sys-state-focus-state-layer-opacity: 0.11999999731779099;
24 /* Hover state layer opacity */
25 --md-sys-state-hover-state-layer-opacity: 0.07999999821186066;
26}
27.hover-state-layer {
28 opacity: var(--md-sys-state-hover-state-layer-opacity);
29}
30.pressed-state-layer {
31 opacity: var(--md-sys-state-pressed-state-layer-opacity);
32}
33.dragged-state-layer {
34 opacity: var(--md-sys-state-dragged-state-layer-opacity);
35}
36.focus-state-layer {
37 opacity: var(--md-sys-state-focus-state-layer-opacity);
38}
39

8. material-tokens / css / typography.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17/* This file is generated */
18
19/* DO NOT EDIT */
20
21:root {
22 /* Label Small */
23 --md-sys-typescale-label-small-text-transform: unset;
24 --md-sys-typescale-label-small-axis-value: unset;
25 --md-sys-typescale-label-small-font-style: unset;
26 --md-sys-typescale-label-small-text-decoration: unset;
27 /* Label Small line height */
28 --md-sys-typescale-label-small-line-height-value: 16px;
29 --md-sys-typescale-label-small-line-height-unit: 2px;
30 --md-sys-typescale-label-small-line-height: 16px;
31 /* Label Small font tracking */
32 --md-sys-typescale-label-small-tracking-value: 0.5px;
33 --md-sys-typescale-label-small-tracking-unit: 2px;
34 --md-sys-typescale-label-small-tracking: 0.5px;
35 /* Label Small font size */
36 --md-sys-typescale-label-small-size-value: 11px;
37 --md-sys-typescale-label-small-size-unit: 2px;
38 --md-sys-typescale-label-small-size: 11px;
39 /* Label Small font weight */
40 --md-sys-typescale-label-small-weight: var(--md-ref-typeface-weight-medium);
41 /* Label Small font name */
42 --md-sys-typescale-label-small-font: var(--md-ref-typeface-plain);
43 /* Label Medium */
44 --md-sys-typescale-label-medium-axis-value: unset;
45 --md-sys-typescale-label-medium-font-style: unset;
46 --md-sys-typescale-label-medium-text-decoration: unset;
47 /* Label Medium text transform */
48 --md-sys-typescale-label-medium-text-transform: 1;
49 /* Label Medium line height */
50 --md-sys-typescale-label-medium-line-height-value: 16px;
51 --md-sys-typescale-label-medium-line-height-unit: 2px;
52 --md-sys-typescale-label-medium-line-height: 16px;
53 /* Label Medium font tracking */
54 --md-sys-typescale-label-medium-tracking-value: 0.5px;
55 --md-sys-typescale-label-medium-tracking-unit: 2px;
56 --md-sys-typescale-label-medium-tracking: 0.5px;
57 /* Label Medium font size */
58 --md-sys-typescale-label-medium-size-value: 12px;
59 --md-sys-typescale-label-medium-size-unit: 2px;
60 --md-sys-typescale-label-medium-size: 12px;
61 /* Label Medium font weight */
62 --md-sys-typescale-label-medium-weight: var(--md-ref-typeface-weight-medium);
63 /* Label Medium font name */
64 --md-sys-typescale-label-medium-font: var(--md-ref-typeface-plain);
65 /* Label Large */
66 --md-sys-typescale-label-large-text-transform: unset;
67 --md-sys-typescale-label-large-axis-value: unset;
68 --md-sys-typescale-label-large-font-style: unset;
69 --md-sys-typescale-label-large-text-decoration: unset;
70 /* Label Large line height */
71 --md-sys-typescale-label-large-line-height-value: 20px;
72 --md-sys-typescale-label-large-line-height-unit: 2px;
73 --md-sys-typescale-label-large-line-height: 20px;
74 /* Label Large font tracking */
75 --md-sys-typescale-label-large-tracking-value: 0.10000000149011612px;
76 --md-sys-typescale-label-large-tracking-unit: 2px;
77 --md-sys-typescale-label-large-tracking: 0.10000000149011612px;
78 /* Label Large font size */
79 --md-sys-typescale-label-large-size-value: 14px;
80 --md-sys-typescale-label-large-size-unit: 2px;
81 --md-sys-typescale-label-large-size: 14px;
82 /* Label Large font weight */
83 --md-sys-typescale-label-large-weight: var(--md-ref-typeface-weight-medium);
84 /* Label Large font name */
85 --md-sys-typescale-label-large-font: var(--md-ref-typeface-plain);
86 /* Body Small */
87 --md-sys-typescale-body-small-text-transform: unset;
88 --md-sys-typescale-body-small-axis-value: unset;
89 --md-sys-typescale-body-small-font-style: unset;
90 --md-sys-typescale-body-small-text-decoration: unset;
91 /* Body Small line height */
92 --md-sys-typescale-body-small-line-height-value: 16px;
93 --md-sys-typescale-body-small-line-height-unit: 2px;
94 --md-sys-typescale-body-small-line-height: 16px;
95 /* Body Small font tracking */
96 --md-sys-typescale-body-small-tracking-value: 0.4000000059604645px;
97 --md-sys-typescale-body-small-tracking-unit: 2px;
98 --md-sys-typescale-body-small-tracking: 0.4000000059604645px;
99 /* Body Small font size */
100 --md-sys-typescale-body-small-size-value: 12px;
101 --md-sys-typescale-body-small-size-unit: 2px;
102 --md-sys-typescale-body-small-size: 12px;
103 /* Body Small font weight */
104 --md-sys-typescale-body-small-weight: var(--md-ref-typeface-weight-regular);
105 /* Body Small font name */
106 --md-sys-typescale-body-small-font: var(--md-ref-typeface-plain);
107 /* Body Medium */
108 --md-sys-typescale-body-medium-text-transform: unset;
109 --md-sys-typescale-body-medium-axis-value: unset;
110 --md-sys-typescale-body-medium-font-style: unset;
111 --md-sys-typescale-body-medium-text-decoration: unset;
112 /* Body Medium line height */
113 --md-sys-typescale-body-medium-line-height-value: 20px;
114 --md-sys-typescale-body-medium-line-height-unit: 2px;
115 --md-sys-typescale-body-medium-line-height: 20px;
116 /* Body Medium font tracking */
117 --md-sys-typescale-body-medium-tracking-value: 0.25px;
118 --md-sys-typescale-body-medium-tracking-unit: 2px;
119 --md-sys-typescale-body-medium-tracking: 0.25px;
120 /* Body Medium font size */
121 --md-sys-typescale-body-medium-size-value: 14px;
122 --md-sys-typescale-body-medium-size-unit: 2px;
123 --md-sys-typescale-body-medium-size: 14px;
124 /* Body Medium font weight */
125 --md-sys-typescale-body-medium-weight: var(--md-ref-typeface-weight-regular);
126 /* Body Medium font name */
127 --md-sys-typescale-body-medium-font: var(--md-ref-typeface-plain);
128 /* Body Large */
129 --md-sys-typescale-body-large-text-transform: unset;
130 --md-sys-typescale-body-large-axis-value: unset;
131 --md-sys-typescale-body-large-font-style: unset;
132 --md-sys-typescale-body-large-text-decoration: unset;
133 /* Body Large line height */
134 --md-sys-typescale-body-large-line-height-value: 24px;
135 --md-sys-typescale-body-large-line-height-unit: 2px;
136 --md-sys-typescale-body-large-line-height: 24px;
137 /* Body Large font tracking */
138 --md-sys-typescale-body-large-tracking-value: 0.5px;
139 --md-sys-typescale-body-large-tracking-unit: 2px;
140 --md-sys-typescale-body-large-tracking: 0.5px;
141 /* Body Large font size */
142 --md-sys-typescale-body-large-size-value: 16px;
143 --md-sys-typescale-body-large-size-unit: 2px;
144 --md-sys-typescale-body-large-size: 16px;
145 /* Body Large font weight */
146 --md-sys-typescale-body-large-weight: var(--md-ref-typeface-weight-regular);
147 /* Body Large font name */
148 --md-sys-typescale-body-large-font: var(--md-ref-typeface-plain);
149 /* Title Small */
150 --md-sys-typescale-title-small-text-transform: unset;
151 --md-sys-typescale-title-small-axis-value: unset;
152 --md-sys-typescale-title-small-font-style: unset;
153 --md-sys-typescale-title-small-text-decoration: unset;
154 /* Title Small line height */
155 --md-sys-typescale-title-small-line-height-value: 20px;
156 --md-sys-typescale-title-small-line-height-unit: 2px;
157 --md-sys-typescale-title-small-line-height: 20px;
158 /* Title Small font tracking */
159 --md-sys-typescale-title-small-tracking-value: 0.10000000149011612px;
160 --md-sys-typescale-title-small-tracking-unit: 2px;
161 --md-sys-typescale-title-small-tracking: 0.10000000149011612px;
162 /* Title Small font size */
163 --md-sys-typescale-title-small-size-value: 14px;
164 --md-sys-typescale-title-small-size-unit: 2px;
165 --md-sys-typescale-title-small-size: 14px;
166 /* Title Small font weight */
167 --md-sys-typescale-title-small-weight: var(--md-ref-typeface-weight-medium);
168 /* Title Small font name */
169 --md-sys-typescale-title-small-font: var(--md-ref-typeface-plain);
170 /* Title Medium */
171 --md-sys-typescale-title-medium-text-transform: unset;
172 --md-sys-typescale-title-medium-axis-value: unset;
173 --md-sys-typescale-title-medium-font-style: unset;
174 --md-sys-typescale-title-medium-text-decoration: unset;
175 /* Title Medium line height */
176 --md-sys-typescale-title-medium-line-height-value: 24px;
177 --md-sys-typescale-title-medium-line-height-unit: 2px;
178 --md-sys-typescale-title-medium-line-height: 24px;
179 /* Title Medium font tracking */
180 --md-sys-typescale-title-medium-tracking-value: 0.15000000596046448px;
181 --md-sys-typescale-title-medium-tracking-unit: 2px;
182 --md-sys-typescale-title-medium-tracking: 0.15000000596046448px;
183 /* Title Medium font size */
184 --md-sys-typescale-title-medium-size-value: 16px;
185 --md-sys-typescale-title-medium-size-unit: 2px;
186 --md-sys-typescale-title-medium-size: 16px;
187 /* Title Medium font weight */
188 --md-sys-typescale-title-medium-weight: var(--md-ref-typeface-weight-medium);
189 /* Title Medium font name */
190 --md-sys-typescale-title-medium-font: var(--md-ref-typeface-plain);
191 /* Title Large */
192 --md-sys-typescale-title-large-text-transform: unset;
193 --md-sys-typescale-title-large-axis-value: unset;
194 --md-sys-typescale-title-large-font-style: unset;
195 --md-sys-typescale-title-large-text-decoration: unset;
196 /* Title Large line height */
197 --md-sys-typescale-title-large-line-height-value: 28px;
198 --md-sys-typescale-title-large-line-height-unit: 2px;
199 --md-sys-typescale-title-large-line-height: 28px;
200 /* Title Large font tracking */
201 --md-sys-typescale-title-large-tracking-value: 0px;
202 --md-sys-typescale-title-large-tracking-unit: 2px;
203 --md-sys-typescale-title-large-tracking: 0px;
204 /* Title Large font size */
205 --md-sys-typescale-title-large-size-value: 22px;
206 --md-sys-typescale-title-large-size-unit: 2px;
207 --md-sys-typescale-title-large-size: 22px;
208 /* Title Large font weight */
209 --md-sys-typescale-title-large-weight: var(--md-ref-typeface-weight-regular);
210 /* Title Large font name */
211 --md-sys-typescale-title-large-font: var(--md-ref-typeface-brand);
212 /* Headline Small */
213 --md-sys-typescale-headline-small-text-transform: unset;
214 --md-sys-typescale-headline-small-axis-value: unset;
215 --md-sys-typescale-headline-small-font-style: unset;
216 --md-sys-typescale-headline-small-text-decoration: unset;
217 /* Headline Small line height */
218 --md-sys-typescale-headline-small-line-height-value: 32px;
219 --md-sys-typescale-headline-small-line-height-unit: 2px;
220 --md-sys-typescale-headline-small-line-height: 32px;
221 /* Headline Small font tracking */
222 --md-sys-typescale-headline-small-tracking-value: 0px;
223 --md-sys-typescale-headline-small-tracking-unit: 2px;
224 --md-sys-typescale-headline-small-tracking: 0px;
225 /* Headline Small font size */
226 --md-sys-typescale-headline-small-size-value: 24px;
227 --md-sys-typescale-headline-small-size-unit: 2px;
228 --md-sys-typescale-headline-small-size: 24px;
229 /* Headline Small font weight */
230 --md-sys-typescale-headline-small-weight: var(
231 --md-ref-typeface-weight-regular
232 );
233 /* Headline Small font name */
234 --md-sys-typescale-headline-small-font: var(--md-ref-typeface-brand);
235 /* Headline Medium */
236 --md-sys-typescale-headline-medium-text-transform: unset;
237 --md-sys-typescale-headline-medium-axis-value: unset;
238 --md-sys-typescale-headline-medium-font-style: unset;
239 --md-sys-typescale-headline-medium-text-decoration: unset;
240 /* Headline Medium line height */
241 --md-sys-typescale-headline-medium-line-height-value: 36px;
242 --md-sys-typescale-headline-medium-line-height-unit: 2px;
243 --md-sys-typescale-headline-medium-line-height: 36px;
244 /* Headline Medium font tracking */
245 --md-sys-typescale-headline-medium-tracking-value: 0px;
246 --md-sys-typescale-headline-medium-tracking-unit: 2px;
247 --md-sys-typescale-headline-medium-tracking: 0px;
248 /* Headline Medium font size */
249 --md-sys-typescale-headline-medium-size-value: 28px;
250 --md-sys-typescale-headline-medium-size-unit: 2px;
251 --md-sys-typescale-headline-medium-size: 28px;
252 /* Headline Medium font weight */
253 --md-sys-typescale-headline-medium-weight: var(
254 --md-ref-typeface-weight-regular
255 );
256 /* Headline Medium font name */
257 --md-sys-typescale-headline-medium-font: var(--md-ref-typeface-brand);
258 /* Headline Large */
259 --md-sys-typescale-headline-large-text-transform: unset;
260 --md-sys-typescale-headline-large-axis-value: unset;
261 --md-sys-typescale-headline-large-font-style: unset;
262 --md-sys-typescale-headline-large-text-decoration: unset;
263 /* Headline Large line height */
264 --md-sys-typescale-headline-large-line-height-value: 40px;
265 --md-sys-typescale-headline-large-line-height-unit: 2px;
266 --md-sys-typescale-headline-large-line-height: 40px;
267 /* Headline Large font tracking */
268 --md-sys-typescale-headline-large-tracking-value: 0px;
269 --md-sys-typescale-headline-large-tracking-unit: 2px;
270 --md-sys-typescale-headline-large-tracking: 0px;
271 /* Headline Large font size */
272 --md-sys-typescale-headline-large-size-value: 32px;
273 --md-sys-typescale-headline-large-size-unit: 2px;
274 --md-sys-typescale-headline-large-size: 32px;
275 /* Headline Large font name */
276 --md-sys-typescale-headline-large-font: var(--md-ref-typeface-brand);
277 /* Headline Large font weight */
278 --md-sys-typescale-headline-large-weight: var(
279 --md-ref-typeface-weight-regular
280 );
281 /* Display Small */
282 --md-sys-typescale-display-small-text-transform: unset;
283 --md-sys-typescale-display-small-axis-value: unset;
284 --md-sys-typescale-display-small-font-style: unset;
285 --md-sys-typescale-display-small-text-decoration: unset;
286 /* Display Small line height */
287 --md-sys-typescale-display-small-line-height-value: 44px;
288 --md-sys-typescale-display-small-line-height-unit: 2px;
289 --md-sys-typescale-display-small-line-height: 44px;
290 /* Display Small font tracking */
291 --md-sys-typescale-display-small-tracking-value: 0px;
292 --md-sys-typescale-display-small-tracking-unit: 2px;
293 --md-sys-typescale-display-small-tracking: 0px;
294 /* Display Small font size */
295 --md-sys-typescale-display-small-size-value: 36px;
296 --md-sys-typescale-display-small-size-unit: 2px;
297 --md-sys-typescale-display-small-size: 36px;
298 /* Display Small font weight */
299 --md-sys-typescale-display-small-weight: var(
300 --md-ref-typeface-weight-regular
301 );
302 /* Display Small font name */
303 --md-sys-typescale-display-small-font: var(--md-ref-typeface-brand);
304 /* Display Medium */
305 --md-sys-typescale-display-medium-text-transform: unset;
306 --md-sys-typescale-display-medium-axis-value: unset;
307 --md-sys-typescale-display-medium-font-style: unset;
308 --md-sys-typescale-display-medium-text-decoration: unset;
309 /* Display Medium line height */
310 --md-sys-typescale-display-medium-line-height-value: 52px;
311 --md-sys-typescale-display-medium-line-height-unit: 2px;
312 --md-sys-typescale-display-medium-line-height: 52px;
313 /* Display Medium font tracking */
314 --md-sys-typescale-display-medium-tracking-value: 0px;
315 --md-sys-typescale-display-medium-tracking-unit: 2px;
316 --md-sys-typescale-display-medium-tracking: 0px;
317 /* Display Medium font size */
318 --md-sys-typescale-display-medium-size-value: 45px;
319 --md-sys-typescale-display-medium-size-unit: 2px;
320 --md-sys-typescale-display-medium-size: 45px;
321 /* Display Medium font weight */
322 --md-sys-typescale-display-medium-weight: var(
323 --md-ref-typeface-weight-regular
324 );
325 /* Display Medium font name */
326 --md-sys-typescale-display-medium-font: var(--md-ref-typeface-brand);
327 /* Display Large */
328 --md-sys-typescale-display-large-text-transform: unset;
329 --md-sys-typescale-display-large-axis-value: unset;
330 --md-sys-typescale-display-large-font-style: unset;
331 --md-sys-typescale-display-large-text-decoration: unset;
332 /* Display Large line height */
333 --md-sys-typescale-display-large-line-height-value: 64px;
334 --md-sys-typescale-display-large-line-height-unit: 2px;
335 --md-sys-typescale-display-large-line-height: 64px;
336 /* Display Large font tracking */
337 --md-sys-typescale-display-large-tracking-value: -0.25px;
338 --md-sys-typescale-display-large-tracking-unit: 2px;
339 --md-sys-typescale-display-large-tracking: -0.25px;
340 /* Display Large font size */
341 --md-sys-typescale-display-large-size-value: 57px;
342 --md-sys-typescale-display-large-size-unit: 2px;
343 --md-sys-typescale-display-large-size: 57px;
344 /* Display Large font weight */
345 --md-sys-typescale-display-large-weight: var(
346 --md-ref-typeface-weight-regular
347 );
348 /* Display Large font name */
349 --md-sys-typescale-display-large-font: var(--md-ref-typeface-brand);
350 /* Plain typeface */
351 --md-ref-typeface-plain: Roboto;
352 /* Brand typeface */
353 --md-ref-typeface-brand: Roboto;
354 /* Bold weight */
355 --md-ref-typeface-weight-bold: 700;
356 /* Medium weight */
357 --md-ref-typeface-weight-medium: 500;
358 /* Regular weight */
359 --md-ref-typeface-weight-regular: 400;
360}
361
362/* Label Small */
363.label-small {
364 font-family: var(--md-sys-typescale-label-small-font);
365 font-weight: var(--md-sys-typescale-label-small-weight);
366 font-size: var(--md-sys-typescale-label-small-size);
367 font-style: var(--md-sys-typescale-label-small-font-style);
368 letter-spacing: var(--md-sys-typescale-label-small-tracking);
369 line-height: var(--md-sys-typescale-label-small-line-height);
370 text-transform: var(--md-sys-typescale-label-small-text-transform);
371 text-decoration: var(--md-sys-typescale-label-small-text-decoration);
372}
373/* Label Medium */
374.label-medium {
375 font-family: var(--md-sys-typescale-label-medium-font);
376 font-weight: var(--md-sys-typescale-label-medium-weight);
377 font-size: var(--md-sys-typescale-label-medium-size);
378 font-style: var(--md-sys-typescale-label-medium-font-style);
379 letter-spacing: var(--md-sys-typescale-label-medium-tracking);
380 line-height: var(--md-sys-typescale-label-medium-line-height);
381 text-transform: var(--md-sys-typescale-label-medium-text-transform);
382 text-decoration: var(--md-sys-typescale-label-medium-text-decoration);
383}
384/* Label Large */
385.label-large {
386 font-family: var(--md-sys-typescale-label-large-font);
387 font-weight: var(--md-sys-typescale-label-large-weight);
388 font-size: var(--md-sys-typescale-label-large-size);
389 font-style: var(--md-sys-typescale-label-large-font-style);
390 letter-spacing: var(--md-sys-typescale-label-large-tracking);
391 line-height: var(--md-sys-typescale-label-large-line-height);
392 text-transform: var(--md-sys-typescale-label-large-text-transform);
393 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
394}
395/* Body Small */
396.body-small {
397 font-family: var(--md-sys-typescale-body-small-font);
398 font-weight: var(--md-sys-typescale-body-small-weight);
399 font-size: var(--md-sys-typescale-body-small-size);
400 font-style: var(--md-sys-typescale-body-small-font-style);
401 letter-spacing: var(--md-sys-typescale-body-small-tracking);
402 line-height: var(--md-sys-typescale-body-small-line-height);
403 text-transform: var(--md-sys-typescale-body-small-text-transform);
404 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
405}
406/* Body Medium */
407.body-medium {
408 font-family: var(--md-sys-typescale-body-medium-font);
409 font-weight: var(--md-sys-typescale-body-medium-weight);
410 font-size: var(--md-sys-typescale-body-medium-size);
411 font-style: var(--md-sys-typescale-body-medium-font-style);
412 letter-spacing: var(--md-sys-typescale-body-medium-tracking);
413 line-height: var(--md-sys-typescale-body-medium-line-height);
414 text-transform: var(--md-sys-typescale-body-medium-text-transform);
415 text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
416}
417/* Body Large */
418.body-large {
419 font-family: var(--md-sys-typescale-body-large-font);
420 font-weight: var(--md-sys-typescale-body-large-weight);
421 font-size: var(--md-sys-typescale-body-large-size);
422 font-style: var(--md-sys-typescale-body-large-font-style);
423 letter-spacing: var(--md-sys-typescale-body-large-tracking);
424 line-height: var(--md-sys-typescale-body-large-line-height);
425 text-transform: var(--md-sys-typescale-body-large-text-transform);
426 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
427}
428/* Title Small */
429.title-small {
430 font-family: var(--md-sys-typescale-title-small-font);
431 font-weight: var(--md-sys-typescale-title-small-weight);
432 font-size: var(--md-sys-typescale-title-small-size);
433 font-style: var(--md-sys-typescale-title-small-font-style);
434 letter-spacing: var(--md-sys-typescale-title-small-tracking);
435 line-height: var(--md-sys-typescale-title-small-line-height);
436 text-transform: var(--md-sys-typescale-title-small-text-transform);
437 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
438}
439/* Title Medium */
440.title-medium {
441 font-family: var(--md-sys-typescale-title-medium-font);
442 font-weight: var(--md-sys-typescale-title-medium-weight);
443 font-size: var(--md-sys-typescale-title-medium-size);
444 font-style: var(--md-sys-typescale-title-medium-font-style);
445 letter-spacing: var(--md-sys-typescale-title-medium-tracking);
446 line-height: var(--md-sys-typescale-title-medium-line-height);
447 text-transform: var(--md-sys-typescale-title-medium-text-transform);
448 text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
449}
450/* Title Large */
451.title-large {
452 font-family: var(--md-sys-typescale-title-large-font);
453 font-weight: var(--md-sys-typescale-title-large-weight);
454 font-size: var(--md-sys-typescale-title-large-size);
455 font-style: var(--md-sys-typescale-title-large-font-style);
456 letter-spacing: var(--md-sys-typescale-title-large-tracking);
457 line-height: var(--md-sys-typescale-title-large-line-height);
458 text-transform: var(--md-sys-typescale-title-large-text-transform);
459 text-decoration: var(--md-sys-typescale-title-large-text-decoration);
460}
461/* Headline Small */
462.headline-small {
463 font-family: var(--md-sys-typescale-headline-small-font);
464 font-weight: var(--md-sys-typescale-headline-small-weight);
465 font-size: var(--md-sys-typescale-headline-small-size);
466 font-style: var(--md-sys-typescale-headline-small-font-style);
467 letter-spacing: var(--md-sys-typescale-headline-small-tracking);
468 line-height: var(--md-sys-typescale-headline-small-line-height);
469 text-transform: var(--md-sys-typescale-headline-small-text-transform);
470 text-decoration: var(--md-sys-typescale-headline-small-text-decoration);
471}
472/* Headline Medium */
473.headline-medium {
474 font-family: var(--md-sys-typescale-headline-medium-font);
475 font-weight: var(--md-sys-typescale-headline-medium-weight);
476 font-size: var(--md-sys-typescale-headline-medium-size);
477 font-style: var(--md-sys-typescale-headline-medium-font-style);
478 letter-spacing: var(--md-sys-typescale-headline-medium-tracking);
479 line-height: var(--md-sys-typescale-headline-medium-line-height);
480 text-transform: var(--md-sys-typescale-headline-medium-text-transform);
481 text-decoration: var(--md-sys-typescale-headline-medium-text-decoration);
482}
483/* Headline Large */
484.headline-large {
485 font-family: var(--md-sys-typescale-headline-large-font);
486 font-weight: var(--md-sys-typescale-headline-large-weight);
487 font-size: var(--md-sys-typescale-headline-large-size);
488 font-style: var(--md-sys-typescale-headline-large-font-style);
489 letter-spacing: var(--md-sys-typescale-headline-large-tracking);
490 line-height: var(--md-sys-typescale-headline-large-line-height);
491 text-transform: var(--md-sys-typescale-headline-large-text-transform);
492 text-decoration: var(--md-sys-typescale-headline-large-text-decoration);
493}
494/* Display Small */
495.display-small {
496 font-family: var(--md-sys-typescale-display-small-font);
497 font-weight: var(--md-sys-typescale-display-small-weight);
498 font-size: var(--md-sys-typescale-display-small-size);
499 font-style: var(--md-sys-typescale-display-small-font-style);
500 letter-spacing: var(--md-sys-typescale-display-small-tracking);
501 line-height: var(--md-sys-typescale-display-small-line-height);
502 text-transform: var(--md-sys-typescale-display-small-text-transform);
503 text-decoration: var(--md-sys-typescale-display-small-text-decoration);
504}
505/* Display Medium */
506.display-medium {
507 font-family: var(--md-sys-typescale-display-medium-font);
508 font-weight: var(--md-sys-typescale-display-medium-weight);
509 font-size: var(--md-sys-typescale-display-medium-size);
510 font-style: var(--md-sys-typescale-display-medium-font-style);
511 letter-spacing: var(--md-sys-typescale-display-medium-tracking);
512 line-height: var(--md-sys-typescale-display-medium-line-height);
513 text-transform: var(--md-sys-typescale-display-medium-text-transform);
514 text-decoration: var(--md-sys-typescale-display-medium-text-decoration);
515}
516/* Display Large */
517.display-large {
518 font-family: var(--md-sys-typescale-display-large-font);
519 font-weight: var(--md-sys-typescale-display-large-weight);
520 font-size: var(--md-sys-typescale-display-large-size);
521 font-style: var(--md-sys-typescale-display-large-font-style);
522 letter-spacing: var(--md-sys-typescale-display-large-tracking);
523 line-height: var(--md-sys-typescale-display-large-line-height);
524 text-transform: var(--md-sys-typescale-display-large-text-transform);
525 text-decoration: var(--md-sys-typescale-display-large-text-decoration);
526}
527

9. Carpeta « material-tokens / css / theme »

A. material-tokens / css / theme / dark.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17:root {
18 color-scheme: dark;
19 /* Surface tint */
20 --md-sys-color-surface-tint: var(--md-sys-color-primary);
21 /* Surface tint color */
22 --md-sys-color-surface-tint-color: var(--md-sys-color-primary);
23 /* On error container */
24 --md-sys-color-on-error-container: var(--md-ref-palette-error80);
25 /* On error */
26 --md-sys-color-on-error: var(--md-ref-palette-error20);
27 /* Error container */
28 --md-sys-color-error-container: var(--md-ref-palette-error30);
29 /* On tertiary container */
30 --md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary90);
31 /* On tertiary */
32 --md-sys-color-on-tertiary: var(--md-ref-palette-tertiary20);
33 /* Tertiary container */
34 --md-sys-color-tertiary-container: var(--md-ref-palette-tertiary30);
35 /* Tertiary */
36 --md-sys-color-tertiary: var(--md-ref-palette-tertiary80);
37 /* Shadow */
38 --md-sys-color-shadow: var(--md-ref-palette-neutral0);
39 /* Error */
40 --md-sys-color-error: var(--md-ref-palette-error80);
41 /* Outline */
42 --md-sys-color-outline: var(--md-ref-palette-neutral-variant60);
43 /* On background */
44 --md-sys-color-on-background: var(--md-ref-palette-neutral90);
45 /* Background */
46 --md-sys-color-background: var(--md-ref-palette-neutral10);
47 /* Inverse on surface */
48 --md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral20);
49 /* Inverse surface */
50 --md-sys-color-inverse-surface: var(--md-ref-palette-neutral90);
51 /* On surface variant */
52 --md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant80);
53 /* On surface */
54 --md-sys-color-on-surface: var(--md-ref-palette-neutral90);
55 /* Surface Variant */
56 --md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant30);
57 /* Surface */
58 --md-sys-color-surface: var(--md-ref-palette-neutral10);
59 /* On secondary container */
60 --md-sys-color-on-secondary-container: var(--md-ref-palette-secondary90);
61 /* On secondary */
62 --md-sys-color-on-secondary: var(--md-ref-palette-secondary20);
63 /* Secondary container */
64 --md-sys-color-secondary-container: var(--md-ref-palette-secondary30);
65 /* Secondary */
66 --md-sys-color-secondary: var(--md-ref-palette-secondary80);
67 /* Inverse primary */
68 --md-sys-color-inverse-primary: var(--md-ref-palette-primary40);
69 /* On primary container */
70 --md-sys-color-on-primary-container: var(--md-ref-palette-primary90);
71 /* On primary */
72 --md-sys-color-on-primary: var(--md-ref-palette-primary20);
73 /* Primary container */
74 --md-sys-color-primary-container: var(--md-ref-palette-primary30);
75 /* Primary */
76 --md-sys-color-primary: var(--md-ref-palette-primary80);
77}
78

B. material-tokens / css / theme / light.css

1/*
2 Copyright 2016 Google Inc. All rights reserved.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17:root {
18 color-scheme: light;
19 /* Surface tint */
20 --md-sys-color-surface-tint: var(--md-sys-color-primary);
21 /* Surface tint color */
22 --md-sys-color-surface-tint-color: var(--md-sys-color-primary);
23 /* On error container */
24 --md-sys-color-on-error-container: var(--md-ref-palette-error10);
25 /* On error */
26 --md-sys-color-on-error: var(--md-ref-palette-error100);
27 /* Error container */
28 --md-sys-color-error-container: var(--md-ref-palette-error90);
29 /* On tertiary container */
30 --md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary10);
31 /* On tertiary */
32 --md-sys-color-on-tertiary: var(--md-ref-palette-tertiary100);
33 /* Tertiary container */
34 --md-sys-color-tertiary-container: var(--md-ref-palette-tertiary90);
35 /* Tertiary */
36 --md-sys-color-tertiary: var(--md-ref-palette-tertiary40);
37 /* Shadow */
38 --md-sys-color-shadow: var(--md-ref-palette-neutral0);
39 /* Error */
40 --md-sys-color-error: var(--md-ref-palette-error40);
41 /* Outline */
42 --md-sys-color-outline: var(--md-ref-palette-neutral-variant50);
43 /* On background */
44 --md-sys-color-on-background: var(--md-ref-palette-neutral10);
45 /* Background */
46 --md-sys-color-background: var(--md-ref-palette-neutral99);
47 /* Inverse on surface */
48 --md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral95);
49 /* Inverse surface */
50 --md-sys-color-inverse-surface: var(--md-ref-palette-neutral20);
51 /* On surface variant */
52 --md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant30);
53 /* On surface */
54 --md-sys-color-on-surface: var(--md-ref-palette-neutral10);
55 /* Surface Variant */
56 --md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant90);
57 /* Surface */
58 --md-sys-color-surface: var(--md-ref-palette-neutral99);
59 /* On secondary container */
60 --md-sys-color-on-secondary-container: var(--md-ref-palette-secondary10);
61 /* On secondary */
62 --md-sys-color-on-secondary: var(--md-ref-palette-secondary100);
63 /* Secondary container */
64 --md-sys-color-secondary-container: var(--md-ref-palette-secondary90);
65 /* Secondary */
66 --md-sys-color-secondary: var(--md-ref-palette-secondary40);
67 /* Inverse primary */
68 --md-sys-color-inverse-primary: var(--md-ref-palette-primary80);
69 /* On primary container */
70 --md-sys-color-on-primary-container: var(--md-ref-palette-primary10);
71 /* On primary */
72 --md-sys-color-on-primary: var(--md-ref-palette-primary100);
73 /* Primary container */
74 --md-sys-color-primary-container: var(--md-ref-palette-primary90);
75 /* Primary */
76 --md-sys-color-primary: var(--md-ref-palette-primary40);
77}
78

34. Carpeta « ungap »

A. ungap / custom-elements.js

35. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "Node16",
7 "moduleResolution": "Node16",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

36. Resumen

  • En esta lección se presentó una PWA con Material Design que incluye:

    • Diseño de formularios para móviles.

    • Elementos de interfaces móviles básicas para Material Design con HTML, CSS y JavaScript.