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