holygrail5 1.0.19 → 1.0.21

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 (77) hide show
  1. package/README.md +88 -18
  2. package/config.json +205 -77
  3. package/dist/assets/fonts/suisse-intl-light.woff +0 -0
  4. package/dist/assets/fonts/suisse-intl-light.woff2 +0 -0
  5. package/dist/assets/fonts/suisse-intl-medium.woff +0 -0
  6. package/dist/assets/fonts/suisse-intl-medium.woff2 +0 -0
  7. package/dist/assets/fonts/suisse-intl-regular.woff +0 -0
  8. package/dist/assets/fonts/suisse-intl-regular.woff2 +0 -0
  9. package/dist/assets/fonts/suisse-intl-semibold.woff +0 -0
  10. package/dist/assets/fonts/suisse-intl-semibold.woff2 +0 -0
  11. package/dist/assets/fonts/suisse-works-bold.woff +0 -0
  12. package/dist/assets/fonts/suisse-works-bold.woff2 +0 -0
  13. package/dist/assets/fonts/suisse-works-medium.woff +0 -0
  14. package/dist/assets/fonts/suisse-works-medium.woff2 +0 -0
  15. package/dist/assets/fonts/suisse-works-regular.woff +0 -0
  16. package/dist/assets/fonts/suisse-works-regular.woff2 +0 -0
  17. package/dist/componentes.html +429 -0
  18. package/dist/developer-guide.md +7 -7
  19. package/dist/guide-styles.css +197 -25
  20. package/dist/index.html +807 -689
  21. package/dist/output.css +217 -114
  22. package/dist/skills.html +14 -8
  23. package/dist/themes/dutti-demo.html +713 -19
  24. package/dist/themes/dutti.css +67 -16
  25. package/dist/themes/limited-demo.html +1121 -0
  26. package/dist/themes/limited.css +2493 -0
  27. package/package.json +1 -1
  28. package/src/.data/.previous-values.json +151 -84
  29. package/src/assets/fonts/suisse-intl-light.woff +0 -0
  30. package/src/assets/fonts/suisse-intl-light.woff2 +0 -0
  31. package/src/assets/fonts/suisse-intl-medium.woff +0 -0
  32. package/src/assets/fonts/suisse-intl-medium.woff2 +0 -0
  33. package/src/assets/fonts/suisse-intl-regular.woff +0 -0
  34. package/src/assets/fonts/suisse-intl-regular.woff2 +0 -0
  35. package/src/assets/fonts/suisse-intl-semibold.woff +0 -0
  36. package/src/assets/fonts/suisse-intl-semibold.woff2 +0 -0
  37. package/src/assets/fonts/suisse-works-bold.woff +0 -0
  38. package/src/assets/fonts/suisse-works-bold.woff2 +0 -0
  39. package/src/assets/fonts/suisse-works-medium.woff +0 -0
  40. package/src/assets/fonts/suisse-works-medium.woff2 +0 -0
  41. package/src/assets/fonts/suisse-works-regular.woff +0 -0
  42. package/src/assets/fonts/suisse-works-regular.woff2 +0 -0
  43. package/src/build/asset-manager.js +94 -3
  44. package/src/build/build-orchestrator.js +74 -12
  45. package/src/build/components-generator.js +584 -0
  46. package/src/build/skills-generator.js +43 -5
  47. package/src/build/theme-config-loader.js +58 -0
  48. package/src/build/theme-transformer.js +109 -16
  49. package/src/build/theme-vars-table-generator.js +349 -0
  50. package/src/build/typo-table-generator.js +154 -0
  51. package/src/docs-generator/guide-styles.css +197 -24
  52. package/src/docs-generator/html-generator.js +92 -246
  53. package/src/docs-generator/sections/colors-section.js +109 -0
  54. package/src/docs-generator/value-tracker.js +154 -0
  55. package/src/generators/typo-generator.js +2 -1
  56. package/src/generators/utils.js +90 -1
  57. package/src/skills.html +1 -0
  58. package/src/watch-config.js +99 -32
  59. package/themes/{dutti → _base}/_buttons.css +2 -2
  60. package/themes/{dutti → _base}/_checkboxes.css +1 -1
  61. package/themes/{dutti → _base}/_forms.css +1 -1
  62. package/themes/{dutti → _base}/_inputs.css +55 -10
  63. package/themes/{dutti → _base}/_labels.css +1 -1
  64. package/themes/dutti/README.md +67 -21
  65. package/themes/dutti/_variables.css +7 -1
  66. package/themes/dutti/demo.html +18 -14
  67. package/themes/dutti/theme.css +22 -44
  68. package/themes/dutti/theme.json +86 -0
  69. package/themes/limited/_variables.css +123 -0
  70. package/themes/limited/demo.html +296 -0
  71. package/themes/limited/theme.css +32 -0
  72. package/themes/limited/theme.json +105 -0
  73. /package/themes/{dutti → _base}/_containers.css +0 -0
  74. /package/themes/{dutti → _base}/_radios.css +0 -0
  75. /package/themes/{dutti → _base}/_switches.css +0 -0
  76. /package/themes/{dutti → _base}/components/_icons.css +0 -0
  77. /package/themes/{dutti → _base}/objects/_grid.css +0 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Components Page Generator
