holygrail5 1.0.2

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.
package/src/guide.js ADDED
@@ -0,0 +1,1798 @@
1
+ // Generador de guía HTML desde JSON
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const { pxToRem, remToPx, getFontFamilyName } = require('./utils');
7
+ const { buildValueMap } = require('./parser');
8
+
9
+ // Lee los valores anteriores guardados en un archivo JSON
10
+ function loadPreviousValues(previousValuesPath) {
11
+ try {
12
+ if (fs.existsSync(previousValuesPath)) {
13
+ const content = fs.readFileSync(previousValuesPath, 'utf8');
14
+ return JSON.parse(content);
15
+ }
16
+ } catch (error) {
17
+ // Si no existe o hay error, devuelve null
18
+ }
19
+ return null;
20
+ }
21
+
22
+ // Guarda los valores actuales para la próxima comparación
23
+ function saveCurrentValues(currentValues, previousValuesPath) {
24
+ try {
25
+ const dir = path.dirname(previousValuesPath);
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+ fs.writeFileSync(previousValuesPath, JSON.stringify(currentValues, null, 2), 'utf8');
30
+ } catch (error) {
31
+ console.warn('⚠️ No se pudo guardar los valores anteriores:', error.message);
32
+ }
33
+ }
34
+
35
+ // Compara valores actuales con anteriores y devuelve un mapa de cambios
36
+ function getChangedValues(currentValues, previousValues) {
37
+ const changes = new Set();
38
+
39
+ // Si no hay valores previos, marca todo como nuevo (primera ejecución)
40
+ if (!previousValues) {
41
+ // Marca todas las variables como nuevas
42
+ if (currentValues.variables) {
43
+ Object.keys(currentValues.variables).forEach(varName => {
44
+ changes.add(`variable.${varName}`);
45
+ });
46
+ }
47
+ // Marca todos los breakpoints como nuevos
48
+ if (currentValues.breakpoints) {
49
+ changes.add('breakpoints.mobile');
50
+ changes.add('breakpoints.desktop');
51
+ }
52
+ // Marca todas las fuentes como nuevas
53
+ if (currentValues.fontFamilyMap) {
54
+ Object.keys(currentValues.fontFamilyMap).forEach(fontName => {
55
+ changes.add(`fontFamilyMap.${fontName}`);
56
+ });
57
+ }
58
+ // Marca todos los colores como nuevos
59
+ if (currentValues.colors) {
60
+ Object.keys(currentValues.colors).forEach(colorName => {
61
+ changes.add(`colors.${colorName}`);
62
+ });
63
+ }
64
+ // Marca todas las clases como nuevas
65
+ if (currentValues.classes) {
66
+ Object.keys(currentValues.classes).forEach(className => {
67
+ const cls = currentValues.classes[className];
68
+ ['fontFamily', 'fontWeight', 'letterSpacing', 'textTransform'].forEach(prop => {
69
+ if (cls[prop] !== undefined) {
70
+ changes.add(`${className}.${prop}`);
71
+ }
72
+ });
73
+ ['mobile', 'desktop'].forEach(bp => {
74
+ if (cls[bp]) {
75
+ if (cls[bp].fontSize) changes.add(`${className}.${bp}.fontSize`);
76
+ if (cls[bp].lineHeight) changes.add(`${className}.${bp}.lineHeight`);
77
+ }
78
+ });
79
+ });
80
+ }
81
+ return changes;
82
+ }
83
+
84
+ // Compara breakpoints
85
+ if (currentValues.breakpoints) {
86
+ if (!previousValues.breakpoints) {
87
+ changes.add('breakpoints.mobile');
88
+ changes.add('breakpoints.desktop');
89
+ } else {
90
+ if (currentValues.breakpoints.mobile !== previousValues.breakpoints.mobile) {
91
+ changes.add('breakpoints.mobile');
92
+ }
93
+ if (currentValues.breakpoints.desktop !== previousValues.breakpoints.desktop) {
94
+ changes.add('breakpoints.desktop');
95
+ }
96
+ }
97
+ }
98
+
99
+ // Compara fontFamilyMap
100
+ if (currentValues.fontFamilyMap) {
101
+ const currentFontMap = currentValues.fontFamilyMap;
102
+ const previousFontMap = previousValues.fontFamilyMap || {};
103
+
104
+ // Compara cada fuente en el mapa
105
+ Object.keys(currentFontMap).forEach(fontName => {
106
+ const currentVal = currentFontMap[fontName];
107
+ const previousVal = previousFontMap[fontName];
108
+
109
+ if (currentVal !== previousVal) {
110
+ changes.add(`fontFamilyMap.${fontName}`);
111
+ }
112
+ });
113
+
114
+ // Detecta fuentes eliminadas
115
+ Object.keys(previousFontMap).forEach(fontName => {
116
+ if (!currentFontMap[fontName]) {
117
+ changes.add(`fontFamilyMap.${fontName}`);
118
+ }
119
+ });
120
+ }
121
+
122
+ // Compara spacingMap
123
+ if (currentValues.spacingMap) {
124
+ const currentSpacingMap = currentValues.spacingMap;
125
+ const previousSpacingMap = previousValues.spacingMap || {};
126
+
127
+ // Compara cada valor de spacing en el mapa
128
+ Object.keys(currentSpacingMap).forEach(spacingKey => {
129
+ const currentVal = currentSpacingMap[spacingKey];
130
+ const previousVal = previousSpacingMap[spacingKey];
131
+
132
+ if (currentVal !== previousVal) {
133
+ changes.add(`spacingMap.${spacingKey}`);
134
+ }
135
+ });
136
+
137
+ // Detecta valores de spacing eliminados
138
+ Object.keys(previousSpacingMap).forEach(spacingKey => {
139
+ if (!currentSpacingMap[spacingKey]) {
140
+ changes.add(`spacingMap.${spacingKey}`);
141
+ }
142
+ });
143
+ }
144
+
145
+ // Compara colors
146
+ if (currentValues.colors) {
147
+ const currentColors = currentValues.colors;
148
+ const previousColors = previousValues.colors || {};
149
+
150
+ // Compara cada color en el mapa
151
+ Object.keys(currentColors).forEach(colorKey => {
152
+ const currentVal = currentColors[colorKey];
153
+ const previousVal = previousColors[colorKey];
154
+
155
+ if (currentVal !== previousVal) {
156
+ changes.add(`colors.${colorKey}`);
157
+ }
158
+ });
159
+
160
+ // Detecta colores eliminados
161
+ Object.keys(previousColors).forEach(colorKey => {
162
+ if (!currentColors[colorKey]) {
163
+ changes.add(`colors.${colorKey}`);
164
+ }
165
+ });
166
+ }
167
+
168
+ // Compara cada clase
169
+ const currentClasses = currentValues.classes || currentValues;
170
+ const previousClasses = previousValues.classes || previousValues;
171
+
172
+ Object.keys(currentClasses).forEach(className => {
173
+ const current = currentClasses[className];
174
+ const previous = previousClasses[className];
175
+
176
+ if (!previous) {
177
+ // Nueva clase, marca todo como cambiado
178
+ Object.keys(current).forEach(prop => {
179
+ if (prop !== 'mobile' && prop !== 'desktop') {
180
+ changes.add(`${className}.${prop}`);
181
+ }
182
+ });
183
+ return;
184
+ }
185
+
186
+ // Compara propiedades base
187
+ ['fontFamily', 'fontWeight', 'letterSpacing', 'textTransform'].forEach(prop => {
188
+ const currentVal = current[prop];
189
+ const previousVal = previous[prop];
190
+ if (currentVal !== previousVal) {
191
+ changes.add(`${className}.${prop}`);
192
+ }
193
+ });
194
+
195
+ // Compara propiedades de breakpoints
196
+ ['mobile', 'desktop'].forEach(bp => {
197
+ if (current[bp]) {
198
+ if (!previous[bp]) {
199
+ // Nuevo breakpoint
200
+ if (current[bp].fontSize) changes.add(`${className}.${bp}.fontSize`);
201
+ if (current[bp].lineHeight) changes.add(`${className}.${bp}.lineHeight`);
202
+ } else {
203
+ // Compara fontSize y lineHeight
204
+ if (current[bp].fontSize !== previous[bp]?.fontSize) {
205
+ changes.add(`${className}.${bp}.fontSize`);
206
+ }
207
+ if (current[bp].lineHeight !== previous[bp]?.lineHeight) {
208
+ changes.add(`${className}.${bp}.lineHeight`);
209
+ }
210
+ }
211
+ }
212
+ });
213
+ });
214
+
215
+ // Compara variables CSS compartidas
216
+ if (currentValues.variables) {
217
+ const currentVars = currentValues.variables;
218
+ const previousVars = previousValues.variables || {};
219
+
220
+ // Detecta nuevas variables o variables con valores cambiados
221
+ Object.keys(currentVars).forEach(varName => {
222
+ const currentVal = currentVars[varName];
223
+ const previousVal = previousVars[varName];
224
+
225
+ // Si no existía antes o el valor cambió, marca como cambiado
226
+ if (previousVal === undefined || currentVal !== previousVal) {
227
+ changes.add(`variable.${varName}`);
228
+ }
229
+ });
230
+ }
231
+
232
+ return changes;
233
+ }
234
+
235
+ // Obtiene el autor del último commit de git
236
+ function getLastCommitAuthor() {
237
+ try {
238
+ const authorName = execSync('git log -1 --pretty=format:"%an"', {
239
+ encoding: 'utf8',
240
+ cwd: path.join(__dirname, '..'),
241
+ stdio: ['ignore', 'pipe', 'ignore']
242
+ }).trim();
243
+ return authorName || null;
244
+ } catch (error) {
245
+ // Si no es un repo git o hay error, devolver null
246
+ return null;
247
+ }
248
+ }
249
+
250
+ // Obtiene la versión del package.json
251
+ function getPackageVersion() {
252
+ try {
253
+ const packagePath = path.join(__dirname, '..', 'package.json');
254
+ if (fs.existsSync(packagePath)) {
255
+ const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
256
+ return packageData.version || null;
257
+ }
258
+ } catch (error) {
259
+ // Si hay error, devolver null
260
+ }
261
+ return null;
262
+ }
263
+
264
+ function generateHTML(configData, previousValuesPath = null) {
265
+ const classNames = Object.keys(configData.classes);
266
+ const prefix = configData.prefix || 'hg';
267
+ const category = configData.category || 'typo';
268
+ const baseFontSize = configData.baseFontSize || 16;
269
+
270
+ // Obtener autor del último commit
271
+ const lastCommitAuthor = getLastCommitAuthor();
272
+ // Obtener versión del package.json
273
+ const packageVersion = getPackageVersion();
274
+
275
+ // Construir variables CSS primero para poder guardarlas
276
+ const { fontFamilyVars, lineHeightVars, fontWeightVars, letterSpacingVars, textTransformVars, fontSizeVars } =
277
+ buildValueMap(configData.classes, configData.fontFamilyMap, prefix, category);
278
+
279
+ // Generar variables de spacing
280
+ const { generateSpacingVariables } = require('./parser');
281
+ const spacingVars = generateSpacingVariables(configData.spacingMap, prefix, baseFontSize);
282
+
283
+ // Generar variables de colores
284
+ const { generateColorVariables } = require('./parser');
285
+ const colorVars = generateColorVariables(configData.colors, prefix);
286
+
287
+ // Construir array de variables (incluyendo spacing y colores)
288
+ const allVariables = [
289
+ ...Array.from(fontFamilyVars.values()),
290
+ ...Array.from(lineHeightVars.values()),
291
+ ...Array.from(fontWeightVars.values()),
292
+ ...Array.from(letterSpacingVars.values()),
293
+ ...Array.from(textTransformVars.values()),
294
+ ...Array.from(fontSizeVars.values()),
295
+ ...spacingVars,
296
+ ...colorVars
297
+ ].map(item => ({ name: item.varName, value: item.value }));
298
+
299
+ // Preparar valores actuales para comparación
300
+ const currentValues = {
301
+ breakpoints: {
302
+ mobile: configData.breakpoints.mobile,
303
+ desktop: configData.breakpoints.desktop
304
+ },
305
+ fontFamilyMap: configData.fontFamilyMap || {},
306
+ spacingMap: configData.spacingMap || {},
307
+ colors: configData.colors || {},
308
+ classes: {},
309
+ variables: {}
310
+ };
311
+
312
+ // Guardar variables CSS en currentValues
313
+ allVariables.forEach(variable => {
314
+ currentValues.variables[variable.name] = variable.value;
315
+ });
316
+
317
+ classNames.forEach(className => {
318
+ const cls = configData.classes[className];
319
+ currentValues.classes[className] = {
320
+ fontFamily: cls.fontFamily,
321
+ fontWeight: cls.fontWeight,
322
+ letterSpacing: cls.letterSpacing,
323
+ textTransform: cls.textTransform,
324
+ mobile: {
325
+ fontSize: cls.mobile?.fontSize,
326
+ lineHeight: cls.mobile?.lineHeight
327
+ },
328
+ desktop: {
329
+ fontSize: cls.desktop?.fontSize,
330
+ lineHeight: cls.desktop?.lineHeight
331
+ }
332
+ };
333
+ });
334
+
335
+ // Cargar valores anteriores y detectar cambios
336
+ const previousValuesPathDefault = previousValuesPath || path.join(__dirname, '..', '.data', '.previous-values.json');
337
+ const previousValues = loadPreviousValues(previousValuesPathDefault);
338
+ const changedValues = getChangedValues(currentValues, previousValues);
339
+
340
+ // Guardar valores actuales para la próxima vez
341
+ saveCurrentValues(currentValues, previousValuesPathDefault);
342
+
343
+ // Función auxiliar para verificar si un valor cambió
344
+ function isChanged(className, prop, breakpoint = null) {
345
+ const key = breakpoint ? `${className}.${breakpoint}.${prop}` : `${className}.${prop}`;
346
+ return changedValues.has(key);
347
+ }
348
+
349
+ // Generar tabla de clases
350
+ const tableRows = classNames.map(className => {
351
+ const cls = configData.classes[className];
352
+ const fontFamilyName = getFontFamilyName(cls.fontFamily, configData.fontFamilyMap);
353
+
354
+ const fontFamilyChanged = isChanged(className, 'fontFamily');
355
+ const fontWeightChanged = isChanged(className, 'fontWeight');
356
+ const letterSpacingChanged = isChanged(className, 'letterSpacing');
357
+ const textTransformChanged = isChanged(className, 'textTransform');
358
+ const mobileFontSizeChanged = isChanged(className, 'fontSize', 'mobile');
359
+ const mobileLineHeightChanged = isChanged(className, 'lineHeight', 'mobile');
360
+ const desktopFontSizeChanged = isChanged(className, 'fontSize', 'desktop');
361
+ const desktopLineHeightChanged = isChanged(className, 'lineHeight', 'desktop');
362
+
363
+ return `
364
+ <tr>
365
+ <td class="table-name">.${className}</td>
366
+ <td class="preview-cell">
367
+ <div class="typography-preview ${className}">Aa</div>
368
+ </td>
369
+ <td class="table-value ${fontFamilyChanged ? 'changed' : ''}">${fontFamilyName || cls.fontFamily || '-'}</td>
370
+ <td class="table-value ${fontWeightChanged ? 'changed' : ''}">${cls.fontWeight || '-'}</td>
371
+ <td class="table-value ${letterSpacingChanged ? 'changed' : ''}">${cls.letterSpacing || '-'}</td>
372
+ <td class="table-value ${textTransformChanged ? 'changed' : ''}">${cls.textTransform || '-'}</td>
373
+ <td class="mobile-value ${mobileFontSizeChanged ? 'changed' : ''}">${cls.mobile?.fontSize ? pxToRem(cls.mobile.fontSize, baseFontSize) : '-'}</td>
374
+ <td class="mobile-value ${mobileLineHeightChanged ? 'changed' : ''}">${cls.mobile?.lineHeight || '-'}</td>
375
+ <td class="desktop-value ${desktopFontSizeChanged ? 'changed' : ''}">${cls.desktop?.fontSize ? pxToRem(cls.desktop.fontSize, baseFontSize) : '-'}</td>
376
+ <td class="desktop-value ${desktopLineHeightChanged ? 'changed' : ''}">${cls.desktop?.lineHeight || '-'}</td>
377
+ </tr>`;
378
+ }).join('');
379
+
380
+ const classesHTML = `
381
+ <div class="guide-table-wrapper">
382
+ <table class="guide-table">
383
+ <thead>
384
+ <tr>
385
+ <th>Clase</th>
386
+ <th>Preview</th>
387
+ <th>Font Family</th>
388
+ <th>Font Weight</th>
389
+ <th>Letter Spacing</th>
390
+ <th>Text Transform</th>
391
+ <th colspan="2" class="mobile-header">Mobile</th>
392
+ <th colspan="2" class="desktop-header">Desktop</th>
393
+ </tr>
394
+ <tr class="sub-header">
395
+ <th colspan="6"></th>
396
+ <th class="mobile-value">Font Size</th>
397
+ <th class="mobile-value">Line Height</th>
398
+ <th class="desktop-value">Font Size</th>
399
+ <th class="desktop-value">Line Height</th>
400
+ </tr>
401
+ </thead>
402
+ <tbody>
403
+ ${tableRows}
404
+ </tbody>
405
+ </table>
406
+ </div>`;
407
+
408
+ // Generar tabla de font families
409
+ const fontFamiliesHTML = configData.fontFamilyMap ? Object.entries(configData.fontFamilyMap).map(([name, value]) => {
410
+ const varName = `--${prefix}-${category}-font-family-${name}`;
411
+ const styleValue = value.replace(/'/g, "\\'");
412
+ const isChanged = changedValues.has(`fontFamilyMap.${name}`);
413
+ return `
414
+ <tr>
415
+ <td class="table-name">${name}</td>
416
+ <td class="font-family-preview" style='font-family: ${styleValue};'>Aa</td>
417
+ <td class="table-value ${isChanged ? 'changed' : ''}">${value}</td>
418
+ <td class="table-value">${varName}</td>
419
+ </tr>`;
420
+ }).join('') : '';
421
+
422
+ const fontFamiliesTableHTML = configData.fontFamilyMap ? `
423
+ <div class="guide-table-wrapper">
424
+ <table class="guide-table">
425
+ <thead>
426
+ <tr>
427
+ <th>Nombre</th>
428
+ <th>Preview</th>
429
+ <th>Valor</th>
430
+ <th>Variable CSS</th>
431
+ </tr>
432
+ </thead>
433
+ <tbody>
434
+ ${fontFamiliesHTML}
435
+ </tbody>
436
+ </table>
437
+ </div>` : '';
438
+
439
+ // Generar tabla de variables
440
+ const variableRows = allVariables.map(variable => {
441
+ const remValue = variable.value.match(/^([\d.]+)rem$/) ? variable.value : '-';
442
+ const pxValue = remValue !== '-' ? remToPx(variable.value, baseFontSize) : '-';
443
+ const isVariableChanged = changedValues.has(`variable.${variable.name}`);
444
+
445
+ return `
446
+ <tr>
447
+ <td class="table-name ${isVariableChanged ? 'changed' : ''}">${variable.name}</td>
448
+ <td class="table-value ${isVariableChanged ? 'changed' : ''}">${variable.value}</td>
449
+ <td class="value-center-blue ${isVariableChanged ? 'changed' : ''}">${remValue}</td>
450
+ <td class="value-center-orange ${isVariableChanged ? 'changed' : ''}">${pxValue}</td>
451
+ </tr>`;
452
+ }).join('');
453
+
454
+ const variablesTableHTML = `
455
+ <div class="guide-table-wrapper">
456
+ <table class="guide-table">
457
+ <thead>
458
+ <tr>
459
+ <th>Variable CSS</th>
460
+ <th>Valor</th>
461
+ <th>Rem</th>
462
+ <th>Píxeles</th>
463
+ </tr>
464
+ </thead>
465
+ <tbody>
466
+ ${variableRows}
467
+ </tbody>
468
+ </table>
469
+ </div>`;
470
+
471
+ // Generar tabla de spacing helpers
472
+ const spacingHelpersHTML = configData.spacingMap ? Object.entries(configData.spacingMap).map(([key, value]) => {
473
+ const hasImportant = configData.spacingImportant && configData.spacingImportant.includes(key);
474
+ const importantLabel = hasImportant ? '<br><strong>Con !important:</strong><br>.*-' + key + '!' : '';
475
+
476
+ const varName = `--${prefix}-spacing-${key}`;
477
+ // Si el valor termina en %, no lo convierte a rem
478
+ const remValue = value.endsWith('%') ? value : pxToRem(value, baseFontSize);
479
+ const pxValue = value;
480
+ const isChanged = changedValues.has(`spacingMap.${key}`);
481
+
482
+ return `
483
+ <tr>
484
+ <td class="table-name">.*-${key}${importantLabel}</td>
485
+ <td class="table-value ${isChanged ? 'changed' : ''}">${varName}</td>
486
+ <td class="value-center-blue ${isChanged ? 'changed' : ''}">${remValue}</td>
487
+ <td class="value-center-orange ${isChanged ? 'changed' : ''}">${pxValue}</td>
488
+ </tr>`;
489
+ }).join('') : '';
490
+
491
+ const spacingHelpersTableHTML = configData.spacingMap ? `
492
+ <div class="guide-table-wrapper">
493
+ <table class="guide-table">
494
+ <thead>
495
+ <tr>
496
+ <th>Clases Helper</th>
497
+ <th>Variable CSS</th>
498
+ <th>Valor (rem)</th>
499
+ <th>Valor (px)</th>
500
+ </tr>
501
+ </thead>
502
+ <tbody>
503
+ ${spacingHelpersHTML}
504
+ </tbody>
505
+ </table>
506
+ </div>` : '';
507
+
508
+ // Estilos CSS compartidos para tablas
509
+ const tableStyles = `
510
+ /* Estilos generales para todas las tablas */
511
+ .guide-table-wrapper {
512
+ overflow: auto;
513
+ }
514
+
515
+ .guide-table {
516
+ width: 100%;
517
+ border-collapse: collapse;
518
+ margin-top: 0rem;
519
+ background: white;
520
+ font-size: 0.875rem;
521
+ }
522
+
523
+ .guide-table th {
524
+ padding: 0.75rem;
525
+ text-align: left;
526
+ font-weight: 600;
527
+ font-size: 0.75rem;
528
+ letter-spacing: 0.05em;
529
+ border-bottom: 1px solid #ddd;
530
+ position: sticky;
531
+ top: 0;
532
+ background: #dcdcdc;
533
+ z-index: 10;
534
+ }
535
+
536
+ .guide-table td {
537
+ padding: 0.75rem;
538
+ border-bottom: 1px solid #e0e0e0;
539
+ vertical-align: middle;
540
+ }
541
+
542
+ .guide-table tbody tr:hover {
543
+ background: #f9f9f9;
544
+ }
545
+
546
+ /* Estilos para nombres/identificadores */
547
+ .guide-table .table-name {
548
+ font-weight: 600;
549
+ color: #000000;
550
+ font-family: 'Courier New', monospace;
551
+ }
552
+
553
+ /* Estilos para valores */
554
+ .guide-table .table-value {
555
+ font-family: 'Courier New', monospace;
556
+ color: #333;
557
+ }
558
+
559
+ /* Estilos para celdas cambiadas */
560
+ .guide-table td.changed {
561
+ background: #d4edda !important;
562
+ border-left: 3px solid #28a745;
563
+ font-weight: 600;
564
+ }
565
+
566
+ /* Estilos específicos de tipografía */
567
+ .guide-table th.mobile-header {
568
+ background: #e6f2ff;
569
+ color: #000000;
570
+
571
+ }
572
+
573
+ .guide-table th.desktop-header {
574
+ background: #fff4e6;
575
+ color: #cc6600;
576
+
577
+ }
578
+
579
+ .guide-table .sub-header th {
580
+ border-top: none;
581
+ border-bottom: 1px solid #ddd;
582
+ font-weight: 500;
583
+ font-size: 0.6875rem;
584
+ }
585
+
586
+ .guide-table .preview-cell {
587
+ }
588
+
589
+ .guide-table .typography-preview {
590
+ padding: 0.5rem;
591
+ font-size: inherit;
592
+ display: flex;
593
+ align-items: center;
594
+ justify-content: center;
595
+ min-height: 20px;
596
+
597
+ }
598
+
599
+ .guide-table .mobile-value {
600
+ background: #f0f8ff;
601
+ color: #000000;
602
+ font-weight: 500;
603
+
604
+ font-family: 'Courier New', monospace;
605
+ }
606
+
607
+ .guide-table .desktop-value {
608
+ background: #fff8f0;
609
+ color: #cc6600;
610
+ font-weight: 500;
611
+
612
+ font-family: 'Courier New', monospace;
613
+ }
614
+
615
+ .guide-table td.mobile-value.changed,
616
+ .guide-table td.desktop-value.changed {
617
+ background: #d4edda !important;
618
+ border-left: 3px solid #28a745;
619
+ }
620
+
621
+ /* Estilos para previews de fuente */
622
+ .guide-table .font-family-preview {
623
+ min-width: 100px;
624
+ padding: 0.75rem;
625
+ min-height: 50px;
626
+ font-size: 1.5rem;
627
+ font-weight: 600;
628
+ }
629
+
630
+ /* Estilos para valores centrados con color */
631
+ .guide-table .value-center-blue {
632
+ color: #000000;
633
+ font-weight: 500;
634
+
635
+ font-family: 'Courier New', monospace;
636
+ }
637
+
638
+ .guide-table .value-center-orange {
639
+ color: #cc6600;
640
+ font-weight: 500;
641
+
642
+ font-family: 'Courier New', monospace;
643
+ }
644
+
645
+ /* Estilos para grid de colores */
646
+ .colors-grid {
647
+ display: grid;
648
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
649
+ gap: 1.5rem;
650
+ margin-top: 2rem;
651
+ padding-inline: 0.5rem;
652
+ padding-bottom: 2rem;
653
+ }
654
+
655
+ .color-card {
656
+ background: white;
657
+ border: 1px solid #e0e0e0;
658
+ border-radius: 8px;
659
+ overflow: hidden;
660
+ transition: transform 0.2s, box-shadow 0.2s;
661
+ }
662
+
663
+ .color-card:hover {
664
+ transform: translateY(-2px);
665
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
666
+ }
667
+
668
+ .color-preview {
669
+ width: 100%;
670
+ height: 120px;
671
+ border-bottom: 1px solid #e0e0e0;
672
+ position: relative;
673
+ }
674
+
675
+ .color-pattern {
676
+ position: absolute;
677
+ top: 0;
678
+ left: 0;
679
+ right: 0;
680
+ bottom: 0;
681
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px);
682
+ pointer-events: none;
683
+ mix-blend-mode: overlay;
684
+ }
685
+
686
+ .color-card-content {
687
+ padding: 1rem;
688
+ }
689
+
690
+ .color-name {
691
+ font-weight: 600;
692
+ font-size: 0.875rem;
693
+ margin-bottom: 0.5rem;
694
+ color: #000;
695
+ }
696
+
697
+ .color-var-name {
698
+ font-size: 0.75rem;
699
+ color: #666;
700
+ margin-bottom: 0.5rem;
701
+ font-family: 'Courier New', monospace;
702
+ word-break: break-all;
703
+ }
704
+
705
+ .color-value {
706
+ font-size: 0.75rem;
707
+ color: #666;
708
+ font-family: 'Courier New', monospace;
709
+ }
710
+
711
+ .color-value.changed {
712
+ background: #d4edda;
713
+ padding: 0.25rem 0.5rem;
714
+ border-radius: 4px;
715
+ }
716
+
717
+ /* Estilos para sidebar header */
718
+ .sidebar-meta {
719
+ font-size: 0.75rem;
720
+ opacity: 0.6;
721
+ margin-top: 0.5rem;
722
+ }
723
+
724
+ .sidebar-meta-small {
725
+ font-size: 0.75rem;
726
+ opacity: 0.6;
727
+ margin-top: 0.25rem;
728
+ }
729
+
730
+ /* Estilos para búsqueda */
731
+ .search-container {
732
+ position: relative;
733
+ max-width: 500px;
734
+ padding-inline-start: 3rem;
735
+ }
736
+
737
+ .search-input {
738
+ width: 100%;
739
+ padding: 0.75rem 1rem 0.75rem 2.75rem;
740
+ border: 2px solid #e0e0e0;
741
+ border-radius: 8px;
742
+ font-size: 1rem;
743
+ outline: none;
744
+ transition: border-color 0.2s;
745
+ }
746
+
747
+ .search-input:focus {
748
+ border-color: #0170e9;
749
+ }
750
+
751
+ .search-icon {
752
+ position: absolute;
753
+ left: 0.875rem;
754
+ top: 50%;
755
+ transform: translateY(-50%);
756
+ color: #999;
757
+ pointer-events: none;
758
+ }
759
+
760
+ .clear-search-btn {
761
+ position: absolute;
762
+ right: 0.5rem;
763
+ top: 50%;
764
+ transform: translateY(-50%);
765
+ background: none;
766
+ border: none;
767
+ color: #999;
768
+ cursor: pointer;
769
+ padding: 0.25rem;
770
+ display: none;
771
+ font-size: 1.25rem;
772
+ line-height: 1;
773
+ }
774
+
775
+ .search-results {
776
+ margin-top: 0.5rem;
777
+ font-size: 0.875rem;
778
+ color: #666;
779
+ display: none;
780
+ }
781
+
782
+ /* Estilos para secciones */
783
+ .section-description {
784
+ margin-top: 1rem;
785
+ letter-spacing: 0;
786
+ }
787
+
788
+ /* Estilos para info boxes */
789
+ .info-box {
790
+ margin-bottom: 2rem;
791
+ padding: 1.5rem 0;
792
+
793
+ }
794
+
795
+ .info-box-warning {
796
+
797
+ }
798
+
799
+ .info-box-info {
800
+
801
+ }
802
+
803
+ .info-box-title {
804
+ margin: 0 0 1rem 0;
805
+ font-size: 1.125rem;
806
+ font-weight: 700;
807
+ }
808
+
809
+ .info-box-title-warning {
810
+ color: #ff9800;
811
+ }
812
+
813
+ .info-box-title-info {
814
+ color: #0170e9;
815
+ }
816
+
817
+ .info-box-text {
818
+ margin: 0 0 0.75rem 0;
819
+ line-height: 1.6;
820
+ }
821
+
822
+ .info-box-list {
823
+ margin: 0 0 0.75rem 0;
824
+ padding-top: 1.5rem;
825
+ line-height: 1.8;
826
+ }
827
+
828
+ .info-box-list-item {
829
+ margin-bottom: 0.5rem;
830
+ }
831
+
832
+ .info-box-code {
833
+ background: #fff8f0;
834
+ padding: 0.125rem 0.375rem;
835
+ border-radius: 3px;
836
+ font-family: 'Courier New', monospace;
837
+ font-size: 0.875rem;
838
+ }
839
+
840
+ .info-box-code-info {
841
+ background: #e6f2ff;
842
+ padding: 0.125rem 0.375rem;
843
+ border-radius: 3px;
844
+ font-family: 'Courier New', monospace;
845
+ font-size: 0.875rem;
846
+ }
847
+
848
+ .info-box-text-small {
849
+ margin: 0;
850
+ line-height: 1.6;
851
+ font-size: 0.875rem;
852
+ opacity: 0.8;
853
+ }
854
+
855
+ .info-box-margin-top {
856
+ margin-top: 2rem;
857
+ }
858
+
859
+ .search-highlight {
860
+ background: #ffeb3b;
861
+ padding: 0.125rem 0.25rem;
862
+ border-radius: 3px;
863
+ }
864
+
865
+ /* Estilos para diagrama de spacing */
866
+ .spacing-diagram {
867
+ width: 50%;
868
+
869
+
870
+ border-radius: 8px;
871
+ display: flex;
872
+ justify-content: center;
873
+ align-items: center;
874
+ position: relative;
875
+ }
876
+ .spacing-text{
877
+ width: 50%;
878
+ }
879
+
880
+ .spacing-diagram-container {
881
+ position: relative;
882
+ width: 100%;
883
+ max-width: 400px;
884
+ height: 300px;
885
+ }
886
+
887
+ .spacing-margin-box {
888
+ position: absolute;
889
+ top: 50%;
890
+ left: 50%;
891
+ transform: translate(-50%, -50%);
892
+ width: 80%;
893
+ height: 70%;
894
+ border: 3px dashed #ff9800;
895
+ background: rgba(255, 152, 0, 0.05);
896
+ border-radius: 4px;
897
+ }
898
+
899
+ .spacing-padding-box {
900
+ position: absolute;
901
+ top: 50%;
902
+ left: 50%;
903
+ transform: translate(-50%, -50%);
904
+ width: 60%;
905
+ height: 50%;
906
+ border: 3px dashed #0170e9;
907
+ background: rgba(1, 112, 233, 0.05);
908
+ border-radius: 4px;
909
+ }
910
+
911
+ .spacing-content {
912
+ position: absolute;
913
+ top: 50%;
914
+ left: 50%;
915
+ transform: translate(-50%, -50%);
916
+ width: 40%;
917
+ height: 30%;
918
+ background: #333;
919
+ border-radius: 4px;
920
+ display: flex;
921
+ align-items: center;
922
+ justify-content: center;
923
+ color: white;
924
+ font-size: 0.75rem;
925
+ font-weight: 600;
926
+ }
927
+
928
+ .spacing-label {
929
+ position: absolute;
930
+ font-size: 0.875rem;
931
+ font-weight: 600;
932
+ font-family: 'Courier New', monospace;
933
+ color: #333;
934
+ background: white;
935
+ padding: 0.25rem 0.5rem;
936
+ border-radius: 4px;
937
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
938
+ white-space: nowrap;
939
+ }
940
+
941
+ .spacing-label-top {
942
+ top: 0;
943
+ left: 50%;
944
+ transform: translateX(-50%);
945
+ }
946
+
947
+ .spacing-label-bottom {
948
+ bottom: 0;
949
+ left: 50%;
950
+ transform: translateX(-50%);
951
+ }
952
+
953
+ .spacing-label-left {
954
+ left: 0;
955
+ top: 50%;
956
+ transform: translateY(-50%);
957
+ }
958
+
959
+ .spacing-label-right {
960
+ right: 0;
961
+ top: 50%;
962
+ transform: translateY(-50%);
963
+ }
964
+
965
+ .spacing-label-margin {
966
+ color: #ff9800;
967
+ }
968
+
969
+ .spacing-label-padding {
970
+ color: #0170e9;
971
+ }
972
+
973
+ .spacing-label-padding-top {
974
+ top: 15%;
975
+ left: 50%;
976
+ transform: translateX(-50%);
977
+ }
978
+
979
+ .spacing-label-padding-right {
980
+ right: 10%;
981
+ top: 50%;
982
+ transform: translateY(-50%);
983
+ }
984
+
985
+ .spacing-label-padding-bottom {
986
+ bottom: 15%;
987
+ left: 50%;
988
+ transform: translateX(-50%);
989
+ }
990
+
991
+ .spacing-label-padding-left {
992
+ left: 10%;
993
+ top: 50%;
994
+ transform: translateY(-50%);
995
+ }`;
996
+
997
+ // Generar tabla de layout helpers
998
+ const layoutHelpersHTML = configData.helpers ? Object.entries(configData.helpers).flatMap(([helperName, config]) => {
999
+ const { property, class: className, responsive, values, useSpacing, description, explanation } = config;
1000
+ const helperDescription = description || explanation || '';
1001
+ const prefix = configData.prefix || 'hg';
1002
+ const baseFontSize = configData.baseFontSize || 16;
1003
+
1004
+ const rows = [];
1005
+
1006
+ if (useSpacing && configData.spacingMap) {
1007
+ Object.entries(configData.spacingMap).forEach(([key, value]) => {
1008
+ const baseClass = `.${prefix}-${className}-${key}`;
1009
+ const responsiveClass = responsive ? `.md:${prefix}-${className}-${key}` : '';
1010
+ const remValue = value.endsWith('%') ? value : pxToRem(value, baseFontSize);
1011
+
1012
+ rows.push(`
1013
+ <tr>
1014
+ <td class="table-name">${baseClass}</td>
1015
+ <td class="table-name">${responsiveClass || '-'}</td>
1016
+ <td class="table-value">${property}: ${remValue}</td>
1017
+ <td class="table-value">${helperDescription || '-'}</td>
1018
+ </tr>`);
1019
+ });
1020
+ } else if (values) {
1021
+ if (Array.isArray(values)) {
1022
+ values.forEach(value => {
1023
+ const baseClass = `.${prefix}-${className}-${value}`;
1024
+ const responsiveClass = responsive ? `.md:${prefix}-${className}-${value}` : '';
1025
+
1026
+ rows.push(`
1027
+ <tr>
1028
+ <td class="table-name">${baseClass}</td>
1029
+ <td class="table-name">${responsiveClass || '-'}</td>
1030
+ <td class="table-value">${property}: ${value}</td>
1031
+ <td class="table-value">${helperDescription || '-'}</td>
1032
+ </tr>`);
1033
+ });
1034
+ } else {
1035
+ Object.entries(values).forEach(([key, value]) => {
1036
+ const baseClass = `.${prefix}-${className}-${key}`;
1037
+ const responsiveClass = responsive ? `.md:${prefix}-${className}-${key}` : '';
1038
+
1039
+ rows.push(`
1040
+ <tr>
1041
+ <td class="table-name">${baseClass}</td>
1042
+ <td class="table-name">${responsiveClass || '-'}</td>
1043
+ <td class="table-value">${property}: ${value}</td>
1044
+ <td class="table-value">${helperDescription || '-'}</td>
1045
+ </tr>`);
1046
+ });
1047
+ }
1048
+ }
1049
+
1050
+ return rows;
1051
+ }).join('') : '';
1052
+
1053
+ const layoutHelpersTableHTML = configData.helpers ? `
1054
+ <div class="guide-table-wrapper">
1055
+ <table class="guide-table">
1056
+ <thead>
1057
+ <tr>
1058
+ <th>Clases Helper</th>
1059
+ <th>Clases Helper (md:)</th>
1060
+ <th>Propiedad CSS</th>
1061
+ <th>Descripción</th>
1062
+ </tr>
1063
+ </thead>
1064
+ <tbody>
1065
+ ${layoutHelpersHTML}
1066
+ </tbody>
1067
+ </table>
1068
+ </div>` : '';
1069
+
1070
+ const colorsGridHTML = configData.colors ? `
1071
+ <div class="colors-grid">
1072
+ ${Object.entries(configData.colors).map(([key, value]) => {
1073
+ const varName = `--${prefix}-color-${key}`;
1074
+ const isChanged = changedValues.has(`colors.${key}`);
1075
+ const normalizedValue = value.trim().toLowerCase();
1076
+ const isLight = normalizedValue === '#ffffff' || normalizedValue === '#f0f0f0' || normalizedValue === '#f4f2ed' || normalizedValue === '#e3e3e3';
1077
+ // Asegurar que el valor del color sea opaco (sin alfa)
1078
+ const opaqueValue = normalizedValue.length === 7 ? normalizedValue : (normalizedValue.length === 9 ? normalizedValue.substring(0, 7) : normalizedValue);
1079
+ return `
1080
+ <div class="color-card">
1081
+ <div class="color-preview" style="background-color: ${opaqueValue};">
1082
+ ${isLight ? `<div class="color-pattern"></div>` : ''}
1083
+ </div>
1084
+ <div class="color-card-content">
1085
+ <div class="color-name">${key}</div>
1086
+ <div class="color-var-name">${varName}</div>
1087
+ <div class="color-value ${isChanged ? 'changed' : ''}">${value}</div>
1088
+ </div>
1089
+ </div>`;
1090
+ }).join('')}
1091
+ </div>` : '';
1092
+
1093
+ // Construir menú lateral
1094
+ const menuItems = [];
1095
+ if (colorsGridHTML) {
1096
+ menuItems.push({ id: 'colors', label: 'Colores' });
1097
+ }
1098
+ if (fontFamiliesTableHTML) {
1099
+ menuItems.push({ id: 'font-families', label: 'Font Families' });
1100
+ }
1101
+ menuItems.push(
1102
+ { id: 'tipografia', label: 'Tipografía' },
1103
+ { id: 'variables', label: 'Variables CSS' }
1104
+ );
1105
+ if (spacingHelpersTableHTML) {
1106
+ menuItems.push({ id: 'spacing', label: 'Helpers de Spacing' });
1107
+ }
1108
+ if (layoutHelpersTableHTML) {
1109
+ menuItems.push({ id: 'layout', label: 'Helpers de Layout' });
1110
+ }
1111
+ menuItems.push({ id: 'breakpoints', label: 'Breakpoints' });
1112
+
1113
+ const menuHTML = menuItems.map(item => `
1114
+ <a href="#${item.id}" class="menu-item" data-section="${item.id}">${item.label}</a>
1115
+ `).join('');
1116
+
1117
+ return `<!DOCTYPE html>
1118
+ <html lang="es">
1119
+ <head>
1120
+ <meta charset="UTF-8">
1121
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1122
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
1123
+ <meta http-equiv="Pragma" content="no-cache">
1124
+ <meta http-equiv="Expires" content="0">
1125
+ <title>HolyGrail5 - Guía de Tipografía</title>
1126
+ <link rel="stylesheet" href="output.css?v=${Date.now()}">
1127
+ <style>
1128
+ * {
1129
+ scroll-behavior: smooth;
1130
+ }
1131
+
1132
+ body {
1133
+ font-family: var(--${prefix}-${category}-font-family-primary);
1134
+ margin: 0;
1135
+ padding: 0;
1136
+
1137
+ display: flex;
1138
+ }
1139
+
1140
+ .sidebar {
1141
+ position: fixed;
1142
+ left: 0;
1143
+ top: 0;
1144
+ width: 250px;
1145
+ height: 100vh;
1146
+ background: white;
1147
+ border-right: 1px solid #e0e0e0;
1148
+ padding: 2rem 0;
1149
+ overflow-y: auto;
1150
+ z-index: 100;
1151
+ box-shadow: 2px 0 8px rgba(0,0,0,0.05);
1152
+ }
1153
+
1154
+ .sidebar-header {
1155
+ padding: 0 1.5rem 2rem 1.5rem;
1156
+ border-bottom: 1px solid #e0e0e0;
1157
+ margin-bottom: 1rem;
1158
+ }
1159
+
1160
+ .sidebar-header h2 {
1161
+ margin: 0;
1162
+ font-size: 1.25rem;
1163
+ font-weight: 700;
1164
+ color: #000;
1165
+ }
1166
+
1167
+ .sidebar-nav {
1168
+ padding: 0 1rem;
1169
+ }
1170
+
1171
+ .menu-item {
1172
+ display: block;
1173
+ padding: 0.75rem 1rem;
1174
+ margin-bottom: 0.25rem;
1175
+ color: #666;
1176
+ text-decoration: none;
1177
+ border-radius: 6px;
1178
+ transition: all 0.2s ease;
1179
+ font-size: 0.875rem;
1180
+ font-weight: 500;
1181
+ }
1182
+
1183
+ .menu-item:hover {
1184
+ background: #f0f0f0;
1185
+ color: #000;
1186
+ }
1187
+
1188
+ .menu-item.active {
1189
+ color: black;
1190
+ }
1191
+
1192
+ .main-content {
1193
+ margin-left: 250px;
1194
+ flex: 1;
1195
+ padding: 0;
1196
+ padding-bottom: 10rem;
1197
+
1198
+
1199
+ max-width: calc(100% - 250px);
1200
+ }
1201
+
1202
+ .menu-toggle {
1203
+ display: none;
1204
+ position: fixed;
1205
+ top: 1rem;
1206
+ left: 1rem;
1207
+ z-index: 101;
1208
+ background: white;
1209
+ border: 1px solid #e0e0e0;
1210
+ padding: 0.5rem 0.75rem;
1211
+ border-radius: 4px;
1212
+ cursor: pointer;
1213
+ font-size: 1.25rem;
1214
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1215
+ }
1216
+
1217
+ @media (max-width: 768px) {
1218
+ .sidebar {
1219
+ transform: translateX(-100%);
1220
+ transition: transform 0.3s ease;
1221
+ }
1222
+
1223
+ .sidebar.open {
1224
+ transform: translateX(0);
1225
+ }
1226
+
1227
+ .main-content {
1228
+ margin-left: 0;
1229
+ max-width: 100%;
1230
+ padding: 1rem 0;
1231
+ }
1232
+
1233
+ .menu-toggle {
1234
+ display: block;
1235
+ }
1236
+ }
1237
+
1238
+ .header {
1239
+ position: sticky;
1240
+ top: 0;
1241
+ z-index: 50;
1242
+ background: #f5f5f5;
1243
+ padding: 1rem;
1244
+
1245
+ border-bottom: 2px solid #000;
1246
+
1247
+
1248
+ }
1249
+
1250
+ .header h1 {
1251
+ margin: 0;
1252
+ font-size: 2.5rem;
1253
+ font-weight: 900;
1254
+ }
1255
+
1256
+ .header p {
1257
+ margin: 1rem 0 0 0;
1258
+ opacity: 0.7;
1259
+ }
1260
+
1261
+ .section {
1262
+
1263
+ background: white;
1264
+ padding: 0rem;
1265
+ border-radius: 8px;
1266
+ }
1267
+
1268
+ .section-title {
1269
+ font-size: 1.5rem;
1270
+ font-weight: 700;
1271
+ padding-top: 6rem;
1272
+ padding-bottom: 2rem;
1273
+ padding-left: 1rem;
1274
+
1275
+ letter-spacing: -0.02em;
1276
+ background: #f5f5f5;
1277
+
1278
+ }
1279
+
1280
+ .section-content {
1281
+ padding-inline: 1rem;
1282
+ }
1283
+
1284
+ .section-content > .guide-table-wrapper {
1285
+ margin-inline: -1rem;
1286
+ }
1287
+
1288
+ .section.section--highlighted {
1289
+ background: #fff;
1290
+ }
1291
+ ${tableStyles}
1292
+ </style>
1293
+ </head>
1294
+ <body>
1295
+ <button class="menu-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">☰</button>
1296
+
1297
+ <aside class="sidebar">
1298
+ <div class="sidebar-header">
1299
+ <h2>HolyGrail5</h2>
1300
+ <p class="text-m sidebar-meta">
1301
+ last update: ${new Date().toLocaleString('es-ES')}
1302
+ </p>
1303
+ ${packageVersion ? `
1304
+ <p class="text-m sidebar-meta-small">
1305
+ Version: ${packageVersion}
1306
+ </p>
1307
+ ` : ''}
1308
+ ${lastCommitAuthor ? `
1309
+ <p class="text-s sidebar-meta-small">
1310
+ Last user: ${lastCommitAuthor}
1311
+ </p>
1312
+ ` : ''}
1313
+ </div>
1314
+
1315
+ <nav class="sidebar-nav">
1316
+ ${menuHTML}
1317
+ </nav>
1318
+ </aside>
1319
+
1320
+ <main class="main-content">
1321
+ <div class="header">
1322
+
1323
+
1324
+
1325
+ <div class="search-container">
1326
+ <input
1327
+ type="text"
1328
+ id="search-input"
1329
+ class="search-input"
1330
+ placeholder="Buscar clases, variables, helpers..."
1331
+ autocomplete="off"
1332
+ />
1333
+ <svg
1334
+ class="search-icon"
1335
+ width="20"
1336
+ height="20"
1337
+ viewBox="0 0 24 24"
1338
+ fill="none"
1339
+ stroke="currentColor"
1340
+ stroke-width="2"
1341
+ >
1342
+ <circle cx="11" cy="11" r="8"></circle>
1343
+ <path d="m21 21-4.35-4.35"></path>
1344
+ </svg>
1345
+ <button
1346
+ id="clear-search"
1347
+ class="clear-search-btn"
1348
+ title="Limpiar búsqueda"
1349
+ >×</button>
1350
+ </div>
1351
+ <div id="search-results" class="search-results"></div>
1352
+ </div>
1353
+
1354
+ ${colorsGridHTML ? `
1355
+ <div class="section section--highlighted" id="colors">
1356
+ <div class="section-title">
1357
+ <h2 >Colores</h2>
1358
+ <p class="text-m section-description">
1359
+ Paleta de colores disponibles con sus variables CSS.
1360
+ </p>
1361
+ </div>
1362
+ <div class="section-content">
1363
+ ${colorsGridHTML}
1364
+ </div>
1365
+ </div>
1366
+ ` : ''}
1367
+
1368
+ ${fontFamiliesTableHTML ? `
1369
+ <div class="section" id="font-families">
1370
+ <div class="section-title">
1371
+ <h2 >Font Families</h2>
1372
+ <p class="text-m section-description">
1373
+ Font families disponibles para la tipografía.
1374
+ </p>
1375
+ </div>
1376
+ <div class="section-content">
1377
+ ${fontFamiliesTableHTML}
1378
+ </div>
1379
+ </div>
1380
+ ` : ''}
1381
+
1382
+ <div class="section" id="tipografia">
1383
+ <div class="section-title">
1384
+ <h2 >Clases de Tipografía</h2>
1385
+ <p class="text-m section-description">
1386
+ Clases de tipografía disponibles.
1387
+ </p>
1388
+ </div>
1389
+ <div class="section-content">
1390
+ ${classesHTML}
1391
+ </div>
1392
+ </div>
1393
+
1394
+ <div class="section" id="variables">
1395
+ <div class="section-title">
1396
+ <h2 >Variables CSS Compartidas</h2>
1397
+ <p class="text-m section-description">
1398
+ Variables CSS compartidas.
1399
+ </p>
1400
+ </div>
1401
+ <div class="section-content">
1402
+ ${variablesTableHTML}
1403
+ </div>
1404
+ </div>
1405
+
1406
+
1407
+ ${spacingHelpersTableHTML ? `
1408
+ <div class="section" id="spacing">
1409
+ <div class="section-title">
1410
+ <h2 >Helpers de Spacing</h2>
1411
+ <p class="text-m section-description">
1412
+ Clases helper para padding y margin basadas en el spacingMap.
1413
+ Usa las variables CSS definidas en :root.
1414
+ </p>
1415
+ </div>
1416
+ <div class="section-content">
1417
+ <div class="info-box info-box-warning hg-d-flex">
1418
+
1419
+
1420
+
1421
+
1422
+ <div class="spacing-text">
1423
+ <h3 class="info-box-title info-box-title-warning">¿Cómo se generan los helpers de espaciado?</h3>
1424
+ <p class="text-m info-box-text">
1425
+ La nomenclatura de las clases helper sigue un patrón simple:
1426
+ </p>
1427
+ <ul class="info-box-list">
1428
+ <li class="text-m info-box-list-item">
1429
+ <strong>Primera letra:</strong> tipo de spacing → <code class="info-box-code">p</code> (padding) o <code class="info-box-code">m</code> (margin)
1430
+ </li>
1431
+ <li class="text-m info-box-list-item">
1432
+ <strong>Segunda letra:</strong> dirección → <code class="info-box-code">t</code> (top), <code class="info-box-code">r</code> (right/end), <code class="info-box-code">b</code> (bottom), <code class="info-box-code">l</code> (left/start)
1433
+ </li>
1434
+ <li class="text-m info-box-list-item">
1435
+ <strong>Guión + valor:</strong> el valor del spacing → <code class="info-box-code">-4</code>, <code class="info-box-code">-16</code>, <code class="info-box-code">-50-percent</code>
1436
+ </li>
1437
+ </ul>
1438
+ <p class="text-m info-box-text">
1439
+ <strong>Ejemplos:</strong> <code class="info-box-code">.p-16</code> (padding all), <code class="info-box-code">.pt-8</code> (padding-top), <code class="info-box-code">.mr-4</code> (margin-right), <code class="info-box-code">.mb-0</code> (margin-bottom)
1440
+ </p>
1441
+ </div>
1442
+
1443
+
1444
+ <div class="spacing-diagram">
1445
+ <div class="spacing-diagram-container">
1446
+ <!-- Etiquetas de margin (exterior) -->
1447
+ <div class="spacing-label spacing-label-top spacing-label-margin">mt-</div>
1448
+ <div class="spacing-label spacing-label-right spacing-label-margin">mr-</div>
1449
+ <div class="spacing-label spacing-label-bottom spacing-label-margin">mb-</div>
1450
+ <div class="spacing-label spacing-label-left spacing-label-margin">ml-</div>
1451
+
1452
+ <!-- Caja de margin (exterior) -->
1453
+ <div class="spacing-margin-box"></div>
1454
+
1455
+ <!-- Etiquetas de padding (interior) -->
1456
+ <div class="spacing-label spacing-label-padding spacing-label-padding-top">pt-</div>
1457
+ <div class="spacing-label spacing-label-padding spacing-label-padding-right">pr-</div>
1458
+ <div class="spacing-label spacing-label-padding spacing-label-padding-bottom">pb-</div>
1459
+ <div class="spacing-label spacing-label-padding spacing-label-padding-left">pl-</div>
1460
+
1461
+ <!-- Caja de padding (interior) -->
1462
+ <div class="spacing-padding-box"></div>
1463
+
1464
+ <!-- Contenido -->
1465
+ <div class="spacing-content">Contenido</div>
1466
+ </div>
1467
+ </div>
1468
+
1469
+
1470
+ </div>
1471
+ ${spacingHelpersTableHTML}
1472
+ <div class="info-box info-box-info info-box-margin-top">
1473
+ <h3 class="info-box-title info-box-title-info">Helpers con prefijo md: (Desktop)</h3>
1474
+ <p class="text-m info-box-text">
1475
+ Los helpers con prefijo <code class="info-box-code-info">md:</code> funcionan como en Tailwind CSS y solo se aplican en el breakpoint desktop (≥${configData.breakpoints.desktop}).
1476
+ </p>
1477
+ <p class="text-m info-box-text">
1478
+ <strong>Ejemplos de uso:</strong>
1479
+ </p>
1480
+ <ul class="info-box-list">
1481
+ <li class="text-m info-box-list-item">
1482
+ <code class="info-box-code-info">.p-4</code> - Aplica padding de 4px en todos los tamaños de pantalla
1483
+ </li>
1484
+ <li class="text-m info-box-list-item">
1485
+ <code class="info-box-code-info">.md:p-4</code> - Aplica padding de 4px solo en desktop (≥${configData.breakpoints.desktop})
1486
+ </li>
1487
+ <li class="text-m info-box-list-item">
1488
+ <code class="info-box-code-info">.md:pr-8</code> - Aplica padding-right de 8px solo en desktop
1489
+ </li>
1490
+ <li class="text-m info-box-list-item">
1491
+ <code class="info-box-code-info">.md:mt-16</code> - Aplica margin-top de 16px solo en desktop
1492
+ </li>
1493
+ <li class="text-m info-box-list-item">
1494
+ <code class="info-box-code-info">.p-0!</code> - Aplica padding de 0 con !important (útil para sobrescribir otros estilos)
1495
+ </li>
1496
+ </ul>
1497
+ <p class="text-m info-box-text-small">
1498
+ <strong>Nota:</strong> Puedes combinar clases base y con prefijo <code class="info-box-code-info">md:</code> para crear diseños responsive. Por ejemplo: <code class="info-box-code-info">.p-4 .md:p-8</code> aplica 4px en mobile y 8px en desktop. Las clases con <code class="info-box-code-info">!</code> aplican !important y tienen prioridad sobre otras reglas CSS.
1499
+ </p>
1500
+ </div>
1501
+ </div>
1502
+ </div>
1503
+ ` : ''}
1504
+
1505
+ ${layoutHelpersTableHTML ? `
1506
+ <div class="section" id="layout">
1507
+ <div class="section-title">
1508
+ <h2 >Helpers de Layout</h2>
1509
+ <p class="text-m section-description">
1510
+ Clases helper para display, flexbox, alignment y gap.
1511
+ Todos los helpers marcados como responsive tienen variantes con prefijo .md: para desktop (≥${configData.breakpoints.desktop}).
1512
+ </p>
1513
+ </div>
1514
+ <div class="section-content">
1515
+ ${layoutHelpersTableHTML}
1516
+ <p class="text-m section-description">
1517
+ Clases helper para display, flexbox, alignment y gap.
1518
+ Todos los helpers marcados como responsive tienen variantes con prefijo .md: para desktop (≥${configData.breakpoints.desktop}).
1519
+ </p>
1520
+
1521
+ <div class="info-box info-box-info info-box-margin-top">
1522
+ <h3 class="info-box-title info-box-title-info">Ejemplos de uso</h3>
1523
+ <ul class="info-box-list">
1524
+ <li class="text-m info-box-list-item">
1525
+ <code class="info-box-code-info">.d-flex</code> - Display flex
1526
+ </li>
1527
+ <li class="text-m info-box-list-item">
1528
+ <code class="info-box-code-info">.flex-column</code> - Flex direction column
1529
+ </li>
1530
+ <li class="text-m info-box-list-item">
1531
+ <code class="info-box-code-info">.justify-center</code> - Justify content center
1532
+ </li>
1533
+ <li class="text-m info-box-list-item">
1534
+ <code class="info-box-code-info">.items-center</code> - Align items center
1535
+ </li>
1536
+ <li class="text-m info-box-list-item">
1537
+ <code class="info-box-code-info">.gap-16</code> - Gap de 16px (1rem)
1538
+ </li>
1539
+ <li class="text-m info-box-list-item">
1540
+ <code class="info-box-code-info">.md:flex-row</code> - Flex direction row solo en desktop
1541
+ </li>
1542
+ </ul>
1543
+ </div>
1544
+ </div>
1545
+ </div>
1546
+ ` : ''}
1547
+
1548
+ <div class="section" id="breakpoints">
1549
+ <div class="section-title">
1550
+ <h2 >Breakpoints</h2>
1551
+ <p class="text-m section-description">
1552
+ Breakpoints disponibles.
1553
+ </p>
1554
+ </div>
1555
+ <div class="section-content">
1556
+ <div class="guide-table-wrapper">
1557
+ <table class="guide-table">
1558
+ <thead>
1559
+ <tr>
1560
+ <th>Breakpoint</th>
1561
+ <th>Min-width</th>
1562
+ </tr>
1563
+ </thead>
1564
+ <tbody>
1565
+ <tr>
1566
+ <td class="table-name">Mobile</td>
1567
+ <td class="table-value ${changedValues.has('breakpoints.mobile') ? 'changed' : ''}">
1568
+ ${configData.breakpoints.mobile}
1569
+ ${configData.breakpoints.mobile.endsWith('px') ? `(${pxToRem(configData.breakpoints.mobile, baseFontSize)})` : ''}
1570
+ </td>
1571
+ </tr>
1572
+ <tr>
1573
+ <td class="table-name">Desktop</td>
1574
+ <td class="table-value ${changedValues.has('breakpoints.desktop') ? 'changed' : ''}">
1575
+ ${configData.breakpoints.desktop}
1576
+ ${configData.breakpoints.desktop.endsWith('px') ? `(${pxToRem(configData.breakpoints.desktop, baseFontSize)})` : ''}
1577
+ </td>
1578
+ </tr>
1579
+ </tbody>
1580
+ </table>
1581
+ </div>
1582
+ <p class="text-m section-description">
1583
+ Las clases de tipografía se adaptan automáticamente a cada breakpoint.
1584
+ Resize la ventana del navegador para ver los cambios.
1585
+ </p>
1586
+ </div>
1587
+ </div>
1588
+ </main>
1589
+
1590
+ <script>
1591
+ // Scroll suave y resaltado de sección activa
1592
+ const menuItems = document.querySelectorAll('.menu-item');
1593
+ const sections = document.querySelectorAll('.section');
1594
+
1595
+ // Manejar clic en menú
1596
+ menuItems.forEach(item => {
1597
+ item.addEventListener('click', (e) => {
1598
+ e.preventDefault();
1599
+ const targetId = item.getAttribute('data-section');
1600
+ const targetSection = document.getElementById(targetId);
1601
+
1602
+ if (targetSection) {
1603
+ const offset = 80; // Offset para compensar header
1604
+ const targetPosition = targetSection.offsetTop - offset;
1605
+
1606
+ window.scrollTo({
1607
+ top: targetPosition,
1608
+ behavior: 'smooth'
1609
+ });
1610
+
1611
+ // Cerrar menú en mobile
1612
+ if (window.innerWidth <= 768) {
1613
+ document.querySelector('.sidebar').classList.remove('open');
1614
+ }
1615
+ }
1616
+ });
1617
+ });
1618
+
1619
+ // Funcionalidad de búsqueda
1620
+ const searchInput = document.getElementById('search-input');
1621
+ const clearSearchBtn = document.getElementById('clear-search');
1622
+ const searchResults = document.getElementById('search-results');
1623
+ let searchTimeout;
1624
+
1625
+ // Función para resaltar texto
1626
+ function highlightText(text, searchTerm) {
1627
+ if (!searchTerm) return text;
1628
+ const escapedTerm = searchTerm.replace(/[.*+?^$()|[\]\\]/g, '\\\\$&');
1629
+ const escapedTerm2 = escapedTerm.replace(/{/g, '\\\\{').replace(/}/g, '\\\\}');
1630
+ const regex = new RegExp('(' + escapedTerm2 + ')', 'gi');
1631
+ return text.replace(regex, '<mark class="search-highlight">$1</mark>');
1632
+ }
1633
+
1634
+ // Función para buscar en tablas y grids
1635
+ function searchInTables(searchTerm) {
1636
+ if (!searchTerm || searchTerm.trim() === '') {
1637
+ // Mostrar todo
1638
+ document.querySelectorAll('.section, .guide-table tbody tr, .spacing-helpers-table tbody tr, [style*="grid-template-columns"] > div').forEach(el => {
1639
+ el.style.display = '';
1640
+ });
1641
+ document.querySelectorAll('mark').forEach(mark => {
1642
+ const parent = mark.parentNode;
1643
+ parent.replaceChild(document.createTextNode(mark.textContent), mark);
1644
+ parent.normalize();
1645
+ });
1646
+ searchResults.style.display = 'none';
1647
+ clearSearchBtn.style.display = 'none';
1648
+ return;
1649
+ }
1650
+
1651
+ const term = searchTerm.toLowerCase().trim();
1652
+ let matchCount = 0;
1653
+ const matchedSections = new Set();
1654
+
1655
+ // Buscar en todas las tablas
1656
+ document.querySelectorAll('.guide-table tbody tr, .spacing-helpers-table tbody tr').forEach(row => {
1657
+ const text = row.textContent.toLowerCase();
1658
+ const cells = row.querySelectorAll('td');
1659
+
1660
+ if (text.includes(term)) {
1661
+ row.style.display = '';
1662
+ matchCount++;
1663
+
1664
+ // Resaltar texto en las celdas
1665
+ cells.forEach(cell => {
1666
+ const originalText = cell.textContent;
1667
+ cell.innerHTML = highlightText(originalText, term);
1668
+ });
1669
+
1670
+ // Encontrar la sección padre
1671
+ let section = row.closest('.section');
1672
+ if (section) {
1673
+ matchedSections.add(section.id);
1674
+ }
1675
+ } else {
1676
+ row.style.display = 'none';
1677
+ }
1678
+ });
1679
+
1680
+ // Buscar en grid de colores
1681
+ document.querySelectorAll('[style*="grid-template-columns"] > div').forEach(card => {
1682
+ const text = card.textContent.toLowerCase();
1683
+
1684
+ if (text.includes(term)) {
1685
+ card.style.display = '';
1686
+ matchCount++;
1687
+
1688
+ // Resaltar texto en la tarjeta
1689
+ const textElements = card.querySelectorAll('div');
1690
+ textElements.forEach(el => {
1691
+ if (el.textContent && !el.style.background) {
1692
+ const originalText = el.textContent;
1693
+ el.innerHTML = highlightText(originalText, term);
1694
+ }
1695
+ });
1696
+
1697
+ // Encontrar la sección padre
1698
+ let section = card.closest('.section');
1699
+ if (section) {
1700
+ matchedSections.add(section.id);
1701
+ }
1702
+ } else {
1703
+ card.style.display = 'none';
1704
+ }
1705
+ });
1706
+
1707
+ // Mostrar/ocultar secciones según si tienen resultados
1708
+ document.querySelectorAll('.section').forEach(section => {
1709
+ const hasVisibleRows = section.querySelector('tbody tr[style=""]') ||
1710
+ section.querySelector('tbody tr:not([style*="display: none"])') ||
1711
+ section.querySelector('[style*="grid-template-columns"] > div[style=""]') ||
1712
+ section.querySelector('[style*="grid-template-columns"] > div:not([style*="display: none"])');
1713
+ if (matchedSections.has(section.id) || hasVisibleRows) {
1714
+ section.style.display = '';
1715
+ } else {
1716
+ section.style.display = 'none';
1717
+ }
1718
+ });
1719
+
1720
+ // Mostrar contador de resultados
1721
+ if (matchCount > 0) {
1722
+ searchResults.textContent = 'Se encontraron ' + matchCount + ' resultado' + (matchCount !== 1 ? 's' : '');
1723
+ searchResults.style.display = 'block';
1724
+ } else {
1725
+ searchResults.textContent = 'No se encontraron resultados';
1726
+ searchResults.style.display = 'block';
1727
+ }
1728
+
1729
+ clearSearchBtn.style.display = 'block';
1730
+ }
1731
+
1732
+ // Event listeners para búsqueda
1733
+ searchInput.addEventListener('input', (e) => {
1734
+ clearTimeout(searchTimeout);
1735
+ searchTimeout = setTimeout(() => {
1736
+ searchInTables(e.target.value);
1737
+ }, 200);
1738
+ });
1739
+
1740
+ searchInput.addEventListener('keydown', (e) => {
1741
+ if (e.key === 'Escape') {
1742
+ searchInput.value = '';
1743
+ searchInTables('');
1744
+ }
1745
+ });
1746
+
1747
+ clearSearchBtn.addEventListener('click', () => {
1748
+ searchInput.value = '';
1749
+ searchInTables('');
1750
+ searchInput.focus();
1751
+ });
1752
+
1753
+ // El estilo de focus ya está en CSS (.search-input:focus)
1754
+
1755
+ // Resaltar sección activa al hacer scroll
1756
+ function updateActiveSection() {
1757
+ const scrollPosition = window.scrollY + 200;
1758
+
1759
+ sections.forEach(section => {
1760
+ const sectionTop = section.offsetTop;
1761
+ const sectionHeight = section.offsetHeight;
1762
+ const sectionId = section.getAttribute('id');
1763
+
1764
+ if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
1765
+ menuItems.forEach(item => {
1766
+ item.classList.remove('active');
1767
+ if (item.getAttribute('data-section') === sectionId) {
1768
+ item.classList.add('active');
1769
+ }
1770
+ });
1771
+ }
1772
+ });
1773
+ }
1774
+
1775
+ window.addEventListener('scroll', updateActiveSection);
1776
+ window.addEventListener('load', updateActiveSection);
1777
+
1778
+ // Cerrar menú al hacer clic fuera en mobile
1779
+ document.addEventListener('click', (e) => {
1780
+ const sidebar = document.querySelector('.sidebar');
1781
+ const menuToggle = document.querySelector('.menu-toggle');
1782
+
1783
+ if (window.innerWidth <= 768 &&
1784
+ sidebar.classList.contains('open') &&
1785
+ !sidebar.contains(e.target) &&
1786
+ !menuToggle.contains(e.target)) {
1787
+ sidebar.classList.remove('open');
1788
+ }
1789
+ });
1790
+ </script>
1791
+ </body>
1792
+ </html>`;
1793
+ }
1794
+
1795
+ module.exports = {
1796
+ generateHTML
1797
+ };
1798
+