utn-cli 2.1.44 → 2.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/templates/backend/servicios/Nucleo/Miscelaneas.js +1 -1
  3. package/templates/frontend/src/app/Componentes/Nucleo/calendario-publico/calendario-publico.component.css +44 -4
  4. package/templates/frontend/src/app/Componentes/Nucleo/calendario-publico/calendario-publico.component.html +10 -8
  5. package/templates/frontend/src/app/Componentes/Nucleo/calendario-publico/calendario-publico.component.ts +2 -2
  6. package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.css +49 -2
  7. package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.html +9 -2
  8. package/templates/frontend/src/app/Componentes/Nucleo/estadisticas-del-modulo/estadisticas-del-modulo.component.ts +2 -2
  9. package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.css +10 -2
  10. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.css +76 -7
  11. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.html +25 -7
  12. package/templates/frontend/src/app/Componentes/Nucleo/manual/manual.component.ts +93 -16
  13. package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.component.css +71 -3
  14. package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.component.html +11 -8
  15. package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.ts +3 -3
  16. package/templates/frontend/src/app/Componentes/Nucleo/mensajes/mensajes.component.css +67 -0
  17. package/templates/frontend/src/app/Componentes/Nucleo/mensajes/mensajes.component.html +21 -9
  18. package/templates/frontend/src/app/Componentes/Nucleo/mensajes/mensajes.component.ts +1 -2
  19. package/templates/frontend/src/app/Componentes/Nucleo/panel-notificaciones/panel-notificaciones.component.css +209 -0
  20. package/templates/frontend/src/app/Componentes/Nucleo/panel-notificaciones/panel-notificaciones.component.html +45 -0
  21. package/templates/frontend/src/app/Componentes/Nucleo/panel-notificaciones/panel-notificaciones.component.ts +53 -0
  22. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-incidencias/reporte-de-incidencias.component.css +106 -13
  23. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-incidencias/reporte-de-incidencias.component.html +43 -32
  24. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-incidencias/reporte-de-incidencias.component.ts +4 -3
  25. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component.css +76 -21
  26. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component.html +23 -14
  27. package/templates/frontend/src/app/Componentes/Nucleo/reporte-de-sugerencias/reporte-de-sugerencias.component.ts +4 -3
  28. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-modulo/tarjeta-modulo.component.css +20 -10
  29. package/templates/frontend/src/app/Componentes/Nucleo/tarjeta-modulo/tarjeta-modulo.component.html +17 -8
  30. package/templates/frontend/src/app/Paginas/Nucleo/accesibilidad/accesibilidad.component.css +20 -1
  31. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.css +143 -48
  32. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +60 -38
  33. package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +28 -6
  34. package/templates/frontend/src/app/Paginas/Nucleo/declaracion-ia/declaracion-ia.component.css +4 -4
  35. package/templates/frontend/src/index.html +7 -0
  36. package/templates/frontend/src/styles.css +14 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utn-cli",
3
- "version": "2.1.44",
3
+ "version": "2.1.46",
4
4
  "description": "Herramienta CLI unificada para la gestión de plantillas en SIGU.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -275,7 +275,7 @@ class Miscelaneo {
275
275
  let datosFinales;
