primer commit
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Dependencias
|
||||||
|
scripts/node_modules/
|
||||||
|
|
||||||
|
# Salidas de build
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Capturas generadas
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# Archivos del sistema
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
name: manuales-factorpesca
|
||||||
|
description: Genera manuales de usuario en PDF con un diseño visual estandarizado (paleta azul marino, tipografía Inter, página Letter) a partir de una aplicación web real. Combina la skill playwright-skill de lackeyjb para capturar pantallas con un generador HTML→PDF basado en Playwright. Aplica estructura editorial (portada, índice, requisitos previos, capítulos numerados, solución de problemas, glosario, aviso de propiedad) y un checklist de calidad antes de entregar. Use when the user asks to create, regenerate, or review a Spanish-language user manual PDF for a web application, or when they need the FactorPesca editorial design system applied to a technical document.
|
||||||
|
license: UNLICENSED
|
||||||
|
metadata:
|
||||||
|
author: factorpesca
|
||||||
|
version: "1.0.0"
|
||||||
|
allowed-tools: Bash(node:*) Bash(npm:*) Read Write Edit Glob Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# manuales-factorpesca
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Crear manuales de usuario en PDF para aplicaciones web de FactorPesca,
|
||||||
|
con un diseño visual consistente (paleta azul marino, Inter, página Letter)
|
||||||
|
y contenido verificado a partir de la aplicación real.
|
||||||
|
|
||||||
|
## Cuándo usar esta skill
|
||||||
|
|
||||||
|
- El usuario pide "manual de usuario", "manual PDF", "documentación para usuarios".
|
||||||
|
- El usuario pide regenerar, actualizar o revisar un manual existente.
|
||||||
|
- Se necesita aplicar el sistema de diseño FactorPesca a un manual nuevo o existente.
|
||||||
|
|
||||||
|
## Herramientas
|
||||||
|
|
||||||
|
- **Captura y navegación**: usa la skill `playwright-skill` de lackeyjb para
|
||||||
|
recorrer la aplicación y obtener las capturas de pantalla. Es la fuente
|
||||||
|
oficial de los artefactos visuales.
|
||||||
|
- **Generación de PDF**: usa el script `scripts/build-manual.js`, que importa
|
||||||
|
Playwright directamente. No es estrictamente necesario instalar Chromium
|
||||||
|
dos veces: la skill de lackeyjb ya lo trae.
|
||||||
|
- **No inventes pantallas**: cada captura debe corresponder a un estado real
|
||||||
|
de la aplicación, capturado con Playwright.
|
||||||
|
|
||||||
|
Instalación de la skill auxiliar (ejecutar una sola vez por entorno):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Global
|
||||||
|
npx skills add lackeyjb/playwright-skill --skill playwright-skill --global --yes
|
||||||
|
|
||||||
|
# Local al proyecto
|
||||||
|
npx skills add lackeyjb/playwright-skill --skill playwright-skill --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estructura de la skill
|
||||||
|
|
||||||
|
```
|
||||||
|
manuales-factorpesca/
|
||||||
|
├── SKILL.md # este archivo
|
||||||
|
├── references/
|
||||||
|
│ ├── estilo.md # paleta, tipografía, componentes, tono
|
||||||
|
│ ├── capturas.md # cómo capturar con playwright-skill
|
||||||
|
│ ├── estructura.md # anatomía obligatoria del documento
|
||||||
|
│ └── calidad.md # checklist pre-entrega
|
||||||
|
├── templates/
|
||||||
|
│ ├── manual.html # plantilla semántica
|
||||||
|
│ ├── manual.css # estilos pantalla + @page Letter
|
||||||
|
│ └── manual.js # auto-TOC, anotaciones, numeración
|
||||||
|
└── scripts/
|
||||||
|
├── build-manual.js # Playwright: HTML → PDF
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Carga `references/estilo.md` antes de empezar para conocer la paleta y los
|
||||||
|
componentes disponibles. Carga `references/estructura.md` para conocer la
|
||||||
|
anatomía obligatoria del documento.
|
||||||
|
|
||||||
|
## Flujo de trabajo
|
||||||
|
|
||||||
|
1. **Identificar** la aplicación, la audiencia y el objetivo del manual.
|
||||||
|
2. **Definir** los procesos que se documentarán. Confirmar con el usuario el
|
||||||
|
alcance antes de empezar.
|
||||||
|
3. **Preparar el entorno**: instalar dependencias de la skill
|
||||||
|
`playwright-skill` y de `scripts/` (`npm install`).
|
||||||
|
4. **Capturar** las pantallas siguiendo `references/capturas.md`. Guardar
|
||||||
|
en `artifacts/` con el patrón `NN-slug.png`.
|
||||||
|
5. **Redactar** el contenido en español claro, una acción principal por paso.
|
||||||
|
6. **Rellenar** `templates/manual.html`: portada, índice, resumen,
|
||||||
|
requisitos, capítulos, troubleshooting, glosario, aviso de propiedad.
|
||||||
|
7. **Construir** el PDF con `node scripts/build-manual.js`.
|
||||||
|
8. **Verificar** el PDF aplicando `references/calidad.md`.
|
||||||
|
9. **Entregar** el archivo final desde `dist/`.
|
||||||
|
|
||||||
|
## Reglas editoriales
|
||||||
|
|
||||||
|
- Títulos descriptivos y pasos numerados. Una acción principal por paso.
|
||||||
|
- Indicar el resultado esperado después de acciones importantes.
|
||||||
|
- Añadir callouts de advertencia antes de operaciones irreversibles.
|
||||||
|
- Evitar párrafos largos. Dividir en pasos o listas.
|
||||||
|
- Mantener consistencia con los nombres de botones y campos de la UI.
|
||||||
|
- Tono: segunda persona, presente indicativo para acciones, futuro para
|
||||||
|
consecuencias. Sin "simplemente", "fácilmente", "obviamente".
|
||||||
|
- No incluir contraseñas, tokens, datos personales reales ni identificadores
|
||||||
|
confidenciales en capturas ni en el texto.
|
||||||
|
|
||||||
|
## Capturas de pantalla
|
||||||
|
|
||||||
|
- Capturar después de que la UI termine de actualizarse
|
||||||
|
(`await page.getByRole(...).waitFor()`).
|
||||||
|
- Usar viewport 1440×900 a 2x de DPI para nitidez en impresión.
|
||||||
|
- Anotar las capturas con círculos numerados
|
||||||
|
(`<ol class="annotations" data-annotations='[{"x":50,"y":30,"n":1}]'>`).
|
||||||
|
- Cerrar banners de cookies, tooltips de onboarding y notificaciones.
|
||||||
|
- Nombres `NN-slug.png` dentro de `artifacts/`.
|
||||||
|
|
||||||
|
Detalles completos en `references/capturas.md`.
|
||||||
|
|
||||||
|
## Estilo visual obligatorio
|
||||||
|
|
||||||
|
- Paleta: azul marino FactorPesca (`#0B2545` primary, `#F4A300` accent).
|
||||||
|
- Tipografía: Inter (cuerpo) y JetBrains Mono (código).
|
||||||
|
- Página Letter 8.5×11", márgenes 22mm.
|
||||||
|
- Cabecera con nombre del capítulo (eyebrow small caps).
|
||||||
|
- Pie con "FactorPesca · Manual de usuario" y "Página X de Y".
|
||||||
|
- Portada a sangre (full bleed navy).
|
||||||
|
|
||||||
|
Detalles y excepciones en `references/estilo.md`.
|
||||||
|
|
||||||
|
## Estructura obligatoria del documento
|
||||||
|
|
||||||
|
1. Portada
|
||||||
|
2. Índice (auto-generado)
|
||||||
|
3. Resumen ejecutivo
|
||||||
|
4. Requisitos previos
|
||||||
|
5. Capítulos (uno por proceso)
|
||||||
|
6. Solución de problemas
|
||||||
|
7. Glosario (opcional)
|
||||||
|
8. Aviso de propiedad y confidencialidad (obligatoria)
|
||||||
|
|
||||||
|
Detalles y reglas de salto de página en `references/estructura.md`.
|
||||||
|
|
||||||
|
## Generación del PDF
|
||||||
|
|
||||||
|
Tras editar `templates/manual.html`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd manuales-factorpesca
|
||||||
|
node scripts/build-manual.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Por defecto produce `dist/manual-{aplicacion}-v{version}.pdf`. Más opciones en
|
||||||
|
`scripts/README.md`.
|
||||||
|
|
||||||
|
## Entrega y calidad
|
||||||
|
|
||||||
|
Antes de declarar el manual como terminado:
|
||||||
|
|
||||||
|
- Recorrer el manual página a página, marcando cualquier desviación.
|
||||||
|
- Aplicar el checklist completo de `references/calidad.md`.
|
||||||
|
- Corregir en el HTML fuente, no en el PDF.
|
||||||
|
- Volver a generar hasta que el checklist esté limpio.
|
||||||
|
- Marcar la versión como `released` y entregar el archivo desde `dist/`.
|
||||||
|
|
||||||
|
## Anti-patrones
|
||||||
|
|
||||||
|
- Capturar pantallas con datos personales reales o placeholders que parezcan reales.
|
||||||
|
- Inventar mensajes, botones, opciones o pantallas que no existen en la app.
|
||||||
|
- Pegar imágenes sin alinearlas con la posición real del elemento.
|
||||||
|
- Usar un color fuera de la paleta autorizada.
|
||||||
|
- Cambiar la tipografía del manual a otra familia que no sea Inter o JetBrains Mono.
|
||||||
|
- Dejar capturas de pantalla con banners de cookies, tooltips o estados de carga.
|
||||||
|
- Declarar terminado un manual con pasos sin captura o capturas sin paso.
|
||||||
|
- Generar el PDF sin `-webkit-print-color-adjust: exact` (los callouts salen en blanco).
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Lista de comprobaciones pre-entrega
|
||||||
|
|
||||||
|
Ejecutar este checklist completo antes de declarar el manual terminado. Si falla cualquier punto, corregir y volver a generar el PDF.
|
||||||
|
|
||||||
|
## 1. Contenido
|
||||||
|
|
||||||
|
- [ ] El título de la portada coincide exactamente con el nombre de la aplicación.
|
||||||
|
- [ ] El índice lista todos los capítulos y secciones presentes en el cuerpo.
|
||||||
|
- [ ] Cada capítulo tiene al menos un objetivo explícito en un callout `info`.
|
||||||
|
- [ ] Cada proceso termina con un callout `tip` "Resultado final".
|
||||||
|
- [ ] La sección "Solución de problemas" existe y cubre al menos los tres errores más comunes.
|
||||||
|
- [ ] Los nombres de los botones, campos y menús son idénticos a los de la UI.
|
||||||
|
- [ ] Las acciones irreversibles están acompañadas de un callout `warning` o `danger`.
|
||||||
|
|
||||||
|
## 2. Capturas de pantalla
|
||||||
|
|
||||||
|
- [ ] Cada paso crítico tiene al menos una captura.
|
||||||
|
- [ ] Las capturas están en `artifacts/` con nombres `NN-slug.png`.
|
||||||
|
- [ ] Ninguna captura contiene contraseñas, tokens, datos personales reales o identificadores confidenciales.
|
||||||
|
- [ ] Los banners de cookies, tooltips de onboarding y notificaciones están cerrados.
|
||||||
|
- [ ] Las anotaciones numeradas coinciden con el orden de los pasos en el texto.
|
||||||
|
- [ ] Las capturas se ven nítidas en la página impresa (probar zoom 100% en el lector de PDF).
|
||||||
|
|
||||||
|
## 3. Estilo y formato
|
||||||
|
|
||||||
|
- [ ] Todos los callouts tienen modificador correcto (`info`, `warning`, `danger`, `tip`, `note`).
|
||||||
|
- [ ] Ningún párrafo excede 4 líneas en pantalla.
|
||||||
|
- [ ] No hay cursivas decorativas; las negritas se usan solo para destacar UI ("Haga clic en **Guardar**").
|
||||||
|
- [ ] No hay colores fuera de la paleta autorizada (ver `estilo.md`).
|
||||||
|
- [ ] No hay tipografías distintas a Inter y JetBrains Mono.
|
||||||
|
- [ ] Los bloques de código usan `pre code` y no se parten a la mitad de página.
|
||||||
|
|
||||||
|
## 4. Numeración y referencias
|
||||||
|
|
||||||
|
- [ ] Los pasos siguen numeración global sin reiniciar.
|
||||||
|
- [ ] Las figuras se enumeran automáticamente y el caption coincide con el contenido de la imagen.
|
||||||
|
- [ ] Las referencias cruzadas (por ejemplo, "ver paso 4") son correctas.
|
||||||
|
- [ ] El índice generado automáticamente coincide con los encabezados reales.
|
||||||
|
|
||||||
|
## 5. Impresión y paginación
|
||||||
|
|
||||||
|
- [ ] Cada capítulo (`h1`) empieza en página nueva.
|
||||||
|
- [ ] Ningún heading queda al final de página sin contenido después.
|
||||||
|
- [ ] Ningún `.step-block`, `.callout` o `figure` se parte a la mitad.
|
||||||
|
- [ ] La cabecera muestra el nombre del capítulo actual.
|
||||||
|
- [ ] El pie muestra "Página X de Y" coherente (X nunca excede Y, Y es el total real).
|
||||||
|
- [ ] La portada y la página de aviso de propiedad no muestran cabecera ni pie.
|
||||||
|
- [ ] El aviso de propiedad menciona explícitamente a "FactorPesca S.A. de C.V." como titular del documento.
|
||||||
|
- [ ] La portada usa sangrado completo (el fondo navy llega al borde de la hoja).
|
||||||
|
- [ ] Las imágenes no pierden color ni contraste al imprimir (`-webkit-print-color-adjust: exact`).
|
||||||
|
|
||||||
|
## 6. PDF final
|
||||||
|
|
||||||
|
- [ ] El PDF se abre sin errores en al menos dos visores (Chromium y un lector externo como Preview o Acrobat).
|
||||||
|
- [ ] El tamaño del PDF es razonable (menos de 25 MB para manuales de hasta 60 páginas; reducir peso si supera).
|
||||||
|
- [ ] Los marcadores de navegación del PDF están generados para cada capítulo.
|
||||||
|
- [ ] El archivo de salida se llama `manual-{aplicacion}-v{X.Y.Z}.pdf` y se entrega en `dist/`.
|
||||||
|
|
||||||
|
## 7. Procedimiento de revisión
|
||||||
|
|
||||||
|
1. Generar el PDF.
|
||||||
|
2. Recorrer el manual página a página. Marcar cualquier desviación.
|
||||||
|
3. Corregir el HTML/CSS/JS fuente (nunca el PDF).
|
||||||
|
4. Volver a generar.
|
||||||
|
5. Repetir hasta que el checklist esté completo.
|
||||||
|
6. Marcar el manual como `released` en el changelog del proyecto.
|
||||||
|
|
||||||
|
## 8. Errores frecuentes
|
||||||
|
|
||||||
|
- **PDF sin color de fondo**: falta `-webkit-print-color-adjust: exact`. Revisar `manual.css`.
|
||||||
|
- **TOC vacío**: los headings no tienen `data-toc` o están dentro de contenedores excluidos. Revisar `manual.js`.
|
||||||
|
- **Anotaciones mal posicionadas**: las coordenadas no están en porcentaje. Revisar `data-annotations`.
|
||||||
|
- **Pasos partiéndose a la mitad**: falta `break-inside: avoid` o el contenido del paso es demasiado alto. Dividir el paso.
|
||||||
|
- **Saltos de página inconsistentes**: la cabecera/pie no están en `@page`. Revisar `manual.css` sección `@page`.
|
||||||
|
|
||||||
|
## 9. Limitaciones técnicas conocidas
|
||||||
|
|
||||||
|
- **Números de página en el índice automático**: el script `manual.js` no
|
||||||
|
puede conocer el número de página final de cada sección en el momento de
|
||||||
|
renderizar el PDF, porque Chromium solo expone esos números después de
|
||||||
|
paginar. Por eso las entradas del TOC muestran `—` como número. Tres
|
||||||
|
opciones para resolverlo:
|
||||||
|
|
||||||
|
1. **Aceptar la limitación**: dejar el guion y añadir a mano los números
|
||||||
|
tras la primera generación, editando `templates/manual.html` y usando
|
||||||
|
`data-toc-label` con la numeración correcta.
|
||||||
|
2. **Hacer un build en dos pasadas**: generar el PDF una vez, leer los
|
||||||
|
números de página reales del PDF resultante y volver a renderizar con
|
||||||
|
los números ya conocidos. Requiere un parser de PDF.
|
||||||
|
3. **Quitar los números del TOC**: ocultar la columna derecha de cada
|
||||||
|
fila del índice con CSS y dejar solo el nombre del capítulo/sección
|
||||||
|
con sus líderes de puntos. Es la opción más limpia visualmente.
|
||||||
|
|
||||||
|
- **Named pages en Chromium**: el uso de `@page cover` y `@page copyright` está
|
||||||
|
soportado pero requiere que la sección use exactamente la clase CSS
|
||||||
|
correspondiente (`cover`, `copyright-page`, etc.) sin selectores anidados.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Capturas de pantalla
|
||||||
|
|
||||||
|
Cómo producir las capturas que acompañarán a los manuales. Esta guía presupone que tienes instalada la skill `playwright-skill` de lackeyjb.
|
||||||
|
|
||||||
|
## Configuración del navegador
|
||||||
|
|
||||||
|
- **Viewport**: 1440×900 px (escritorio estándar).
|
||||||
|
- **Factor de escala**: 2 (DPR 2). Resultado: imagen a 2880×1800. Reduce al 50% en el CSS con `max-width: 100%` y `image-rendering: -webkit-optimize-contrast`.
|
||||||
|
- **Localización**: `es-ES` (o la del usuario). Forzar la zona horaria de la operación si afecta a la UI.
|
||||||
|
- **User agent**: dejar el de Chromium por defecto.
|
||||||
|
- **Modo headless**: recomendado. Usar `headless: true` para ejecuciones desatendidas. Reservar headed para depuración puntual.
|
||||||
|
|
||||||
|
Plantilla de configuración para `playwright-skill`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PW_HEADLESS=true
|
||||||
|
export PW_ARTIFACT_DIR="$PWD/artifacts"
|
||||||
|
node "$SKILL_DIR/run.js" "$TMP_DIR/capturar-flujo.js"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolución y nombrado
|
||||||
|
|
||||||
|
- Tamaño final recomendado: entre 1200 y 1600 px de ancho (después del downscale). Más grande no mejora la impresión y aumenta el peso del PDF.
|
||||||
|
- Formato: PNG para capturas con texto nítido. JPEG (`quality: 80`) para pantallas con muchas fotos.
|
||||||
|
- Nombres de archivo: `NN-slug-corto.png` donde `NN` es el número de paso. Ejemplo: `01-login.png`, `02-panel-principal.png`, `03-formulario-cliente.png`.
|
||||||
|
- Guardar todas las capturas dentro de `artifacts/` en la raíz del manual. El build copia esa carpeta al directorio temporal de render.
|
||||||
|
|
||||||
|
## Antes de capturar
|
||||||
|
|
||||||
|
1. **Sesión limpia**: cierra banners de cookies, notificaciones push y tooltips de onboarding. Si la app tiene un banner de "tour", descártalo.
|
||||||
|
2. **Datos consistentes**: usa siempre el mismo usuario de pruebas, mismo idioma y mismo conjunto de datos. Cualquier inconsistencia se notará en el manual.
|
||||||
|
3. **Quita lo accesorio**: oculta elementos beta, flags, datos personales reales, mensajes transitorios. No falsees pantallas: si un paso intermedio no es representativo, no lo fotografíes.
|
||||||
|
4. **Espera activa**: usa `await page.locator(...).waitFor({ state: 'visible' })` o `await page.getByRole(...).waitFor()` en lugar de `waitForTimeout`. La captura debe hacerse después de que la UI termine de actualizarse.
|
||||||
|
|
||||||
|
## Encuadre
|
||||||
|
|
||||||
|
- Captura la pantalla completa con `fullPage: true` solo si la página entera es el contenido relevante. Para vistas con scroll, prefiere captura del viewport (`fullPage: false`).
|
||||||
|
- Recorta a la zona de interés con `page.locator(selector).screenshot({ path })` cuando solo importa un componente (por ejemplo, un modal).
|
||||||
|
- Deja al menos 16pt de margen visual alrededor del elemento principal.
|
||||||
|
- Incluye en la captura el contexto suficiente para que el lector se ubique: la barra de navegación, el breadcrumb o el encabezado de sección son válidos.
|
||||||
|
- Evita capturas con scrollbars visibles a menos que sea indispensable.
|
||||||
|
|
||||||
|
## Privacidad y seguridad
|
||||||
|
|
||||||
|
- **Nunca** incluyas contraseñas, tokens, claves API, datos personales reales o números de identificación reales. Sustituye por placeholders tipográficos como `usuario@ejemplo.com`, `••••••••`, `1234567X` o cuentas de prueba claramente ficticias.
|
||||||
|
- Antes de capturar, valida que la vista no contenga datos sensibles: usa cuentas de test y datos sintéticos.
|
||||||
|
- Si necesitas capturar un error real, anonimiza la URL y los IDs antes de pegarla en el manual.
|
||||||
|
- Revisa el fondo de la captura: pueden aparecer datos de otras pestañas sincronizadas, notificaciones del sistema o fechas de la máquina.
|
||||||
|
|
||||||
|
## Anotaciones
|
||||||
|
|
||||||
|
Las anotaciones se aplican en el HTML con la clase `.annotations` y el atributo `data-annotations`. Cada elemento del array es un marcador posicionado en porcentaje respecto a la imagen:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<figure class="screenshot-frame">
|
||||||
|
<img src="artifacts/01-login.png" alt="Pantalla de inicio de sesión" />
|
||||||
|
<ol class="annotations" data-annotations='[
|
||||||
|
{"x": 62, "y": 58, "n": 1, "label": "Botón Iniciar sesión"},
|
||||||
|
{"x": 50, "y": 74, "n": 2, "label": "Campo contraseña"}
|
||||||
|
]'></ol>
|
||||||
|
<figcaption>Inicio de sesión con credenciales de prueba.</figcaption>
|
||||||
|
</figure>
|
||||||
|
```
|
||||||
|
|
||||||
|
Convenciones:
|
||||||
|
|
||||||
|
- `x` y `y` son porcentajes sobre el ancho y alto de la imagen. 0/0 es la esquina superior izquierda, 100/100 la inferior derecha.
|
||||||
|
- `n` es el número que se muestra en el círculo. Debe coincidir con el orden lógico del paso (1, 2, 3…).
|
||||||
|
- `label` es texto accesible (lectores de pantalla) y se usa como tooltip al pasar el cursor.
|
||||||
|
- En el manual impreso, los marcadores se muestran en blanco con borde primary, número primary.
|
||||||
|
|
||||||
|
## Plantilla de script Playwright
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const TARGET = process.env.TARGET_URL || 'http://localhost:3000';
|
||||||
|
const ART = process.env.PW_ARTIFACT_DIR || path.join(process.cwd(), 'artifacts');
|
||||||
|
fs.mkdirSync(ART, { recursive: true });
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
locale: 'es-ES',
|
||||||
|
timezoneId: 'Europe/Madrid',
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
// Paso 1: login
|
||||||
|
await page.goto(`${TARGET}/login`);
|
||||||
|
await page.getByLabel('Email').waitFor();
|
||||||
|
await page.screenshot({ path: path.join(ART, '01-login.png'), fullPage: false });
|
||||||
|
|
||||||
|
// Paso 2: panel principal
|
||||||
|
await page.getByLabel('Email').fill('demo@factorpesca.com');
|
||||||
|
await page.getByLabel('Contraseña').fill('Demo-2025!');
|
||||||
|
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
|
||||||
|
await page.getByRole('heading', { name: /panel/i }).waitFor();
|
||||||
|
await page.screenshot({ path: path.join(ART, '02-panel.png'), fullPage: false });
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
Guarda el script como `scripts/capturar.js` y ejecútalo a través de la skill `playwright-skill` con `node "$SKILL_DIR/run.js" capturar.js`.
|
||||||
|
|
||||||
|
## Verificación rápida
|
||||||
|
|
||||||
|
Antes de pasar al redactor, ejecuta este checklist:
|
||||||
|
|
||||||
|
- [ ] Las capturas se ven nítidas a tamaño real (sin pixelado al 100%).
|
||||||
|
- [ ] El texto de la UI es legible.
|
||||||
|
- [ ] No hay datos personales reales.
|
||||||
|
- [ ] El número de capturas coincide con el número de pasos previstos.
|
||||||
|
- [ ] Los nombres de archivo siguen el patrón `NN-slug.png`.
|
||||||
|
- [ ] Cada captura muestra el estado final del paso anterior, no un estado intermedio de carga.
|
||||||
|
- [ ] Los banners de cookies, tooltips de onboarding y notificaciones de "tour" están cerrados.
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Estilo visual
|
||||||
|
|
||||||
|
Identidad obligatoria para todos los manuales PDF generados por esta skill. Cualquier desviación rompe la consistencia de la marca FactorPesca.
|
||||||
|
|
||||||
|
## Paleta de colores
|
||||||
|
|
||||||
|
| Token | Hex | Uso principal |
|
||||||
|
|---|---|---|
|
||||||
|
| `--color-primary` | `#0B2545` | Portada, H1, H2, banda superior, línea fina decorativa |
|
||||||
|
| `--color-primary-600` | `#13315C` | H3, texto sobre fondos claros cuando se necesita profundidad |
|
||||||
|
| `--color-primary-100` | `#E6ECF2` | Fondo de callouts suaves, filas alternadas sutiles |
|
||||||
|
| `--color-accent` | `#F4A300` | Círculos numeradores de pasos, marcadores de anotación, subtítulo de portada, foco |
|
||||||
|
| `--color-accent-600` | `#D88C00` | Estados activos y hover del accent |
|
||||||
|
| `--text-primary` | `#1A1A1A` | Cuerpo del documento |
|
||||||
|
| `--text-secondary` | `#404040` | Subtítulos, descripciones cortas |
|
||||||
|
| `--text-muted` | `#6B7280` | Metadatos, captions, fechas |
|
||||||
|
| `--surface` | `#FFFFFF` | Fondo de página |
|
||||||
|
| `--surface-alt` | `#F3F4F6` | Filas alternadas de tablas, zonas de respiro |
|
||||||
|
| `--border` | `#E5E7EB` | Bordes de tablas, marcos de capturas |
|
||||||
|
| `--success` | `#047857` | Callouts `tip` y resultados exitosos |
|
||||||
|
| `--warning` | `#B45309` | Callouts `warning` y advertencias |
|
||||||
|
| `--danger` | `#B91C1C` | Callouts `danger` y operaciones destructivas |
|
||||||
|
| `--info` | `#1D4ED8` | Callouts `info` y aclaraciones |
|
||||||
|
|
||||||
|
Reglas de uso:
|
||||||
|
|
||||||
|
- El blanco de página es la única superficie de fondo permitida para el contenido.
|
||||||
|
- El `--color-accent` se reserva para numeración, foco y elementos interactivos. **No** se usa como fondo de grandes áreas.
|
||||||
|
- Los callouts siempre combinan barra lateral de 4pt coloreada + fondo `--color-primary-100` (versión suave) o blanco con borde coloreado (versión estricta).
|
||||||
|
- Contraste mínimo: 4.5:1 para texto sobre fondo (cumple WCAG AA).
|
||||||
|
|
||||||
|
## Tipografía
|
||||||
|
|
||||||
|
Familias (cargar desde Google Fonts dentro de `manual.css`):
|
||||||
|
|
||||||
|
- **Inter** 300 / 400 / 600 / 700 — títulos, cuerpo, UI.
|
||||||
|
- **JetBrains Mono** 400 / 500 — código, rutas, comandos, datos identificadores.
|
||||||
|
|
||||||
|
Escala en puntos (optimizada para página Letter 8.5×11"):
|
||||||
|
|
||||||
|
| Estilo | Familia | Peso | Tamaño | Interlineado | Uso |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Display (portada) | Inter | 700 | 36pt | 1.15 | Título de la portada |
|
||||||
|
| H1 (capítulo) | Inter | 700 | 28pt | 1.20 | Inicio de cada capítulo |
|
||||||
|
| H2 | Inter | 700 | 20pt | 1.25 | Secciones dentro de un capítulo |
|
||||||
|
| H3 | Inter | 600 | 15pt | 1.30 | Sub-secciones |
|
||||||
|
| H4 | Inter | 600 | 12pt | 1.30 | Etiquetas, mini-encabezados |
|
||||||
|
| Body | Inter | 400 | 11pt | 1.55 | Párrafos del manual |
|
||||||
|
| Body énfasis | Inter | 600 | 11pt | 1.55 | Texto destacado en línea |
|
||||||
|
| Caption | Inter | 400 | 9pt | 1.40 | Subtítulos de figuras, pies de tabla |
|
||||||
|
| Eyebrow | Inter | 600 | 9pt tracking 0.12em uppercase | 1.40 | Etiquetas small caps, banda superior |
|
||||||
|
| Code | JetBrains Mono | 400 | 9.5pt | 1.50 | Bloques `pre code` y teclas |
|
||||||
|
| Code énfasis | JetBrains Mono | 500 | 9.5pt | 1.50 | Palabras reservadas inline `code` |
|
||||||
|
|
||||||
|
Reglas de uso:
|
||||||
|
|
||||||
|
- Los números de paso se renderizan en Inter 700 13pt blanco sobre `--color-accent`.
|
||||||
|
- Los nombres de botones o campos se escriben exactamente como aparecen en la UI, entre comillas tipográficas « », o en `code` si son identificadores técnicos.
|
||||||
|
- No usar cursiva. Las cursivas solo se reservan para nombres científicos o títulos de obras externas.
|
||||||
|
- No usar subrayado excepto en enlaces web (que en el PDF se muestran como `--color-info` sin subrayado en la versión final).
|
||||||
|
|
||||||
|
## Componentes
|
||||||
|
|
||||||
|
Todos los componentes se aplican con clases CSS. El HTML sólo necesita marcar la estructura semántica y la clase adecuada.
|
||||||
|
|
||||||
|
### Callout
|
||||||
|
|
||||||
|
```html
|
||||||
|
<aside class="callout callout-warning">
|
||||||
|
<div class="callout-title">Advertencia</div>
|
||||||
|
<p>Esta acción cierra la temporada y no se puede deshacer.</p>
|
||||||
|
</aside>
|
||||||
|
```
|
||||||
|
|
||||||
|
Modificadores: `callout-info`, `callout-warning`, `callout-danger`, `callout-tip`, `callout-note`.
|
||||||
|
|
||||||
|
Anatomía: barra lateral 4pt coloreada + caja con fondo `--color-primary-100` + icono 18pt a la izquierda (Unicode o SVG inline) + título 12pt bold + cuerpo 11pt.
|
||||||
|
|
||||||
|
### Bloque de paso (`.step-block`)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<article class="step-block" data-step>
|
||||||
|
<div class="step-number" aria-hidden="true">1</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h3 class="step-title">Iniciar sesión en la plataforma</h3>
|
||||||
|
<p>Abra el navegador y vaya a <code>https://app.factorpesca.com</code>.</p>
|
||||||
|
<figure class="screenshot-frame">
|
||||||
|
<img src="artifacts/01-login.png" alt="Pantalla de inicio de sesión" />
|
||||||
|
<ol class="annotations" data-annotations='[{"x":62,"y":58,"n":1},{"x":50,"y":74,"n":2}]'></ol>
|
||||||
|
</figure>
|
||||||
|
<aside class="callout callout-info callout-result">
|
||||||
|
<div class="callout-title">Resultado esperado</div>
|
||||||
|
<p>Verá el panel principal con su nombre en la esquina superior derecha.</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
El círculo del paso usa `--color-accent`. El JS inyecta el número automáticamente si se omite, pero se recomienda fijarlo en el HTML.
|
||||||
|
|
||||||
|
### Captura con anotaciones (`.screenshot-frame`)
|
||||||
|
|
||||||
|
- Imagen con `box-shadow: 0 2pt 8pt rgba(0,0,0,0.08)` y borde 1pt `--color-border`.
|
||||||
|
- Caption automática con prefijo "Figura N." y descripción leída de `figcaption` o `alt`.
|
||||||
|
- Anotaciones: círculos blancos 22pt con borde 2pt `--color-primary` y número `--color-primary` 11pt bold. Se posicionan con `top` / `left` en porcentajes sobre la imagen.
|
||||||
|
- En impresión, las anotaciones conservan tamaño y color (requiere `-webkit-print-color-adjust: exact`).
|
||||||
|
|
||||||
|
### Bloque de código (`.code-block`)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<pre class="code-block"><code>npx skills add lackeyjb/playwright-skill --skill playwright-skill --yes</code></pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
Fondo `#1E1E2E`, texto `#F8F8F2`, padding 14pt, radio 4pt, JetBrains Mono 9.5pt, números de línea automáticos.
|
||||||
|
|
||||||
|
### Tabla de datos (`.data-table`)
|
||||||
|
|
||||||
|
Header con fondo `--color-primary` y texto blanco Inter 600 10pt. Filas alternadas blanco / `--color-surface-alt`. Borde inferior 0.5pt `--color-border`. Padding 8pt vertical, 10pt horizontal.
|
||||||
|
|
||||||
|
### Badge
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="badge badge-version">NUEVO</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Fondo `--color-accent`, texto blanco, padding 2pt 8pt, radio 12pt, Inter 700 8pt uppercase.
|
||||||
|
|
||||||
|
## Iconografía
|
||||||
|
|
||||||
|
- Iconos en línea con Unicode cuando sea viable: ⚠ info, ⚠ warning, ⛔ danger, ✅ tip, ⓘ note.
|
||||||
|
- Iconos decorativos en SVG inline con `currentColor` para heredar el color del contexto.
|
||||||
|
- Los iconos no son funcionales: cada callout lleva además texto explícito.
|
||||||
|
|
||||||
|
## Tono editorial
|
||||||
|
|
||||||
|
- Voz: segunda persona del singular o plural según el caso ("Abra el menú", "Vayan al panel").
|
||||||
|
- Tiempo verbal: presente indicativo para acciones ("Haga clic"), futuro para consecuencias ("Aparecerá un mensaje").
|
||||||
|
- Direccionalidad: "arriba a la derecha", "en la columna central", nunca "donde indica la flecha".
|
||||||
|
- Términos: usar los nombres exactos de la UI; si se traduce, mantener coherencia durante todo el manual.
|
||||||
|
- Evitar: "simplemente", "fácilmente", "obviamente", "basta con", jerga innecesaria.
|
||||||
|
- Preferir: verbos directos, frases cortas, una idea por oración.
|
||||||
|
- Párrafos: máximo 4 líneas en pantalla. Si excede, dividir en pasos o lista.
|
||||||
|
- Listas: usar listas numeradas para procedimientos secuenciales, con viñetas para enumeraciones sin orden.
|
||||||
|
|
||||||
|
## Reglas de impresión
|
||||||
|
|
||||||
|
- Tamaño Letter 8.5×11" obligatorio.
|
||||||
|
- Margen 22mm en los cuatro lados. La portada se imprime sin márgenes (full bleed).
|
||||||
|
- Cabecera: nombre del capítulo actual (eyebrow) en `Inter 600 9pt`, color `--color-primary-600`, línea inferior 0.5pt `--color-border`.
|
||||||
|
- Pie: "FactorPesca · Manual de usuario" a la izquierda, "Página X de Y" a la derecha.
|
||||||
|
- Salto de página:
|
||||||
|
- Cada `h1` de capítulo empieza en página nueva.
|
||||||
|
- `h2`, `h3`, `h4` no quedan huérfanos al final de página.
|
||||||
|
- `.step-block`, `.callout`, `.screenshot-frame`, `.code-block`, `figure`, `table` no se parten.
|
||||||
|
- Color: `-webkit-print-color-adjust: exact;` y `print-color-adjust: exact;` en todos los elementos coloreados.
|
||||||
|
- Imágenes: máximo 700px de ancho real (escala 2x retina queda a 1400px).
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# Estructura del documento
|
||||||
|
|
||||||
|
Anatomía obligatoria de cualquier manual generado por esta skill. Omite o añade secciones solo si el usuario lo pide explícitamente.
|
||||||
|
|
||||||
|
## Visión general
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Portada
|
||||||
|
2. Índice
|
||||||
|
3. Resumen ejecutivo
|
||||||
|
4. Requisitos previos
|
||||||
|
5. Capítulo 1
|
||||||
|
6. Capítulo 2
|
||||||
|
...
|
||||||
|
N. Solución de problemas
|
||||||
|
N+1. Glosario (opcional)
|
||||||
|
N+2. Aviso de propiedad y confidencialidad (obligatoria)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Portada
|
||||||
|
|
||||||
|
Elementos obligatorios:
|
||||||
|
|
||||||
|
- Logo de FactorPesca (placeholder hasta que se sustituya por el definitivo).
|
||||||
|
- Eyebrow en accent color (por defecto «Manual de usuario»).
|
||||||
|
- Título del manual: "Manual de [Aplicación]".
|
||||||
|
- Subtítulo descriptivo de una línea.
|
||||||
|
|
||||||
|
La portada es deliberadamente minimalista: solo logo, eyebrow, título y
|
||||||
|
subtítulo. Sin versión, fecha de emisión, autor ni metadatos.
|
||||||
|
|
||||||
|
Plantilla HTML: la primera página dentro de `templates/manual.html` con clase `cover`. Forzar `break-after: page` en `@media print`.
|
||||||
|
|
||||||
|
## 2. Índice
|
||||||
|
|
||||||
|
- Generado automáticamente por `manual.js` a partir de los `h1`, `h2`, `h3` del documento.
|
||||||
|
- Numeración con líderes de puntos (CSS `leader`).
|
||||||
|
- Mostrar solo `h1` y `h2` por defecto. Si un capítulo tiene muchas sub-secciones, incluir `h3` discretamente con sangrado.
|
||||||
|
- Numeración: capítulos en romanos (I, II, III), secciones en arábigos (1.1, 1.2).
|
||||||
|
|
||||||
|
## 3. Resumen ejecutivo
|
||||||
|
|
||||||
|
- Una página con tres bloques:
|
||||||
|
1. **Objetivo**: para qué sirve el manual y qué aprenderá el lector.
|
||||||
|
2. **Audiencia**: a quién va dirigido (operadores, supervisores, administradores, etc.).
|
||||||
|
3. **Alcance**: qué procesos cubre y qué procesos quedan fuera.
|
||||||
|
|
||||||
|
Si el manual es muy largo (>40 páginas), dividir el resumen en "objetivo" y "cómo leer este manual".
|
||||||
|
|
||||||
|
## 4. Requisitos previos
|
||||||
|
|
||||||
|
Lista de todo lo que el lector necesita antes de empezar:
|
||||||
|
|
||||||
|
- **Software**: navegador, sistema operativo, plugins.
|
||||||
|
- **Cuenta**: tipo de usuario y permisos necesarios.
|
||||||
|
- **Datos**: archivos, credenciales o recursos de entrada.
|
||||||
|
- **Conocimientos**: formación previa recomendada.
|
||||||
|
|
||||||
|
Estructura como `.checklist` o `.data-table` con dos columnas ("Requisito", "Detalle").
|
||||||
|
|
||||||
|
## 5. Capítulos (núcleo del manual)
|
||||||
|
|
||||||
|
Un capítulo por proceso de usuario. Estructura interna:
|
||||||
|
|
||||||
|
1. **Encabezado del capítulo** (`<h1 data-chapter>`)
|
||||||
|
2. **Objetivo del capítulo** (caja `.callout-info` con título "Objetivo").
|
||||||
|
3. **Pasos** (cada uno un `.step-block`).
|
||||||
|
4. **Resultado del proceso** (caja `.callout-tip` con título "Resultado final").
|
||||||
|
5. **Errores comunes** (caja `.callout-warning` con título "Errores comunes"), si aplica.
|
||||||
|
|
||||||
|
Reglas:
|
||||||
|
|
||||||
|
- Un capítulo describe un proceso completo. Si el proceso es muy largo, dividirlo en varios capítulos, pero numerarlos con un prefijo común (4.1, 4.2, 4.3).
|
||||||
|
- El primer paso de cada capítulo debe incluir una captura del estado inicial (pantalla previa).
|
||||||
|
- El último paso debe mostrar el estado final con un callout `tip` "Resultado final".
|
||||||
|
- Evitar capítulo con más de 12 pasos. Si los hay, dividir.
|
||||||
|
|
||||||
|
## 6. Solución de problemas
|
||||||
|
|
||||||
|
Apartado obligatorio. Estructura:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section id="troubleshooting">
|
||||||
|
<h1>Solución de problemas</h1>
|
||||||
|
<p>Si encuentra algún error, busque el síntoma en esta sección antes de contactar al soporte.</p>
|
||||||
|
|
||||||
|
<article class="trouble">
|
||||||
|
<h3 class="trouble-symptom">No puedo iniciar sesión</h3>
|
||||||
|
<p><strong>Causa probable:</strong> la cuenta está bloqueada tras tres intentos fallidos.</p>
|
||||||
|
<p><strong>Solución:</strong> espere 15 minutos o solicite el desbloqueo al administrador.</p>
|
||||||
|
</article>
|
||||||
|
...
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
Cada problema: síntoma, causa, solución. Como máximo cinco problemas por bloque; si hay más, agrupar por categoría con `<h2>`.
|
||||||
|
|
||||||
|
## 7. Glosario (opcional)
|
||||||
|
|
||||||
|
Tabla de dos columnas: término, definición. Incluir solo si el manual usa jerga técnica o nombres propios.
|
||||||
|
|
||||||
|
## 8. Aviso de propiedad y confidencialidad (obligatoria)
|
||||||
|
|
||||||
|
Última página del manual. Su propósito es reforzar visualmente que el
|
||||||
|
documento es **propiedad privada de FactorPesca S.A. de C.V.** y
|
||||||
|
establecer las restricciones de uso, reproducción y divulgación.
|
||||||
|
|
||||||
|
Elementos obligatorios:
|
||||||
|
|
||||||
|
- Marca `©` grande en la parte superior.
|
||||||
|
- Razón social completa: `FactorPesca S.A. de C.V.`
|
||||||
|
- Subtítulo "Todos los derechos reservados."
|
||||||
|
- Título centrado: "Aviso de propiedad y confidencialidad".
|
||||||
|
- Cinco párrafos numerados en lenguaje formal que cubran:
|
||||||
|
1. Propiedad intelectual sobre todos los elementos del documento.
|
||||||
|
2. Prohibición de reproducción, distribución y transformación sin
|
||||||
|
autorización escrita.
|
||||||
|
3. Carácter privado y confidencial de la información, y obligación
|
||||||
|
de devolución o destrucción al término de la relación.
|
||||||
|
4. Reconocimiento de marcas registradas de terceros.
|
||||||
|
5. Ausencia de garantía y limitación de responsabilidad.
|
||||||
|
- Caja de contacto (callout primario) con el correo del área legal:
|
||||||
|
`legal@factorpesca.com`.
|
||||||
|
- Pie pequeño con la atribución de la skill y la tipografía usada.
|
||||||
|
|
||||||
|
Sin cabecera ni pie de página (clase `no-chrome` y named page
|
||||||
|
`copyright`). El borde superior de 6pt en color primary marca la
|
||||||
|
transición desde el contenido del manual hacia el cierre legal.
|
||||||
|
|
||||||
|
## Numeración y jerarquía
|
||||||
|
|
||||||
|
- Capítulos: `I`, `II`, `III`...
|
||||||
|
- Secciones: `1.1`, `1.2`, `2.1`...
|
||||||
|
- Sub-secciones: `1.1.1`, `1.1.2`...
|
||||||
|
- Pasos: numeración global con círculo de accent (`1`, `2`, `3`...). No reiniciar en cada capítulo.
|
||||||
|
- Figuras: `Figura 1.`, `Figura 2.`... (auto-numeradas por `manual.js`).
|
||||||
|
- Tablas: `Tabla 1.`, `Tabla 2.`... (auto-numeradas por `manual.js`).
|
||||||
|
|
||||||
|
## Saltos de página
|
||||||
|
|
||||||
|
- `h1` siempre en página nueva (`break-before: page`).
|
||||||
|
- `h2` puede fluir, pero nunca quedar al final de la página (`break-after: avoid` en el heading + `break-before: avoid` en el primer párrafo).
|
||||||
|
- Ningún `.step-block` puede partirse a la mitad (`break-inside: avoid`).
|
||||||
|
- Una figura o tabla debe vivir entera en la misma página, salvo que sea muy alta: en ese caso, el caption se imprime en ambas páginas.
|
||||||
|
|
||||||
|
## Encabezados y pies
|
||||||
|
|
||||||
|
Configurados en `manual.css` con `@page` Paged Media. Cada página muestra:
|
||||||
|
|
||||||
|
- Cabecera: nombre del capítulo actual (eyebrow small caps).
|
||||||
|
- Pie izquierda: "FactorPesca · Manual de usuario de [Aplicación]".
|
||||||
|
- Pie derecha: "Página X de Y".
|
||||||
|
|
||||||
|
Las páginas marcadas con `.no-chrome` (portada, aviso de propiedad) ocultan cabecera y pie.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# scripts/build-manual.js
|
||||||
|
|
||||||
|
Genera el PDF del manual a partir de `templates/manual.html` usando Playwright (Chromium headless).
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Node.js 18 o superior.
|
||||||
|
- Playwright instalado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
npm install
|
||||||
|
npx playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
Si ya tienes la skill `playwright-skill` de lackeyjb, no necesitas instalar Chromium dos veces: la skill lo trae.
|
||||||
|
|
||||||
|
## Uso básico
|
||||||
|
|
||||||
|
Desde la raíz de la skill:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/build-manual.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto:
|
||||||
|
|
||||||
|
1. Lee `templates/manual.html`.
|
||||||
|
2. Carga `templates/manual.css` y `templates/manual.js`.
|
||||||
|
3. Copia `artifacts/` a un directorio temporal (si existe).
|
||||||
|
4. Renderiza el HTML con Chromium y exporta a `dist/manual-{aplicacion}-v{version}.pdf`.
|
||||||
|
|
||||||
|
## Opciones
|
||||||
|
|
||||||
|
```text
|
||||||
|
--html <ruta> HTML fuente (por defecto templates/manual.html).
|
||||||
|
--out <archivo> Ruta exacta del PDF de salida.
|
||||||
|
--out-dir <carpeta> Carpeta de salida (por defecto dist/).
|
||||||
|
--no-background No imprime colores de fondo.
|
||||||
|
--landscape Orientación horizontal.
|
||||||
|
--margin-{top,bottom,left,right} <valor>
|
||||||
|
Margen CSS cuando --no-css-page-size está activo.
|
||||||
|
--no-css-page-size Ignora @page y usa los márgenes dados.
|
||||||
|
--show-header-footer Habilita la cabecera/pie nativa de Chromium.
|
||||||
|
-h, --help Ayuda.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejemplos
|
||||||
|
|
||||||
|
Generar el PDF por defecto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/build-manual.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Generar otro manual (por ejemplo, el manual de administración):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/build-manual.js --html templates/admin.html --out dist/manual-admin-v1.0.0.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Sobrescribir el archivo de salida en una ruta absoluta:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/build-manual.js --out /tmp/borrador.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estructura esperada del proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
manuales-factorpesca/
|
||||||
|
├── SKILL.md
|
||||||
|
├── references/
|
||||||
|
├── templates/
|
||||||
|
│ ├── manual.html # fuente
|
||||||
|
│ ├── manual.css # estilos pantalla + impresión
|
||||||
|
│ └── manual.js # lógica de presentación
|
||||||
|
├── scripts/
|
||||||
|
│ ├── build-manual.js # este script
|
||||||
|
│ └── package.json
|
||||||
|
├── artifacts/ # capturas de pantalla (opcional)
|
||||||
|
│ ├── 01-login.png
|
||||||
|
│ └── 02-panel.png
|
||||||
|
└── dist/ # PDFs generados (se crea al ejecutar)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solución de problemas
|
||||||
|
|
||||||
|
- **"No se encuentra 'playwright'"**: ejecuta `npm install` en `scripts/`.
|
||||||
|
- **"Chromium no encontrado"**: ejecuta `npx playwright install chromium`.
|
||||||
|
- **El PDF sale sin color**: revisa `references/estilo.md` y asegúrate de que la regla `-webkit-print-color-adjust: exact` sigue presente en `manual.css`.
|
||||||
|
- **El índice está vacío**: el HTML no contiene elementos con `data-chapter` o los `h1` están fuera de `[data-chapters]`.
|
||||||
|
- **Las anotaciones aparecen en otra posición**: comprueba que `data-annotations` use porcentajes (`x`, `y` de 0 a 100) y que la imagen ocupe todo el ancho de `.screenshot-frame`.
|
||||||
|
- **La cabecera del PDF no muestra el capítulo**: Chromium necesita que el texto se asigne vía `string(chapter-title)` Paged Media. Si no se ve, asegúrate de que la versión de Chromium es reciente (Playwright 1.40+).
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* =========================================================
|
||||||
|
manuales-factorpesca / scripts/build-manual.js
|
||||||
|
Convierte templates/manual.html en un PDF Letter profesional.
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
node scripts/build-manual.js
|
||||||
|
node scripts/build-manual.js --html ruta/al/manual.html
|
||||||
|
node scripts/build-manual.js --out dist/manual-v1.0.0.pdf
|
||||||
|
node scripts/build-manual.js --slug capturas-app
|
||||||
|
|
||||||
|
Por defecto busca el HTML en templates/manual.html y escribe
|
||||||
|
el PDF en dist/manual.pdf.
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
|
||||||
|
const SKILL_DIR = path.resolve(__dirname, '..');
|
||||||
|
const DEFAULT_HTML = path.join(SKILL_DIR, 'templates', 'manual.html');
|
||||||
|
const DEFAULT_OUT_DIR = path.join(SKILL_DIR, 'dist');
|
||||||
|
const ARTIFACTS_DIR = path.join(SKILL_DIR, 'artifacts');
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const out = {
|
||||||
|
html: DEFAULT_HTML,
|
||||||
|
outDir: DEFAULT_OUT_DIR,
|
||||||
|
outFile: null,
|
||||||
|
printBackground: true,
|
||||||
|
landscape: false,
|
||||||
|
marginTop: '0',
|
||||||
|
marginBottom: '0',
|
||||||
|
marginLeft: '0',
|
||||||
|
marginRight: '0',
|
||||||
|
preferCSSPageSize: true,
|
||||||
|
displayHeaderFooter: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 2; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
switch (a) {
|
||||||
|
case '--html': out.html = path.resolve(argv[++i]); break;
|
||||||
|
case '--out': out.outFile = path.resolve(argv[++i]); break;
|
||||||
|
case '--out-dir': out.outDir = path.resolve(argv[++i]); break;
|
||||||
|
case '--no-background': out.printBackground = false; break;
|
||||||
|
case '--landscape': out.landscape = true; break;
|
||||||
|
case '--margin-top': out.marginTop = argv[++i]; break;
|
||||||
|
case '--margin-bottom': out.marginBottom = argv[++i]; break;
|
||||||
|
case '--margin-left': out.marginLeft = argv[++i]; break;
|
||||||
|
case '--margin-right': out.marginRight = argv[++i]; break;
|
||||||
|
case '--no-css-page-size': out.preferCSSPageSize = false; break;
|
||||||
|
case '--show-header-footer': out.displayHeaderFooter = true; break;
|
||||||
|
case '--help':
|
||||||
|
case '-h':
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`[build-manual] argumento desconocido ignorado: ${a}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Uso: node build-manual.js [opciones]
|
||||||
|
|
||||||
|
Opciones:
|
||||||
|
--html <ruta> Ruta al HTML fuente. Por defecto templates/manual.html
|
||||||
|
--out <archivo> Ruta completa del PDF de salida.
|
||||||
|
--out-dir <carpeta> Carpeta de salida. Por defecto dist/.
|
||||||
|
--no-background No imprimir colores de fondo.
|
||||||
|
--landscape Orientación horizontal.
|
||||||
|
--margin-top <valor> Margen superior CSS (p. ej. 22mm).
|
||||||
|
--margin-bottom <valor> Margen inferior CSS.
|
||||||
|
--margin-left <valor> Margen izquierdo CSS.
|
||||||
|
--margin-right <valor> Margen derecho CSS.
|
||||||
|
--no-css-page-size Ignorar @page del CSS y usar los márgenes dados.
|
||||||
|
--show-header-footer Habilita la cabecera/pie de Chromium (no usado).
|
||||||
|
-h, --help Muestra esta ayuda.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePlaywright() {
|
||||||
|
try {
|
||||||
|
return require('playwright');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[build-manual] No se encontró 'playwright' en el PATH.
|
||||||
|
Instálalo ejecutando:
|
||||||
|
npm install playwright
|
||||||
|
o usa la skill playwright-skill de lackeyjb para preparar el entorno:
|
||||||
|
npx skills add lackeyjb/playwright-skill --skill playwright-skill --yes
|
||||||
|
`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPdfUrl(htmlPath) {
|
||||||
|
const abs = path.resolve(htmlPath);
|
||||||
|
if (!fs.existsSync(abs)) {
|
||||||
|
console.error(`[build-manual] No se encuentra el HTML: ${abs}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return 'file://' + abs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyArtifactsIntoTemp(htmlPath) {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'manual-build-'));
|
||||||
|
const htmlDst = path.join(tmp, path.basename(htmlPath));
|
||||||
|
fs.copyFileSync(htmlPath, htmlDst);
|
||||||
|
const cssSrc = path.join(path.dirname(htmlPath), 'manual.css');
|
||||||
|
const jsSrc = path.join(path.dirname(htmlPath), 'manual.js');
|
||||||
|
if (fs.existsSync(cssSrc)) fs.copyFileSync(cssSrc, path.join(tmp, 'manual.css'));
|
||||||
|
if (fs.existsSync(jsSrc)) fs.copyFileSync(jsSrc, path.join(tmp, 'manual.js'));
|
||||||
|
if (fs.existsSync(ARTIFACTS_DIR)) {
|
||||||
|
copyDir(ARTIFACTS_DIR, path.join(tmp, 'artifacts'));
|
||||||
|
console.log(`[build-manual] Capturas copiadas a ${path.join(tmp, 'artifacts')}`);
|
||||||
|
}
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDir(src, dst) {
|
||||||
|
if (!fs.existsSync(src)) return;
|
||||||
|
fs.mkdirSync(dst, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||||
|
const s = path.join(src, entry.name);
|
||||||
|
const d = path.join(dst, entry.name);
|
||||||
|
if (entry.isDirectory()) copyDir(s, d);
|
||||||
|
else fs.copyFileSync(s, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultOutFile(htmlPath) {
|
||||||
|
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||||
|
const base = path.basename(htmlPath, path.extname(htmlPath));
|
||||||
|
const versionMatch = html.match(/data-meta-version[^>]*>([^<]+)/);
|
||||||
|
const version = (versionMatch ? versionMatch[1].trim() : '0.0.0').replace(/^v/i, '').replace(/[^a-zA-Z0-9._-]/g, '') || '0.0.0';
|
||||||
|
const slug = base.replace(/^manual[-_]?/, '').replace(/[^a-zA-Z0-9._-]/g, '') || 'app';
|
||||||
|
return `manual-${slug}-v${version}.pdf`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForFonts(page) {
|
||||||
|
try {
|
||||||
|
await page.evaluate(() => document.fonts && document.fonts.ready);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv);
|
||||||
|
const playwright = ensurePlaywright();
|
||||||
|
const { chromium } = playwright;
|
||||||
|
|
||||||
|
if (!fs.existsSync(args.html)) {
|
||||||
|
console.error(`[build-manual] HTML no encontrado: ${args.html}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(args.outDir, { recursive: true });
|
||||||
|
|
||||||
|
const outFile = args.outFile || path.join(args.outDir, defaultOutFile(args.html));
|
||||||
|
const assetsTmp = copyArtifactsIntoTemp(args.html);
|
||||||
|
const url = 'file://' + path.join(assetsTmp, path.basename(args.html));
|
||||||
|
|
||||||
|
console.log(`[build-manual] Renderizando: ${args.html}`);
|
||||||
|
console.log(`[build-manual] Salida: ${outFile}`);
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1100, height: 1400 },
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
page.on('pageerror', (e) => console.error('[page error]', e));
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'error') console.error('[page console]', msg.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 60_000 });
|
||||||
|
await waitForFonts(page);
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
|
||||||
|
const pdfOptions = {
|
||||||
|
path: outFile,
|
||||||
|
printBackground: args.printBackground,
|
||||||
|
preferCSSPageSize: args.preferCSSPageSize,
|
||||||
|
displayHeaderFooter: args.displayHeaderFooter,
|
||||||
|
};
|
||||||
|
if (!args.preferCSSPageSize) {
|
||||||
|
pdfOptions.format = args.landscape ? { width: '11in', height: '8.5in' } : { width: '8.5in', height: '11in' };
|
||||||
|
pdfOptions.margin = {
|
||||||
|
top: args.marginTop,
|
||||||
|
bottom: args.marginBottom,
|
||||||
|
left: args.marginLeft,
|
||||||
|
right: args.marginRight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.pdf(pdfOptions);
|
||||||
|
const stat = fs.statSync(outFile);
|
||||||
|
console.log(`[build-manual] PDF generado: ${outFile} (${(stat.size / 1024).toFixed(1)} KB)`);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
if (assetsTmp) {
|
||||||
|
try { fs.rmSync(assetsTmp, { recursive: true, force: true }); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('[build-manual] Error inesperado:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Generated
+46
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "manuales-factorpesca-build",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "manuales-factorpesca-build",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.62.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.64.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz",
|
||||||
|
"integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.64.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.64.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz",
|
||||||
|
"integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "manuales-factorpesca-build",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Generador de PDF para manuales FactorPesca. Usa Playwright (Chromium) para renderizar templates/manual.html con su CSS de impresión.",
|
||||||
|
"main": "build-manual.js",
|
||||||
|
"scripts": {
|
||||||
|
"setup": "npm install",
|
||||||
|
"build": "node build-manual.js",
|
||||||
|
"build:html": "node build-manual.js --html",
|
||||||
|
"build:out": "node build-manual.js --out"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.62.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"factorpesca",
|
||||||
|
"manual",
|
||||||
|
"pdf",
|
||||||
|
"playwright",
|
||||||
|
"documentation"
|
||||||
|
],
|
||||||
|
"license": "UNLICENSED"
|
||||||
|
}
|
||||||
@@ -0,0 +1,876 @@
|
|||||||
|
/* =========================================================
|
||||||
|
manuales-factorpesca / manual.css
|
||||||
|
Hoja de estilos principal para todos los manuales PDF.
|
||||||
|
Cubre pantalla (vista previa) e impresión (PDF final).
|
||||||
|
Tamaño: Letter 8.5x11".
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Paleta de marca */
|
||||||
|
--color-primary: #0B2545;
|
||||||
|
--color-primary-600: #13315C;
|
||||||
|
--color-primary-100: #E6ECF2;
|
||||||
|
--color-accent: #F4A300;
|
||||||
|
--color-accent-600: #D88C00;
|
||||||
|
|
||||||
|
/* Tipografía */
|
||||||
|
--font-sans: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, monospace;
|
||||||
|
|
||||||
|
/* Texto y superficies */
|
||||||
|
--text-primary: #1A1A1A;
|
||||||
|
--text-secondary: #404040;
|
||||||
|
--text-muted: #6B7280;
|
||||||
|
--surface: #FFFFFF;
|
||||||
|
--surface-alt: #F3F4F6;
|
||||||
|
--border: #E5E7EB;
|
||||||
|
|
||||||
|
/* Semánticos */
|
||||||
|
--success: #047857;
|
||||||
|
--warning: #B45309;
|
||||||
|
--danger: #B91C1C;
|
||||||
|
--info: #1D4ED8;
|
||||||
|
|
||||||
|
/* Espaciado (8pt baseline) */
|
||||||
|
--space-1: 4pt;
|
||||||
|
--space-2: 8pt;
|
||||||
|
--space-3: 12pt;
|
||||||
|
--space-4: 16pt;
|
||||||
|
--space-5: 24pt;
|
||||||
|
--space-6: 32pt;
|
||||||
|
--space-7: 48pt;
|
||||||
|
--space-8: 64pt;
|
||||||
|
|
||||||
|
/* Escala tipográfica (en puntos) */
|
||||||
|
--fs-cover-title: 36pt;
|
||||||
|
--fs-cover-subtitle: 16pt;
|
||||||
|
--fs-h1: 28pt;
|
||||||
|
--fs-h2: 20pt;
|
||||||
|
--fs-h3: 15pt;
|
||||||
|
--fs-h4: 12pt;
|
||||||
|
--fs-body: 11pt;
|
||||||
|
--fs-emphasis: 11pt;
|
||||||
|
--fs-caption: 9pt;
|
||||||
|
--fs-eyebrow: 9pt;
|
||||||
|
--fs-code: 9.5pt;
|
||||||
|
--fs-step-num: 13pt;
|
||||||
|
--fs-badge: 8pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--fs-body);
|
||||||
|
line-height: 1.55;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
p { margin: 0 0 var(--space-3); }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--info);
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--fs-code);
|
||||||
|
background: var(--surface-alt);
|
||||||
|
padding: 1pt 4pt;
|
||||||
|
border-radius: 2pt;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
strong, b { font-weight: 600; }
|
||||||
|
|
||||||
|
img { max-width: 100%; height: auto; display: block; }
|
||||||
|
|
||||||
|
ul, ol { margin: 0 0 var(--space-3); padding-left: 22pt; }
|
||||||
|
li { margin-bottom: var(--space-1); }
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
PORTADA
|
||||||
|
========================================================= */
|
||||||
|
.cover {
|
||||||
|
width: 8.5in;
|
||||||
|
height: 11in;
|
||||||
|
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-600) 100%);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
page-break-after: always;
|
||||||
|
break-after: page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-inner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.85in 0.85in 0.7in 0.85in;
|
||||||
|
gap: 2.2in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-logo {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14pt;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: #fff;
|
||||||
|
border: 1.5pt solid #fff;
|
||||||
|
padding: 8pt 12pt;
|
||||||
|
border-radius: 2pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title-block { margin-top: 0; }
|
||||||
|
|
||||||
|
.cover-eyebrow {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-accent);
|
||||||
|
margin: 0 0 var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title {
|
||||||
|
font-size: var(--fs-cover-title);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin: 0 0 var(--space-4);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-subtitle {
|
||||||
|
font-size: var(--fs-cover-subtitle);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
max-width: 5.5in;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta {
|
||||||
|
border-top: 1pt solid rgba(255, 255, 255, 0.25);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.2in 1fr;
|
||||||
|
gap: var(--space-2) var(--space-4);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta dt {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta dd {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
ÍNDICE
|
||||||
|
========================================================= */
|
||||||
|
.toc-page {
|
||||||
|
page-break-before: always;
|
||||||
|
break-before: page;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-title {
|
||||||
|
font-size: var(--fs-h1);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 0 0 var(--space-5);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list li {
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: 0.5pt dotted var(--border);
|
||||||
|
padding: 6pt 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list .toc-text {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list .toc-text::after {
|
||||||
|
content: leader('.') ' ';
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list .toc-dots {
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 0.5pt dotted var(--text-muted);
|
||||||
|
transform: translateY(-3pt);
|
||||||
|
margin: 0 6pt;
|
||||||
|
min-width: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list .toc-page-num {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list .toc-h2 { padding-left: 0; }
|
||||||
|
.toc-list .toc-h3 { padding-left: 22pt; font-size: 10pt; color: var(--text-secondary); }
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
ENCABEZADOS
|
||||||
|
========================================================= */
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: var(--color-primary);
|
||||||
|
break-after: avoid;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: var(--fs-h1);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.18;
|
||||||
|
margin: 0 0 var(--space-4);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
counter-reset: chapter-section;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--fs-h2);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin: var(--space-5) 0 var(--space-3);
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--fs-h3);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: var(--space-4) 0 var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: var(--fs-h4);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: var(--space-3) 0 var(--space-2);
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Numeración de capítulos y secciones (CSS counters) */
|
||||||
|
[data-chapters] {
|
||||||
|
counter-reset: chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-chapter] {
|
||||||
|
counter-reset: section;
|
||||||
|
page-break-before: always;
|
||||||
|
break-before: page;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-chapter] > h1 {
|
||||||
|
counter-increment: chapter;
|
||||||
|
counter-reset: section;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-chapter] > h1::before {
|
||||||
|
content: "Capítulo " counter(chapter, upper-roman) " · ";
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-chapter] h2 {
|
||||||
|
counter-increment: section;
|
||||||
|
counter-reset: subsection;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-chapter] h2::before {
|
||||||
|
content: counter(chapter) "." counter(section) " ";
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Los h2 dentro de un step-block llevan el prefijo del step circle,
|
||||||
|
no el contador de sección editorial. */
|
||||||
|
[data-chapter] h2.step-title::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-chapter] h3 {
|
||||||
|
counter-increment: subsection;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-chapter] h3::before {
|
||||||
|
content: counter(chapter) "." counter(section) "." counter(subsection) " ";
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12pt;
|
||||||
|
margin-right: 4pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-page,
|
||||||
|
.prereq-page,
|
||||||
|
.troubleshooting-page,
|
||||||
|
.glossary-page {
|
||||||
|
page-break-before: always;
|
||||||
|
break-before: page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
CALLOUTS
|
||||||
|
========================================================= */
|
||||||
|
.callout {
|
||||||
|
background: var(--color-primary-100);
|
||||||
|
border-left: 4pt solid var(--info);
|
||||||
|
border-radius: 2pt;
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
margin: var(--space-4) 0;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11pt;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.callout-info { border-left-color: var(--info); background: #EAF1FB; }
|
||||||
|
.callout-info .callout-title::before { content: "ⓘ"; color: var(--info); }
|
||||||
|
|
||||||
|
.callout-warning { border-left-color: var(--warning); background: #FBF1E1; }
|
||||||
|
.callout-warning .callout-title::before { content: "⚠"; color: var(--warning); }
|
||||||
|
|
||||||
|
.callout-danger { border-left-color: var(--danger); background: #FBE7E7; }
|
||||||
|
.callout-danger .callout-title::before { content: "⛔"; color: var(--danger); }
|
||||||
|
|
||||||
|
.callout-tip { border-left-color: var(--success); background: #E5F4EE; }
|
||||||
|
.callout-tip .callout-title::before { content: "✓"; color: var(--success); }
|
||||||
|
|
||||||
|
.callout-note { border-left-color: var(--text-muted); background: var(--surface-alt); }
|
||||||
|
.callout-note .callout-title::before { content: "▸"; color: var(--text-muted); }
|
||||||
|
|
||||||
|
.callout-result,
|
||||||
|
.callout-chapter-end {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
PASO NUMERADO
|
||||||
|
========================================================= */
|
||||||
|
.step-block {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 36pt 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin: var(--space-5) 0;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
counter-increment: step;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 32pt;
|
||||||
|
height: 32pt;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--fs-step-num);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2pt;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-block .step-number::before {
|
||||||
|
content: counter(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content { min-width: 0; }
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-size: var(--fs-h3);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
counter-increment: none;
|
||||||
|
counter-reset: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { counter-reset: step; }
|
||||||
|
|
||||||
|
[data-step] {
|
||||||
|
counter-increment: step;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cuando se quiere forzar un número (no automático), usar .step-number[data-static] */
|
||||||
|
.step-number[data-static] {
|
||||||
|
/* No aplicar counter, mostrar texto directo */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
CAPTURAS CON ANOTACIONES
|
||||||
|
========================================================= */
|
||||||
|
.screenshot-frame {
|
||||||
|
margin: var(--space-3) 0;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-frame img {
|
||||||
|
border: 1pt solid var(--border);
|
||||||
|
border-radius: 3pt;
|
||||||
|
box-shadow: 0 1pt 4pt rgba(11, 37, 69, 0.08);
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-frame {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotations {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
inset: 1pt;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation {
|
||||||
|
position: absolute;
|
||||||
|
width: 22pt;
|
||||||
|
height: 22pt;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
border: 2pt solid var(--color-primary);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 10pt;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
box-shadow: 0 1pt 3pt rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-frame figcaption {
|
||||||
|
font-size: var(--fs-caption);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
text-align: center;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
counter-reset: figure;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure.screenshot-frame {
|
||||||
|
counter-increment: figure;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-frame figcaption::before {
|
||||||
|
content: "Figura " counter(figure) ". ";
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
BLOQUE DE CÓDIGO
|
||||||
|
========================================================= */
|
||||||
|
pre.code-block {
|
||||||
|
background: #1E1E2E;
|
||||||
|
color: #F8F8F2;
|
||||||
|
border-radius: 4pt;
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--fs-code);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: var(--space-3) 0;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-block code {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
TABLAS
|
||||||
|
========================================================= */
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: var(--space-3) 0;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th,
|
||||||
|
.data-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8pt 10pt;
|
||||||
|
border-bottom: 0.5pt solid var(--border);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead th {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 10pt;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:nth-child(even) td {
|
||||||
|
background: var(--surface-alt);
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
BADGE
|
||||||
|
========================================================= */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2pt 8pt;
|
||||||
|
border-radius: 12pt;
|
||||||
|
font-size: var(--fs-badge);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.4;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-version {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
SOLUCIÓN DE PROBLEMAS
|
||||||
|
========================================================= */
|
||||||
|
.trouble {
|
||||||
|
border: 0.5pt solid var(--border);
|
||||||
|
border-left: 4pt solid var(--warning);
|
||||||
|
border-radius: 2pt;
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
margin: var(--space-3) 0;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trouble-symptom {
|
||||||
|
font-size: 12pt;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trouble p { margin: var(--space-1) 0; font-size: 10.5pt; }
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
GLOSARIO
|
||||||
|
========================================================= */
|
||||||
|
.glossary-page .data-table td:first-child {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
AVISO DE PROPIEDAD Y CONFIDENCIALIDAD
|
||||||
|
Última página del manual. Diseño formal, sin cabecera ni pie.
|
||||||
|
========================================================= */
|
||||||
|
.copyright-page {
|
||||||
|
page-break-before: always;
|
||||||
|
break-before: page;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--surface);
|
||||||
|
border-top: 6pt solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-inner {
|
||||||
|
max-width: 6in;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.4in 0.85in 0.5in 0.85in;
|
||||||
|
text-align: justify;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-mark {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 14pt;
|
||||||
|
line-height: 0.9;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 4pt;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-company {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 var(--space-4);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-company strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-title {
|
||||||
|
font-size: 10.5pt;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 var(--space-3);
|
||||||
|
padding-bottom: 6pt;
|
||||||
|
border-bottom: 0.5pt solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-inner p {
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-inner strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-contact {
|
||||||
|
margin: var(--space-2) 0 var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: var(--color-primary-100);
|
||||||
|
border-left: 4pt solid var(--color-primary);
|
||||||
|
border-radius: 2pt;
|
||||||
|
text-align: left;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-contact-label {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 4pt !important;
|
||||||
|
line-height: 1.35 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-contact-line {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 !important;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-contact-line code {
|
||||||
|
font-size: 10.5pt;
|
||||||
|
font-weight: 500;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
ESTILOS SOLO PANTALLA
|
||||||
|
========================================================= */
|
||||||
|
@media screen {
|
||||||
|
body {
|
||||||
|
background: #E5E7EB;
|
||||||
|
padding: 24pt;
|
||||||
|
}
|
||||||
|
body > section,
|
||||||
|
body > main {
|
||||||
|
background: #fff;
|
||||||
|
max-width: 8.5in;
|
||||||
|
margin: 0 auto var(--space-5);
|
||||||
|
padding: 0.85in;
|
||||||
|
box-shadow: 0 4pt 24pt rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.cover {
|
||||||
|
box-shadow: 0 4pt 24pt rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.summary-page,
|
||||||
|
.prereq-page,
|
||||||
|
.troubleshooting-page,
|
||||||
|
.glossary-page,
|
||||||
|
.copyright-page,
|
||||||
|
.toc-page { padding: 0.85in; }
|
||||||
|
[data-chapter] { padding: 0.85in; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
ESTILOS DE IMPRESIÓN
|
||||||
|
========================================================= */
|
||||||
|
@media print {
|
||||||
|
html, body {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
body > section,
|
||||||
|
body > main {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
/* Quitar sombras y bordes de pantalla en impresión */
|
||||||
|
.screenshot-frame img {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
/* Asegurar colores en impresión */
|
||||||
|
* {
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
}
|
||||||
|
/* Anular pantalla de "vista previa" */
|
||||||
|
.summary-page,
|
||||||
|
.prereq-page,
|
||||||
|
.troubleshooting-page,
|
||||||
|
.glossary-page,
|
||||||
|
.copyright-page,
|
||||||
|
.toc-page,
|
||||||
|
[data-chapter] { padding: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
REGLAS @page Paged Media
|
||||||
|
Chromium soporta margin boxes, que es lo que usaremos
|
||||||
|
para inyectar cabecera y pie con counter(page).
|
||||||
|
========================================================= */
|
||||||
|
@page {
|
||||||
|
size: 8.5in 11in;
|
||||||
|
margin: 22mm 22mm 22mm 22mm;
|
||||||
|
|
||||||
|
@top-left {
|
||||||
|
content: string(chapter-title);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
vertical-align: bottom;
|
||||||
|
padding-bottom: 4pt;
|
||||||
|
border-bottom: 0.5pt solid var(--border);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bottom-left {
|
||||||
|
content: "FactorPesca · Manual de usuario";
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--text-muted);
|
||||||
|
vertical-align: top;
|
||||||
|
padding-top: 6pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bottom-right {
|
||||||
|
content: "Página " counter(page) " de " counter(pages);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: top;
|
||||||
|
padding-top: 6pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@page :first {
|
||||||
|
margin: 0;
|
||||||
|
@top-left { content: none; border: none; }
|
||||||
|
@bottom-left { content: none; }
|
||||||
|
@bottom-right { content: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@page cover {
|
||||||
|
margin: 0;
|
||||||
|
@top-left { content: none; }
|
||||||
|
@bottom-left { content: none; }
|
||||||
|
@bottom-right { content: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@page copyright {
|
||||||
|
margin: 22mm 22mm 22mm 22mm;
|
||||||
|
@top-left { content: none; border: none; }
|
||||||
|
@bottom-left { content: none; }
|
||||||
|
@bottom-right { content: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover { page: cover; }
|
||||||
|
.copyright-page { page: copyright; }
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title data-meta-title>Manual de usuario de la aplicación</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="manual.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- =========================================================
|
||||||
|
PORTADA
|
||||||
|
La portada tiene clase "cover" y se imprime sin márgenes.
|
||||||
|
Sustituye los placeholders antes de generar el PDF.
|
||||||
|
========================================================= -->
|
||||||
|
<section class="cover no-chrome" data-cover>
|
||||||
|
<div class="cover-inner">
|
||||||
|
<div class="cover-brand">
|
||||||
|
<div class="cover-logo" aria-label="Logo de Factor Pesca">Factor Pesca</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover-title-block">
|
||||||
|
<p class="cover-eyebrow" data-meta-eyebrow>Manual de usuario</p>
|
||||||
|
<h1 class="cover-title" data-meta-title-display>Manual de la aplicación</h1>
|
||||||
|
<p class="cover-subtitle" data-meta-subtitle>Guía de procedimientos paso a paso para operadores.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
ÍNDICE
|
||||||
|
Generado automáticamente por manual.js.
|
||||||
|
========================================================= -->
|
||||||
|
<section class="toc-page" id="toc">
|
||||||
|
<h1 class="toc-title">Índice</h1>
|
||||||
|
<ol class="toc-list" data-toc-output></ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
RESUMEN EJECUTIVO
|
||||||
|
========================================================= -->
|
||||||
|
<section class="summary-page">
|
||||||
|
<h1>Resumen ejecutivo</h1>
|
||||||
|
|
||||||
|
<article class="summary-block">
|
||||||
|
<h2>Objetivo</h2>
|
||||||
|
<p data-summary-objective>
|
||||||
|
Este manual describe los procedimientos operativos de la aplicación,
|
||||||
|
paso a paso, con capturas reales y anotaciones para que cualquier
|
||||||
|
operador pueda completar las tareas más frecuentes sin ambigüedad.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="summary-block">
|
||||||
|
<h2>Audiencia</h2>
|
||||||
|
<p data-summary-audience>
|
||||||
|
Operadores de captura, supervisores de turno y administradores del sistema.
|
||||||
|
No se requieren conocimientos técnicos previos; sí se recomienda
|
||||||
|
familiaridad con la gestión de datos pesqueros.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="summary-block">
|
||||||
|
<h2>Alcance</h2>
|
||||||
|
<p data-summary-scope>
|
||||||
|
Cubre los procesos de alta de capturas, consulta de histórico y
|
||||||
|
exportación de reportes. No cubre la configuración del servidor ni
|
||||||
|
los procedimientos de copia de seguridad.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
REQUISITOS PREVIOS
|
||||||
|
========================================================= -->
|
||||||
|
<section class="prereq-page">
|
||||||
|
<h1>Requisitos previos</h1>
|
||||||
|
<p>Asegúrese de cumplir con los siguientes requisitos antes de iniciar.</p>
|
||||||
|
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Requisito</th>
|
||||||
|
<th>Detalle</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody data-prereq-body>
|
||||||
|
<tr>
|
||||||
|
<td>Navegador</td>
|
||||||
|
<td>Chromium 120+, Firefox 121+ o Safari 17+.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Resolución</td>
|
||||||
|
<td>Mínimo 1280×800 px; recomendado 1440×900 px.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Cuenta</td>
|
||||||
|
<td>Usuario activo con rol de Operador o superior.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Conectividad</td>
|
||||||
|
<td>Acceso a la red corporativa o VPN configurada.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
CAPÍTULOS
|
||||||
|
Cada capítulo es una <section data-chapter> con su h1.
|
||||||
|
El número se asigna automáticamente por manual.js.
|
||||||
|
========================================================= -->
|
||||||
|
<main data-chapters>
|
||||||
|
<!-- Plantilla de capítulo: duplicar este bloque por cada proceso -->
|
||||||
|
<section data-chapter>
|
||||||
|
<h1 data-chapter-title>Nombre del proceso</h1>
|
||||||
|
|
||||||
|
<aside class="callout callout-info">
|
||||||
|
<div class="callout-title">Objetivo del capítulo</div>
|
||||||
|
<p data-chapter-objective>
|
||||||
|
En este capítulo aprenderá a realizar la tarea X desde la pantalla
|
||||||
|
inicial hasta la confirmación final.
|
||||||
|
</p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<article class="step-block" data-step>
|
||||||
|
<div class="step-number" aria-hidden="true"></div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h2 class="step-title" data-step-title>Abrir la pantalla inicial</h2>
|
||||||
|
<p data-step-text>
|
||||||
|
Vaya al menú principal y seleccione la opción «X». Aparecerá la
|
||||||
|
pantalla de inicio con el listado vacío.
|
||||||
|
</p>
|
||||||
|
<figure class="screenshot-frame">
|
||||||
|
<img src="artifacts/01-inicio.png" alt="Pantalla inicial del proceso" />
|
||||||
|
<ol class="annotations" data-annotations='[{"x":50,"y":24,"n":1,"label":"Menú principal"}]'></ol>
|
||||||
|
<figcaption>Pantalla inicial con el menú principal.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<aside class="callout callout-info callout-result">
|
||||||
|
<div class="callout-title">Resultado esperado</div>
|
||||||
|
<p>Verá la pantalla con el encabezado en color primario y el listado vacío debajo.</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="step-block" data-step>
|
||||||
|
<div class="step-number" aria-hidden="true"></div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h2 class="step-title" data-step-title>Completar el formulario</h2>
|
||||||
|
<p data-step-text>
|
||||||
|
Rellene los campos obligatorios marcados con asterisco.
|
||||||
|
El botón «Guardar» permanecerá deshabilitado hasta que todos los
|
||||||
|
campos sean válidos.
|
||||||
|
</p>
|
||||||
|
<figure class="screenshot-frame">
|
||||||
|
<img src="artifacts/02-formulario.png" alt="Formulario de alta" />
|
||||||
|
<ol class="annotations" data-annotations='[{"x":24,"y":18,"n":1,"label":"Campo nombre"},{"x":24,"y":46,"n":2,"label":"Campo fecha"}]'></ol>
|
||||||
|
<figcaption>Formulario con los campos obligatorios.</figcaption>
|
||||||
|
</figure>
|
||||||
|
<aside class="callout callout-warning">
|
||||||
|
<div class="callout-title">Advertencia</div>
|
||||||
|
<p>Si introduce una fecha futura, el sistema rechazará el guardado.</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<aside class="callout callout-tip callout-chapter-end">
|
||||||
|
<div class="callout-title">Resultado final</div>
|
||||||
|
<p>El registro aparecerá al principio del listado y se enviará una notificación al supervisor.</p>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
<!-- /Plantilla de capítulo -->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
SOLUCIÓN DE PROBLEMAS
|
||||||
|
========================================================= -->
|
||||||
|
<section class="troubleshooting-page" id="troubleshooting">
|
||||||
|
<h1>Solución de problemas</h1>
|
||||||
|
<p>
|
||||||
|
Si encuentra algún error durante los procedimientos descritos,
|
||||||
|
busque el síntoma en esta sección antes de contactar al soporte.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<article class="trouble">
|
||||||
|
<h3 class="trouble-symptom">El botón «Guardar» permanece deshabilitado</h3>
|
||||||
|
<p><strong>Causa probable:</strong> uno o más campos obligatorios están vacíos o tienen un formato no válido.</p>
|
||||||
|
<p><strong>Solución:</strong> revise los campos marcados en rojo y verifique que las fechas tengan el formato <code>DD/MM/AAAA</code>.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="trouble">
|
||||||
|
<h3 class="trouble-symptom">La aplicación muestra un error 500 al guardar</h3>
|
||||||
|
<p><strong>Causa probable:</strong> pérdida temporal de conectividad con el servicio de persistencia.</p>
|
||||||
|
<p><strong>Solución:</strong> espere treinta segundos y vuelva a intentarlo. Si el error persiste, contacte con soporte adjuntando la hora exacta del incidente.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="trouble">
|
||||||
|
<h3 class="trouble-symptom">No recibo notificaciones por correo</h3>
|
||||||
|
<p><strong>Causa probable:</strong> la dirección de notificación del usuario no está validada.</p>
|
||||||
|
<p><strong>Solución:</strong> acceda a «Mi perfil» y verifique la dirección de correo. Recibirá un mensaje de confirmación en menos de un minuto.</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
GLOSARIO (opcional, eliminar si no aplica)
|
||||||
|
========================================================= -->
|
||||||
|
<section class="glossary-page">
|
||||||
|
<h1>Glosario</h1>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Término</th><th>Definición</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Captura</td><td>Registro individual de pesca asociado a un lote y una fecha.</td></tr>
|
||||||
|
<tr><td>Lote</td><td>Conjunto de capturas agrupadas bajo un mismo identificador.</td></tr>
|
||||||
|
<tr><td>Operador</td><td>Usuario con permisos de lectura y escritura sobre capturas propias.</td></tr>
|
||||||
|
<tr><td>Supervisor</td><td>Usuario con permisos de lectura y escritura sobre todas las capturas del área.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- =========================================================
|
||||||
|
AVISO DE PROPIEDAD Y CONFIDENCIALIDAD
|
||||||
|
Última página del manual. Sin cabecera ni pie.
|
||||||
|
Refuerza que el documento es propiedad privada de la empresa.
|
||||||
|
========================================================= -->
|
||||||
|
<section class="copyright-page no-chrome" id="copyright">
|
||||||
|
<div class="copyright-inner">
|
||||||
|
<div class="copyright-mark" aria-hidden="true">©</div>
|
||||||
|
<p class="copyright-company">
|
||||||
|
<strong>Factor Pesca S.A. de C.V.</strong><br />
|
||||||
|
Todos los derechos reservados.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 class="copyright-title">Aviso de propiedad y confidencialidad</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Este documento y la totalidad de su contenido —textos, gráficos,
|
||||||
|
logotipos, iconos, imágenes, capturas de pantalla, marcas,
|
||||||
|
diseño de composición y selección de los materiales— son
|
||||||
|
<strong>propiedad exclusiva de Factor Pesca S.A. de C.V.</strong>
|
||||||
|
y están protegidos por las leyes nacionales e internacionales
|
||||||
|
en materia de propiedad intelectual, derechos de autor y
|
||||||
|
secretos industriales.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Queda <strong>estrictamente prohibida</strong> la reproducción
|
||||||
|
total o parcial, distribución, comunicación pública,
|
||||||
|
transformación, traducción, adaptación, compilación,
|
||||||
|
desensamblado o cualquier otra forma de explotación de este
|
||||||
|
documento, por cualquier medio y para cualquier finalidad, sin
|
||||||
|
el consentimiento previo, expreso y por escrito de Factor Pesca
|
||||||
|
S.A. de C.V. El uso no autorizado será perseguido y
|
||||||
|
sancionado conforme a la legislación civil, penal y mercantil
|
||||||
|
aplicable.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="manual.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/* =========================================================
|
||||||
|
manuales-factorpesca / manual.js
|
||||||
|
Lógica de presentación del manual:
|
||||||
|
- Genera el índice automático a partir de h1, h2, h3.
|
||||||
|
- Dibuja anotaciones numeradas sobre las capturas.
|
||||||
|
- Captura el nombre del capítulo actual para la cabecera.
|
||||||
|
- Numera figuras y pasos (complementa a los CSS counters).
|
||||||
|
- Prepara el contador de página tras la carga de fuentes.
|
||||||
|
|
||||||
|
Este script NO añade interactividad: solo prepara el DOM
|
||||||
|
para que Chromium imprima un PDF bien paginado.
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (document.fonts && document.fonts.ready) {
|
||||||
|
document.fonts.ready.then(afterFonts);
|
||||||
|
} else {
|
||||||
|
afterFonts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterFonts() {
|
||||||
|
try {
|
||||||
|
buildTOC();
|
||||||
|
renderAnnotations();
|
||||||
|
setupChapterString();
|
||||||
|
ensureAnchors();
|
||||||
|
ensureImagePaths();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[manual.js] Error preparando el manual:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
1. Índice automático
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
function buildTOC() {
|
||||||
|
const out = document.querySelector('[data-toc-output]');
|
||||||
|
if (!out) return;
|
||||||
|
|
||||||
|
const chapters = document.querySelectorAll('[data-chapter]');
|
||||||
|
if (!chapters.length) {
|
||||||
|
out.innerHTML = '<li class="toc-empty">Sin capítulos definidos.</li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
let chapterIndex = 0;
|
||||||
|
|
||||||
|
chapters.forEach((chapter) => {
|
||||||
|
chapterIndex += 1;
|
||||||
|
const title = chapter.querySelector('[data-chapter-title]') || chapter.querySelector('h1');
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'toc-h1';
|
||||||
|
li.appendChild(tocRow(romanize(chapterIndex) + '. ' + title.textContent.trim()));
|
||||||
|
frag.appendChild(li);
|
||||||
|
|
||||||
|
const subsections = chapter.querySelectorAll('h2');
|
||||||
|
subsections.forEach((sub) => {
|
||||||
|
const row = document.createElement('li');
|
||||||
|
row.className = 'toc-h2';
|
||||||
|
row.appendChild(tocRow(' ' + (sub.dataset.tocLabel || sub.textContent.trim())));
|
||||||
|
frag.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
const subsub = chapter.querySelectorAll('h3');
|
||||||
|
subsub.forEach((sub) => {
|
||||||
|
const row = document.createElement('li');
|
||||||
|
row.className = 'toc-h3';
|
||||||
|
row.appendChild(tocRow(' ' + (sub.dataset.tocLabel || sub.textContent.trim())));
|
||||||
|
frag.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
out.innerHTML = '';
|
||||||
|
out.appendChild(frag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tocRow(label) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'toc-row';
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.className = 'toc-text';
|
||||||
|
text.textContent = label;
|
||||||
|
const dots = document.createElement('span');
|
||||||
|
dots.className = 'toc-dots';
|
||||||
|
const num = document.createElement('span');
|
||||||
|
num.className = 'toc-page-num';
|
||||||
|
num.textContent = '—';
|
||||||
|
span.appendChild(text);
|
||||||
|
span.appendChild(dots);
|
||||||
|
span.appendChild(num);
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function romanize(num) {
|
||||||
|
const map = [
|
||||||
|
['M', 1000], ['CM', 900], ['D', 500], ['CD', 400],
|
||||||
|
['C', 100], ['XC', 90], ['L', 50], ['XL', 40],
|
||||||
|
['X', 10], ['IX', 9], ['V', 5], ['IV', 4], ['I', 1],
|
||||||
|
];
|
||||||
|
let result = '';
|
||||||
|
let n = num;
|
||||||
|
for (const [letter, value] of map) {
|
||||||
|
while (n >= value) {
|
||||||
|
result += letter;
|
||||||
|
n -= value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
2. Anotaciones sobre capturas
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
function renderAnnotations() {
|
||||||
|
document.querySelectorAll('.annotations[data-annotations]').forEach((list) => {
|
||||||
|
let data = [];
|
||||||
|
try {
|
||||||
|
data = JSON.parse(list.getAttribute('data-annotations') || '[]');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[manual.js] data-annotations inválido:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
|
list.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sorted = data.slice().sort((a, b) => (a.n || 0) - (b.n || 0));
|
||||||
|
sorted.forEach((entry) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'annotation';
|
||||||
|
li.textContent = String(entry.n);
|
||||||
|
if (typeof entry.x === 'number' && typeof entry.y === 'number') {
|
||||||
|
li.style.left = entry.x + '%';
|
||||||
|
li.style.top = entry.y + '%';
|
||||||
|
}
|
||||||
|
if (entry.label) {
|
||||||
|
li.setAttribute('aria-label', entry.label);
|
||||||
|
li.setAttribute('title', entry.label);
|
||||||
|
}
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
3. Cabecera dinámica con el nombre del capítulo actual
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
function setupChapterString() {
|
||||||
|
const chapters = document.querySelectorAll('[data-chapter]');
|
||||||
|
chapters.forEach((chapter) => {
|
||||||
|
const title = chapter.querySelector('[data-chapter-title]') || chapter.querySelector('h1');
|
||||||
|
if (!title) return;
|
||||||
|
title.setAttribute('data-string', title.textContent.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
4. Anclas automáticas para headings
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
function ensureAnchors() {
|
||||||
|
const headings = document.querySelectorAll('[data-chapter] h1, [data-chapter] h2, [data-chapter] h3, [data-chapter] h4');
|
||||||
|
headings.forEach((h) => {
|
||||||
|
if (!h.id) {
|
||||||
|
const slug = (h.textContent || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/(^-|-$)/g, '');
|
||||||
|
h.id = slug || 'h-' + Math.random().toString(36).slice(2, 8);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
5. Resolver rutas de imágenes a absolutas
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
function ensureImagePaths() {
|
||||||
|
const base = window.location.href.replace(/[^/]*$/, '');
|
||||||
|
document.querySelectorAll('img').forEach((img) => {
|
||||||
|
const src = img.getAttribute('src');
|
||||||
|
if (!src || src.startsWith('http') || src.startsWith('data:') || src.startsWith('file:')) return;
|
||||||
|
const absolute = new URL(src, base).href;
|
||||||
|
img.src = absolute;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user