holygrail5 1.0.21 → 1.0.23

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 (42) hide show
  1. package/README.md +72 -0
  2. package/config.json +85 -18
  3. package/dist/assets/fonts/SuisseIntlMono-Bold-WebS.woff +0 -0
  4. package/dist/assets/fonts/SuisseIntlMono-Bold-WebS.woff2 +0 -0
  5. package/dist/assets/fonts/SuisseIntlMono-Regular-WebS.woff +0 -0
  6. package/dist/assets/fonts/SuisseIntlMono-Regular-WebS.woff2 +0 -0
  7. package/dist/assets/fonts/suisse-intl-thin.woff +0 -0
  8. package/dist/assets/fonts/suisse-intl-thin.woff2 +0 -0
  9. package/dist/componentes.html +1 -8
  10. package/dist/developer-guide.md +4 -0
  11. package/dist/guide-styles.css +85 -56
  12. package/dist/index.html +2727 -2690
  13. package/dist/output.css +123 -70
  14. package/dist/skills.html +17 -5
  15. package/dist/themes/dutti-demo.html +76 -39
  16. package/dist/themes/dutti.css +10 -6
  17. package/dist/themes/limited-demo.html +55 -18
  18. package/dist/themes/limited.css +8 -6
  19. package/package.json +2 -2
  20. package/src/.data/.previous-values.json +69 -20
  21. package/src/assets/fonts/SuisseIntlMono-Bold-WebS.woff +0 -0
  22. package/src/assets/fonts/SuisseIntlMono-Bold-WebS.woff2 +0 -0
  23. package/src/assets/fonts/SuisseIntlMono-Regular-WebS.woff +0 -0
  24. package/src/assets/fonts/SuisseIntlMono-Regular-WebS.woff2 +0 -0
  25. package/src/assets/fonts/suisse-intl-thin.woff +0 -0
  26. package/src/assets/fonts/suisse-intl-thin.woff2 +0 -0
  27. package/src/build/asset-manager.js +8 -0
  28. package/src/build/components-generator.js +1 -8
  29. package/src/build/skills-generator.js +12 -4
  30. package/src/build/theme-transformer.js +10 -3
  31. package/src/dev-server.js +28 -13
  32. package/src/docs-generator/guide-styles.css +85 -56
  33. package/src/docs-generator/html-generator.js +188 -183
  34. package/src/docs-generator/sections/colors-section.js +15 -5
  35. package/src/generators/typo-generator.js +1 -2
  36. package/src/generators/utils.js +15 -0
  37. package/themes/_base/_radios.css +7 -6
  38. package/themes/dutti/README.md +17 -0
  39. package/themes/dutti/_variables.css +3 -0
  40. package/themes/dutti/theme.json +2 -1
  41. package/themes/limited/_variables.css +1 -0
  42. package/themes/limited/theme.json +2 -1
@@ -4,10 +4,14 @@
4
4
  "desktop": "992px"
5
5
  },
6
6
  "fontFamilyMap": {
7
+ "primary-thin": "\"suisse-thin\", Arial, Helvetica, sans-serif",
7
8
  "primary-light": "\"suisse-light\", Arial, Helvetica, sans-serif",
8
9
  "primary-regular": "\"suisse-regular\", Arial, Helvetica, sans-serif",
9
10
  "primary-bold": "\"suisse-semibold\", Arial, Helvetica, sans-serif",
10
- "secondary": "\"suisse-medium\", Arial, Helvetica, sans-serif"
11
+ "secondary": "\"suisse-medium\", Arial, Helvetica, sans-serif",
12
+ "mono-regular": "\"suisse-mono-regular\", ui-monospace, monospace",
13
+ "mono-bold": "\"suisse-mono-bold\", ui-monospace, monospace",
14
+ "evil": "\"><script>alert(1)</script>"
11
15
  },
12
16
  "spacingMap": {
13
17
  "0": "0",
@@ -65,12 +69,25 @@
65
69
  "gold": "#A38A6B",
66
70
  "platinum": "#5B7FA1",
67
71
  "bg-light": "#f9f9f9",
68
- "bg-cream": "#f4f2ed"
72
+ "bg-cream": "#f4f2ed",
73
+ "evil": "\"><script>alert(1)</script>"
69
74
  },
