@webmate-studio/builder 0.2.130 → 0.2.133

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.
@@ -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
+ }