@webmate-studio/builder 0.2.130 → 0.2.132
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/package.json +1 -1
- package/src/design-tokens-v2-css.js +710 -0
- package/src/design-tokens-v2-migrate.js +510 -0
- package/src/design-tokens-v2.js +739 -0
- package/src/index.js +10 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Token V2 — CSS-Generierung
|
|
3
|
+
*
|
|
4
|
+
* Generiert CSS-Variablen, Utility-Klassen und @layer-Blöcke
|
|
5
|
+
* aus einem V2-Token-Objekt.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
COLOR_WORLDS,
|
|
10
|
+
SEMANTIC_COLOR_WORLDS,
|
|
11
|
+
TEXT_VOICES,
|
|
12
|
+
TEXT_LEVELS,
|
|
13
|
+
BUTTON_VARIANTS,
|
|
14
|
+
BUTTON_SIZES,
|
|
15
|
+
calculateOnColor,
|
|
16
|
+
defaultDesignTokensV2
|
|
17
|
+
} from './design-tokens-v2.js';
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// ─── Hauptfunktionen ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generiert { themeVars, globalStyles } für Tailwind v4.
|
|
24
|
+
* themeVars = CSS-Variablen für :root
|
|
25
|
+
* globalStyles = @layer components/utilities mit Utility-Klassen
|
|
26
|
+
*/
|
|
27
|
+
export function generateTailwindV4ThemeV2(tokens) {
|
|
28
|
+
const t = tokens || defaultDesignTokensV2;
|
|
29
|
+
const lines = [];
|
|
30
|
+
const responsiveLines = {};
|
|
31
|
+
|
|
32
|
+
// ── Farb-Variablen ──
|
|
33
|
+
generateColorVariables(t, lines);
|
|
34
|
+
|
|
35
|
+
// ── Typografie-Variablen ──
|
|
36
|
+
generateTypographyVariables(t, lines, responsiveLines);
|
|
37
|
+
|
|
38
|
+
// ── Button-Variablen ──
|
|
39
|
+
generateButtonVariables(t, lines);
|
|
40
|
+
|
|
41
|
+
// ── Layout-Variablen ──
|
|
42
|
+
generateLayoutVariables(t, lines);
|
|
43
|
+
|
|
44
|
+
const themeVars = lines.join('\n');
|
|
45
|
+
|
|
46
|
+
// ── Global Styles ──
|
|
47
|
+
let globalStyles = generateBaseStyles(t);
|
|
48
|
+
globalStyles += generateColorUtilities(t);
|
|
49
|
+
globalStyles += generateTextStyleClasses(t);
|
|
50
|
+
globalStyles += generateTextAliasClasses(t);
|
|
51
|
+
globalStyles += generateTextResponsiveVariants(t);
|
|
52
|
+
globalStyles += generateButtonClasses(t);
|
|
53
|
+
globalStyles += generateResponsiveMediaQueries(responsiveLines);
|
|
54
|
+
|
|
55
|
+
return { themeVars, globalStyles };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generiert vollständiges CSS (Variablen + Utilities) als einzelnen String.
|
|
60
|
+
* Für Endpunkte die kein Tailwind verwenden.
|
|
61
|
+
*/
|
|
62
|
+
export function generateCSSFromTokensV2(tokens) {
|
|
63
|
+
const { themeVars, globalStyles } = generateTailwindV4ThemeV2(tokens);
|
|
64
|
+
|
|
65
|
+
return `:root {\n${themeVars}\n}\n\n${globalStyles}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generiert @import-Anweisungen für Web-Fonts.
|
|
70
|
+
*/
|
|
71
|
+
export function generateFontImportsV2(tokens) {
|
|
72
|
+
const t = tokens || defaultDesignTokensV2;
|
|
73
|
+
const imports = [];
|
|
74
|
+
const loadedFonts = new Set();
|
|
75
|
+
|
|
76
|
+
// Fonts aus der offenen Liste
|
|
77
|
+
if (t.typography?.fonts) {
|
|
78
|
+
for (const font of t.typography.fonts) {
|
|
79
|
+
if (font.source === 'fontsource' && font.id && !loadedFonts.has(font.id)) {
|
|
80
|
+
loadedFonts.add(font.id);
|
|
81
|
+
const fontSlug = font.id.replace('fontsource:', '');
|
|
82
|
+
imports.push(`@import url('https://fonts.bunny.net/css?family=${fontSlug}:100,200,300,400,500,600,700,800,900');`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Mono-Font
|
|
88
|
+
if (t.typography?.monoFont?.source === 'fontsource' && t.typography.monoFont.id && !loadedFonts.has(t.typography.monoFont.id)) {
|
|
89
|
+
loadedFonts.add(t.typography.monoFont.id);
|
|
90
|
+
const fontSlug = t.typography.monoFont.id.replace('fontsource:', '');
|
|
91
|
+
imports.push(`@import url('https://fonts.bunny.net/css?family=${fontSlug}:100,200,300,400,500,600,700,800,900');`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return imports.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// ─── Farb-Variablen ────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function generateColorVariables(t, lines) {
|
|
101
|
+
if (!t.colors) return;
|
|
102
|
+
|
|
103
|
+
for (const world of COLOR_WORLDS) {
|
|
104
|
+
const color = t.colors[world];
|
|
105
|
+
if (!color?.scale) continue;
|
|
106
|
+
|
|
107
|
+
lines.push(` /* ${world} */`);
|
|
108
|
+
for (let step = 1; step <= 12; step++) {
|
|
109
|
+
if (color.scale[step]) {
|
|
110
|
+
lines.push(` --color-${world}-${step}: ${color.scale[step]};`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// on-{welt} Kontrastfarben
|
|
116
|
+
const neutralScale = t.colors.neutral?.scale;
|
|
117
|
+
if (neutralScale) {
|
|
118
|
+
lines.push(' /* on-colors (auto-contrast) */');
|
|
119
|
+
for (const world of SEMANTIC_COLOR_WORLDS) {
|
|
120
|
+
const color = t.colors[world];
|
|
121
|
+
if (!color?.scale?.[9]) continue;
|
|
122
|
+
|
|
123
|
+
const onColor = calculateOnColor(color.scale[9], neutralScale);
|
|
124
|
+
lines.push(` --color-on-${world}: ${onColor};`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Semantische Mappings
|
|
129
|
+
if (t.semanticMappings) {
|
|
130
|
+
lines.push(' /* semantic mappings */');
|
|
131
|
+
for (const world of SEMANTIC_COLOR_WORLDS) {
|
|
132
|
+
const mapping = t.semanticMappings[world];
|
|
133
|
+
const color = t.colors[world];
|
|
134
|
+
if (!mapping || !color?.scale) continue;
|
|
135
|
+
|
|
136
|
+
if (mapping.bg) lines.push(` --color-${world}-bg: var(--color-${world}-${mapping.bg});`);
|
|
137
|
+
if (mapping.bgHover) lines.push(` --color-${world}-bg-hover: var(--color-${world}-${mapping.bgHover});`);
|
|
138
|
+
if (mapping.bgSubtle) lines.push(` --color-${world}-bg-subtle: var(--color-${world}-${mapping.bgSubtle});`);
|
|
139
|
+
if (mapping.bgSubtleHover) lines.push(` --color-${world}-bg-subtle-hover: var(--color-${world}-${mapping.bgSubtleHover});`);
|
|
140
|
+
if (mapping.text) lines.push(` --color-${world}-text: var(--color-${world}-${mapping.text});`);
|
|
141
|
+
if (mapping.border) lines.push(` --color-${world}-border: var(--color-${world}-${mapping.border});`);
|
|
142
|
+
if (mapping.ring) lines.push(` --color-${world}-ring: var(--color-${world}-${mapping.ring});`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
// ─── Farb-Utilities ─────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function generateColorUtilities(t) {
|
|
151
|
+
if (!t.colors) return '';
|
|
152
|
+
|
|
153
|
+
let css = '\n\n/* Color Utilities */\n@layer utilities {';
|
|
154
|
+
const processed = new Set();
|
|
155
|
+
const opacitySteps = [10, 20, 30, 40, 50, 60, 70, 80, 90];
|
|
156
|
+
|
|
157
|
+
// Stufen-Utilities: bg-primary-3, text-neutral-12, etc.
|
|
158
|
+
for (const world of COLOR_WORLDS) {
|
|
159
|
+
if (!t.colors[world]?.scale) continue;
|
|
160
|
+
|
|
161
|
+
for (let step = 1; step <= 12; step++) {
|
|
162
|
+
const varRef = `--color-${world}-${step}`;
|
|
163
|
+
addColorUtility(css = css, processed, `bg-${world}-${step}`, 'background-color', varRef);
|
|
164
|
+
addColorUtility(css = css, processed, `text-${world}-${step}`, 'color', varRef);
|
|
165
|
+
addColorUtility(css = css, processed, `border-${world}-${step}`, 'border-color', varRef);
|
|
166
|
+
|
|
167
|
+
// Opacity-Varianten
|
|
168
|
+
for (const step2 of opacitySteps) {
|
|
169
|
+
css += `\n.bg-${world}-${step}\\/${step2} { background-color: color-mix(in srgb, var(${varRef}) ${step2}%, transparent); }`;
|
|
170
|
+
css += `\n.text-${world}-${step}\\/${step2} { color: color-mix(in srgb, var(${varRef}) ${step2}%, transparent); }`;
|
|
171
|
+
css += `\n.border-${world}-${step}\\/${step2} { border-color: color-mix(in srgb, var(${varRef}) ${step2}%, transparent); }`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Semantische Utilities: bg-primary, text-on-primary, border-error, etc.
|
|
177
|
+
for (const world of SEMANTIC_COLOR_WORLDS) {
|
|
178
|
+
if (!t.colors[world]?.scale) continue;
|
|
179
|
+
|
|
180
|
+
const semantics = [
|
|
181
|
+
{ cls: `bg-${world}`, prop: 'background-color', varRef: `--color-${world}-bg` },
|
|
182
|
+
{ cls: `bg-${world}-hover`, prop: 'background-color', varRef: `--color-${world}-bg-hover` },
|
|
183
|
+
{ cls: `bg-${world}-subtle`, prop: 'background-color', varRef: `--color-${world}-bg-subtle` },
|
|
184
|
+
{ cls: `bg-${world}-subtle-hover`, prop: 'background-color', varRef: `--color-${world}-bg-subtle-hover` },
|
|
185
|
+
{ cls: `text-${world}`, prop: 'color', varRef: `--color-${world}-text` },
|
|
186
|
+
{ cls: `text-on-${world}`, prop: 'color', varRef: `--color-on-${world}` },
|
|
187
|
+
{ cls: `border-${world}`, prop: 'border-color', varRef: `--color-${world}-border` },
|
|
188
|
+
{ cls: `ring-${world}`, prop: '--tw-ring-color', varRef: `--color-${world}-ring` },
|
|
189
|
+
{ cls: `border-on-${world}`, prop: 'border-color', varRef: `--color-on-${world}` },
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
for (const { cls, prop, varRef } of semantics) {
|
|
193
|
+
if (!processed.has(cls)) {
|
|
194
|
+
processed.add(cls);
|
|
195
|
+
css += `\n.${cls} { ${prop}: var(${varRef}); }`;
|
|
196
|
+
css += `\n.hover\\:${cls}:hover { ${prop}: var(${varRef}); }`;
|
|
197
|
+
css += `\n.focus\\:${cls}:focus { ${prop}: var(${varRef}); }`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Opacity-Varianten für on-{welt}
|
|
202
|
+
for (const step of opacitySteps) {
|
|
203
|
+
css += `\n.text-on-${world}\\/${step} { color: color-mix(in srgb, var(--color-on-${world}) ${step}%, transparent); }`;
|
|
204
|
+
css += `\n.border-on-${world}\\/${step} { border-color: color-mix(in srgb, var(--color-on-${world}) ${step}%, transparent); }`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
css += '\n}'; // Close @layer utilities
|
|
209
|
+
return css;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function addColorUtility(css, processed, cls, prop, varRef) {
|
|
213
|
+
if (processed.has(cls)) return css;
|
|
214
|
+
processed.add(cls);
|
|
215
|
+
return css + `\n.${cls} { ${prop}: var(${varRef}); }` +
|
|
216
|
+
`\n.hover\\:${cls}:hover { ${prop}: var(${varRef}); }` +
|
|
217
|
+
`\n.focus\\:${cls}:focus { ${prop}: var(${varRef}); }`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
// ─── Typografie-Variablen ───────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
function generateTypographyVariables(t, lines, responsiveLines) {
|
|
224
|
+
if (!t.typography) return;
|
|
225
|
+
|
|
226
|
+
// Font-Variablen
|
|
227
|
+
if (t.typography.fonts?.length > 0) {
|
|
228
|
+
lines.push(' /* fonts */');
|
|
229
|
+
// Erste Font als Button-Font
|
|
230
|
+
const primaryFont = t.typography.fonts[0];
|
|
231
|
+
lines.push(` --btn-font: "${primaryFont.name}", ${primaryFont.fallback || 'sans-serif'};`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (t.typography.monoFont) {
|
|
235
|
+
const mono = t.typography.monoFont;
|
|
236
|
+
lines.push(` --font-mono: "${mono.name}", ${mono.fallback || 'monospace'};`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Text-Stil-Variablen
|
|
240
|
+
const textStyles = t.typography.textStyles;
|
|
241
|
+
if (!textStyles) return;
|
|
242
|
+
|
|
243
|
+
for (const voice of TEXT_VOICES) {
|
|
244
|
+
if (!textStyles[voice]) continue;
|
|
245
|
+
|
|
246
|
+
lines.push(` /* text-${voice} */`);
|
|
247
|
+
|
|
248
|
+
for (const level of TEXT_LEVELS) {
|
|
249
|
+
const style = textStyles[voice][level];
|
|
250
|
+
if (!style) continue;
|
|
251
|
+
|
|
252
|
+
const prefix = `--text-${voice}-${level}`;
|
|
253
|
+
|
|
254
|
+
// Font
|
|
255
|
+
if (style.font) {
|
|
256
|
+
const font = findFont(t, style.font);
|
|
257
|
+
const fallback = font?.fallback || 'sans-serif';
|
|
258
|
+
lines.push(` ${prefix}-font: "${style.font}", ${fallback};`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// fontWeight, letterSpacing, textTransform — nicht responsive
|
|
262
|
+
if (style.fontWeight != null) lines.push(` ${prefix}-weight: ${style.fontWeight};`);
|
|
263
|
+
if (style.textTransform) lines.push(` ${prefix}-transform: ${style.textTransform};`);
|
|
264
|
+
|
|
265
|
+
// fontSize — kann responsive sein
|
|
266
|
+
addResponsiveVar(prefix + '-size', style.fontSize, lines, responsiveLines);
|
|
267
|
+
|
|
268
|
+
// lineHeight — kann responsive sein
|
|
269
|
+
addResponsiveVar(prefix + '-line-height', style.lineHeight, lines, responsiveLines);
|
|
270
|
+
|
|
271
|
+
// letterSpacing — kann responsive sein
|
|
272
|
+
addResponsiveVar(prefix + '-letter-spacing', style.letterSpacing, lines, responsiveLines);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function addResponsiveVar(varName, value, lines, responsiveLines) {
|
|
278
|
+
if (value == null) return;
|
|
279
|
+
|
|
280
|
+
if (typeof value === 'object') {
|
|
281
|
+
if (value.base) lines.push(` ${varName}: ${value.base};`);
|
|
282
|
+
for (const [bp, bpValue] of Object.entries(value)) {
|
|
283
|
+
if (bp !== 'base' && bpValue) {
|
|
284
|
+
responsiveLines[bp] = responsiveLines[bp] || [];
|
|
285
|
+
responsiveLines[bp].push(` ${varName}: ${bpValue};`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
lines.push(` ${varName}: ${value};`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function findFont(t, fontName) {
|
|
294
|
+
if (!t.typography?.fonts) return null;
|
|
295
|
+
return t.typography.fonts.find(f => f.name === fontName);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
// ─── Text-Stil-Klassen ─────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
function generateTextStyleClasses(t) {
|
|
302
|
+
const textStyles = t.typography?.textStyles;
|
|
303
|
+
if (!textStyles) return '';
|
|
304
|
+
|
|
305
|
+
let css = '\n\n/* Text Style Utilities */\n@layer components {';
|
|
306
|
+
|
|
307
|
+
for (const voice of TEXT_VOICES) {
|
|
308
|
+
if (!textStyles[voice]) continue;
|
|
309
|
+
|
|
310
|
+
for (const level of TEXT_LEVELS) {
|
|
311
|
+
const style = textStyles[voice][level];
|
|
312
|
+
if (!style) continue;
|
|
313
|
+
|
|
314
|
+
const prefix = `--text-${voice}-${level}`;
|
|
315
|
+
css += `\n.text-${voice}-${level} {`;
|
|
316
|
+
if (style.font) css += `\n font-family: var(${prefix}-font);`;
|
|
317
|
+
css += `\n font-size: var(${prefix}-size);`;
|
|
318
|
+
if (style.fontWeight != null) css += `\n font-weight: var(${prefix}-weight);`;
|
|
319
|
+
css += `\n line-height: var(${prefix}-line-height);`;
|
|
320
|
+
if (style.letterSpacing) css += `\n letter-spacing: var(${prefix}-letter-spacing);`;
|
|
321
|
+
if (style.textTransform) css += `\n text-transform: var(${prefix}-transform);`;
|
|
322
|
+
css += '\n}';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
css += '\n}'; // Close @layer components
|
|
327
|
+
return css;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function generateTextAliasClasses(t) {
|
|
331
|
+
const aliases = t.typography?.aliases;
|
|
332
|
+
if (!aliases) return '';
|
|
333
|
+
|
|
334
|
+
let css = '\n\n/* Text Aliases */\n@layer components {';
|
|
335
|
+
|
|
336
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
337
|
+
// target = "display-1" → voice = "display", level = "1"
|
|
338
|
+
const prefix = `--text-${target}`;
|
|
339
|
+
css += `\n.text-${alias} {`;
|
|
340
|
+
css += `\n font-family: var(${prefix}-font);`;
|
|
341
|
+
css += `\n font-size: var(${prefix}-size);`;
|
|
342
|
+
css += `\n font-weight: var(${prefix}-weight);`;
|
|
343
|
+
css += `\n line-height: var(${prefix}-line-height);`;
|
|
344
|
+
css += `\n letter-spacing: var(${prefix}-letter-spacing);`;
|
|
345
|
+
css += `\n text-transform: var(${prefix}-transform);`;
|
|
346
|
+
css += '\n}';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
css += '\n}';
|
|
350
|
+
return css;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function generateTextResponsiveVariants(t) {
|
|
354
|
+
const textStyles = t.typography?.textStyles;
|
|
355
|
+
if (!textStyles) return '';
|
|
356
|
+
|
|
357
|
+
const breakpoints = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px' };
|
|
358
|
+
let css = '';
|
|
359
|
+
|
|
360
|
+
for (const [bp, minWidth] of Object.entries(breakpoints)) {
|
|
361
|
+
css += `\n@media (min-width: ${minWidth}) {\n@layer components {`;
|
|
362
|
+
|
|
363
|
+
for (const voice of TEXT_VOICES) {
|
|
364
|
+
if (!textStyles[voice]) continue;
|
|
365
|
+
for (const level of TEXT_LEVELS) {
|
|
366
|
+
const style = textStyles[voice][level];
|
|
367
|
+
if (!style) continue;
|
|
368
|
+
|
|
369
|
+
const prefix = `--text-${voice}-${level}`;
|
|
370
|
+
css += `\n .${bp}\\:text-${voice}-${level} {`;
|
|
371
|
+
if (style.font) css += `\n font-family: var(${prefix}-font);`;
|
|
372
|
+
css += `\n font-size: var(${prefix}-size);`;
|
|
373
|
+
if (style.fontWeight != null) css += `\n font-weight: var(${prefix}-weight);`;
|
|
374
|
+
css += `\n line-height: var(${prefix}-line-height);`;
|
|
375
|
+
if (style.letterSpacing) css += `\n letter-spacing: var(${prefix}-letter-spacing);`;
|
|
376
|
+
if (style.textTransform) css += `\n text-transform: var(${prefix}-transform);`;
|
|
377
|
+
css += '\n }';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Responsive alias variants
|
|
382
|
+
const aliases = t.typography?.aliases;
|
|
383
|
+
if (aliases) {
|
|
384
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
385
|
+
const prefix = `--text-${target}`;
|
|
386
|
+
css += `\n .${bp}\\:text-${alias} {`;
|
|
387
|
+
css += `\n font-family: var(${prefix}-font);`;
|
|
388
|
+
css += `\n font-size: var(${prefix}-size);`;
|
|
389
|
+
css += `\n font-weight: var(${prefix}-weight);`;
|
|
390
|
+
css += `\n line-height: var(${prefix}-line-height);`;
|
|
391
|
+
css += `\n letter-spacing: var(${prefix}-letter-spacing);`;
|
|
392
|
+
css += `\n text-transform: var(${prefix}-transform);`;
|
|
393
|
+
css += '\n }';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
css += '\n}\n}'; // Close @layer + @media
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return css;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
// ─── Button-Variablen ───────────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
function generateButtonVariables(t, lines) {
|
|
407
|
+
if (!t.buttons) return;
|
|
408
|
+
|
|
409
|
+
// Global
|
|
410
|
+
if (t.buttons.global) {
|
|
411
|
+
lines.push(' /* buttons global */');
|
|
412
|
+
lines.push(` --btn-transition: ${t.buttons.global.transition || 'all 150ms ease'};`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Sizes
|
|
416
|
+
if (t.buttons.sizes) {
|
|
417
|
+
for (const size of BUTTON_SIZES) {
|
|
418
|
+
const s = t.buttons.sizes[size];
|
|
419
|
+
if (!s) continue;
|
|
420
|
+
|
|
421
|
+
lines.push(` /* btn-${size} */`);
|
|
422
|
+
lines.push(` --btn-${size}-padding-x: ${s.paddingX};`);
|
|
423
|
+
lines.push(` --btn-${size}-padding-y: ${s.paddingY};`);
|
|
424
|
+
lines.push(` --btn-${size}-font-size: ${s.fontSize};`);
|
|
425
|
+
lines.push(` --btn-${size}-font-weight: ${s.fontWeight};`);
|
|
426
|
+
lines.push(` --btn-${size}-line-height: ${s.lineHeight};`);
|
|
427
|
+
lines.push(` --btn-${size}-letter-spacing: ${s.letterSpacing};`);
|
|
428
|
+
lines.push(` --btn-${size}-border-radius: ${s.borderRadius};`);
|
|
429
|
+
lines.push(` --btn-${size}-min-height: ${s.minHeight};`);
|
|
430
|
+
lines.push(` --btn-${size}-gap: ${s.gap};`);
|
|
431
|
+
lines.push(` --btn-${size}-icon-size: ${s.iconSize};`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Variants
|
|
436
|
+
if (t.buttons.variants) {
|
|
437
|
+
for (const variant of BUTTON_VARIANTS) {
|
|
438
|
+
const v = t.buttons.variants[variant];
|
|
439
|
+
if (!v) continue;
|
|
440
|
+
|
|
441
|
+
// Normal mode
|
|
442
|
+
if (v.normal && !v.normal.customCSS) {
|
|
443
|
+
lines.push(` /* btn-${variant} normal */`);
|
|
444
|
+
addButtonVariantVars(lines, `btn-${variant}`, v.normal, t);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// On-surface mode
|
|
448
|
+
if (v.onSurface && !v.onSurface.customCSS) {
|
|
449
|
+
lines.push(` /* btn-${variant} on-surface */`);
|
|
450
|
+
addButtonVariantVars(lines, `btn-${variant}-on-surface`, v.onSurface, t);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function addButtonVariantVars(lines, prefix, variant, t) {
|
|
457
|
+
if (variant.bg) lines.push(` --${prefix}-bg: ${resolveColorRef(variant.bg, t)};`);
|
|
458
|
+
if (variant.bgHover) lines.push(` --${prefix}-bg-hover: ${resolveColorRef(variant.bgHover, t)};`);
|
|
459
|
+
if (variant.text) lines.push(` --${prefix}-text: ${resolveColorRef(variant.text, t)};`);
|
|
460
|
+
if (variant.textHover) lines.push(` --${prefix}-text-hover: ${resolveColorRef(variant.textHover, t)};`);
|
|
461
|
+
if (variant.border && variant.border !== 'none') lines.push(` --${prefix}-border: 1px solid ${resolveColorRef(variant.border, t)};`);
|
|
462
|
+
if (variant.borderHover && variant.borderHover !== 'none') lines.push(` --${prefix}-border-hover: 1px solid ${resolveColorRef(variant.borderHover, t)};`);
|
|
463
|
+
if (variant.shadow && variant.shadow !== 'none') lines.push(` --${prefix}-shadow: ${variant.shadow};`);
|
|
464
|
+
if (variant.shadowHover && variant.shadowHover !== 'none') lines.push(` --${prefix}-shadow-hover: ${variant.shadowHover};`);
|
|
465
|
+
if (variant.focusRingColor) lines.push(` --${prefix}-focus-ring: ${resolveColorRef(variant.focusRingColor, t)};`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Löst eine Farbreferenz auf.
|
|
470
|
+
* "primary-9" → "var(--color-primary-9)"
|
|
471
|
+
* "on-primary" → "var(--color-on-primary)"
|
|
472
|
+
* "neutral-1/15" → "color-mix(in srgb, var(--color-neutral-1) 15%, transparent)"
|
|
473
|
+
* "transparent" → "transparent"
|
|
474
|
+
* "none" → bleibt "none"
|
|
475
|
+
*/
|
|
476
|
+
function resolveColorRef(ref, t) {
|
|
477
|
+
if (!ref || ref === 'none') return 'none';
|
|
478
|
+
if (ref === 'transparent') return 'transparent';
|
|
479
|
+
|
|
480
|
+
// Opacity-Syntax: "neutral-1/15"
|
|
481
|
+
const opacityMatch = ref.match(/^(.+)\/(\d+)$/);
|
|
482
|
+
if (opacityMatch) {
|
|
483
|
+
const colorRef = opacityMatch[1];
|
|
484
|
+
const opacity = opacityMatch[2];
|
|
485
|
+
return `color-mix(in srgb, var(--color-${colorRef}) ${opacity}%, transparent)`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return `var(--color-${ref})`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
// ─── Button-Klassen ─────────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
function generateButtonClasses(t) {
|
|
495
|
+
if (!t.buttons) return '';
|
|
496
|
+
|
|
497
|
+
let css = '\n\n/* Button Utilities */\n@layer components {';
|
|
498
|
+
|
|
499
|
+
// Base .btn class
|
|
500
|
+
css += `\n.btn {
|
|
501
|
+
display: inline-flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
justify-content: center;
|
|
504
|
+
font-family: var(--btn-font);
|
|
505
|
+
cursor: pointer;
|
|
506
|
+
text-decoration: none;
|
|
507
|
+
white-space: nowrap;
|
|
508
|
+
transition: var(--btn-transition);
|
|
509
|
+
border: none;
|
|
510
|
+
}`;
|
|
511
|
+
|
|
512
|
+
// Disabled
|
|
513
|
+
css += `\n.btn:disabled, .btn[aria-disabled="true"] {
|
|
514
|
+
opacity: ${t.buttons.global?.disabledOpacity ?? 0.5};
|
|
515
|
+
pointer-events: none;
|
|
516
|
+
cursor: not-allowed;
|
|
517
|
+
}`;
|
|
518
|
+
|
|
519
|
+
// Size classes
|
|
520
|
+
for (const size of BUTTON_SIZES) {
|
|
521
|
+
if (!t.buttons.sizes?.[size]) continue;
|
|
522
|
+
css += `\n.btn-${size} {
|
|
523
|
+
padding: var(--btn-${size}-padding-y) var(--btn-${size}-padding-x);
|
|
524
|
+
font-size: var(--btn-${size}-font-size);
|
|
525
|
+
font-weight: var(--btn-${size}-font-weight);
|
|
526
|
+
line-height: var(--btn-${size}-line-height);
|
|
527
|
+
letter-spacing: var(--btn-${size}-letter-spacing);
|
|
528
|
+
border-radius: var(--btn-${size}-border-radius);
|
|
529
|
+
min-height: var(--btn-${size}-min-height);
|
|
530
|
+
gap: var(--btn-${size}-gap);
|
|
531
|
+
}`;
|
|
532
|
+
css += `\n.btn-${size} iconify-icon { font-size: var(--btn-${size}-icon-size); }`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Variant classes
|
|
536
|
+
for (const variant of BUTTON_VARIANTS) {
|
|
537
|
+
const v = t.buttons.variants?.[variant];
|
|
538
|
+
if (!v) continue;
|
|
539
|
+
|
|
540
|
+
// Normal mode
|
|
541
|
+
if (v.normal?.customCSS) {
|
|
542
|
+
// customCSS Escape-Hatch
|
|
543
|
+
css += generateCustomCSSBlock(`btn-${variant}`, v.normal.customCSS);
|
|
544
|
+
} else if (v.normal) {
|
|
545
|
+
css += generateVariantNormalCSS(variant, v);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// On-surface mode
|
|
549
|
+
if (v.onSurface?.customCSS) {
|
|
550
|
+
css += generateCustomCSSBlock(`btn-on-surface.btn-${variant}`, v.onSurface.customCSS);
|
|
551
|
+
} else if (v.onSurface) {
|
|
552
|
+
css += generateVariantOnSurfaceCSS(variant, v, t);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
css += '\n}'; // Close @layer components
|
|
557
|
+
return css;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function generateVariantNormalCSS(variant, v) {
|
|
561
|
+
const p = `btn-${variant}`;
|
|
562
|
+
let css = `\n.${p} {
|
|
563
|
+
background: var(--${p}-bg);
|
|
564
|
+
color: var(--${p}-text);`;
|
|
565
|
+
|
|
566
|
+
if (v.normal.border && v.normal.border !== 'none') {
|
|
567
|
+
css += `\n border: var(--${p}-border);`;
|
|
568
|
+
}
|
|
569
|
+
if (v.normal.shadow && v.normal.shadow !== 'none') {
|
|
570
|
+
css += `\n box-shadow: var(--${p}-shadow);`;
|
|
571
|
+
}
|
|
572
|
+
if (v.normal.textDecoration) {
|
|
573
|
+
css += `\n text-decoration: ${v.normal.textDecoration};`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
css += '\n}';
|
|
577
|
+
|
|
578
|
+
// Hover
|
|
579
|
+
css += `\n.${p}:hover {
|
|
580
|
+
background: var(--${p}-bg-hover);
|
|
581
|
+
color: var(--${p}-text-hover);`;
|
|
582
|
+
|
|
583
|
+
if (v.normal.borderHover && v.normal.borderHover !== 'none') {
|
|
584
|
+
css += `\n border: var(--${p}-border-hover);`;
|
|
585
|
+
}
|
|
586
|
+
if (v.normal.shadowHover && v.normal.shadowHover !== 'none') {
|
|
587
|
+
css += `\n box-shadow: var(--${p}-shadow-hover);`;
|
|
588
|
+
}
|
|
589
|
+
if (v.normal.textDecorationHover) {
|
|
590
|
+
css += `\n text-decoration: ${v.normal.textDecorationHover};`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
css += '\n}';
|
|
594
|
+
|
|
595
|
+
// Focus
|
|
596
|
+
if (v.normal.focusRingColor) {
|
|
597
|
+
css += `\n.${p}:focus-visible {
|
|
598
|
+
outline: 2px solid var(--${p}-focus-ring);
|
|
599
|
+
outline-offset: 2px;
|
|
600
|
+
}`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return css;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function generateVariantOnSurfaceCSS(variant, v, t) {
|
|
607
|
+
const p = `btn-${variant}`;
|
|
608
|
+
const osp = `btn-${variant}-on-surface`;
|
|
609
|
+
let css = `\n.btn-on-surface.${p} {
|
|
610
|
+
background: var(--${osp}-bg);
|
|
611
|
+
color: var(--${osp}-text);`;
|
|
612
|
+
|
|
613
|
+
if (v.onSurface.border && v.onSurface.border !== 'none') {
|
|
614
|
+
css += `\n border: var(--${osp}-border);`;
|
|
615
|
+
}
|
|
616
|
+
if (v.onSurface.textDecoration) {
|
|
617
|
+
css += `\n text-decoration: ${v.onSurface.textDecoration};`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
css += '\n}';
|
|
621
|
+
|
|
622
|
+
// Hover
|
|
623
|
+
css += `\n.btn-on-surface.${p}:hover {
|
|
624
|
+
background: var(--${osp}-bg-hover);
|
|
625
|
+
color: var(--${osp}-text-hover);`;
|
|
626
|
+
|
|
627
|
+
if (v.onSurface.borderHover && v.onSurface.borderHover !== 'none') {
|
|
628
|
+
css += `\n border: var(--${osp}-border-hover);`;
|
|
629
|
+
}
|
|
630
|
+
if (v.onSurface.textDecorationHover) {
|
|
631
|
+
css += `\n text-decoration: ${v.onSurface.textDecorationHover};`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
css += '\n}';
|
|
635
|
+
|
|
636
|
+
// Focus (on-surface)
|
|
637
|
+
const focusColor = t.buttons.global?.onSurfaceFocusRingColor;
|
|
638
|
+
if (focusColor) {
|
|
639
|
+
css += `\n.btn-on-surface.${p}:focus-visible {
|
|
640
|
+
outline: 2px solid ${resolveColorRef(focusColor, t)};
|
|
641
|
+
outline-offset: 2px;
|
|
642
|
+
}`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return css;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function generateCustomCSSBlock(selector, customCSS) {
|
|
649
|
+
let css = '';
|
|
650
|
+
|
|
651
|
+
if (customCSS.default) {
|
|
652
|
+
css += `\n.${selector} {\n ${customCSS.default}\n}`;
|
|
653
|
+
}
|
|
654
|
+
if (customCSS.hover) {
|
|
655
|
+
css += `\n.${selector}:hover {\n ${customCSS.hover}\n}`;
|
|
656
|
+
}
|
|
657
|
+
if (customCSS.focus) {
|
|
658
|
+
css += `\n.${selector}:focus-visible {\n ${customCSS.focus}\n}`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return css;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
// ─── Layout-Variablen ───────────────────────────────────────────────────────
|
|
666
|
+
|
|
667
|
+
function generateLayoutVariables(t, lines) {
|
|
668
|
+
if (t.borderWidth) lines.push(` --border-width: ${t.borderWidth};`);
|
|
669
|
+
if (t.borderRadius) lines.push(` --radius: ${t.borderRadius};`);
|
|
670
|
+
|
|
671
|
+
if (t.breakpoints) {
|
|
672
|
+
lines.push(' /* breakpoints */');
|
|
673
|
+
for (const [key, value] of Object.entries(t.breakpoints)) {
|
|
674
|
+
lines.push(` --breakpoint-${key}: ${value};`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
// ─── Basis-Styles ───────────────────────────────────────────────────────────
|
|
681
|
+
|
|
682
|
+
function generateBaseStyles(t) {
|
|
683
|
+
const primaryFont = t.typography?.fonts?.[0];
|
|
684
|
+
const fontFamily = primaryFont ? `"${primaryFont.name}", ${primaryFont.fallback || 'sans-serif'}` : 'system-ui, sans-serif';
|
|
685
|
+
|
|
686
|
+
return `\n/* Global baseline styles */
|
|
687
|
+
body {
|
|
688
|
+
font-family: ${fontFamily};
|
|
689
|
+
background-color: var(--color-neutral-1);
|
|
690
|
+
color: var(--color-neutral-12);
|
|
691
|
+
}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
// ─── Responsive Media Queries ───────────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
function generateResponsiveMediaQueries(responsiveLines) {
|
|
698
|
+
const breakpoints = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px' };
|
|
699
|
+
let css = '';
|
|
700
|
+
|
|
701
|
+
for (const [bp, minWidth] of Object.entries(breakpoints)) {
|
|
702
|
+
if (responsiveLines[bp]?.length > 0) {
|
|
703
|
+
css += `\n\n@media (min-width: ${minWidth}) {\n :root {\n`;
|
|
704
|
+
css += responsiveLines[bp].join('\n');
|
|
705
|
+
css += '\n }\n}';
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return css;
|
|
710
|
+
}
|