primer commit

This commit is contained in:
absandoval
2026-10-13 21:22:54 -06:00
commit b6b4bc3f01
13 changed files with 2428 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# Dependencias
scripts/node_modules/
# Salidas de build
dist/
# Capturas generadas
artifacts/
# Archivos del sistema
.DS_Store
Thumbs.db
# Logs
*.log
+165
View File
@@ -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).
+96
View File
@@ -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.
+121
View File
@@ -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.
+156
View File
@@ -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).
+154
View File
@@ -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.
+94
View File
@@ -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+).
+218
View File
@@ -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);
});
+46
View File
@@ -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"
}
}
}
}
+27
View File
@@ -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"
}
+876
View File
@@ -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; }
+265
View File
@@ -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>
+195
View File
@@ -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;
});
}
})();