3
+ *
4
+ * Genera dist/componentes.html: una página que muestra todos los
5
+ * componentes base (themes/_base/) con preview vivo + el nombre de
6
+ * clase junto a cada variante.
7
+ *
8
+ * Se renderiza con el tema DUTTI como base genérica (tema neutro del
9
+ * framework). Sobre él se pueden aplicar otros temas en el futuro si
10
+ * añadimos un theme switcher. Por tanto la página enlaza:
11
+ * - dist/output.css → tokens --hg-* del framework
12
+ * - dist/themes/dutti.css → mapeo de variables + reglas de componente
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { resolveActiveThemes } = require('../generators/utils');
18
+
19
+ /**
20
+ * Nombre del tema que se usa como "base genérica" para renderizar la
21
+ * página. Si en algún momento se quiere cambiar, basta con modificar
22
+ * esta constante (o exponerla en config.json).
23
+ */
24
+ const BASE_THEME = 'dutti';
25
+
26
+ /**
27
+ * Lista canónica de componentes mostrados en la página.
28
+ */
29
+ const COMPONENT_SECTIONS = [
30
+ {
31
+ id: 'buttons',
32
+ title: 'Botones',
33
+ description:
34
+ 'Variantes estándar del framework: <code>primary</code>, <code>secondary</code>, <code>tertiary</code>, <code>label-m</code>, <code>link</code> y <code>badge</code>. Clases en <code>themes/_base/_buttons.css</code>. Cada tema puede sobreescribirlas con su propia identidad visual.',
35
+ examples: [
36
+ {
37
+ subtitle: 'Variantes',
38
+ items: [
39
+ { html: '<button class="btn btn-primary">Primary</button>', cls: '.btn .btn-primary' },
40
+ { html: '<button class="btn btn-secondary">Secondary</button>', cls: '.btn .btn-secondary' },
41
+ { html: '<button class="btn btn-tertiary">Tertiary</button>', cls: '.btn .btn-tertiary' },
42
+ {
43
+ html: '<button class="btn btn-tertiary hg-text-underline">Tertiary underline</button>',
44
+ cls: '.btn .btn-tertiary .hg-text-underline'
45
+ },
46
+ { html: '<button class="btn btn-label-m">Label M</button>', cls: '.btn .btn-label-m' },
47
+ { html: '<button class="btn btn-link">Link</button>', cls: '.btn .btn-link' },
48
+ { html: '<button class="btn btn-badge">Badge</button>', cls: '.btn .btn-badge' },
49
+ { html: '<button class="btn btn-primary" disabled>Disabled</button>', cls: '.btn[disabled]' }
50
+ ]
51
+ },
52
+ {
53
+ subtitle: 'Tamaños',
54
+ items: [
55
+ { html: '<button class="btn btn-primary btn-sm">Small</button>', cls: '.btn .btn-sm' },
56
+ { html: '<button class="btn btn-primary btn-md">Medium</button>', cls: '.btn .btn-md' },
57
+ { html: '<button class="btn btn-primary btn-lg">Large</button>', cls: '.btn .btn-lg' }
58
+ ]
59
+ },
60
+ {
61
+ subtitle: 'Ancho completo',
62
+ items: [
63
+ {
64
+ html: '<button class="btn btn-primary btn-md btn-full">Botón ancho completo</button>',
65
+ cls: '.btn .btn-full'
66
+ }
67
+ ]
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ id: 'inputs',
73
+ title: 'Inputs',
74
+ description:
75
+ 'Campos de formulario base con <strong>floating label</strong>: texto, email, password, textarea y select. Cada input vive dentro de <code>.form-input-label-2</code> para que el label se anime al enfocar o al contener valor. Clases en <code>themes/_base/_inputs.css</code>.',
76
+ examples: [
77
+ {
78
+ subtitle: 'Tipos',
79
+ items: [
80
+ {
81
+ html:
82
+ '<div class="form-input-label-2"><input type="text" id="cmp-input-text" class="input" placeholder=" " /><label for="cmp-input-text">Texto</label></div>',
83
+ cls: '.form-input-label-2 > .input'
84
+ },
85
+ {
86
+ html:
87
+ '<div class="form-input-label-2"><input type="email" id="cmp-input-email" class="input" placeholder=" " /><label for="cmp-input-email">Email</label></div>',
88
+ cls: '.input (type=email)'
89
+ },
90
+ {
91
+ html:
92
+ '<div class="form-input-label-2"><input type="password" id="cmp-input-password" class="input" placeholder=" " /><label for="cmp-input-password">Contraseña</label></div>',
93
+ cls: '.input (type=password)'
94
+ },
95
+ {
96
+ html:
97
+ '<div class="form-input-label-2"><textarea id="cmp-input-textarea" class="input" placeholder=" " rows="3"></textarea><label for="cmp-input-textarea">Comentario</label></div>',
98
+ cls: '.input (textarea)'
99
+ },
100
+ {
101
+ html:
102
+ '<div class="form-input-label-2"><select id="cmp-input-select" class="input"><option>Opción A</option><option>Opción B</option></select><label for="cmp-input-select">Selecciona</label></div>',
103
+ cls: '.input (select)'
104
+ }
105
+ ]
106
+ },
107
+ {
108
+ subtitle: 'Estados',
109
+ items: [
110
+ {
111
+ html:
112
+ '<div class="form-input-label-2"><input type="text" id="cmp-input-error" class="input input-error" value="Valor inválido" placeholder=" " /><label for="cmp-input-error">Error</label></div><span class="helper-text helper-text-error">Este campo tiene un error</span>',
113
+ cls: '.input-error + .helper-text-error'
114
+ },
115
+ {
116
+ html:
117
+ '<div class="form-input-label-2"><input type="text" id="cmp-input-success" class="input input-success" value="Valor válido" placeholder=" " /><label for="cmp-input-success">Success</label></div><span class="helper-text helper-text-success">Campo válido</span>',
118
+ cls: '.input-success + .helper-text-success'
119
+ },
120
+ {
121
+ html:
122
+ '<div class="form-input-label-2"><input type="text" id="cmp-input-warning" class="input input-warning" value="Atención" placeholder=" " /><label for="cmp-input-warning">Warning</label></div><span class="helper-text helper-text-warning">Revisa este campo</span>',
123
+ cls: '.input-warning + .helper-text-warning'
124
+ },
125
+ {
126
+ html:
127
+ '<div class="form-input-label-2"><input type="text" id="cmp-input-disabled" class="input" value="No editable" placeholder=" " disabled /><label for="cmp-input-disabled">Disabled</label></div>',
128
+ cls: '.input[disabled]'
129
+ }
130
+ ]
131
+ }
132
+ ]
133
+ },
134
+ {
135
+ id: 'labels',
136
+ title: 'Labels',
137
+ description:
138
+ 'Etiquetas de formulario: base, obligatoria e inline. Clases en <code>themes/_base/_labels.css</code>.',
139
+ examples: [
140
+ {
141
+ subtitle: 'Variantes',
142
+ items: [
143
+ { html: '<label class="label">Nombre</label>', cls: '.label' },
144
+ { html: '<label class="label label-required">Email</label>', cls: '.label .label-required' },
145
+ {
146
+ html:
147
+ '<label class="label label-inline"><input type="checkbox" /> Acepto los términos</label>',
148
+ cls: '.label .label-inline'
149
+ }
150
+ ]
151
+ }
152
+ ]
153
+ },
154
+ {
155
+ id: 'checkboxes',
156
+ title: 'Checkboxes',
157
+ description:
158
+ 'Checkbox con input nativo oculto y marca SVG inline dentro de <code>.checkbox-indicator</code>. El estado visible se controla 100% con CSS (sin JS). Clases en <code>themes/_base/_checkboxes.css</code>.',
159
+ examples: [
160
+ {
161
+ subtitle: 'Estados',
162
+ items: [
163
+ {
164
+ html:
165
+ '<label class="checkbox"><input type="checkbox" /><span class="checkbox-indicator"><svg class="cbox__icon" viewBox="0 0 10 8" width="10" height="8" aria-hidden="true" focusable="false"><path d="M9.05823.198273 9.69185.801721 3.5417 7.25937.308228 3.86422.941848 3.26077 3.5417 5.99062 9.05823.198273Z" fill="currentColor"/></svg></span><span class="checkbox-label">Sin marcar</span></label>',
166
+ cls: '.checkbox'
167
+ },
168
+ {
169
+ html:
170
+ '<label class="checkbox"><input type="checkbox" checked /><span class="checkbox-indicator"><svg class="cbox__icon" viewBox="0 0 10 8" width="10" height="8" aria-hidden="true" focusable="false"><path d="M9.05823.198273 9.69185.801721 3.5417 7.25937.308228 3.86422.941848 3.26077 3.5417 5.99062 9.05823.198273Z" fill="currentColor"/></svg></span><span class="checkbox-label">Marcado</span></label>',
171
+ cls: '.checkbox (checked)'
172
+ },
173
+ {
174
+ html:
175
+ '<label class="checkbox"><input type="checkbox" disabled /><span class="checkbox-indicator"><svg class="cbox__icon" viewBox="0 0 10 8" width="10" height="8" aria-hidden="true" focusable="false"><path d="M9.05823.198273 9.69185.801721 3.5417 7.25937.308228 3.86422.941848 3.26077 3.5417 5.99062 9.05823.198273Z" fill="currentColor"/></svg></span><span class="checkbox-label">Disabled</span></label>',
176
+ cls: '.checkbox[disabled]'
177
+ }
178
+ ]
179
+ }
180
+ ]
181
+ },
182
+ {
183
+ id: 'radios',
184
+ title: 'Radios',
185
+ description:
186
+ 'Radio buttons con el patrón <code>.checkbox-radio</code>: el input nativo se oculta visualmente y el círculo se pinta con <code>label::before</code>. Clases en <code>themes/_base/_radios.css</code>.',
187
+ examples: [
188
+ {
189
+ subtitle: 'Grupo',
190
+ items: [
191
+ {
192
+ html:
193
+ '<div class="checkbox-radio"><input id="cmp-radio-1" name="cmp-radio" type="radio" value="A" /><label for="cmp-radio-1"><i class="ico-radio"></i><span class="title-m">Opción A</span></label></div>',
194
+ cls: '.checkbox-radio'
195
+ },
196
+ {
197
+ html:
198
+ '<div class="checkbox-radio"><input id="cmp-radio-2" name="cmp-radio" type="radio" value="B" checked /><label for="cmp-radio-2"><i class="ico-radio"></i><span class="title-m">Opción B (activa)</span></label></div>',
199
+ cls: '.checkbox-radio (checked)'
200
+ },
201
+ {
202
+ html:
203
+ '<div class="checkbox-radio"><input id="cmp-radio-3" name="cmp-radio-2" type="radio" value="C" disabled /><label for="cmp-radio-3"><i class="ico-radio"></i><span class="title-m">Disabled</span></label></div>',
204
+ cls: '.checkbox-radio[disabled]'
205
+ }
206
+ ]
207
+ }
208
+ ]
209
+ },
210
+ {
211
+ id: 'switches',
212
+ title: 'Switches',
213
+ description:
214
+ 'Interruptores on/off con el patrón <code>.checkbox-item-2</code>: pista rectangular y un <code>.checkbox-circle</code> que se desplaza al marcar. Clases en <code>themes/_base/_switches.css</code>.',
215
+ examples: [
216
+ {
217
+ subtitle: 'Estados',
218
+ items: [
219
+ {
220
+ html:
221
+ '<div class="checkbox-item-2"><input id="cmp-switch-1" name="cmp-switch-1" type="checkbox" /><label for="cmp-switch-1"><div class="checkbox-circle"></div><span class="theta">Inactivo</span></label></div>',
222
+ cls: '.checkbox-item-2'
223
+ },
224
+ {
225
+ html:
226
+ '<div class="checkbox-item-2"><input id="cmp-switch-2" name="cmp-switch-2" type="checkbox" checked /><label for="cmp-switch-2"><div class="checkbox-circle"></div><span class="theta">Activado</span></label></div>',
227
+ cls: '.checkbox-item-2 (checked)'
228
+ },
229
+ {
230
+ html:
231
+ '<div class="checkbox-item-2"><input id="cmp-switch-3" name="cmp-switch-3" type="checkbox" disabled /><label for="cmp-switch-3"><div class="checkbox-circle"></div><span class="theta">Disabled</span></label></div>',
232
+ cls: '.checkbox-item-2[disabled]'
233
+ },
234
+ {
235
+ html:
236
+ '<div class="checkbox-item-2 is-error"><input id="cmp-switch-4" name="cmp-switch-4" type="checkbox" /><label for="cmp-switch-4"><div class="checkbox-circle"></div><span class="theta">Error</span></label></div>',
237
+ cls: '.checkbox-item-2.is-error'
238
+ }
239
+ ]
240
+ }
241
+ ]
242
+ },
243
+ {
244
+ id: 'forms',
245
+ title: 'Formularios',
246
+ description:
247
+ 'Composición de campos con label flotante + estado. <code>.form-group</code> apila verticalmente los campos; cada uno usa <code>.form-input-label-2</code> para el floating label y (opcionalmente) <code>.helper-text</code> para el mensaje de estado. Clases en <code>themes/_base/_forms.css</code>.',
248
+ examples: [
249
+ {
250
+ subtitle: 'Grupo de formulario',
251
+ items: [
252
+ {
253
+ html:
254
+ '<div class="form-group"><div class="form-input-label-2"><input type="email" id="cmp-form-email" class="input" placeholder=" " /><label for="cmp-form-email">Email</label></div></div>',
255
+ cls: '.form-group > .form-input-label-2'
256
+ },
257
+ {
258
+ html:
259
+ '<div class="form-group"><div class="form-input-label-2"><textarea id="cmp-form-msg" class="input" rows="3" placeholder=" "></textarea><label for="cmp-form-msg">Mensaje</label></div></div>',
260
+ cls: '.form-group (con textarea)'
261
+ },
262
+ {
263
+ html:
264
+ '<div class="form-group"><div class="form-input-label-2"><input type="text" id="cmp-form-err" class="input input-error" value="" placeholder=" " /><label for="cmp-form-err">Nombre</label></div><span class="helper-text helper-text-error">Este campo es obligatorio</span></div>',
265
+ cls: '.form-group (con helper-text)'
266
+ }
267
+ ]
268
+ }
269
+ ]
270
+ },
271
+ {
272
+ id: 'containers',
273
+ title: 'Containers',
274
+ description:
275
+ 'Contenedores centrados con <code>max-width</code> responsivo y/o fijo. <code>.container</code> escala con los breakpoints; <code>.container-2</code> es más estrecho; las variantes <code>.container-640</code>, <code>.container-512</code> y <code>.container-360</code> fijan un ancho concreto. Clases en <code>themes/_base/_containers.css</code>.',
276
+ examples: [
277
+ {
278
+ subtitle: 'Responsivos',
279
+ items: [
280
+ {
281
+ html:
282
+ '<div class="container" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">.container</div>',
283
+ cls: '.container'
284
+ },
285
+ {
286
+ html:
287
+ '<div class="container-2" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">.container-2</div>',
288
+ cls: '.container-2'
289
+ }
290
+ ]
291
+ },
292
+ {
293
+ subtitle: 'Anchos fijos',
294
+ items: [
295
+ {
296
+ html:
297
+ '<div class="container-640" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">.container-640</div>',
298
+ cls: '.container-640'
299
+ },
300
+ {
301
+ html:
302
+ '<div class="container-512" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">.container-512</div>',
303
+ cls: '.container-512'
304
+ },
305
+ {
306
+ html:
307
+ '<div class="container-360" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">.container-360</div>',
308
+ cls: '.container-360'
309
+ }
310
+ ]
311
+ }
312
+ ]
313
+ },
314
+ {
315
+ id: 'grid',
316
+ title: 'Grid',
317
+ description:
318
+ 'Utilidades de CSS Grid inspiradas en Tailwind. El contenedor debe tener <code>display:grid</code> y usar <code>.hg-grid-cols-N</code> para definir N columnas; los hijos usan <code>.hg-col-span-N</code> para ocupar varias. Con el prefijo <code>md:</code> se activan a partir de 768&nbsp;px. Clases en <code>themes/_base/objects/_grid.css</code>.',
319
+ examples: [
320
+ {
321
+ subtitle: 'Columnas iguales',
322
+ items: [
323
+ {
324
+ html:
325
+ '<div class="hg-grid-cols-3" style="display:grid; gap:var(--hg-spacing-8);"><div style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">1</div><div style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">2</div><div style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">3</div></div>',
326
+ cls: '.hg-grid-cols-3'
327
+ },
328
+ {
329
+ html:
330
+ '<div class="hg-grid-cols-4" style="display:grid; gap:var(--hg-spacing-8);"><div style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">1</div><div style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">2</div><div style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">3</div><div style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">4</div></div>',
331
+ cls: '.hg-grid-cols-4'
332
+ }
333
+ ]
334
+ },
335
+ {
336
+ subtitle: 'Col-span',
337
+ items: [
338
+ {
339
+ html:
340
+ '<div class="hg-grid-cols-12" style="display:grid; gap:var(--hg-spacing-8);"><div class="hg-col-span-8" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">span 8</div><div class="hg-col-span-4" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">span 4</div></div>',
341
+ cls: '.hg-grid-cols-12 > .hg-col-span-{8,4}'
342
+ },
343
+ {
344
+ html:
345
+ '<div class="hg-grid-cols-12" style="display:grid; gap:var(--hg-spacing-8);"><div class="hg-col-span-6" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">span 6</div><div class="hg-col-span-6" style="background:var(--hg-color-light-grey); padding:var(--hg-spacing-16);">span 6</div></div>',
346
+ cls: '.hg-grid-cols-12 > .hg-col-span-6'
347
+ }
348
+ ]
349
+ }
350
+ ]
351
+ }
352
+ ];
353
+
354
+ // Escape HTML para mostrar el nombre de clase dentro de <code>.
355
+ function escapeHtml(str) {
356
+ return String(str)
357
+ .replace(/&/g, '&amp;')
358
+ .replace(/</g, '&lt;')
359
+ .replace(/>/g, '&gt;');
360
+ }
361
+
362
+ function renderSection(section) {
363
+ const blocks = section.examples
364
+ .map((group) => {
365
+ const items = group.items
366
+ .map(
367
+ (it) => `
368
+ <div class="demo-item">
369
+ <div class="cmp-preview">${it.html}</div>
370
+ <div class="demo-code">${escapeHtml(it.cls)}</div>
371
+ </div>`
372
+ )
373
+ .join('');
374
+ return `
375
+ <h3 class="demo-subtitle">${group.subtitle}</h3>
376
+ <div class="demo-grid">${items}
377
+ </div>`;
378
+ })
379
+ .join('');
380
+
381
+ return `
382
+ <section class="demo-section" id="${section.id}">
383
+ <h2 class="demo-title">${section.title}</h2>
384
+ <p class="cmp-desc">${section.description}</p>
385
+ ${blocks}
386
+ </section>`;
387
+ }
388
+
389
+ /**
390
+ * Construye el header + sidebar de la página Componentes, usando la
391
+ * misma estructura que las demos de tema (`buildHeaderAndSidebar` en
392
+ * theme-transformer.js). Enlaces relativos al mismo nivel que los
393
+ * theme demos para mantener coherencia.
394
+ */
395
+ function buildHeaderAndSidebar(activeThemes) {
396
+ const themeLinks = (activeThemes || [])
397
+ .map((t) => ` <a href="themes/${t.name}-demo.html">${t.label}</a>`)
398
+ .join('\n');
399
+
400
+ const sidebarLinks = COMPONENT_SECTIONS.map(
401
+ (s) => ` <a href="#${s.id}" class="guide-menu-item">${s.title}</a>`
402
+ ).join('\n');
403
+
404
+ return `
405
+ <div class="guide-header">
406
+ <a href="index.html" class="guide-logo" style="text-decoration:none;">HolyGrail5</a>
407
+ <div style="display: flex; align-items: center; gap: 1rem;">
408
+ <nav class="guide-nav">
409
+ <a href="index.html">Guía</a>
410
+ <a href="componentes.html" class="active">Componentes</a>
411
+ ${themeLinks}
412
+ <a href="skills.html">Skills</a>
413
+ </nav>
414
+ <button class="guide-header-button" onclick="toggleSidebar()">☰</button>
415
+ </div>
416
+ </div>
417
+
418
+ <div class="guide-sidebar-overlay" onclick="toggleSidebar()"></div>
419
+
420
+ <aside class="guide-sidebar">
421
+ <div class="guide-sidebar-header">
422
+ <div>HolyGrail5</div>
423
+ <p class="guide-sidebar-subtitle">Componentes base</p>
424
+ </div>
425
+
426
+ <nav class="guide-sidebar-nav">
427
+ <p class="guide-sidebar-title">Componentes</p>
428
+
429
+ ${sidebarLinks}
430
+ </nav>
431
+ </aside>
432
+
433
+ <script>
434
+ function toggleSidebar() {
435
+ const sidebar = document.querySelector('.guide-sidebar');
436
+ const overlay = document.querySelector('.guide-sidebar-overlay');
437
+ sidebar.classList.toggle('open');
438
+ overlay.classList.toggle('active');
439
+ }
440
+
441
+ function closeSidebar() {
442
+ const sidebar = document.querySelector('.guide-sidebar');
443
+ const overlay = document.querySelector('.guide-sidebar-overlay');
444
+ sidebar.classList.remove('open');
445
+ overlay.classList.remove('active');
446
+ }
447
+
448
+ window.toggleSidebar = toggleSidebar;
449
+ window.closeSidebar = closeSidebar;
450
+ </script>`;
451
+ }
452
+
453
+ /**
454
+ * Genera el HTML completo de dist/componentes.html.
455
+ *
456
+ * La página adopta la misma estructura que las demos de tema
457
+ * (`guide-header` + `guide-sidebar` + `demo-container.guide-main-content`)
458
+ * y reutiliza `guide-styles.css` para mantener un flow consistente
459
+ * con el resto del sitio.
460
+ *
461
+ * @param {string} projectRoot
462
+ * @param {Object} [configData] - Config ya cargado (para nav dinámica).
463
+ * @returns {string|null}
464
+ */
465
+ function generateComponentsPage(projectRoot, configData = null) {
466
+ const baseDir = path.join(projectRoot, 'themes', '_base');
467
+ if (!fs.existsSync(baseDir)) {
468
+ console.warn('⚠️ No se encontró themes/_base/, omitiendo componentes.html');
469
+ return null;
470
+ }
471
+
472
+ const activeThemes = configData ? resolveActiveThemes(configData) : [];
473
+ const sectionsHtml = COMPONENT_SECTIONS.map(renderSection).join('\n');
474
+ const headerAndSidebar = buildHeaderAndSidebar(activeThemes);
475
+
476
+ return `<!DOCTYPE html>
477
+ <html lang="es">
478
+ <head>
479
+ <meta charset="UTF-8">
480
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
481
+ <title>HolyGrail5 — Componentes base</title>
482
+ <link rel="preconnect" href="https://fonts.googleapis.com">
483
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
484
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Instrument+Sans:regular,100,500,600,700">
485
+ <!-- Framework base -->
486
+ <link rel="stylesheet" href="output.css">
487
+ <!-- Tema base genérico: ${BASE_THEME} (variables + componentes) -->
488
+ <link rel="stylesheet" href="themes/${BASE_THEME}.css">
489
+ <!-- Estilos compartidos de guía (header, sidebar, demo-*) -->
490
+ <link rel="stylesheet" href="guide-styles.css">
491
+ <style>
492
+ body {
493
+ font-family: 'Instrument Sans', sans-serif !important;
494
+ }
495
+
496
+ /* Descripción de cada sección (debajo del título) */
497
+ .cmp-desc {
498
+ font-size: 14px;
499
+ line-height: 1.6;
500
+ color: #555;
501
+ margin: 0 0 1.5rem;
502
+ max-width: 720px;
503
+ }
504
+ .cmp-desc code {
505
+ background: #f3f3f3;
506
+ padding: 2px 6px;
507
+ border-radius: 4px;
508
+ font-size: 0.88em;
509
+ }
510
+
511
+ /* Preview de cada componente dentro de .demo-item */
512
+ .cmp-preview {
513
+ min-height: 48px;
514
+ display: flex;
515
+ align-items: center;
516
+ flex-wrap: wrap;
517
+ gap: 0.5rem;
518
+ margin-bottom: var(--hg-spacing-12);
519
+ }
520
+
521
+ /* Los inputs con floating label necesitan respirar: el label flota
522
+ encima, el helper-text se apila debajo. */
523
+ #inputs .cmp-preview,
524
+ #forms .cmp-preview {
525
+ display: block;
526
+ }
527
+ #inputs .cmp-preview > .form-input-label-2,
528
+ #forms .cmp-preview > .form-group {
529
+ width: 100%;
530
+ }
531
+
532
+ /* Containers y Grid son estructuras de layout: interesan como
533
+ bloques a 100% del item, no como chips alineados horizontalmente. */
534
+ #containers .demo-grid,
535
+ #grid .demo-grid {
536
+ grid-template-columns: 1fr;
537
+ }
538
+ #containers .cmp-preview,
539
+ #grid .cmp-preview {
540
+ display: block;
541
+ }
542
+ #containers .cmp-preview > [class^="container"],
543
+ #grid .cmp-preview > [class^="hg-grid-"] {
544
+ width: 100%;
545
+ max-width: 100%;
546
+ }
547
+ </style>
548
+ </head>
549
+ <body>
550
+ ${headerAndSidebar}
551
+
552
+ <main class="demo-container guide-main-content">
553
+ <h2 class="demo-title">Componentes base</h2>
554
+
555
+ <div class="demo-section-2">
556
+ <h3>¿Qué es esta página?</h3>
557
+ <p class="mb-16">
558
+ Librería de componentes compartidos que viven en
559
+ <code>themes/_base/</code>. Se renderizan con el tema
560
+ <strong>${BASE_THEME[0].toUpperCase() + BASE_THEME.slice(1)}</strong>
561
+ como base genérica del framework; cualquier otro tema puede
562
+ aplicarse encima para redefinir la identidad visual sin tocar
563
+ el HTML.
564
+ </p>
565
+ </div>
566
+
567
+ ${sectionsHtml}
568
+ </main>
569
+ </body>
570
+ </html>`;
571
+ }
572
+
573
+ // CLI
574
+ if (require.main === module) {
575
+ const projectRoot = path.join(__dirname, '..', '..');
576
+ const html = generateComponentsPage(projectRoot);
577
+ if (html) {
578
+ const outputPath = path.join(projectRoot, 'dist', 'componentes.html');
579
+ fs.writeFileSync(outputPath, html, 'utf-8');
580
+ console.log('✅ componentes.html generado en dist/');
581
+ }
582
+ }
583
+
584
+ module.exports = { generateComponentsPage };
@@ -5,6 +5,17 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const { resolveActiveThemes } = require('../generators/utils');
9
+
10
+ // Fallback usado cuando el llamador NO inyecta un config (p. ej. CLI
11
+ // standalone `node src/build/skills-generator.js`). Permite seguir
12
+ // generando skills.html con los temas históricos aunque falte el config
13
+ // activo. Si el BuildOrchestrator llama a `generateSkillsPage(root, config)`,
14
+ // este fallback queda ignorado y se usa la lista real de temas activos.
15
+ const FALLBACK_THEMES_IN_NAV = [
16
+ { name: 'dutti', label: 'Tema Dutti' },
17
+ { name: 'limited', label: 'Tema Limited' }
18
+ ];
8
19
 
9
20
  /**
10
21
  * Parsea Markdown básico a HTML
@@ -204,8 +215,16 @@ function extractSections(md) {
204
215
 
205
216
  /**
206
217
  * Main generator
218
+ *
219
+ * @param {string} projectRoot - Raíz del proyecto (donde vive skills/).
220
+ * @param {Object} [configData] - config.json ya cargado. Si se pasa,
221
+ * los enlaces a demos en la nav de skills.html se construyen a partir
222
+ * de los temas realmente activos (`config.themes[]` o `config.theme`),
223
+ * evitando enlazar a ficheros `dutti-demo.html` / `limited-demo.html`
224
+ * que quizá no existan en dist/. Si se omite, se usa el fallback
225
+ * histórico con Dutti + Limited para no romper ejecuciones standalone.
207
226
  */
208
- function generateSkillsPage(projectRoot) {
227
+ function generateSkillsPage(projectRoot, configData = null) {
209
228
  const skillsDir = path.join(projectRoot, 'skills');
210
229
 
211
230
  if (!fs.existsSync(skillsDir)) {
@@ -235,12 +254,26 @@ function generateSkillsPage(projectRoot) {
235
254
  raw: md
236
255
  };
237
256
 
257
+ // Resolver temas activos para la nav. Si el llamador no pasó un
258
+ // config, usamos el fallback estático.
259
+ const activeThemes = configData
260
+ ? resolveActiveThemes(configData)
261
+ : FALLBACK_THEMES_IN_NAV;
262
+
238
263
  // Generate HTML
239
- const html = buildPage(devGuide);
264
+ const html = buildPage(devGuide, activeThemes);
240
265
  return html;
241
266
  }
242
267
 
243
- function buildPage(skill) {
268
+ function buildPage(skill, activeThemes = FALLBACK_THEMES_IN_NAV) {
269
+ // Nav de temas construida dinámicamente a partir de la lista activa.
270
+ // Se respeta el orden definido en config.themes[].
271
+ const themeNavLinks = (activeThemes && activeThemes.length > 0
272
+ ? activeThemes
273
+ : FALLBACK_THEMES_IN_NAV
274
+ )
275
+ .map(t => ` <a href="themes/${t.name}-demo.html">${t.label}</a>`)
276
+ .join('\n');
244
277
  const toc = skill.sections.map(sec =>
245
278
  `<a href="#${sec.id}" class="sk-toc-link">${sec.text}</a>`
246
279
  ).join('\n ');
@@ -281,7 +314,11 @@ function buildPage(skill) {
281
314
  text-decoration: none; color: #000; width: max-content;
282
315
  }
283
316
  .guide-nav { display: flex; gap: 1.5rem; align-items: center; }
284
- .guide-nav a { font-size: 13px; color: #666; text-decoration: none; transition: color 0.2s; }
317
+ .guide-nav a {
318
+ font-family: var(--hg-typo-font-family-primary-regular);
319
+ font-size: 13px; color: #666; text-decoration: none;
320
+ transition: color 0.2s; text-transform: uppercase; letter-spacing: 0.05em;
321
+ }
285
322
  .guide-nav a:hover { color: #000; }
286
323
  .guide-nav a.active { color: #000; font-weight: 600; }
287
324
 
@@ -386,7 +423,8 @@ function buildPage(skill) {
386
423
  <a href="index.html" class="guide-logo" style="text-decoration:none;">HolyGrail5</a>
387
424
  <nav class="guide-nav">
388
425
  <a href="index.html">Guía</a>
389
- <a href="themes/dutti-demo.html">Tema Dutti</a>
426
+ <a href="componentes.html">Componentes</a>
427
+ ${themeNavLinks}
390
428
  <a href="skills.html" class="active">Skills</a>
391
429
  </nav>
392
430
  </div>