commit b6b4bc3f01c37001a6901f8cb136af5edce36be4 Author: absandoval Date: Tue Oct 13 21:22:54 2026 -0600 primer commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a5d2b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Dependencias +scripts/node_modules/ + +# Salidas de build +dist/ + +# Capturas generadas +artifacts/ + +# Archivos del sistema +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..231dadd --- /dev/null +++ b/SKILL.md @@ -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 + (`
    `). +- 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). diff --git a/references/calidad.md b/references/calidad.md new file mode 100644 index 0000000..15777a7 --- /dev/null +++ b/references/calidad.md @@ -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. diff --git a/references/capturas.md b/references/capturas.md new file mode 100644 index 0000000..f5c1065 --- /dev/null +++ b/references/capturas.md @@ -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 +
    + Pantalla de inicio de sesión +
      +
      Inicio de sesión con credenciales de prueba.
      +
      +``` + +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. diff --git a/references/estilo.md b/references/estilo.md new file mode 100644 index 0000000..fd47572 --- /dev/null +++ b/references/estilo.md @@ -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 + +``` + +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 +
      + +
      +

      Iniciar sesión en la plataforma

      +

      Abra el navegador y vaya a https://app.factorpesca.com.

      +
      + Pantalla de inicio de sesión +
        +
        + +
        +
        +``` + +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 +
        npx skills add lackeyjb/playwright-skill --skill playwright-skill --yes
        +``` + +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 +NUEVO +``` + +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). diff --git a/references/estructura.md b/references/estructura.md new file mode 100644 index 0000000..44583b4 --- /dev/null +++ b/references/estructura.md @@ -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** (`

        `) +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 +
        +

        Solución de problemas

        +

        Si encuentra algún error, busque el síntoma en esta sección antes de contactar al soporte.

        + +
        +

        No puedo iniciar sesión

        +

        Causa probable: la cuenta está bloqueada tras tres intentos fallidos.

        +

        Solución: espere 15 minutos o solicite el desbloqueo al administrador.

        +
        + ... +
        +``` + +Cada problema: síntoma, causa, solución. Como máximo cinco problemas por bloque; si hay más, agrupar por categoría con `

        `. + +## 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. diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..6bf6f03 --- /dev/null +++ b/scripts/README.md @@ -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 HTML fuente (por defecto templates/manual.html). +--out Ruta exacta del PDF de salida. +--out-dir Carpeta de salida (por defecto dist/). +--no-background No imprime colores de fondo. +--landscape Orientación horizontal. +--margin-{top,bottom,left,right} + 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+). diff --git a/scripts/build-manual.js b/scripts/build-manual.js new file mode 100644 index 0000000..09172ad --- /dev/null +++ b/scripts/build-manual.js @@ -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 al HTML fuente. Por defecto templates/manual.html + --out Ruta completa del PDF de salida. + --out-dir Carpeta de salida. Por defecto dist/. + --no-background No imprimir colores de fondo. + --landscape Orientación horizontal. + --margin-top Margen superior CSS (p. ej. 22mm). + --margin-bottom Margen inferior CSS. + --margin-left Margen izquierdo CSS. + --margin-right 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); +}); diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 0000000..14dd021 --- /dev/null +++ b/scripts/package-lock.json @@ -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" + } + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..dde3c8a --- /dev/null +++ b/scripts/package.json @@ -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" +} diff --git a/templates/manual.css b/templates/manual.css new file mode 100644 index 0000000..8ef8cf6 --- /dev/null +++ b/templates/manual.css @@ -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; } diff --git a/templates/manual.html b/templates/manual.html new file mode 100644 index 0000000..fcdc704 --- /dev/null +++ b/templates/manual.html @@ -0,0 +1,265 @@ + + + + + + Manual de usuario de la aplicación + + + + + + + +
        +
        +
        + +
        + +
        +

        Manual de usuario

        +

        Manual de la aplicación

        +

        Guía de procedimientos paso a paso para operadores.

        +
        +
        +
        + + +
        +

        Índice

        +
          +
          + + +
          +

          Resumen ejecutivo

          + +
          +

          Objetivo

          +

          + 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. +

          +
          + +
          +

          Audiencia

          +

          + 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. +

          +
          + +
          +

          Alcance

          +

          + 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. +

          +
          +
          + + +
          +

          Requisitos previos

          +

          Asegúrese de cumplir con los siguientes requisitos antes de iniciar.

          + + + + + + + + + + + + + + + + + + + + + + + + + + +
          RequisitoDetalle
          NavegadorChromium 120+, Firefox 121+ o Safari 17+.
          ResoluciónMínimo 1280×800 px; recomendado 1440×900 px.
          CuentaUsuario activo con rol de Operador o superior.
          ConectividadAcceso a la red corporativa o VPN configurada.
          +
          + + +
          + +
          +

          Nombre del proceso

          + + + +
          + +
          +

          Abrir la pantalla inicial

          +

          + Vaya al menú principal y seleccione la opción «X». Aparecerá la + pantalla de inicio con el listado vacío. +

          +
          + Pantalla inicial del proceso +
            +
            Pantalla inicial con el menú principal.
            +
            + +
            +
            + +
            + +
            +

            Completar el formulario

            +

            + Rellene los campos obligatorios marcados con asterisco. + El botón «Guardar» permanecerá deshabilitado hasta que todos los + campos sean válidos. +

            +
            + Formulario de alta +
              +
              Formulario con los campos obligatorios.
              +
              + +
              +
              + + +
              + +
              + + +
              +

              Solución de problemas

              +

              + Si encuentra algún error durante los procedimientos descritos, + busque el síntoma en esta sección antes de contactar al soporte. +

              + +
              +

              El botón «Guardar» permanece deshabilitado

              +

              Causa probable: uno o más campos obligatorios están vacíos o tienen un formato no válido.

              +

              Solución: revise los campos marcados en rojo y verifique que las fechas tengan el formato DD/MM/AAAA.

              +
              + +
              +

              La aplicación muestra un error 500 al guardar

              +

              Causa probable: pérdida temporal de conectividad con el servicio de persistencia.

              +

              Solución: espere treinta segundos y vuelva a intentarlo. Si el error persiste, contacte con soporte adjuntando la hora exacta del incidente.

              +
              + +
              +

              No recibo notificaciones por correo

              +

              Causa probable: la dirección de notificación del usuario no está validada.

              +

              Solución: acceda a «Mi perfil» y verifique la dirección de correo. Recibirá un mensaje de confirmación en menos de un minuto.

              +
              +
              + + +
              +

              Glosario

              + + + + + + + + + + +
              TérminoDefinición
              CapturaRegistro individual de pesca asociado a un lote y una fecha.
              LoteConjunto de capturas agrupadas bajo un mismo identificador.
              OperadorUsuario con permisos de lectura y escritura sobre capturas propias.
              SupervisorUsuario con permisos de lectura y escritura sobre todas las capturas del área.
              +
              + + + + + + + diff --git a/templates/manual.js b/templates/manual.js new file mode 100644 index 0000000..a08db05 --- /dev/null +++ b/templates/manual.js @@ -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 = '
            1. Sin capítulos definidos.
            2. '; + 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; + }); + } +})();