academia/generate-docs.sh

1555 lines
53 KiB
Bash
Raw Normal View History

#!/bin/bash
# Genera index.html con documentos y SVGs embebidos
# Estilo Apple Design System
# Uso: ./generate-docs.sh
cd "$(dirname "$0")"
# Función para escapar contenido para JavaScript
escape_js() {
sed 's/\\/\\\\/g; s/`/\\`/g; s/\$/\\$/g' | tr '\n' '\r' | sed 's/\r/\\n/g'
}
# Función para convertir SVG a base64 data URI
svg_to_base64() {
local file="$1"
if [[ -f "$file" ]]; then
echo "data:image/svg+xml;base64,$(base64 -w0 "$file")"
fi
}
echo "Generando index.html con diseño Apple..."
# Inicio del HTML con diseño Apple completo
cat > index.html << 'HTMLHEAD'
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Academia - Documentación</title>
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
/* ═══════════════════════════════════════════════════════════
APPLE DESIGN SYSTEM - Inspired by apple.com & developer.apple.com
═══════════════════════════════════════════════════════════ */
/* ─── Design Tokens ─── */
:root {
/* Typography Scale (Apple's SF Pro) */
--font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-mono: "SF Mono", ui-monospace, Menlo, Monaco, "Cascadia Code", monospace;
/* Spacing (8pt grid) */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
/* Radius (Apple's continuous corners) */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 18px;
--radius-xl: 22px;
/* Transitions (Apple's spring-like easing) */
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.1);
--duration-fast: 0.15s;
--duration-normal: 0.25s;
--duration-slow: 0.4s;
/* Layout */
--sidebar-width: 260px;
--header-height: 48px;
--content-max-width: 780px;
/* Light Mode Colors (Apple semantic) */
--color-bg: #ffffff;
--color-bg-secondary: #f5f5f7;
--color-bg-tertiary: #fbfbfd;
--color-bg-elevated: #ffffff;
--color-surface: rgba(255, 255, 255, 0.72);
--color-text: #1d1d1f;
--color-text-secondary: #86868b;
--color-text-tertiary: #6e6e73;
--color-accent: #0071e3;
--color-accent-hover: #0077ed;
--color-border: rgba(0, 0, 0, 0.08);
--color-border-strong: rgba(0, 0, 0, 0.12);
--color-separator: rgba(60, 60, 67, 0.12);
--color-fill: rgba(120, 120, 128, 0.08);
--color-fill-hover: rgba(120, 120, 128, 0.12);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.06);
--shadow-float: 0 22px 70px 4px rgba(0, 0, 0, 0.15);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #000000;
--color-bg-secondary: #1c1c1e;
--color-bg-tertiary: #2c2c2e;
--color-bg-elevated: #1c1c1e;
--color-surface: rgba(28, 28, 30, 0.72);
--color-text: #f5f5f7;
--color-text-secondary: #98989d;
--color-text-tertiary: #8e8e93;
--color-accent: #2997ff;
--color-accent-hover: #40a9ff;
--color-border: rgba(255, 255, 255, 0.08);
--color-border-strong: rgba(255, 255, 255, 0.12);
--color-separator: rgba(84, 84, 88, 0.65);
--color-fill: rgba(120, 120, 128, 0.24);
--color-fill-hover: rgba(120, 120, 128, 0.32);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.5);
--shadow-float: 0 22px 70px 4px rgba(0, 0, 0, 0.6);
}
}
/* ─── Reset & Base ─── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 17px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
scroll-behavior: smooth;
}
body {
font-family: var(--font-family);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.47059;
letter-spacing: -0.022em;
overflow-x: hidden;
}
/* ─── Header (Apple Nav Bar) ─── */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--header-height);
background: var(--color-surface);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-bottom: 0.5px solid var(--color-separator);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 var(--space-5);
gap: var(--space-3);
}
.header-logo {
display: flex;
align-items: center;
gap: var(--space-2);
}
.header-logo svg {
width: 20px;
height: 20px;
fill: var(--color-accent);
}
.header-title {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.022em;
color: var(--color-text);
}
.header-divider {
width: 1px;
height: 20px;
background: var(--color-separator);
margin: 0 var(--space-2);
}
.header-subtitle {
font-size: 13px;
font-weight: 400;
color: var(--color-text-secondary);
letter-spacing: -0.008em;
}
/* ─── Layout ─── */
.layout {
display: flex;
min-height: 100vh;
padding-top: var(--header-height);
}
/* ─── Sidebar (Apple Developer Docs style) ─── */
.sidebar {
width: var(--sidebar-width);
background: var(--color-bg-secondary);
border-right: 0.5px solid var(--color-separator);
height: calc(100vh - var(--header-height));
position: fixed;
left: 0;
top: var(--header-height);
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-4) 0;
}
.sidebar::-webkit-scrollbar {
width: 8px;
}
.sidebar::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--color-fill);
border-radius: 4px;
border: 2px solid var(--color-bg-secondary);
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: var(--color-fill-hover);
}
/* Search Box */
.search-wrap {
padding: 0 var(--space-4) var(--space-4);
}
.search-box {
position: relative;
}
.search-box input {
width: 100%;
height: 36px;
padding: 0 var(--space-3) 0 36px;
background: var(--color-fill);
border: none;
border-radius: var(--radius-sm);
font-size: 15px;
font-family: var(--font-family);
color: var(--color-text);
transition: background var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out);
}
.search-box input::placeholder {
color: var(--color-text-tertiary);
}
.search-box input:focus {
outline: none;
background: var(--color-bg);
box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.24);
}
.search-box::before {
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 15px;
height: 15px;
background: var(--color-text-tertiary);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'/%3E%3C/svg%3E") center/contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* Navigation */
.nav-group {
margin-bottom: var(--space-2);
}
.nav-group-title {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
cursor: pointer;
user-select: none;
transition: color var(--duration-fast) var(--ease-out);
}
.nav-group-title:hover {
color: var(--color-text);
}
.nav-group-title .chevron {
width: 12px;
height: 12px;
transition: transform var(--duration-normal) var(--ease-spring);
opacity: 0.5;
}
.nav-group.open .nav-group-title .chevron {
transform: rotate(90deg);
}
.nav-list {
display: none;
padding: var(--space-1) var(--space-2);
}
.nav-group.open .nav-list {
display: block;
}
.nav-item {
display: block;
padding: var(--space-2) var(--space-3);
margin: 1px 0;
font-size: 13px;
font-weight: 400;
color: var(--color-text-secondary);
text-decoration: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-out);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-item:hover {
background: var(--color-fill);
color: var(--color-text);
}
.nav-item.active {
background: var(--color-accent);
color: white;
font-weight: 500;
}
/* ─── Main Content ─── */
.main {
flex: 1;
margin-left: var(--sidebar-width);
min-height: calc(100vh - var(--header-height));
}
.content-wrapper {
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--space-12) var(--space-8) var(--space-20);
}
/* ─── Typography (Apple Style) ─── */
.content h1 {
font-size: 40px;
line-height: 1.1;
font-weight: 700;
letter-spacing: -0.015em;
color: var(--color-text);
margin: 0 0 var(--space-6);
}
.content h2 {
font-size: 28px;
line-height: 1.14286;
font-weight: 600;
letter-spacing: 0.007em;
color: var(--color-text);
margin: var(--space-12) 0 var(--space-4);
padding-top: var(--space-4);
}
.content h3 {
font-size: 21px;
line-height: 1.19048;
font-weight: 600;
letter-spacing: 0.011em;
color: var(--color-text);
margin: var(--space-8) 0 var(--space-3);
}
.content h4 {
font-size: 17px;
line-height: 1.23536;
font-weight: 600;
letter-spacing: -0.022em;
color: var(--color-text-secondary);
margin: var(--space-6) 0 var(--space-2);
}
.content p {
font-size: 17px;
line-height: 1.52941;
color: var(--color-text-tertiary);
margin: var(--space-4) 0;
}
.content strong {
font-weight: 600;
color: var(--color-text);
}
.content a {
color: var(--color-accent);
text-decoration: none;
transition: opacity var(--duration-fast) var(--ease-out);
}
.content a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Lists */
.content ul, .content ol {
margin: var(--space-4) 0;
padding-left: var(--space-6);
}
.content li {
font-size: 17px;
line-height: 1.52941;
color: var(--color-text-tertiary);
margin: var(--space-2) 0;
}
.content li::marker {
color: var(--color-accent);
}
/* Code (Apple Developer style) */
.content code {
font-family: var(--font-mono);
font-size: 0.88em;
background: var(--color-fill);
padding: 2px 6px;
border-radius: 4px;
color: var(--color-text);
}
.content pre {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
overflow-x: auto;
margin: var(--space-5) 0;
}
.content pre code {
background: none;
padding: 0;
font-size: 14px;
line-height: 1.6;
color: var(--color-text);
}
/* Blockquote (Apple callout style) */
.content blockquote {
background: linear-gradient(135deg, rgba(0, 113, 227, 0.08) 0%, rgba(88, 86, 214, 0.08) 100%);
border-left: 4px solid var(--color-accent);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
padding: var(--space-4) var(--space-5);
margin: var(--space-5) 0;
}
.content blockquote p {
margin: 0;
color: var(--color-text-secondary);
font-style: normal;
}
/* Tables (Apple clean style) */
.content table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: var(--space-5) 0;
font-size: 15px;
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-border);
}
.content th {
background: var(--color-bg-secondary);
font-weight: 600;
color: var(--color-text);
text-align: left;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.content td {
padding: var(--space-3) var(--space-4);
color: var(--color-text-tertiary);
border-bottom: 1px solid var(--color-border);
}
.content tr:last-child td {
border-bottom: none;
}
.content tbody tr:hover {
background: var(--color-fill);
}
/* Horizontal Rule */
.content hr {
border: none;
height: 1px;
background: var(--color-separator);
margin: var(--space-10) 0;
}
/* ─── Images & Diagrams (Apple Gallery style) ─── */
.content img {
display: block;
max-width: 100%;
height: auto;
margin: var(--space-6) auto;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
background: var(--color-bg);
}
.diagram-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
padding: var(--space-6);
margin: var(--space-6) 0;
box-shadow: var(--shadow-sm);
transition: box-shadow var(--duration-normal) var(--ease-out),
transform var(--duration-normal) var(--ease-out);
}
.diagram-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.diagram-card img {
margin: 0;
border-radius: var(--radius-md);
box-shadow: none;
width: 100%;
}
.diagram-title {
font-size: 15px;
font-weight: 600;
color: var(--color-text);
margin-top: var(--space-4);
text-align: center;
}
/* ─── Welcome Screen ─── */
.welcome {
text-align: center;
padding: var(--space-20) var(--space-8);
max-width: 480px;
margin: 0 auto;
}
.welcome-icon {
width: 96px;
height: 96px;
margin: 0 auto var(--space-6);
background: linear-gradient(135deg, var(--color-accent) 0%, #5856d6 100%);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
box-shadow: var(--shadow-lg);
}
.welcome h1 {
font-size: 32px;
font-weight: 700;
margin-bottom: var(--space-3);
}
.welcome p {
color: var(--color-text-secondary);
font-size: 17px;
line-height: 1.52941;
}
/* ─── Diagrams Gallery ─── */
.diagrams-section {
margin: var(--space-10) 0;
}
.diagrams-section h2 {
margin-bottom: var(--space-6);
}
.diagrams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: var(--space-5);
}
/* ─── Lightbox Modal (Apple style with Zoom & Pan) ─── */
.lightbox {
position: fixed;
inset: 0;
z-index: 2000;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
opacity: 0;
transition: opacity var(--duration-normal) var(--ease-out);
}
.lightbox.active {
display: flex;
opacity: 1;
}
/* Image Container with Pan support */
.lightbox-viewport {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
}
.lightbox-viewport.dragging {
cursor: grabbing;
}
.lightbox-viewport.zoomed-out {
cursor: zoom-in;
}
.lightbox-content {
position: absolute;
transform-origin: center center;
transition: transform 0.3s var(--ease-spring);
will-change: transform;
animation: lightbox-in var(--duration-slow) var(--ease-spring);
}
.lightbox-content.no-transition {
transition: none;
}
@keyframes lightbox-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.lightbox-content img {
max-width: 90vw;
max-height: 85vh;
object-fit: contain;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
background: white;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
/* Top Toolbar (Apple style pill) */
.lightbox-toolbar {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 2px;
padding: 6px;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
}
.lightbox-toolbar button {
width: 36px;
height: 36px;
border-radius: 50%;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) var(--ease-out);
color: white;
}
.lightbox-toolbar button:hover {
background: rgba(255, 255, 255, 0.15);
}
.lightbox-toolbar button:active {
transform: scale(0.95);
}
.lightbox-toolbar button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.lightbox-toolbar button svg {
width: 18px;
height: 18px;
stroke: white;
stroke-width: 2;
fill: none;
}
.lightbox-toolbar .divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.2);
margin: 0 4px;
}
.zoom-level {
font-size: 13px;
font-weight: 500;
color: white;
min-width: 50px;
text-align: center;
font-variant-numeric: tabular-nums;
}
/* Bottom Info Bar */
.lightbox-info {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.lightbox-title {
font-size: 15px;
font-weight: 500;
color: white;
white-space: nowrap;
}
.lightbox-counter {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
}
/* Navigation Buttons */
.lightbox-close {
position: fixed;
top: 20px;
right: 20px;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) var(--ease-out);
z-index: 10;
}
.lightbox-close:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.lightbox-close:active {
transform: scale(0.95);
}
.lightbox-close svg {
width: 18px;
height: 18px;
stroke: white;
stroke-width: 2;
}
.lightbox-nav {
position: fixed;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) var(--ease-out);
z-index: 10;
}
.lightbox-nav:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.05);
}
.lightbox-nav:active {
transform: translateY(-50%) scale(0.95);
}
.lightbox-nav svg {
width: 22px;
height: 22px;
stroke: white;
stroke-width: 2;
}
.lightbox-prev { left: 20px; }
.lightbox-next { right: 20px; }
/* Zoom hint toast */
.zoom-hint {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
padding: 10px 16px;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(10px);
border-radius: 10px;
font-size: 13px;
color: white;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 20;
}
.zoom-hint.visible {
opacity: 1;
}
/* ─── Utilities ─── */
.hidden { display: none !important; }
/* ─── Mobile Responsive ─── */
@media (max-width: 960px) {
:root {
--sidebar-width: 240px;
}
.content-wrapper {
padding: var(--space-8) var(--space-5);
}
}
@media (max-width: 768px) {
.sidebar {
position: relative;
width: 100%;
height: auto;
border-right: none;
border-bottom: 1px solid var(--color-separator);
}
.main {
margin-left: 0;
}
.layout {
flex-direction: column;
}
.content h1 {
font-size: 28px;
}
.content h2 {
font-size: 22px;
}
.diagrams-grid {
grid-template-columns: 1fr;
}
}
/* ─── Print ─── */
@media print {
.header, .sidebar {
display: none;
}
.main {
margin-left: 0;
}
.content-wrapper {
max-width: none;
padding: 0;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-logo">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<span class="header-title">Academia</span>
</div>
<div class="header-divider"></div>
<span class="header-subtitle">Documentación del Sistema</span>
</header>
<div class="layout">
<aside class="sidebar">
<div class="search-wrap">
<div class="search-box">
<input type="text" id="search" placeholder="Buscar documentos...">
</div>
</div>
<nav id="nav"></nav>
</aside>
<main class="main">
<div class="content-wrapper">
<article class="content" id="content">
<div class="welcome">
<div class="welcome-icon">📚</div>
<h1>Documentación</h1>
<p>Selecciona un documento del menú lateral para explorar la documentación del sistema.</p>
</div>
</article>
</div>
</main>
</div>
<!-- Lightbox Modal with Zoom & Pan -->
<div class="lightbox" id="lightbox">
<!-- Top Toolbar -->
<div class="lightbox-toolbar">
<button onclick="zoomOut()" id="btn-zoom-out" aria-label="Alejar">
<svg viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" stroke-linecap="round"/></svg>
</button>
<span class="zoom-level" id="zoom-level">100%</span>
<button onclick="zoomIn()" id="btn-zoom-in" aria-label="Acercar">
<svg viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7" stroke-linecap="round"/></svg>
</button>
<div class="divider"></div>
<button onclick="resetZoom()" aria-label="Restablecer zoom">
<svg viewBox="0 0 24 24"><path d="M4 4v5h5M20 20v-5h-5M4 9a8 8 0 0114-5.3M20 15a8 8 0 01-14 5.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button onclick="fitToScreen()" aria-label="Ajustar a pantalla">
<svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<!-- Close Button -->
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Cerrar">
<svg viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/>
</svg>
</button>
<!-- Navigation -->
<button class="lightbox-nav lightbox-prev" onclick="navigateLightbox(-1)" aria-label="Anterior">
<svg viewBox="0 0 24 24" fill="none">
<path d="M15 18l-6-6 6-6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="lightbox-nav lightbox-next" onclick="navigateLightbox(1)" aria-label="Siguiente">
<svg viewBox="0 0 24 24" fill="none">
<path d="M9 18l6-6-6-6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<!-- Image Viewport (for pan & zoom) -->
<div class="lightbox-viewport" id="lightbox-viewport">
<div class="lightbox-content" id="lightbox-content">
<img id="lightbox-img" src="" alt="" draggable="false">
</div>
</div>
<!-- Bottom Info Bar -->
<div class="lightbox-info">
<span class="lightbox-title" id="lightbox-title"></span>
<span class="lightbox-counter" id="lightbox-counter"></span>
</div>
<!-- Zoom Hint Toast -->
<div class="zoom-hint" id="zoom-hint">Usa la rueda del ratón o pellizca para hacer zoom</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════
// EMBEDDED DOCUMENTS & SVG DIAGRAMS
// ═══════════════════════════════════════════════════════════
const diagrams = {
HTMLHEAD
# Embeber SVGs como base64
echo "Embebiendo diagramas SVG..."
echo ' "01-use-cases": "'"$(svg_to_base64 "docs/architecture/diagrams/01-use-cases.svg")"'",' >> index.html
echo ' "02-domain-model": "'"$(svg_to_base64 "docs/architecture/diagrams/02-domain-model.svg")"'",' >> index.html
echo ' "03-sequence-enrollment": "'"$(svg_to_base64 "docs/architecture/diagrams/03-sequence-enrollment.svg")"'",' >> index.html
echo ' "04-components": "'"$(svg_to_base64 "docs/architecture/diagrams/04-components.svg")"'",' >> index.html
echo ' "05-entity-relationship": "'"$(svg_to_base64 "docs/architecture/diagrams/05-entity-relationship.svg")"'",' >> index.html
echo ' "06-state-enrollment": "'"$(svg_to_base64 "docs/architecture/diagrams/06-state-enrollment.svg")"'",' >> index.html
echo ' "07-deployment": "'"$(svg_to_base64 "docs/architecture/diagrams/07-deployment.svg")"'",' >> index.html
echo ' "08-c4-context": "'"$(svg_to_base64 "docs/architecture/diagrams/08-c4-context.svg")"'",' >> index.html
cat >> index.html << 'HTMLMID'
};
const diagramNames = {
"01-use-cases": "Casos de Uso",
"02-domain-model": "Modelo de Dominio",
"03-sequence-enrollment": "Secuencia: Inscripción",
"04-components": "Componentes",
"05-entity-relationship": "Entidad-Relación",
"06-state-enrollment": "Estados: Inscripción",
"07-deployment": "Despliegue",
"08-c4-context": "Contexto C4"
};
const diagramKeys = Object.keys(diagramNames);
let currentDiagramIndex = 0;
// ═══════════════════════════════════════════════════════════
// LIGHTBOX WITH ZOOM & PAN (Apple-style)
// ═══════════════════════════════════════════════════════════
// Zoom state
let currentZoom = 1;
let panX = 0;
let panY = 0;
let isDragging = false;
let startX = 0;
let startY = 0;
let lastPanX = 0;
let lastPanY = 0;
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 5;
const ZOOM_STEP = 0.25;
function openLightbox(key) {
const lightbox = document.getElementById('lightbox');
const img = document.getElementById('lightbox-img');
const title = document.getElementById('lightbox-title');
const counter = document.getElementById('lightbox-counter');
currentDiagramIndex = diagramKeys.indexOf(key);
img.src = diagrams[key];
title.textContent = diagramNames[key] || key;
counter.textContent = `${currentDiagramIndex + 1} / ${diagramKeys.length}`;
// Reset zoom and pan
resetZoom();
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
// Show zoom hint briefly
showZoomHint();
}
function closeLightbox() {
const lightbox = document.getElementById('lightbox');
lightbox.classList.remove('active');
document.body.style.overflow = '';
resetZoom();
}
function navigateLightbox(direction) {
currentDiagramIndex = (currentDiagramIndex + direction + diagramKeys.length) % diagramKeys.length;
const key = diagramKeys[currentDiagramIndex];
document.getElementById('lightbox-img').src = diagrams[key];
document.getElementById('lightbox-title').textContent = diagramNames[key] || key;
document.getElementById('lightbox-counter').textContent = `${currentDiagramIndex + 1} / ${diagramKeys.length}`;
resetZoom();
}
// Zoom functions
function setZoom(newZoom, centerX, centerY) {
const oldZoom = currentZoom;
currentZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom));
// Adjust pan to zoom towards cursor position
if (centerX !== undefined && centerY !== undefined) {
const viewport = document.getElementById('lightbox-viewport');
const rect = viewport.getBoundingClientRect();
const relX = centerX - rect.left - rect.width / 2;
const relY = centerY - rect.top - rect.height / 2;
const scale = currentZoom / oldZoom;
panX = relX - (relX - panX) * scale;
panY = relY - (relY - panY) * scale;
}
updateTransform();
updateZoomUI();
}
function zoomIn() {
setZoom(currentZoom + ZOOM_STEP);
}
function zoomOut() {
setZoom(currentZoom - ZOOM_STEP);
}
function resetZoom() {
currentZoom = 1;
panX = 0;
panY = 0;
updateTransform();
updateZoomUI();
}
function fitToScreen() {
currentZoom = 1;
panX = 0;
panY = 0;
updateTransform();
updateZoomUI();
}
function updateTransform() {
const content = document.getElementById('lightbox-content');
const viewport = document.getElementById('lightbox-viewport');
// Constrain pan when zoomed out
if (currentZoom <= 1) {
panX = 0;
panY = 0;
viewport.classList.add('zoomed-out');
} else {
viewport.classList.remove('zoomed-out');
}
content.style.transform = `translate(${panX}px, ${panY}px) scale(${currentZoom})`;
}
function updateZoomUI() {
document.getElementById('zoom-level').textContent = Math.round(currentZoom * 100) + '%';
document.getElementById('btn-zoom-in').disabled = currentZoom >= MAX_ZOOM;
document.getElementById('btn-zoom-out').disabled = currentZoom <= MIN_ZOOM;
}
function showZoomHint() {
const hint = document.getElementById('zoom-hint');
hint.classList.add('visible');
setTimeout(() => hint.classList.remove('visible'), 3000);
}
// Mouse wheel zoom
document.getElementById('lightbox-viewport')?.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
setZoom(currentZoom + delta, e.clientX, e.clientY);
}, { passive: false });
// Pan with mouse drag
const viewport = document.getElementById('lightbox-viewport');
viewport?.addEventListener('mousedown', (e) => {
if (currentZoom <= 1) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
lastPanX = panX;
lastPanY = panY;
viewport.classList.add('dragging');
document.getElementById('lightbox-content').classList.add('no-transition');
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panX = lastPanX + (e.clientX - startX);
panY = lastPanY + (e.clientY - startY);
updateTransform();
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
viewport?.classList.remove('dragging');
document.getElementById('lightbox-content')?.classList.remove('no-transition');
}
});
// Touch support for pinch zoom and pan
let lastTouchDistance = 0;
let lastTouchCenter = { x: 0, y: 0 };
viewport?.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
lastTouchDistance = getTouchDistance(e.touches);
lastTouchCenter = getTouchCenter(e.touches);
} else if (e.touches.length === 1 && currentZoom > 1) {
isDragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
lastPanX = panX;
lastPanY = panY;
document.getElementById('lightbox-content').classList.add('no-transition');
}
}, { passive: true });
viewport?.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
const newDistance = getTouchDistance(e.touches);
const newCenter = getTouchCenter(e.touches);
const scale = newDistance / lastTouchDistance;
setZoom(currentZoom * scale, newCenter.x, newCenter.y);
lastTouchDistance = newDistance;
lastTouchCenter = newCenter;
} else if (e.touches.length === 1 && isDragging) {
panX = lastPanX + (e.touches[0].clientX - startX);
panY = lastPanY + (e.touches[0].clientY - startY);
updateTransform();
}
}, { passive: false });
viewport?.addEventListener('touchend', () => {
isDragging = false;
lastTouchDistance = 0;
document.getElementById('lightbox-content')?.classList.remove('no-transition');
});
function getTouchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
function getTouchCenter(touches) {
return {
x: (touches[0].clientX + touches[1].clientX) / 2,
y: (touches[0].clientY + touches[1].clientY) / 2
};
}
// Double-click to toggle zoom
viewport?.addEventListener('dblclick', (e) => {
if (currentZoom === 1) {
setZoom(2, e.clientX, e.clientY);
} else {
resetZoom();
}
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
const lightbox = document.getElementById('lightbox');
if (!lightbox.classList.contains('active')) return;
switch(e.key) {
case 'Escape': closeLightbox(); break;
case 'ArrowLeft': navigateLightbox(-1); break;
case 'ArrowRight': navigateLightbox(1); break;
case '+': case '=': zoomIn(); break;
case '-': zoomOut(); break;
case '0': resetZoom(); break;
}
});
const docs = {
HTMLMID
# Función para agregar documento
add_doc() {
local name="$1"
local file="$2"
if [[ -f "$file" ]]; then
local content=$(cat "$file" | escape_js)
echo " \"$name\": \`$content\`," >> index.html
fi
}
echo "Embebiendo documentos..."
# Inicio
echo ' "Inicio": {' >> index.html
add_doc "README" "README.md"
add_doc "Entregables" "docs/ENTREGABLES.md"
add_doc "Prueba Técnica" "docs/Prueba Técnica.md"
echo ' },' >> index.html
# Análisis
echo ' "Análisis": {' >> index.html
add_doc "AN-001 Requisitos Funcionales" "docs/entregables/01-analisis/requisitos/AN-001-requisitos-funcionales.md"
add_doc "AN-002 Reglas de Negocio" "docs/entregables/01-analisis/reglas-negocio/AN-002-reglas-negocio.md"
add_doc "AN-003 Historias de Usuario" "docs/entregables/01-analisis/historias-usuario/AN-003-historias-usuario.md"
add_doc "AN-004 Requisitos No Funcionales" "docs/entregables/01-analisis/requisitos/AN-004-requisitos-no-funcionales.md"
add_doc "AN-005 Riesgos Técnicos" "docs/entregables/01-analisis/riesgos/AN-005-riesgos-tecnicos.md"
echo ' },' >> index.html
# Diseño
echo ' "Diseño": {' >> index.html
add_doc "DI-001 Arquitectura Backend" "docs/entregables/02-diseno/arquitectura/DI-001-arquitectura-backend.md"
add_doc "DI-002 Modelo de Dominio" "docs/entregables/02-diseno/modelo-dominio/DI-002-modelo-dominio.md"
add_doc "DI-003 Diseño Base de Datos" "docs/entregables/02-diseno/base-datos/DI-003-diseno-base-datos.md"
add_doc "DI-004 Esquema GraphQL" "docs/entregables/02-diseno/esquema-graphql/DI-004-esquema-graphql.md"
add_doc "DI-005 Arquitectura Frontend" "docs/entregables/02-diseno/arquitectura/DI-005-arquitectura-frontend.md"
add_doc "DI-006 Componentes UI" "docs/entregables/02-diseno/componentes-ui/DI-006-componentes-ui.md"
add_doc "DI-007 Contratos DTOs" "docs/entregables/02-diseno/esquema-graphql/DI-007-contratos-dtos.md"
add_doc "DI-008 Manejo de Errores" "docs/entregables/02-diseno/arquitectura/DI-008-manejo-errores.md"
echo ' },' >> index.html
# Configuración
echo ' "Configuración": {' >> index.html
add_doc "DV-001 Configuración Repositorio" "docs/entregables/03-configuracion/DV-001-configuracion-repositorio.md"
add_doc "DV-002 Configuración .NET" "docs/entregables/03-configuracion/DV-002-configuracion-dotnet.md"
add_doc "DV-003 Configuración Angular" "docs/entregables/03-configuracion/DV-003-configuracion-angular.md"
add_doc "DV-004 Configuración Base Datos" "docs/entregables/03-configuracion/DV-004-configuracion-base-datos.md"
add_doc "DV-005 Variables de Entorno" "docs/entregables/03-configuracion/DV-005-variables-entorno.md"
add_doc "DV-006 Herramientas de Calidad" "docs/entregables/03-configuracion/DV-006-herramientas-calidad.md"
echo ' },' >> index.html
# Arquitectura
echo ' "Arquitectura": {' >> index.html
add_doc "ADR-001 Clean Architecture" "docs/architecture/decisions/ADR-001-clean-architecture.md"
add_doc "ADR-002 GraphQL vs REST" "docs/architecture/decisions/ADR-002-graphql-vs-rest.md"
add_doc "ADR-003 Angular Signals" "docs/architecture/decisions/ADR-003-angular-signals.md"
add_doc "ADR-004 Validation Strategy" "docs/architecture/decisions/ADR-004-validation-strategy.md"
echo ' },' >> index.html
# Despliegue
echo ' "Despliegue": {' >> index.html
add_doc "Manual de Despliegue" "docs/DEPLOYMENT.md"
add_doc "Plan de Actividades" "docs/PLAN_ACTIVIDADES.md"
echo ' },' >> index.html
# Calidad
echo ' "Calidad": {' >> index.html
add_doc "Code Review Checklist" "docs/CODE_REVIEW_CHECKLIST.md"
add_doc "OWASP Checklist" "docs/OWASP_CHECKLIST.md"
add_doc "Recomendaciones" "docs/RECOMMENDATIONS.md"
add_doc "Defectos QA" "docs/DEFECTOS_QA.md"
echo ' },' >> index.html
# Cerrar docs y continuar con el script
cat >> index.html << 'HTMLFOOT'
};
// ═══════════════════════════════════════════════════════════
// APP LOGIC
// ═══════════════════════════════════════════════════════════
marked.setOptions({ gfm: true, breaks: true });
// Build navigation
function buildNav() {
const nav = document.getElementById('nav');
let html = '';
// Add diagrams section first
html += `
<div class="nav-group open" data-group="Diagramas">
<div class="nav-group-title" onclick="toggleGroup(this.parentElement)">
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18l6-6-6-6"/>
</svg>
Diagramas UML
</div>
<div class="nav-list">
<div class="nav-item" data-key="diagrams:all" onclick="showDiagrams()">Ver todos los diagramas</div>
</div>
</div>
`;
// Add document sections
for (const [section, items] of Object.entries(docs)) {
if (typeof items !== 'object') continue;
html += `
<div class="nav-group" data-group="${section}">
<div class="nav-group-title" onclick="toggleGroup(this.parentElement)">
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18l6-6-6-6"/>
</svg>
${section}
</div>
<div class="nav-list">
`;
for (const name of Object.keys(items)) {
html += `<div class="nav-item" data-key="${section}|${name}" onclick="loadDoc('${section}', '${name}', this)">${name}</div>`;
}
html += `</div></div>`;
}
nav.innerHTML = html;
}
function toggleGroup(el) {
el.classList.toggle('open');
}
// Show all diagrams in a gallery
function showDiagrams() {
const content = document.getElementById('content');
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
document.querySelector('[data-key="diagrams:all"]')?.classList.add('active');
let html = `
<h1>Diagramas de Arquitectura</h1>
<p>Diagramas UML que documentan la arquitectura y diseño del sistema.</p>
<div class="diagrams-grid">
`;
for (const [key, src] of Object.entries(diagrams)) {
if (!src) continue;
const name = diagramNames[key] || key;
html += `
<div class="diagram-card" onclick="showSingleDiagram('${key}')">
<img src="${src}" alt="${name}" loading="lazy">
<div class="diagram-title">${name}</div>
</div>
`;
}
html += '</div>';
content.innerHTML = html;
window.scrollTo(0, 0);
window.location.hash = 'diagrams';
}
// Show single diagram in lightbox (full size)
function showSingleDiagram(key) {
openLightbox(key);
}
// Load document
function loadDoc(section, name, navItem) {
const content = document.getElementById('content');
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
navItem?.classList.add('active');
try {
let markdown = docs[section]?.[name];
if (!markdown) throw new Error('Documento no encontrado');
// Replace SVG links with embedded images
markdown = markdown.replace(
/\[([^\]]+)\]\(docs\/architecture\/diagrams\/(\d+-[^)]+\.svg)\)/g,
(match, text, file) => {
const key = file.replace('.svg', '');
if (diagrams[key]) {
return `![${text}](${diagrams[key]})`;
}
return match;
}
);
content.innerHTML = marked.parse(markdown);
window.scrollTo(0, 0);
window.location.hash = encodeURIComponent(`${section}|${name}`);
} catch (error) {
content.innerHTML = `<div class="welcome"><h1>Error</h1><p>${error.message}</p></div>`;
}
}
// Search
document.getElementById('search').addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
document.querySelectorAll('.nav-item').forEach(item => {
const matches = item.textContent.toLowerCase().includes(query);
item.classList.toggle('hidden', !matches && query);
});
document.querySelectorAll('.nav-group').forEach(group => {
const hasVisible = group.querySelector('.nav-item:not(.hidden)');
group.classList.toggle('hidden', !hasVisible && query);
if (hasVisible && query) group.classList.add('open');
});
});
// Init
buildNav();
// Load from hash
if (window.location.hash) {
const hash = decodeURIComponent(window.location.hash.slice(1));
if (hash === 'diagrams') {
showDiagrams();
} else {
const [section, name] = hash.split('|');
if (docs[section]?.[name]) {
const navItem = document.querySelector(`[data-key="${hash}"]`);
navItem?.closest('.nav-group')?.classList.add('open');
loadDoc(section, name, navItem);
}
}
}
</script>
</body>
</html>
HTMLFOOT
# Estadísticas finales
SIZE=$(wc -c < index.html | tr -d ' ')
SIZE_KB=$((SIZE / 1024))
echo ""
echo "✅ index.html generado exitosamente"
echo " Tamaño: ${SIZE_KB} KB (${SIZE} bytes)"
echo " Diagramas: 8 SVGs embebidos"
echo " Documentos: $(find docs -name "*.md" -not -path "*/qa/*" | wc -l) archivos MD"
echo ""
echo "🌐 Abre index.html en tu navegador"