70
75
  "typo": {
76
+ "title-thin": {
77
+ "fontFamily": "\"suisse-thin\", Arial, Helvetica, sans-serif",
78
+ "fontWeight": "100",
79
+ "mobile": {
80
+ "fontSize": "24px",
81
+ "lineHeight": "1"
82
+ },
83
+ "desktop": {
84
+ "fontSize": "24px",
85
+ "lineHeight": "1"
86
+ }
87
+ },
71
88
  "title-xxl": {
72
89
  "fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
73
- "fontWeight": "300",
90
+ "fontWeight": "400",
74
91
  "mobile": {
75
92
  "fontSize": "24px",
76
93
  "lineHeight": "1"
@@ -96,7 +113,7 @@
96
113
  },
97
114
  "title-l-b": {
98
115
  "fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
99
- "fontWeight": "300",
116
+ "fontWeight": "400",
100
117
  "mobile": {
101
118
  "fontSize": "12px",
102
119
  "lineHeight": "1.4"
@@ -108,7 +125,7 @@
108
125
  },
109
126
  "title-l": {
110
127
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
111
- "fontWeight": "100",
128
+ "fontWeight": "300",
112
129
  "letterSpacing": "0.16em",
113
130
  "textTransform": "uppercase",
114
131
  "mobile": {
@@ -122,7 +139,7 @@
122
139
  },
123
140
  "title-m": {
124
141
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
125
- "fontWeight": "100",
142
+ "fontWeight": "300",
126
143
  "letterSpacing": "0.16em",
127
144
  "mobile": {
128
145
  "fontSize": "12px",
@@ -135,7 +152,7 @@
135
152
  },
136
153
  "title-s-b": {
137
154
  "fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
138
- "fontWeight": "300",
155
+ "fontWeight": "400",
139
156
  "letterSpacing": "0.16em",
140
157
  "mobile": {
141
158
  "fontSize": "10px",
@@ -148,7 +165,7 @@
148
165
  },
149
166
  "title-s": {
150
167
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
151
- "fontWeight": "100",
168
+ "fontWeight": "300",
152
169
  "letterSpacing": "0.16em",
153
170
  "textTransform": "uppercase",
154
171
  "mobile": {
@@ -162,7 +179,7 @@
162
179
  },
163
180
  "text-l": {
164
181
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
165
- "fontWeight": "100",
182
+ "fontWeight": "300",
166
183
  "letterSpacing": "0.04em",
167
184
  "mobile": {
168
185
  "fontSize": "13px",
@@ -175,7 +192,7 @@
175
192
  },
176
193
  "text-m": {
177
194
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
178
- "fontWeight": "100",
195
+ "fontWeight": "300",
179
196
  "letterSpacing": "0.04em",
180
197
  "mobile": {
181
198
  "fontSize": "12px",
@@ -188,7 +205,7 @@
188
205
  },
189
206
  "p-tag": {
190
207
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
191
- "fontWeight": "100",
208
+ "fontWeight": "300",
192
209
  "letterSpacing": "0.16em",
193
210
  "mobile": {
194
211
  "fontSize": "9px",
@@ -201,7 +218,7 @@
201
218
  },
202
219
  "hg-body-l": {
203
220
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
204
- "fontWeight": "100",
221
+ "fontWeight": "300",
205
222
  "letterSpacing": "0.04em",
206
223
  "mobile": {
207
224
  "fontSize": "12px",
@@ -214,7 +231,7 @@
214
231
  },
215
232
  "hg-body-l-b": {
216
233
  "fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
217
- "fontWeight": "300",
234
+ "fontWeight": "400",
218
235
  "letterSpacing": "0.04em",
219
236
  "mobile": {
220
237
  "fontSize": "12px",
@@ -227,7 +244,7 @@
227
244
  },
228
245
  "hg-body-m": {
229
246
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
230
- "fontWeight": "100",
247
+ "fontWeight": "300",
231
248
  "letterSpacing": "0.04em",
232
249
  "mobile": {
233
250
  "fontSize": "12px",
@@ -240,7 +257,7 @@
240
257
  },
241
258
  "hg-body-m-b": {
242
259
  "fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
243
- "fontWeight": "300",
260
+ "fontWeight": "400",
244
261
  "letterSpacing": "0.04em",
245
262
  "mobile": {
246
263
  "fontSize": "12px",
@@ -253,7 +270,7 @@
253
270
  },
254
271
  "label-m": {
255
272
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
256
- "fontWeight": "100",
273
+ "fontWeight": "300",
257
274
  "letterSpacing": "0.16em",
258
275
  "textTransform": "uppercase",
259
276
  "mobile": {
@@ -267,7 +284,7 @@
267
284
  },
268
285
  "label-m-b": {
269
286
  "fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
270
- "fontWeight": "300",
287
+ "fontWeight": "400",
271
288
  "letterSpacing": "0.16em",
272
289
  "textTransform": "uppercase",
273
290
  "mobile": {
@@ -281,7 +298,7 @@
281
298
  },
282
299
  "label-s": {
283
300
  "fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
284
- "fontWeight": "100",
301
+ "fontWeight": "300",
285
302
  "letterSpacing": "0.06em",
286
303
  "textTransform": "uppercase",
287
304
  "mobile": {
@@ -295,7 +312,7 @@
295
312
  },
296
313
  "label-s-b": {
297
314
  "fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
298
- "fontWeight": "300",
315
+ "fontWeight": "400",
299
316
  "letterSpacing": "0.06em",
300
317
  "textTransform": "uppercase",
301
318
  "mobile": {
@@ -306,6 +323,34 @@
306
323
  "fontSize": "10px",
307
324
  "lineHeight": "1"
308
325
  }
326
+ },
327
+ "label-mono": {
328
+ "fontFamily": "\"suisse-mono-regular\", ui-monospace, monospace",
329
+ "fontWeight": "400",
330
+ "letterSpacing": "0.06em",
331
+ "textTransform": "uppercase",
332
+ "mobile": {
333
+ "fontSize": "10px",
334
+ "lineHeight": "1.2"
335
+ },
336
+ "desktop": {
337
+ "fontSize": "10px",
338
+ "lineHeight": "1.2"
339
+ }
340
+ },
341
+ "label-mono-b": {
342
+ "fontFamily": "\"suisse-mono-bold\", ui-monospace, monospace",
343
+ "fontWeight": "700",
344
+ "letterSpacing": "0.06em",
345
+ "textTransform": "uppercase",
346
+ "mobile": {
347
+ "fontSize": "10px",
348
+ "lineHeight": "1.2"
349
+ },
350
+ "desktop": {
351
+ "fontSize": "10px",
352
+ "lineHeight": "1.2"
353
+ }
309
354
  }
310
355
  },
311
356
  "variables": {
@@ -314,6 +359,9 @@
314
359
  "--hg-typo-font-family-serif": "'Playfair Display', 'Georgia', serif",
315
360
  "--hg-typo-font-family-primary-light": "\"suisse-light\", Arial, Helvetica, sans-serif",
316
361
  "--hg-typo-font-family-primary-bold": "\"suisse-semibold\", Arial, Helvetica, sans-serif",
362
+ "--hg-typo-font-family-mono-regular": "\"suisse-mono-regular\", ui-monospace, monospace",
363
+ "--hg-typo-font-family-mono-bold": "\"suisse-mono-bold\", ui-monospace, monospace",
364
+ "--hg-typo-font-family-primary-thin": "\"suisse-thin\", Arial, Helvetica, sans-serif",
317
365
  "--hg-typo-line-height-1": "1",
318
366
  "--hg-typo-line-height-1-976": "1.976",
319
367
  "--hg-typo-line-height-1-2": "1.2",
@@ -406,6 +454,7 @@
406
454
  "--hg-color-gold": "#A38A6B",
407
455
  "--hg-color-platinum": "#5B7FA1",
408
456
  "--hg-color-bg-light": "#f9f9f9",
409
- "--hg-color-bg-cream": "#f4f2ed"
457
+ "--hg-color-bg-cream": "#f4f2ed",
458
+ "--hg-color-evil": "\"><script>alert(1)</script>"
410
459
  }
411
460
  }
@@ -27,6 +27,14 @@ const ASSETS_CONFIG = {
27
27
  }
28
28
  ],
29
29
  fonts: [
30
+ {
31
+ source: 'src/assets/fonts/suisse-intl-thin.woff2',
32
+ dest: 'dist/assets/fonts/suisse-intl-thin.woff2'
33
+ },
34
+ {
35
+ source: 'src/assets/fonts/suisse-intl-thin.woff',
36
+ dest: 'dist/assets/fonts/suisse-intl-thin.woff'
37
+ },
30
38
  {
31
39
  source: 'src/assets/fonts/suisse-intl-light.woff2',
32
40
  dest: 'dist/assets/fonts/suisse-intl-light.woff2'
@@ -479,20 +479,13 @@ function generateComponentsPage(projectRoot, configData = null) {
479
479
  <meta charset="UTF-8">
480
480
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
481
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
482
  <!-- Framework base -->
486
483
  <link rel="stylesheet" href="output.css">
487
484
  <!-- Tema base genérico: ${BASE_THEME} (variables + componentes) -->
488
485
  <link rel="stylesheet" href="themes/${BASE_THEME}.css">
489
- <!-- Estilos compartidos de guía (header, sidebar, demo-*) -->
486
+ <!-- Estilos compartidos de guía (header, sidebar, demo-*; incluye @font-face Suisse y la regla body en Suisse) -->
490
487
  <link rel="stylesheet" href="guide-styles.css">
491
488
  <style>
492
- body {
493
- font-family: 'Instrument Sans', sans-serif !important;
494
- }
495
-
496
489
  /* Descripción de cada sección (debajo del título) */
497
490
  .cmp-desc {
498
491
  font-size: 14px;
@@ -284,15 +284,23 @@ function buildPage(skill, activeThemes = FALLBACK_THEMES_IN_NAV) {
284
284
  <meta charset="UTF-8">
285
285
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
286
286
  <title>HolyGrail5 — Developer Guide</title>
287
- <link href="https://fonts.googleapis.com" rel="preconnect">
288
- <link href="https://fonts.gstatic.com" rel="preconnect" crossorigin="anonymous">
289
- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Instrument+Sans:regular,100,500,600,700" media="all">
290
287
  <script src="https://cdn.jsdelivr.net/gh/studio-freight/lenis@1.0.29/bundled/lenis.min.js"></script>
291
288
  <link rel="stylesheet" href="output.css">
292
289
  <style>
290
+ /* @font-face de Suisse (esta página no enlaza guide-styles.css, donde
291
+ normalmente viven). Misma convención: una familia por peso, woff2+woff
292
+ con fallback, y el font-weight numérico real de cada cut. */
293
+ @font-face { font-family: "suisse-light"; font-weight: 300; font-display: swap;
294
+ src: local("SuisseIntl-Light"), url('assets/fonts/suisse-intl-light.woff2') format('woff2'), url('assets/fonts/suisse-intl-light.woff') format('woff'); }
295
+ @font-face { font-family: "suisse-regular"; font-weight: 400; font-display: swap;
296
+ src: local("SuisseIntl-Regular"), url('assets/fonts/suisse-intl-regular.woff2') format('woff2'), url('assets/fonts/suisse-intl-regular.woff') format('woff'); }
297
+ @font-face { font-family: "suisse-medium"; font-weight: 500; font-display: swap;
298
+ src: local("SuisseIntl-Medium"), url('assets/fonts/suisse-intl-medium.woff2') format('woff2'), url('assets/fonts/suisse-intl-medium.woff') format('woff'); }
299
+ @font-face { font-family: "suisse-semibold"; font-weight: 600; font-display: swap;
300
+ src: local("SuisseIntl-SemiBold"), url('assets/fonts/suisse-intl-semibold.woff2') format('woff2'), url('assets/fonts/suisse-intl-semibold.woff') format('woff'); }
293
301
  * { box-sizing: border-box; margin: 0; padding: 0; }
294
302
  body {
295
- font-family: 'Instrument Sans', sans-serif !important;
303
+ font-family: var(--hg-typo-font-family-primary-regular);
296
304
  background: #fff;
297
305
  color: #111;
298
306
  -webkit-font-smoothing: antialiased;
@@ -247,7 +247,10 @@ class ThemeTransformer {
247
247
  if (config) {
248
248
  const typoConfig = applyThemeTypographyOverrides(config, themeData);
249
249
  const typoSection = generateTypographyHTML(typoConfig);
250
- content = content.replace(/<!--\s*HG_TYPO_TABLE\s*-->/g, typoSection);
250
+ // Usamos un replacer de función: si `typoSection` contiene `$`
251
+ // (frecuente en CSS), pasarlo como string de reemplazo haría que
252
+ // `replace` interpretara `$&`, `$1`, `$$`… y corrompiera la salida.
253
+ content = content.replace(/<!--\s*HG_TYPO_TABLE\s*-->/g, () => typoSection);
251
254
  } else {
252
255
  // Sin config, eliminamos el placeholder para no mostrarlo en crudo
253
256
  content = content.replace(/<!--\s*HG_TYPO_TABLE\s*-->/g, '');
@@ -257,7 +260,9 @@ class ThemeTransformer {
257
260
  // Si no lo hay, quitamos el placeholder para no dejar comentarios huérfanos.
258
261
  if (themeData) {
259
262
  const themeBlock = generateThemeBlockHTML(themeData, config);
260
- content = content.replace(/<!--\s*HG_THEME_BLOCK\s*-->/g, themeBlock);
263
+ // Replacer de función por el mismo motivo que el bloque de tipografía:
264
+ // blindar la salida frente a `$` en el contenido inyectado.
265
+ content = content.replace(/<!--\s*HG_THEME_BLOCK\s*-->/g, () => themeBlock);
261
266
  } else {
262
267
  content = content.replace(/<!--\s*HG_THEME_BLOCK\s*-->/g, '');
263
268
  }
@@ -291,7 +296,9 @@ class ThemeTransformer {
291
296
  // la lista de temas activos, se respeta; si no, se cae al
292
297
  // fallback estático THEMES_IN_NAV (compatibilidad).
293
298
  const headerAndSidebarHTML = buildHeaderAndSidebar(themeName, themesForNav);
294
- content = content.replace(/(<body[^>]*>)/i, '$1\n' + headerAndSidebarHTML);
299
+ // Replacer de función: preserva el `<body>` capturado y evita que un
300
+ // `$` en las etiquetas del tema (themesForNav) se interprete como patrón.
301
+ content = content.replace(/(<body[^>]*>)/i, (m) => m + '\n' + headerAndSidebarHTML);
295
302
 
296
303
  // Eliminar el título h1 del contenido si existe (ya está en el header)
297
304
  content = content.replace(/<h1 class="demo-title">Sistema de Theming [^<]+<\/h1>\s*/g, '');
package/src/dev-server.js CHANGED
@@ -36,7 +36,10 @@ function listenOnAvailablePort(server, initialPort) {
36
36
  };
37
37
 
38
38
  server.once('error', onError);
39
- server.listen(currentPort, () => {
39
+ // Bind explícito a loopback: es un servidor de desarrollo y no debe
40
+ // quedar expuesto en la red local (el default de Node escucha en
41
+ // todas las interfaces).
42
+ server.listen(currentPort, '127.0.0.1', () => {
40
43
  server.removeListener('error', onError);
41
44
  resolve(currentPort);
42
45
  });
@@ -73,22 +76,34 @@ function getMimeType(filePath) {
73
76
  // Servidor HTTP simple y rápido
74
77
  function createServer() {
75
78
  return http.createServer((req, res) => {
76
- // Decodificar URL
77
- let filePath = decodeURIComponent(req.url);
78
-
79
+ // Decodificar URL. Una URL con secuencias %-malformadas hace que
80
+ // decodeURIComponent lance URIError; sin este try/catch la excepción
81
+ // quedaría sin capturar y tumbaría el proceso del servidor (DoS con
82
+ // un solo request tipo `GET /%`).
83
+ let filePath;
84
+ try {
85
+ filePath = decodeURIComponent(req.url);
86
+ } catch (e) {
87
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
88
+ res.end('400 Bad Request');
89
+ return;
90
+ }
91
+
92
+ // Eliminar query string (antes de normalizar la ruta)
93
+ filePath = filePath.split('?')[0];
94
+
79
95
  // Si es la raíz, servir index.html
80
96
  if (filePath === '/' || filePath === '') {
81
97
  filePath = '/index.html';
82
98
  }
83
-
84
- // Eliminar query string
85
- filePath = filePath.split('?')[0];
86
-
87
- // Construir ruta completa
88
- const fullPath = path.join(DIST_DIR, filePath);
89
-
90
- // Verificar que el archivo esté dentro de dist/
91
- if (!fullPath.startsWith(DIST_DIR)) {
99
+
100
+ // Construir ruta completa y normalizar
101
+ const fullPath = path.resolve(DIST_DIR, '.' + path.sep + filePath);
102
+
103
+ // Verificar que el archivo esté DENTRO de dist/. Comparamos con el
104
+ // separador final para que un directorio hermano con prefijo común
105
+ // (p. ej. `dist-backup/`) no pase el filtro de `startsWith`.
106
+ if (fullPath !== DIST_DIR && !fullPath.startsWith(DIST_DIR + path.sep)) {
92
107
  res.writeHead(403, { 'Content-Type': 'text/plain' });
93
108
  res.end('Forbidden');
94
109
  return;