276
276
  if (existentes.length > 0) {
277
277
  const datosExistentes = typeof existentes[0].Datos === 'string' ? JSON.parse(existentes[0].Datos) : existentes[0].Datos;
278
- datosFinales = { ...datosExistentes, ...tarjeta, 'Posición': existentes[0].Posicion, 'Título': existentes[0].Titulo, 'Descripción': existentes[0].Descripcion, 'Archivo': archivo };
278
+ datosFinales = { 'Archivo': archivo, ...tarjeta, ...datosExistentes };
279
279
  await ejecutarConsultaSIGU(
280
280
  `UPDATE \`SIGU\`.\`SIGU_ModulosV2Tarjetas\` SET \`Datos\` = ?, \`LastUser\` = 'Sistema' WHERE \`Modulo\` = ? AND JSON_VALUE(\`Datos\`, '$."Título"') = ?`,
281
281
  [JSON.stringify(datosFinales), this.NombreCanonicoDelModulo, titulo]
@@ -1,15 +1,55 @@
1
- .titulo-dialog {
1
+ /* Header */
2
+ .dialogo-header {
2
3
  display: flex;
3
4
  align-items: center;
4
- gap: 8px;
5
+ gap: 10px;
6
+ background: linear-gradient(90deg, rgba(1, 33, 105, 1) 0%, rgba(63, 97, 171, 1) 100%);
7
+ color: white;
8
+ padding: 24px 20px 16px;
9
+ margin: -8px -8px 0;
10
+ }
11
+
12
+ .dialogo-icono {
13
+ font-size: 24px;
14
+ width: 24px;
15
+ height: 24px;
16
+ flex-shrink: 0;
17
+ }
18
+
19
+ .dialogo-titulo {
20
+ font-size: 1.1rem;
21
+ font-weight: 600;
22
+ font-family: 'Roboto', sans-serif;
23
+ letter-spacing: 0.2px;
5
24
  }
6
25
 
7
- .titulo-dialog small {
26
+ .dialogo-titulo small {
8
27
  font-size: 0.65em;
9
- opacity: 0.6;
28
+ opacity: 0.7;
10
29
  font-weight: normal;
11
30
  }
12
31
 
32
+ /* Botón cerrar */
33
+ mat-dialog-actions {
34
+ display: flex;
35
+ justify-content: flex-end;
36
+ padding: 8px 20px 16px;
37
+ }
38
+
39
+ .btn-cancelar {
40
+ color: #012169;
41
+ border-color: #012169;
42
+ font-family: 'Roboto', sans-serif;
43
+ font-weight: 500;
44
+ }
45
+
46
+ /* Pie */
47
+ .dialogo-pie {
48
+ height: 20px;
49
+ background: linear-gradient(90deg, rgba(1, 33, 105, 1) 0%, rgba(63, 97, 171, 1) 100%);
50
+ margin: 0 -8px;
51
+ }
52
+
13
53
  /* ── Buscador ── */
14
54
  .buscador {
15
55
  width: 100%;
@@ -1,9 +1,9 @@
1
- <h2 mat-dialog-title class="titulo-dialog">
2
- <mat-icon>calendar_month</mat-icon>
3
- <span *ngIf="cargando">Cargando...</span>
4
- <span *ngIf="!cargando && calendario">{{ calendario.Titulo }}&nbsp;<small>{{ calendario.Version }}</small></span>
5
- <span *ngIf="!cargando && !calendario">Calendario institucional</span>
6
- </h2>
1
+ <div class="dialogo-header">
2
+ <mat-icon class="dialogo-icono">calendar_month</mat-icon>
3
+ <span class="dialogo-titulo" *ngIf="cargando">Cargando...</span>
4
+ <span class="dialogo-titulo" *ngIf="!cargando && calendario">{{ calendario.Titulo }}&nbsp;<small>{{ calendario.Version }}</small></span>
5
+ <span class="dialogo-titulo" *ngIf="!cargando && !calendario">Calendario institucional</span>
6
+ </div>
7
7
 
8
8
  <mat-dialog-content>
9
9
 
@@ -167,6 +167,8 @@
167
167
  </ng-container>
168
168
  </mat-dialog-content>
169
169
 
170
- <mat-dialog-actions align="end">
171
- <button mat-button (click)="cerrar()">Cerrar</button>
170
+ <mat-dialog-actions>
171
+ <button mat-stroked-button class="btn-cancelar" (click)="cerrar()">Cerrar</button>
172
172
  </mat-dialog-actions>
173
+
174
+ <div class="dialogo-pie"></div>
@@ -1,6 +1,6 @@
1
1
  import { Component, OnInit, inject } from '@angular/core';
2
2
  import { HttpClient } from '@angular/common/http';
3
- import { MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
3
+ import { MatDialogActions, MatDialogContent, MatDialogRef } from '@angular/material/dialog';
4
4
  import { MatButtonModule } from '@angular/material/button';
5
5
  import { MatIconModule } from '@angular/material/icon';
6
6
  import { MatProgressBarModule } from '@angular/material/progress-bar';
@@ -16,7 +16,7 @@ import { DatosGlobalesService } from '../../../datos-globales.service';
16
16
  styleUrl: './calendario-publico.component.css',
17
17
  imports: [
18
18
  CommonModule,
19
- MatDialogContent, MatDialogActions, MatDialogTitle,
19
+ MatDialogContent, MatDialogActions,
20
20
  MatButtonModule, MatIconModule,
21
21
  MatProgressBarModule, MatExpansionModule,
22
22
  MatFormFieldModule, MatInputModule
@@ -1,8 +1,34 @@
1
+ /* Header */
2
+ .dialogo-header {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 10px;
6
+ background: linear-gradient(90deg, rgba(1, 33, 105, 1) 0%, rgba(63, 97, 171, 1) 100%);
7
+ color: white;
8
+ padding: 24px 20px 16px;
9
+ margin: -8px -8px 0;
10
+ }
11
+
12
+ .dialogo-icono {
13
+ font-size: 24px;
14
+ width: 24px;
15
+ height: 24px;
16
+ flex-shrink: 0;
17
+ }
18
+
19
+ .dialogo-titulo {
20
+ font-size: 1.1rem;
21
+ font-weight: 600;
22
+ font-family: 'Roboto', sans-serif;
23
+ letter-spacing: 0.2px;
24
+ }
25
+
26
+ /* Contenido */
1
27
  .contenedor {
2
28
  display: flex;
3
29
  flex-direction: column;
4
30
  gap: 1rem;
5
- padding: 0.5rem 0;
31
+ padding: 16px 0 8px;
6
32
  min-width: 260px;
7
33
  }
8
34
 
@@ -39,6 +65,27 @@
39
65
  .estadistica-valor {
40
66
  font-size: 1.5rem;
41
67
  font-weight: 700;
42
- color: #002f6b;
68
+ color: #012169;
43
69
  line-height: 1.2;
44
70
  }
71
+
72
+ /* Botones */
73
+ mat-dialog-actions {
74
+ display: flex;
75
+ justify-content: flex-end;
76
+ padding: 8px 20px 16px;
77
+ }
78
+
79
+ .btn-cerrar {
80
+ color: #012169;
81
+ border-color: #012169;
82
+ font-family: 'Roboto', sans-serif;
83
+ font-weight: 500;
84
+ }
85
+
86
+ /* Pie */
87
+ .dialogo-pie {
88
+ height: 20px;
89
+ background: linear-gradient(90deg, rgba(1, 33, 105, 1) 0%, rgba(63, 97, 171, 1) 100%);
90
+ margin: 0 -8px;
91
+ }
@@ -1,4 +1,8 @@
1
- <h2 mat-dialog-title>Estadísticas del módulo</h2>
1
+ <div class="dialogo-header">
2
+ <mat-icon class="dialogo-icono">bar_chart</mat-icon>
3
+ <span class="dialogo-titulo">Estadísticas del módulo</span>
4
+ </div>
5
+
2
6
  <mat-dialog-content>
3
7
  <div class="contenedor">
4
8
  <div class="estadistica">
@@ -17,6 +21,9 @@
17
21
  </div>
18
22
  </div>
19
23
  </mat-dialog-content>
24
+
20
25
  <mat-dialog-actions>
21
- <button mat-button (click)="Cerrar()">Cerrar</button>
26
+ <button mat-stroked-button class="btn-cerrar" (click)="Cerrar()">Cerrar</button>
22
27
  </mat-dialog-actions>
28
+
29
+ <div class="dialogo-pie"></div>
@@ -1,5 +1,5 @@
1
1
  import { Component, Inject, inject } from '@angular/core';
2
- import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
2
+ import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef } from '@angular/material/dialog';
3
3
  import { MatButtonModule } from '@angular/material/button';
4
4
  import { MatIconModule } from '@angular/material/icon';
5
5
  import { CommonModule } from '@angular/common';
@@ -8,7 +8,7 @@ import { CommonModule } from '@angular/common';
8
8
  selector: 'app-estadisticas-del-modulo',
9
9
  templateUrl: './estadisticas-del-modulo.component.html',
10
10
  styleUrl: './estadisticas-del-modulo.component.css',
11
- imports: [MatDialogContent, MatDialogActions, MatDialogTitle, MatButtonModule, MatIconModule, CommonModule]
11
+ imports: [MatDialogContent, MatDialogActions, MatButtonModule, MatIconModule, CommonModule]
12
12
  })
13
13
  export class EstadisticasDelModuloComponent {
14
14
  readonly dialogRef = inject(MatDialogRef<EstadisticasDelModuloComponent>);
@@ -211,14 +211,22 @@ mat-progress-bar {
211
211
  flex-direction: column;
212
212
  align-items: flex-start;
213
213
  }
214
-
214
+
215
215
  .item-icono {
216
216
  margin-bottom: 12px;
217
217
  }
218
-
218
+
219
219
  .item-fecha {
220
220
  margin-left: 0;
221
221
  margin-top: 12px;
222
222
  align-items: flex-start;
223
223
  }
224
+
225
+ .item-detalles {
226
+ max-width: 100%;
227
+ }
228
+
229
+ .detalle-browser {
230
+ max-width: 55vw;
231
+ }
224
232
  }
@@ -13,6 +13,10 @@
13
13
  align-items: start;
14
14
  min-height: 100%;
15
15
  font-family: 'Roboto', 'Helvetica Neue', sans-serif;
16
+ margin-top: 24px;
17
+ max-width: 1100px;
18
+ margin-left: auto;
19
+ margin-right: auto;
16
20
  }
17
21
 
18
22
  /* ═══════════════════════════════════════════════════════════
@@ -94,6 +98,19 @@
94
98
  font-size: 0.78rem;
95
99
  }
96
100
 
101
+ .toc-activo > .toc-enlace {
102
+ background: #dce9ff;
103
+ color: #002f6b;
104
+ font-weight: 700;
105
+ border-left: 3px solid #1976d2;
106
+ }
107
+
108
+ .toc-sublista {
109
+ list-style: none;
110
+ padding: 0;
111
+ margin: 0;
112
+ }
113
+
97
114
  .toc-descargar {
98
115
  display: block;
99
116
  width: calc(100% - 1rem);
@@ -119,10 +136,25 @@
119
136
  Área principal del artículo
120
137
  ══════════════════════════════════════════════════════════ */
121
138
  .manual-main {
122
- padding: 1.5rem 2.5rem 4rem 2rem;
139
+ padding: 1.5rem 2.5rem 4rem 5rem;
123
140
  min-width: 0;
124
141
  }
125
142
 
143
+ /* Título principal fijo */
144
+ .manual-titulo-principal {
145
+ font-size: 1.35rem;
146
+ font-weight: 700;
147
+ color: #012169;
148
+ margin: 0 0 1.5rem;
149
+ padding-bottom: 0.6rem;
150
+ border-bottom: 3px solid #2e6da4;
151
+ }
152
+
153
+ /* En móvil se muestra el externo y se oculta el de dentro del main */
154
+ .manual-titulo-movil {
155
+ display: none;
156
+ }
157
+
126
158
  /* ═══════════════════════════════════════════════════════════
127
159
  Estados: cargando / error
128
160
  ══════════════════════════════════════════════════════════ */
@@ -161,11 +193,12 @@
161
193
  scroll-margin-top: 1rem;
162
194
  }
163
195
 
164
- .manual-articulo h2 {
196
+ .manual-articulo h2,
197
+ :host ::ng-deep .manual-articulo h2 {
165
198
  font-size: 1.25rem;
166
199
  font-weight: 700;
167
200
  color: #002f6b;
168
- margin: 2.5rem 0 0.75rem;
201
+ margin: 1rem 0 0.75rem;
169
202
  padding: 0.4rem 0.9rem;
170
203
  background: #e8f0fe;
171
204
  border-left: 4px solid #1976d2;
@@ -173,11 +206,12 @@
173
206
  scroll-margin-top: 1rem;
174
207
  }
175
208
 
176
- .manual-articulo h3 {
209
+ .manual-articulo h3,
210
+ :host ::ng-deep .manual-articulo h3 {
177
211
  font-size: 1.05rem;
178
212
  font-weight: 600;
179
213
  color: #0b4fce;
180
- margin: 1.75rem 0 0.5rem;
214
+ margin: 3rem 0 0.5rem;
181
215
  scroll-margin-top: 1rem;
182
216
  }
183
217
 
@@ -190,8 +224,10 @@
190
224
  }
191
225
 
192
226
  /* Párrafos */
193
- .manual-articulo p {
194
- margin: 0 0 0.9rem;
227
+ .manual-articulo p,
228
+ :host ::ng-deep .manual-articulo p {
229
+ margin: 0 0 1.2rem;
230
+ text-align: justify;
195
231
  }
196
232
 
197
233
  /* Listas */
@@ -319,7 +355,40 @@
319
355
  /* ═══════════════════════════════════════════════════════════
320
356
  Responsivo — pantallas pequeñas
321
357
  ══════════════════════════════════════════════════════════ */
358
+ /* Botón volver arriba — solo móvil */
359
+ .btn-volver-arriba {
360
+ display: none;
361
+ }
362
+
363
+ @media (max-width: 768px) {
364
+ .btn-volver-arriba {
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ margin: 2rem auto 1rem;
369
+ padding: 0.6rem 1.4rem;
370
+ background: linear-gradient(90deg, rgba(1, 33, 105, 1) 0%, rgba(63, 97, 171, 1) 100%);
371
+ color: white;
372
+ border: none;
373
+ border-radius: 24px;
374
+ font-family: 'Roboto', sans-serif;
375
+ font-size: 0.9rem;
376
+ font-weight: 500;
377
+ cursor: pointer;
378
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
379
+ }
380
+ }
381
+
322
382
  @media (max-width: 768px) {
383
+ .manual-titulo-movil {
384
+ display: block;
385
+ margin-top: 16px;
386
+ }
387
+
388
+ .manual-main .manual-titulo-principal:not(.manual-titulo-movil) {
389
+ display: none;
390
+ }
391
+
323
392
  .manual-layout {
324
393
  grid-template-columns: 1fr;
325
394
  }
@@ -1,3 +1,5 @@
1
+ <h1 class="manual-titulo-principal manual-titulo-movil">Manual de usuario</h1>
2
+
1
3
  <div class="manual-layout">
2
4
 
3
5
  <!-- ── Índice lateral ─────────────────────────────────── -->
@@ -5,11 +7,22 @@
5
7
  <p class="toc-titulo">Contenido</p>
6
8
  <nav>
7
9
  <ul class="toc-lista">
8
- @for (entrada of toc; track entrada.id) {
9
- <li [class]="'toc-nivel-' + entrada.nivel">
10
- <button class="toc-enlace" (click)="irA(entrada.id)" [title]="entrada.texto">
11
- {{ entrada.texto }}
10
+ @for (seccion of secciones; track seccion.id) {
11
+ <li class="toc-nivel-2" [class.toc-activo]="seccionActiva?.id === seccion.id">
12
+ <button class="toc-enlace" (click)="seleccionarSeccion(seccion)" (focus)="seleccionarSeccion(seccion)" [title]="seccion.titulo">
13
+ {{ seccion.titulo }}
12
14
  </button>
15
+ @if (seccionActiva?.id === seccion.id && seccion.subEntradas.length > 0) {
16
+ <ul class="toc-sublista">
17
+ @for (sub of seccion.subEntradas; track sub.id) {
18
+ <li class="toc-nivel-3">
19
+ <button class="toc-enlace" (click)="irA(sub.id)" [title]="sub.texto">
20
+ {{ sub.texto }}
21
+ </button>
22
+ </li>
23
+ }
24
+ </ul>
25
+ }
13
26
  </li>
14
27
  }
15
28
  </ul>
@@ -23,6 +36,7 @@
23
36
 
24
37
  <!-- ── Artículo principal ─────────────────────────────── -->
25
38
  <main class="manual-main" role="main" aria-label="Manual de usuario">
39
+ <h1 class="manual-titulo-principal">Manual de usuario</h1>
26
40
 
27
41
  @if (cargando) {
28
42
  <div class="manual-estado" role="status" aria-live="polite" aria-busy="true">
@@ -39,11 +53,15 @@
39
53
  </div>
40
54
  }
41
55
 
42
- @if (!cargando && !error) {
43
- <article #contenidoManual class="manual-articulo" [innerHTML]="contenido" (click)="manejarClicEnContenido($event)">
56
+ @if (!cargando && !error && seccionActiva) {
57
+ <article #contenidoManual class="manual-articulo" [innerHTML]="seccionActiva.html"
58
+ (click)="manejarClicEnContenido($event)">
44
59
  </article>
60
+ <button class="btn-volver-arriba" (click)="irAlInicio()" title="Volver arriba">
61
+ ↑ Volver arriba
62
+ </button>
45
63
  }
46
64
 
47
65
  </main>
48
66
 
49
- </div>
67
+ </div>
@@ -13,8 +13,25 @@ interface EntradaToc {
13
13
  id: string;
14
14
  }
15
15
 
16
+ interface Seccion {
17
+ id: string;
18
+ titulo: string;
19
+ html: SafeHtml;
20
+ subEntradas: EntradaToc[];
21
+ }
22
+
16
23
  const DIACRITICOS = new RegExp('[̀-ͯ]', 'g');
17
24
 
25
+ function generarId(texto: string): string {
26
+ return texto
27
+ .toLowerCase()
28
+ .normalize('NFD')
29
+ .replace(DIACRITICOS, '')
30
+ .replace(/[^a-z0-9\s-]/g, '')
31
+ .trim()
32
+ .replace(/\s+/g, '-');
33
+ }
34
+
18
35
  @Component({
19
36
  selector: 'app-manual',
20
37
  standalone: true,
@@ -23,18 +40,23 @@ const DIACRITICOS = new RegExp('[̀-ͯ]', 'g');
23
40
  styleUrl: './manual.component.css'
24
41
  })
25
42
  export class ManualComponent implements OnInit, OnDestroy {
26
- public contenido: SafeHtml = '';
27
- public toc: EntradaToc[] = [];
43
+ public secciones: Seccion[] = [];
44
+ public seccionActiva: Seccion | null = null;
28
45
  public cargando: boolean = true;
29
46
  public error: boolean = false;
30
47
  private markdownRaw: string = '';
31
48
  private _destroy$ = new Subject<void>();
32
49
  @ViewChild('contenidoManual') contenidoManualRef!: ElementRef;
33
50
 
34
- constructor(private http: HttpClient, private sanitizer: DomSanitizer, private datosGlobalesService: DatosGlobalesService) { }
51
+ constructor(
52
+ private http: HttpClient,
53
+ private sanitizer: DomSanitizer,
54
+ private datosGlobalesService: DatosGlobalesService
55
+ ) {}
35
56
 
36
57
  ngOnInit(): void {
37
- this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/VistaDelManual`).pipe(takeUntil(this._destroy$)).subscribe({ error: () => { } });
58
+ this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/VistaDelManual`)
59
+ .pipe(takeUntil(this._destroy$)).subscribe({ error: () => {} });
38
60
 
39
61
  this.http.get('/Manual.md', { responseType: 'text' }).pipe(takeUntil(this._destroy$)).subscribe({
40
62
  next: (markdown) => {
@@ -44,21 +66,54 @@ export class ManualComponent implements OnInit, OnDestroy {
44
66
  const parser = new DOMParser();
45
67
  const doc = parser.parseFromString(html, 'text/html');
46
68
 
69
+ // Asignar IDs a todos los headings
47
70
  doc.querySelectorAll('h1, h2, h3').forEach((heading) => {
48
71
  const texto = heading.textContent ?? '';
49
- const id = texto
50
- .toLowerCase()
51
- .normalize('NFD')
52
- .replace(DIACRITICOS, '')
53
- .replace(/[^a-z0-9\s-]/g, '')
54
- .trim()
55
- .replace(/\s+/g, '-');
56
- heading.id = id;
57
- const nivel = parseInt(heading.tagName[1], 10);
58
- this.toc.push({ nivel, texto, id });
72
+ heading.id = generarId(texto);
59
73
  });
60
74
 
61
- this.contenido = this.sanitizer.bypassSecurityTrustHtml(doc.body.innerHTML);
75
+ // Dividir en secciones por h2
76
+ const secciones: Seccion[] = [];
77
+ let nodos: Node[] = [];
78
+ let tituloActual = '';
79
+ let idActual = '';
80
+ let subEntradasActuales: EntradaToc[] = [];
81
+
82
+ const guardarSeccion = () => {
83
+ if (!idActual) return;
84
+ const div = document.createElement('div');
85
+ nodos.forEach(n => div.appendChild(n.cloneNode(true)));
86
+ secciones.push({
87
+ id: idActual,
88
+ titulo: tituloActual,
89
+ html: this.sanitizer.bypassSecurityTrustHtml(div.innerHTML),
90
+ subEntradas: [...subEntradasActuales]
91
+ });
92
+ };
93
+
94
+ Array.from(doc.body.childNodes).forEach(node => {
95
+ const el = node as Element;
96
+ if (el.tagName === 'H2') {
97
+ guardarSeccion();
98
+ tituloActual = el.textContent ?? '';
99
+ idActual = generarId(tituloActual);
100
+ el.id = idActual;
101
+ nodos = [node];
102
+ subEntradasActuales = [];
103
+ } else if (el.tagName === 'H3' && idActual) {
104
+ const texto = el.textContent ?? '';
105
+ const id = generarId(texto);
106
+ el.id = id;
107
+ subEntradasActuales.push({ nivel: 3, texto, id });
108
+ nodos.push(node);
109
+ } else {
110
+ if (idActual) nodos.push(node);
111
+ }
112
+ });
113
+ guardarSeccion();
114
+
115
+ this.secciones = secciones;
116
+ this.seccionActiva = secciones[0] ?? null;
62
117
  this.cargando = false;
63
118
  },
64
119
  error: () => {
@@ -73,8 +128,21 @@ export class ManualComponent implements OnInit, OnDestroy {
73
128
  this._destroy$.complete();
74
129
  }
75
130
 
131
+ seleccionarSeccion(seccion: Seccion): void {
132
+ this.seccionActiva = seccion;
133
+ }
134
+
76
135
  irA(id: string): void {
77
- document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
136
+ // Buscar la sección que contiene ese id
137
+ const seccionContenedora = this.secciones.find(
138
+ s => s.id === id || s.subEntradas.some(e => e.id === id)
139
+ );
140
+ if (seccionContenedora) {
141
+ this.seccionActiva = seccionContenedora;
142
+ }
143
+ setTimeout(() => {
144
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
145
+ }, 50);
78
146
  }
79
147
 
80
148
  descargar(): void {
@@ -87,6 +155,15 @@ export class ManualComponent implements OnInit, OnDestroy {
87
155
  URL.revokeObjectURL(url);
88
156
  }
89
157
 
158
+ irAlInicio(): void {
159
+ const contenedor = document.querySelector('.zona-scrollable');
160
+ if (contenedor) {
161
+ contenedor.scrollTo({ top: 0, behavior: 'smooth' });
162
+ } else {
163
+ window.scrollTo({ top: 0, behavior: 'smooth' });
164
+ }
165
+ }
166
+
90
167
  manejarClicEnContenido(event: MouseEvent): void {
91
168
  const anchor = (event.target as Element).closest('a');
92
169
  if (!anchor) return;
@@ -1,5 +1,73 @@
1
+ /* Header */
2
+ .dialogo-header {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 10px;
6
+ background: linear-gradient(90deg, rgba(1, 33, 105, 1) 0%, rgba(63, 97, 171, 1) 100%);
7
+ color: white;
8
+ padding: 24px 20px 16px;
9
+ margin: -8px -8px 0;
10
+ }
11
+
12
+ .dialogo-icono {
13
+ font-size: 24px;
14
+ width: 24px;
15
+ height: 24px;
16
+ flex-shrink: 0;
17
+ }
18
+
19
+ .dialogo-titulo {
20
+ font-size: 1.1rem;
21
+ font-weight: 600;
22
+ font-family: 'Roboto', sans-serif;
23
+ letter-spacing: 0.2px;
24
+ }
25
+
26
+ /* Mensaje */
27
+ .dialogo-mensaje {
28
+ margin: 20px 0 12px;
29
+ font-size: 0.95rem;
30
+ color: #333;
31
+ font-family: 'Roboto', sans-serif;
32
+ line-height: 1.5;
33
+ }
34
+
35
+ /* Campo de comentario */
1
36
  .mensaje {
2
- margin-top: 1%;
37
+ width: 100%;
38
+ margin-top: 4px;
39
+ }
40
+
41
+ /* Botones */
42
+ mat-dialog-actions {
3
43
  display: flex;
4
- justify-content: center;
5
- }
44
+ justify-content: flex-end;
45
+ gap: 10px;
46
+ padding: 8px 20px 16px;
47
+ }
48
+
49
+ .btn-cancelar {
50
+ color: #012169;
51
+ border-color: #012169;
52
+ font-family: 'Roboto', sans-serif;
53
+ font-weight: 500;
54
+ }
55
+
56
+ .btn-aceptar {
57
+ background-color: #012169;
58
+ color: white;
59
+ font-family: 'Roboto', sans-serif;
60
+ font-weight: 500;
61
+ }
62
+
63
+ .btn-aceptar:disabled {
64
+ background-color: #a0b0c4;
65
+ color: white;
66
+ }
67
+
68
+ /* Pie */
69
+ .dialogo-pie {
70
+ height: 20px;
71
+ background: linear-gradient(90deg, rgba(1, 33, 105, 1) 0%, rgba(63, 97, 171, 1) 100%);
72
+ margin: 0 -8px;
73
+ }