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.
- package/README.md +88 -18
- package/config.json +205 -77
- package/dist/assets/fonts/suisse-intl-light.woff +0 -0
- package/dist/assets/fonts/suisse-intl-light.woff2 +0 -0
- package/dist/assets/fonts/suisse-intl-medium.woff +0 -0
- package/dist/assets/fonts/suisse-intl-medium.woff2 +0 -0
- package/dist/assets/fonts/suisse-intl-regular.woff +0 -0
- package/dist/assets/fonts/suisse-intl-regular.woff2 +0 -0
- package/dist/assets/fonts/suisse-intl-semibold.woff +0 -0
- package/dist/assets/fonts/suisse-intl-semibold.woff2 +0 -0
- package/dist/assets/fonts/suisse-works-bold.woff +0 -0
- package/dist/assets/fonts/suisse-works-bold.woff2 +0 -0
- package/dist/assets/fonts/suisse-works-medium.woff +0 -0
- package/dist/assets/fonts/suisse-works-medium.woff2 +0 -0
- package/dist/assets/fonts/suisse-works-regular.woff +0 -0
- package/dist/assets/fonts/suisse-works-regular.woff2 +0 -0
- package/dist/componentes.html +429 -0
- package/dist/developer-guide.md +7 -7
- package/dist/guide-styles.css +197 -25
- package/dist/index.html +807 -689
- package/dist/output.css +217 -114
- package/dist/skills.html +14 -8
- package/dist/themes/dutti-demo.html +713 -19
- package/dist/themes/dutti.css +67 -16
- package/dist/themes/limited-demo.html +1121 -0
- package/dist/themes/limited.css +2493 -0
- package/package.json +1 -1
- package/src/.data/.previous-values.json +151 -84
- package/src/assets/fonts/suisse-intl-light.woff +0 -0
- package/src/assets/fonts/suisse-intl-light.woff2 +0 -0
- package/src/assets/fonts/suisse-intl-medium.woff +0 -0
- package/src/assets/fonts/suisse-intl-medium.woff2 +0 -0
- package/src/assets/fonts/suisse-intl-regular.woff +0 -0
- package/src/assets/fonts/suisse-intl-regular.woff2 +0 -0
- package/src/assets/fonts/suisse-intl-semibold.woff +0 -0
- package/src/assets/fonts/suisse-intl-semibold.woff2 +0 -0
- package/src/assets/fonts/suisse-works-bold.woff +0 -0
- package/src/assets/fonts/suisse-works-bold.woff2 +0 -0
- package/src/assets/fonts/suisse-works-medium.woff +0 -0
- package/src/assets/fonts/suisse-works-medium.woff2 +0 -0
- package/src/assets/fonts/suisse-works-regular.woff +0 -0
- package/src/assets/fonts/suisse-works-regular.woff2 +0 -0
- package/src/build/asset-manager.js +94 -3
- package/src/build/build-orchestrator.js +74 -12
- package/src/build/components-generator.js +584 -0
- package/src/build/skills-generator.js +43 -5
- package/src/build/theme-config-loader.js +58 -0
- package/src/build/theme-transformer.js +109 -16
- package/src/build/theme-vars-table-generator.js +349 -0
- package/src/build/typo-table-generator.js +154 -0
- package/src/docs-generator/guide-styles.css +197 -24
- package/src/docs-generator/html-generator.js +92 -246
- package/src/docs-generator/sections/colors-section.js +109 -0
- package/src/docs-generator/value-tracker.js +154 -0
- package/src/generators/typo-generator.js +2 -1
- package/src/generators/utils.js +90 -1
- package/src/skills.html +1 -0
- package/src/watch-config.js +99 -32
- package/themes/{dutti → _base}/_buttons.css +2 -2
- package/themes/{dutti → _base}/_checkboxes.css +1 -1
- package/themes/{dutti → _base}/_forms.css +1 -1
- package/themes/{dutti → _base}/_inputs.css +55 -10
- package/themes/{dutti → _base}/_labels.css +1 -1
- package/themes/dutti/README.md +67 -21
- package/themes/dutti/_variables.css +7 -1
- package/themes/dutti/demo.html +18 -14
- package/themes/dutti/theme.css +22 -44
- package/themes/dutti/theme.json +86 -0
- package/themes/limited/_variables.css +123 -0
- package/themes/limited/demo.html +296 -0
- package/themes/limited/theme.css +32 -0
- package/themes/limited/theme.json +105 -0
- /package/themes/{dutti → _base}/_containers.css +0 -0
- /package/themes/{dutti → _base}/_radios.css +0 -0
- /package/themes/{dutti → _base}/_switches.css +0 -0
- /package/themes/{dutti → _base}/components/_icons.css +0 -0
- /package/themes/{dutti → _base}/objects/_grid.css +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Loader del JSON de configuración de tema
|
|
2
|
+
// Cada tema vive en themes/<nombre>/ y puede tener un theme.json con:
|
|
3
|
+
// - meta → nombre, descripción, versión, autor (para la demo)
|
|
4
|
+
// - tokenOverrides → overrides de tokens HG5 por tema (color/spacing)
|
|
5
|
+
// - componentVars → variables CSS de componentes (--btn-*, --input-*, etc.)
|
|
6
|
+
// - design → tokens de diseño del tema (border-radius, transition…)
|
|
7
|
+
//
|
|
8
|
+
// El archivo es OPCIONAL. Si no existe, devolvemos null y el resto del build
|
|
9
|
+
// sigue funcionando exactamente como antes — la única consecuencia es que la
|
|
10
|
+
// demo no mostrará la cabecera de meta ni las tablas de variables del tema.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Devuelve la ruta absoluta esperada del theme.json para un tema dado.
|
|
17
|
+
*/
|
|
18
|
+
function getThemeConfigPath(projectRoot, themeName) {
|
|
19
|
+
return path.join(projectRoot, 'themes', themeName, 'theme.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Carga themes/<themeName>/theme.json si existe. Si no existe o el JSON es
|
|
24
|
+
* inválido, devuelve null (con un warning si silent === false). Nunca lanza:
|
|
25
|
+
* el config de tema es opcional y no debe romper el build.
|
|
26
|
+
*/
|
|
27
|
+
function loadThemeConfig(projectRoot, themeName, silent = false) {
|
|
28
|
+
if (!themeName) return null;
|
|
29
|
+
|
|
30
|
+
const themeConfigPath = getThemeConfigPath(projectRoot, themeName);
|
|
31
|
+
if (!fs.existsSync(themeConfigPath)) return null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(themeConfigPath, 'utf8');
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
|
|
37
|
+
// Normalizamos para que el resto del código asuma que las claves existen
|
|
38
|
+
parsed.meta = parsed.meta || {};
|
|
39
|
+
parsed.tokenOverrides = parsed.tokenOverrides || {};
|
|
40
|
+
parsed.componentVars = parsed.componentVars || {};
|
|
41
|
+
parsed.design = parsed.design || {};
|
|
42
|
+
|
|
43
|
+
// Si el JSON no trae nombre, usamos el del directorio
|
|
44
|
+
if (!parsed.meta.name) parsed.meta.name = themeName;
|
|
45
|
+
|
|
46
|
+
if (!silent) {
|
|
47
|
+
console.log(`✅ theme.json cargado: themes/${themeName}/theme.json`);
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!silent) {
|
|
52
|
+
console.warn(`⚠️ No se pudo cargar themes/${themeName}/theme.json:`, error.message);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { loadThemeConfig, getThemeConfigPath };
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { generateTypographyHTML } = require('./typo-table-generator');
|
|
7
|
+
const { generateThemeBlockHTML } = require('./theme-vars-table-generator');
|
|
8
|
+
const { applyThemeTypographyOverrides } = require('../generators/utils');
|
|
6
9
|
|
|
7
10
|
// Estilos del sidebar + Lenis (solo para dutti-demo.html en dist)
|
|
8
11
|
const SIDEBAR_STYLES = `
|
|
@@ -24,31 +27,84 @@ const SIDEBAR_STYLES = `
|
|
|
24
27
|
}
|
|
25
28
|
`;
|
|
26
29
|
|
|
27
|
-
//
|
|
28
|
-
|
|
30
|
+
// Fallback de temas conocidos para la navegación de cada demo, usado
|
|
31
|
+
// cuando el llamador NO inyecta una lista dinámica (p. ej. tests o
|
|
32
|
+
// ejecuciones standalone del ThemeTransformer sin pasar por
|
|
33
|
+
// BuildOrchestrator). En modo build normal, esta lista se sustituye por
|
|
34
|
+
// los temas realmente activos en `config.themes[]`, de forma que
|
|
35
|
+
// nunca se genere un enlace `../themes/<x>-demo.html` a un fichero
|
|
36
|
+
// que no existe en `dist/themes/`.
|
|
37
|
+
// El orden aquí determina el orden en el que aparecen los enlaces.
|
|
38
|
+
// `name` es el slug del tema (coincide con la carpeta themes/<name>/ y
|
|
39
|
+
// con el fichero generado themes/<name>-demo.html). `label` es el
|
|
40
|
+
// texto visible en la nav principal.
|
|
41
|
+
const THEMES_IN_NAV = [
|
|
42
|
+
{ name: 'dutti', label: 'Tema Dutti' },
|
|
43
|
+
{ name: 'limited', label: 'Tema Limited' }
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Capitaliza un slug para usarlo como nombre legible en el sidebar
|
|
47
|
+
// (p. ej. 'dutti' → 'Dutti', 'limited' → 'Limited').
|
|
48
|
+
function capitalize(s) {
|
|
49
|
+
if (!s) return '';
|
|
50
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Construye el HTML del header + sidebar para una demo concreta.
|
|
55
|
+
* El enlace del tema actual se marca con la clase `active` para que
|
|
56
|
+
* el usuario vea en qué tema está. Los enlaces a otros temas apuntan
|
|
57
|
+
* al fichero `../themes/<slug>-demo.html` correspondiente.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} currentTheme - slug del tema activo (p. ej. 'dutti')
|
|
60
|
+
* @param {Array<{name:string,label:string}>} [themesForNav] - Lista de
|
|
61
|
+
* temas a mostrar en la nav. Si se omite o viene vacía, se usa el
|
|
62
|
+
* fallback `THEMES_IN_NAV`. Inyectar esta lista desde el build evita
|
|
63
|
+
* que la demo enlace a temas que no se han generado en `dist/themes/`.
|
|
64
|
+
* @returns {string} HTML listo para inyectar tras `<body>`.
|
|
65
|
+
*/
|
|
66
|
+
function buildHeaderAndSidebar(currentTheme, themesForNav = null) {
|
|
67
|
+
const list = Array.isArray(themesForNav) && themesForNav.length > 0
|
|
68
|
+
? themesForNav
|
|
69
|
+
: THEMES_IN_NAV;
|
|
70
|
+
|
|
71
|
+
const themeLinks = list.map(t => {
|
|
72
|
+
const cls = t.name === currentTheme ? ' class="active"' : '';
|
|
73
|
+
return ` <a href="../themes/${t.name}-demo.html"${cls}>${t.label}</a>`;
|
|
74
|
+
}).join('\n');
|
|
75
|
+
|
|
76
|
+
const displayName = capitalize(currentTheme);
|
|
77
|
+
|
|
78
|
+
return `
|
|
29
79
|
<div class="guide-header">
|
|
30
80
|
<a href="../index.html" class="guide-logo" style="text-decoration:none;">HolyGrail5</a>
|
|
31
81
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
32
82
|
<nav class="guide-nav">
|
|
33
83
|
<a href="../index.html">Guía</a>
|
|
34
|
-
<a href="../
|
|
84
|
+
<a href="../componentes.html">Componentes</a>
|
|
85
|
+
${themeLinks}
|
|
35
86
|
<a href="../skills.html">Skills</a>
|
|
36
87
|
</nav>
|
|
37
88
|
<button class="guide-header-button" onclick="toggleSidebar()">☰</button>
|
|
38
89
|
</div>
|
|
39
90
|
</div>
|
|
40
|
-
|
|
91
|
+
|
|
41
92
|
<div class="guide-sidebar-overlay" onclick="toggleSidebar()"></div>
|
|
42
|
-
|
|
93
|
+
|
|
43
94
|
<aside class="guide-sidebar">
|
|
44
95
|
<div class="guide-sidebar-header">
|
|
45
96
|
<div>HolyGrail5</div>
|
|
46
|
-
<p class="guide-sidebar-subtitle">Demo Tema
|
|
97
|
+
<p class="guide-sidebar-subtitle">Demo Tema ${displayName}</p>
|
|
47
98
|
</div>
|
|
48
99
|
|
|
49
100
|
<nav class="guide-sidebar-nav">
|
|
101
|
+
<p class="guide-sidebar-title">Tema</p>
|
|
102
|
+
|
|
103
|
+
<a href="#theme-vars" class="guide-menu-item">Variables del tema</a>
|
|
104
|
+
|
|
50
105
|
<p class="guide-sidebar-title">Componentes</p>
|
|
51
|
-
|
|
106
|
+
|
|
107
|
+
<a href="#typography" class="guide-menu-item">Tipografía</a>
|
|
52
108
|
<a href="#buttons" class="guide-menu-item">Botones</a>
|
|
53
109
|
<a href="#inputs" class="guide-menu-item">Inputs</a>
|
|
54
110
|
<a href="#checkboxes" class="guide-menu-item">Checkboxes</a>
|
|
@@ -57,7 +113,7 @@ const HEADER_AND_SIDEBAR_HTML = `
|
|
|
57
113
|
<a href="#forms" class="guide-menu-item">Formularios</a>
|
|
58
114
|
</nav>
|
|
59
115
|
</aside>
|
|
60
|
-
|
|
116
|
+
|
|
61
117
|
<script>
|
|
62
118
|
function toggleSidebar() {
|
|
63
119
|
const sidebar = document.querySelector('.guide-sidebar');
|
|
@@ -65,19 +121,20 @@ const HEADER_AND_SIDEBAR_HTML = `
|
|
|
65
121
|
sidebar.classList.toggle('open');
|
|
66
122
|
overlay.classList.toggle('active');
|
|
67
123
|
}
|
|
68
|
-
|
|
124
|
+
|
|
69
125
|
function closeSidebar() {
|
|
70
126
|
const sidebar = document.querySelector('.guide-sidebar');
|
|
71
127
|
const overlay = document.querySelector('.guide-sidebar-overlay');
|
|
72
128
|
sidebar.classList.remove('open');
|
|
73
129
|
overlay.classList.remove('active');
|
|
74
130
|
}
|
|
75
|
-
|
|
131
|
+
|
|
76
132
|
// Hacer funciones globales
|
|
77
133
|
window.toggleSidebar = toggleSidebar;
|
|
78
134
|
window.closeSidebar = closeSidebar;
|
|
79
135
|
</script>
|
|
80
136
|
`;
|
|
137
|
+
}
|
|
81
138
|
|
|
82
139
|
// Script de Lenis para el head
|
|
83
140
|
const LENIS_HEAD_SCRIPT = `
|
|
@@ -153,9 +210,14 @@ class ThemeTransformer {
|
|
|
153
210
|
* @param {string} destPath - Ruta donde guardar el HTML transformado
|
|
154
211
|
* @param {string} themeName - Nombre del tema (para personalización)
|
|
155
212
|
* @param {boolean} silent - Si true, no muestra mensajes
|
|
213
|
+
* @param {Object} [config] - Config cargado de config.json (para inyectar tablas dinámicas como tipografía)
|
|
214
|
+
* @param {Object} [themeData] - theme.json parseado (para inyectar meta + tablas de variables del tema)
|
|
215
|
+
* @param {Array<{name:string,label:string}>} [themesForNav] - Lista de
|
|
216
|
+
* temas activos a exponer en la nav del demo. Si se omite, se usa
|
|
217
|
+
* el fallback estático de theme-transformer (dutti + limited).
|
|
156
218
|
* @returns {boolean} - true si se transformó exitosamente
|
|
157
219
|
*/
|
|
158
|
-
transform(sourcePath, destPath, themeName = 'dutti', silent = false) {
|
|
220
|
+
transform(sourcePath, destPath, themeName = 'dutti', silent = false, config = null, themeData = null, themesForNav = null) {
|
|
159
221
|
const fullSourcePath = path.isAbsolute(sourcePath)
|
|
160
222
|
? sourcePath
|
|
161
223
|
: path.join(this.projectRoot, sourcePath);
|
|
@@ -174,7 +236,32 @@ class ThemeTransformer {
|
|
|
174
236
|
try {
|
|
175
237
|
// Leer el contenido
|
|
176
238
|
let content = fs.readFileSync(fullSourcePath, 'utf8');
|
|
177
|
-
|
|
239
|
+
|
|
240
|
+
// Inyectar bloques dinámicos derivados del config (p. ej. tabla de tipografía).
|
|
241
|
+
// Si el HTML fuente no contiene el placeholder, se ignora silenciosamente.
|
|
242
|
+
//
|
|
243
|
+
// Para cada tema aplicamos los overrides de tipografía declarados
|
|
244
|
+
// en `theme.json → typography.fontFamilyMap`. Así la tabla
|
|
245
|
+
// muestra la fuente real del tema (Suisse Works en Limited, por
|
|
246
|
+
// ejemplo) en vez de la fuente base de config.json.
|
|
247
|
+
if (config) {
|
|
248
|
+
const typoConfig = applyThemeTypographyOverrides(config, themeData);
|
|
249
|
+
const typoSection = generateTypographyHTML(typoConfig);
|
|
250
|
+
content = content.replace(/<!--\s*HG_TYPO_TABLE\s*-->/g, typoSection);
|
|
251
|
+
} else {
|
|
252
|
+
// Sin config, eliminamos el placeholder para no mostrarlo en crudo
|
|
253
|
+
content = content.replace(/<!--\s*HG_TYPO_TABLE\s*-->/g, '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Inyectar bloque del tema (meta + tablas de variables) si hay theme.json.
|
|
257
|
+
// Si no lo hay, quitamos el placeholder para no dejar comentarios huérfanos.
|
|
258
|
+
if (themeData) {
|
|
259
|
+
const themeBlock = generateThemeBlockHTML(themeData, config);
|
|
260
|
+
content = content.replace(/<!--\s*HG_THEME_BLOCK\s*-->/g, themeBlock);
|
|
261
|
+
} else {
|
|
262
|
+
content = content.replace(/<!--\s*HG_THEME_BLOCK\s*-->/g, '');
|
|
263
|
+
}
|
|
264
|
+
|
|
178
265
|
// Ajustar rutas CSS
|
|
179
266
|
content = content.replace(/href="\.\.\/\.\.\/dist\/output\.css"/g, 'href="../output.css"');
|
|
180
267
|
content = content.replace(/href="\.\.\/output\.css"/g, 'href="../output.css"');
|
|
@@ -197,11 +284,17 @@ class ThemeTransformer {
|
|
|
197
284
|
// Añadir inicialización de Lenis antes de </body>
|
|
198
285
|
content = content.replace('</body>', `${LENIS_INIT_SCRIPT}\n</body>`);
|
|
199
286
|
|
|
200
|
-
// Añadir header y sidebar después del <body
|
|
201
|
-
|
|
202
|
-
|
|
287
|
+
// Añadir header y sidebar después del <body>.
|
|
288
|
+
// La nav principal se construye dinámicamente a partir del tema
|
|
289
|
+
// activo, marcando su enlace con `active` y dejando los otros
|
|
290
|
+
// temas accesibles como enlaces normales. Si el build ha pasado
|
|
291
|
+
// la lista de temas activos, se respeta; si no, se cae al
|
|
292
|
+
// fallback estático THEMES_IN_NAV (compatibilidad).
|
|
293
|
+
const headerAndSidebarHTML = buildHeaderAndSidebar(themeName, themesForNav);
|
|
294
|
+
content = content.replace(/(<body[^>]*>)/i, '$1\n' + headerAndSidebarHTML);
|
|
295
|
+
|
|
203
296
|
// Eliminar el título h1 del contenido si existe (ya está en el header)
|
|
204
|
-
content = content.replace(/<h1 class="demo-title">Sistema de Theming
|
|
297
|
+
content = content.replace(/<h1 class="demo-title">Sistema de Theming [^<]+<\/h1>\s*/g, '');
|
|
205
298
|
|
|
206
299
|
// Envolver el contenido de demo-container con guide-container
|
|
207
300
|
content = content.replace(
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Generador de la cabecera de meta y de las tablas de variables del tema.
|
|
2
|
+
// Lee el theme.json (cargado por theme-config-loader) y produce HTML para
|
|
3
|
+
// inyectar en la demo del tema. Si no hay theme.json o el bloque está vacío,
|
|
4
|
+
// devuelve string vacío para mantener la demo limpia.
|
|
5
|
+
|
|
6
|
+
function escapeHtml(str) {
|
|
7
|
+
if (str === undefined || str === null) return '';
|
|
8
|
+
return String(str)
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/"/g, '"');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Estilos comunes para las tablas del tema. Se inyectan una sola vez.
|
|
17
|
+
*/
|
|
18
|
+
const SHARED_STYLES = `
|
|
19
|
+
<style>
|
|
20
|
+
.hg-theme-meta {
|
|
21
|
+
margin-bottom: 32px;
|
|
22
|
+
padding: 20px 24px;
|
|
23
|
+
border-left: 4px solid var(--hg-color-primary, #000);
|
|
24
|
+
background: var(--hg-color-bg-light, #f0f0f0);
|
|
25
|
+
}
|
|
26
|
+
.hg-theme-meta h3 { margin: 0 0 4px; font-size: 22px; font-family: arial, sans-serif; }
|
|
27
|
+
.hg-theme-meta .hg-theme-meta-line {
|
|
28
|
+
font-size: 12px; color: var(--hg-color-dark-grey, #737373);
|
|
29
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
30
|
+
}
|
|
31
|
+
.hg-theme-meta p { margin: 12px 0 0; font-size: 14px; line-height: 1.6; }
|
|
32
|
+
.hg-vars-group { margin: 0; }
|
|
33
|
+
.hg-vars-group h4 {
|
|
34
|
+
margin: 0 0 8px; font-family: arial, sans-serif; font-size: 14px;
|
|
35
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
36
|
+
color: var(--hg-color-dark-grey, #737373);
|
|
37
|
+
}
|
|
38
|
+
/* 3 columnas verticales manuales (grid), balanceadas por item count */
|
|
39
|
+
.hg-vars-columns {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
42
|
+
gap: 32px;
|
|
43
|
+
font-family: arial, sans-serif;
|
|
44
|
+
font-size: 11px;
|
|
45
|
+
align-items: start;
|
|
46
|
+
}
|
|
47
|
+
@media (max-width: 960px) {
|
|
48
|
+
.hg-vars-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
49
|
+
}
|
|
50
|
+
@media (max-width: 600px) {
|
|
51
|
+
.hg-vars-columns { grid-template-columns: 1fr; }
|
|
52
|
+
}
|
|
53
|
+
.hg-vars-col {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 24px;
|
|
57
|
+
min-width: 0;
|
|
58
|
+
}
|
|
59
|
+
/* Lista de variables dentro del grupo: nombre izquierda, valor derecha */
|
|
60
|
+
.hg-vars-list {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
}
|
|
64
|
+
.hg-vars-item {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: space-between;
|
|
68
|
+
gap: 8px;
|
|
69
|
+
padding: 5px 0;
|
|
70
|
+
border-bottom: 1px solid var(--hg-color-middle-grey, #a9a9a9);
|
|
71
|
+
min-width: 0;
|
|
72
|
+
}
|
|
73
|
+
.hg-vars-item .hg-vars-name,
|
|
74
|
+
.hg-vars-item .hg-vars-value {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
}
|
|
79
|
+
.hg-vars-item .hg-vars-name { flex: 1 1 auto; }
|
|
80
|
+
.hg-vars-item .hg-vars-value { flex: 0 0 auto; justify-content: flex-end; text-align: right; }
|
|
81
|
+
.hg-vars-item code {
|
|
82
|
+
background: var(--hg-color-bg-light, #f0f0f0);
|
|
83
|
+
padding: 2px 5px;
|
|
84
|
+
border-radius: 3px;
|
|
85
|
+
font-size: 10.5px;
|
|
86
|
+
line-height: 1.35;
|
|
87
|
+
word-break: break-all;
|
|
88
|
+
white-space: normal;
|
|
89
|
+
}
|
|
90
|
+
.hg-vars-item .hg-vars-name code { color: #000; font-weight: 600; }
|
|
91
|
+
.hg-vars-item .hg-vars-value code { color: #333; }
|
|
92
|
+
.hg-vars-item.is-overridden .hg-vars-name code { color: var(--hg-color-feel-dark, #c94c07); }
|
|
93
|
+
.hg-vars-item .hg-vars-badge {
|
|
94
|
+
display: inline-block;
|
|
95
|
+
margin-left: 6px;
|
|
96
|
+
padding: 1px 5px;
|
|
97
|
+
font-size: 9px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
letter-spacing: 0.04em;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
background: var(--hg-color-feel, #fb9962);
|
|
102
|
+
color: #fff;
|
|
103
|
+
border-radius: 2px;
|
|
104
|
+
}
|
|
105
|
+
.hg-vars-swatch {
|
|
106
|
+
display: inline-block; width: 12px; height: 12px;
|
|
107
|
+
border-radius: 3px; vertical-align: middle;
|
|
108
|
+
margin-left: 6px;
|
|
109
|
+
border: 1px solid var(--hg-color-middle-grey, #a9a9a9);
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
order: 2;
|
|
112
|
+
}
|
|
113
|
+
/* Sección de colores semánticos: swatch al final de la fila, 18x18 */
|
|
114
|
+
.hg-vars-group-semantic .hg-vars-swatch {
|
|
115
|
+
width: 18px; height: 18px;
|
|
116
|
+
margin-right: 0;
|
|
117
|
+
margin-left: 8px;
|
|
118
|
+
order: 2;
|
|
119
|
+
}
|
|
120
|
+
.hg-vars-group-semantic .hg-vars-item { padding: 7px 0; }
|
|
121
|
+
.hg-vars-group-semantic .hg-vars-value { gap: 0; }
|
|
122
|
+
.hg-vars-empty { color: var(--hg-color-middle-grey, #a9a9a9); font-style: italic; padding: 12px; }
|
|
123
|
+
</style>`;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Cabecera con la meta del tema (nombre, descripción, versión, autor).
|
|
127
|
+
*/
|
|
128
|
+
function generateThemeMetaHTML(themeData) {
|
|
129
|
+
if (!themeData || !themeData.meta) return '';
|
|
130
|
+
const m = themeData.meta;
|
|
131
|
+
if (!m.displayName && !m.name && !m.description) return '';
|
|
132
|
+
|
|
133
|
+
const displayName = m.displayName || m.name || 'Tema';
|
|
134
|
+
const lineParts = [];
|
|
135
|
+
if (m.name) lineParts.push(escapeHtml(m.name));
|
|
136
|
+
if (m.version) lineParts.push(`v${escapeHtml(m.version)}`);
|
|
137
|
+
if (m.author) lineParts.push(escapeHtml(m.author));
|
|
138
|
+
const line = lineParts.join(' · ');
|
|
139
|
+
|
|
140
|
+
return ` <div class="hg-theme-meta">
|
|
141
|
+
<div class="hg-theme-meta-line">${line || 'Tema'}</div>
|
|
142
|
+
<h3>${escapeHtml(displayName)}</h3>
|
|
143
|
+
${m.description ? `<p>${escapeHtml(m.description)}</p>` : ''}
|
|
144
|
+
</div>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resuelve un valor que puede ser un literal de color (#…, rgb…, hsl…)
|
|
149
|
+
* o una referencia a una variable CSS `var(--hg-color-xxx)`. Devuelve el
|
|
150
|
+
* color final aplicable o null si no se puede resolver.
|
|
151
|
+
*/
|
|
152
|
+
function resolveColor(value, config, themeData, depth = 0) {
|
|
153
|
+
if (!value || depth > 5) return null;
|
|
154
|
+
if (/^(#|rgb|hsl)/i.test(value)) return value;
|
|
155
|
+
const m = String(value).match(/var\(\s*--hg-color-([\w-]+)\s*\)/);
|
|
156
|
+
if (!m) return null;
|
|
157
|
+
const key = m[1];
|
|
158
|
+
const overrides = (themeData && themeData.tokenOverrides && themeData.tokenOverrides.color) || {};
|
|
159
|
+
const baseColors = (config && config.colors) || {};
|
|
160
|
+
const resolved = Object.prototype.hasOwnProperty.call(overrides, key)
|
|
161
|
+
? overrides[key]
|
|
162
|
+
: baseColors[key];
|
|
163
|
+
if (!resolved) return null;
|
|
164
|
+
return resolveColor(resolved, config, themeData, depth + 1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderRow(name, value, prefix, opts = {}) {
|
|
168
|
+
const fullVar = prefix ? `--${prefix}-${name}` : `--${name}`;
|
|
169
|
+
// Swatch: si el valor es un color literal o un var(--hg-color-*) resoluble, lo mostramos
|
|
170
|
+
const resolvedColor = resolveColor(value, opts.config, opts.themeData);
|
|
171
|
+
const swatch = resolvedColor
|
|
172
|
+
? `<span class="hg-vars-swatch" style="background:${escapeHtml(resolvedColor)}" title="${escapeHtml(resolvedColor)}"></span>`
|
|
173
|
+
: '';
|
|
174
|
+
const cls = opts.overridden ? ' is-overridden' : '';
|
|
175
|
+
const badge = opts.overridden ? '<span class="hg-vars-badge" title="Sobrescrito por el tema">tema</span>' : '';
|
|
176
|
+
// En la sección de colores semánticos el swatch va AL FINAL de la fila (después del valor).
|
|
177
|
+
// En el resto (componentes…) sigue yendo delante del valor.
|
|
178
|
+
const swatchAtEnd = !!opts.swatchAtEnd;
|
|
179
|
+
const valueHTML = swatchAtEnd
|
|
180
|
+
? `<code>${escapeHtml(value)}</code>${swatch}`
|
|
181
|
+
: `${swatch}<code>${escapeHtml(value)}</code>`;
|
|
182
|
+
return ` <div class="hg-vars-item${cls}">
|
|
183
|
+
<div class="hg-vars-name"><code>${escapeHtml(fullVar)}</code>${badge}</div>
|
|
184
|
+
<div class="hg-vars-value">${valueHTML}</div>
|
|
185
|
+
</div>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Construye un grupo (título + lista de variables) y devuelve
|
|
190
|
+
* { html, count } para que la distribución en columnas pueda balancear.
|
|
191
|
+
*/
|
|
192
|
+
function buildGroup(title, entries, prefix, overriddenKeys = new Set(), ctx = {}, extraClass = '', rowOpts = {}) {
|
|
193
|
+
if (!entries || entries.length === 0) return null;
|
|
194
|
+
const rows = entries.map(([name, value]) => {
|
|
195
|
+
return renderRow(name, value, prefix, {
|
|
196
|
+
overridden: overriddenKeys.has(name),
|
|
197
|
+
config: ctx.config,
|
|
198
|
+
themeData: ctx.themeData,
|
|
199
|
+
swatchAtEnd: !!rowOpts.swatchAtEnd
|
|
200
|
+
});
|
|
201
|
+
}).join('\n');
|
|
202
|
+
const cls = extraClass ? ` ${extraClass}` : '';
|
|
203
|
+
const html = ` <div class="hg-vars-group${cls}">
|
|
204
|
+
<h4>${escapeHtml(title)} <span style="color:var(--hg-color-middle-grey, #a9a9a9);font-weight:400;">(${entries.length})</span></h4>
|
|
205
|
+
<div class="hg-vars-list">
|
|
206
|
+
${rows}
|
|
207
|
+
</div>
|
|
208
|
+
</div>`;
|
|
209
|
+
return { html, count: entries.length + 1 /* +1 por el header */ };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Distribuye los grupos en N columnas balanceando el total de items (+header).
|
|
214
|
+
* Algoritmo greedy: cada grupo va a la columna con menor altura acumulada.
|
|
215
|
+
* Preserva el orden original dentro de cada columna.
|
|
216
|
+
* Los grupos con `pinLast: true` se anclan al final de la última columna,
|
|
217
|
+
* independientemente del balance.
|
|
218
|
+
*/
|
|
219
|
+
function distributeIntoColumns(groups, numCols = 3) {
|
|
220
|
+
const cols = Array.from({ length: numCols }, () => ({ items: [], count: 0 }));
|
|
221
|
+
const pinned = [];
|
|
222
|
+
groups.forEach(g => {
|
|
223
|
+
if (g.pinLast) {
|
|
224
|
+
pinned.push(g);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Encontrar columna con menor count (en caso de empate, la primera)
|
|
228
|
+
let target = cols[0];
|
|
229
|
+
for (let i = 1; i < cols.length; i++) {
|
|
230
|
+
if (cols[i].count < target.count) target = cols[i];
|
|
231
|
+
}
|
|
232
|
+
target.items.push(g.html);
|
|
233
|
+
target.count += g.count;
|
|
234
|
+
});
|
|
235
|
+
// Los grupos "pinLast" van siempre al final de la última columna
|
|
236
|
+
if (pinned.length) {
|
|
237
|
+
const last = cols[cols.length - 1];
|
|
238
|
+
pinned.forEach(g => {
|
|
239
|
+
last.items.push(g.html);
|
|
240
|
+
last.count += g.count;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return cols.map(c => ` <div class="hg-vars-col">\n${c.items.join('\n')}\n </div>`).join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sección con todas las variables del tema, agrupadas:
|
|
248
|
+
* - Colores semánticos (del base, con overrides del tema si hay)
|
|
249
|
+
* - Spacing overrides → si hay
|
|
250
|
+
* - Component vars (btn, input, label…)
|
|
251
|
+
* - Design tokens (border-radius, transition…)
|
|
252
|
+
*/
|
|
253
|
+
function generateThemeVarsHTML(themeData, config = null) {
|
|
254
|
+
if (!themeData) return '';
|
|
255
|
+
|
|
256
|
+
const groups = [];
|
|
257
|
+
|
|
258
|
+
const tokenOverrides = themeData.tokenOverrides || {};
|
|
259
|
+
const colorOverrides = tokenOverrides.color || {};
|
|
260
|
+
const tokenSpacing = tokenOverrides.spacing || {};
|
|
261
|
+
|
|
262
|
+
// 1) Colores semánticos — base + overrides del tema (si los hay)
|
|
263
|
+
// Se excluyen neutrales/utilitarios (blanco, negro, greys, backgrounds).
|
|
264
|
+
const NON_SEMANTIC_COLORS = new Set([
|
|
265
|
+
'white', 'black',
|
|
266
|
+
'dark-grey', 'middle-grey', 'light-grey', 'grey-ultra',
|
|
267
|
+
'bg-light', 'bg-cream'
|
|
268
|
+
]);
|
|
269
|
+
const baseColors = (config && config.colors) || {};
|
|
270
|
+
const allColorKeys = new Set([
|
|
271
|
+
...Object.keys(baseColors),
|
|
272
|
+
...Object.keys(colorOverrides)
|
|
273
|
+
]);
|
|
274
|
+
// Filtrar neutrales
|
|
275
|
+
const semanticKeys = Array.from(allColorKeys).filter(k => !NON_SEMANTIC_COLORS.has(k));
|
|
276
|
+
const ctx = { config, themeData };
|
|
277
|
+
|
|
278
|
+
// 1) Colores semánticos (al principio). Swatch al final de la fila, 18x18.
|
|
279
|
+
if (semanticKeys.length > 0) {
|
|
280
|
+
const overriddenKeys = new Set(Object.keys(colorOverrides));
|
|
281
|
+
const entries = semanticKeys.map(name => {
|
|
282
|
+
const value = overriddenKeys.has(name) ? colorOverrides[name] : baseColors[name];
|
|
283
|
+
return [name, value];
|
|
284
|
+
});
|
|
285
|
+
const g = buildGroup('Colores semánticos', entries, 'hg-color', overriddenKeys, ctx, 'hg-vars-group-semantic', { swatchAtEnd: true });
|
|
286
|
+
if (g) groups.push(g);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 2) Spacing overrides (si hay)
|
|
290
|
+
if (Object.keys(tokenSpacing).length) {
|
|
291
|
+
const g = buildGroup('Spacing overrides', Object.entries(tokenSpacing), 'hg-spacing', new Set(), ctx, '', { swatchAtEnd: true });
|
|
292
|
+
if (g) groups.push(g);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 3) Component vars (btn, input, label…)
|
|
296
|
+
const componentVars = themeData.componentVars || {};
|
|
297
|
+
Object.entries(componentVars).forEach(([component, vars]) => {
|
|
298
|
+
const g = buildGroup(`Componente: ${component}`, Object.entries(vars), component, new Set(), ctx, '', { swatchAtEnd: true });
|
|
299
|
+
if (g) groups.push(g);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 4) Design tokens (sin prefijo)
|
|
303
|
+
const design = themeData.design || {};
|
|
304
|
+
if (Object.keys(design).length) {
|
|
305
|
+
const g = buildGroup('Design tokens', Object.entries(design), '', new Set(), ctx, '', { swatchAtEnd: true });
|
|
306
|
+
if (g) groups.push(g);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (groups.length === 0) {
|
|
310
|
+
return ` <section class="demo-section" id="theme-vars">
|
|
311
|
+
<h2 class="demo-title">Variables del tema</h2>
|
|
312
|
+
<p class="hg-vars-empty">El theme.json no define variables. Añade <code>componentVars</code>, <code>tokenOverrides</code> o <code>design</code> para verlos aquí.</p>
|
|
313
|
+
</section>`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const columnsHTML = distributeIntoColumns(groups, 3);
|
|
317
|
+
|
|
318
|
+
return ` <section class="demo-section" id="theme-vars">
|
|
319
|
+
<h2 class="demo-title">Variables del tema</h2>
|
|
320
|
+
<p class="mb-24">
|
|
321
|
+
Definidas en <code>themes/${escapeHtml(themeData.meta?.name || 'dutti')}/theme.json</code>. La fuente del CSS sigue siendo
|
|
322
|
+
<code>themes/${escapeHtml(themeData.meta?.name || 'dutti')}/_variables.css</code>; este JSON sirve como manifiesto del tema y para que
|
|
323
|
+
la demo muestre tablas siempre coherentes con sus tokens.
|
|
324
|
+
</p>
|
|
325
|
+
<div class="hg-vars-columns">
|
|
326
|
+
${columnsHTML}
|
|
327
|
+
</div>
|
|
328
|
+
</section>`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Bloque combinado: meta + sección variables. Devuelve string vacío si no hay datos.
|
|
333
|
+
* Incluye los estilos compartidos en la primera invocación.
|
|
334
|
+
*/
|
|
335
|
+
function generateThemeBlockHTML(themeData, config = null) {
|
|
336
|
+
if (!themeData) return '';
|
|
337
|
+
const meta = generateThemeMetaHTML(themeData);
|
|
338
|
+
const vars = generateThemeVarsHTML(themeData, config);
|
|
339
|
+
if (!meta && !vars) return '';
|
|
340
|
+
return `${SHARED_STYLES}
|
|
341
|
+
${meta}
|
|
342
|
+
${vars}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
generateThemeMetaHTML,
|
|
347
|
+
generateThemeVarsHTML,
|
|
348
|
+
generateThemeBlockHTML
|
|
349
|
+
};
|