@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.
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Design Token V2 — Migration + Validation
3
+ *
4
+ * Konvertiert V1-Token-Objekte in V2-Format.
5
+ * Validiert V2-Token-Objekte und füllt fehlende Felder mit Defaults auf.
6
+ */
7
+
8
+ import {
9
+ defaultDesignTokensV2,
10
+ generateColorScale,
11
+ isV1Format,
12
+ isV2Format,
13
+ COLOR_WORLDS,
14
+ SEMANTIC_COLOR_WORLDS,
15
+ TEXT_VOICES,
16
+ TEXT_LEVELS,
17
+ BUTTON_VARIANTS,
18
+ BUTTON_SIZES,
19
+ DEFAULT_SEMANTIC_MAPPINGS,
20
+ DEFAULT_TEXT_ALIASES
21
+ } from './design-tokens-v2.js';
22
+
23
+
24
+ // ─── Hauptfunktionen ────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Migriert ein V1-Token-Objekt zu V2.
28
+ * Gibt ein vollständiges V2-Token-Objekt zurück.
29
+ */
30
+ export function migrateDesignTokensV1toV2(v1) {
31
+ if (!v1) return structuredClone(defaultDesignTokensV2);
32
+ if (isV2Format(v1)) return validateDesignTokensV2(v1);
33
+
34
+ const v2 = structuredClone(defaultDesignTokensV2);
35
+
36
+ // ── Farben migrieren ──
37
+ migrateColors(v1, v2);
38
+
39
+ // ── Fonts migrieren ──
40
+ migrateFonts(v1, v2);
41
+
42
+ // ── Text-Stile migrieren ──
43
+ migrateTextStyles(v1, v2);
44
+
45
+ // ── Buttons migrieren ──
46
+ migrateButtons(v1, v2);
47
+
48
+ // ── Layout migrieren ──
49
+ if (v1.borderRadius) v2.borderRadius = v1.borderRadius;
50
+ if (v1.borderWidth) v2.borderWidth = v1.borderWidth;
51
+ if (v1.spacing) v2.spacing = structuredClone(v1.spacing);
52
+ if (v1.container) v2.container = structuredClone(v1.container);
53
+ if (v1.breakpoints) v2.breakpoints = structuredClone(v1.breakpoints);
54
+
55
+ return v2;
56
+ }
57
+
58
+ /**
59
+ * Validiert ein V2-Token-Objekt und füllt fehlende Felder auf.
60
+ * Gibt ein vollständiges V2-Token-Objekt zurück.
61
+ */
62
+ export function validateDesignTokensV2(tokens) {
63
+ if (!tokens) return structuredClone(defaultDesignTokensV2);
64
+
65
+ const v2 = structuredClone(tokens);
66
+ v2.version = 2;
67
+
68
+ // ── Farben validieren ──
69
+ if (!v2.colors) v2.colors = structuredClone(defaultDesignTokensV2.colors);
70
+
71
+ for (const world of COLOR_WORLDS) {
72
+ if (!v2.colors[world]) {
73
+ v2.colors[world] = structuredClone(defaultDesignTokensV2.colors[world]);
74
+ } else if (v2.colors[world].base && !v2.colors[world].scale) {
75
+ // Basisfarbe vorhanden aber keine Skala → generieren
76
+ v2.colors[world].scale = generateColorScale(v2.colors[world].base);
77
+ }
78
+ }
79
+
80
+ // Semantic Mappings
81
+ if (!v2.semanticMappings) v2.semanticMappings = {};
82
+ for (const world of SEMANTIC_COLOR_WORLDS) {
83
+ if (!v2.semanticMappings[world]) {
84
+ v2.semanticMappings[world] = { ...DEFAULT_SEMANTIC_MAPPINGS };
85
+ }
86
+ }
87
+
88
+ // Dark Mode
89
+ if (!v2.darkMode) v2.darkMode = { enabled: false, colors: null, semanticMappings: null };
90
+
91
+ // ── Typografie validieren ──
92
+ if (!v2.typography) v2.typography = structuredClone(defaultDesignTokensV2.typography);
93
+ if (!v2.typography.fonts || v2.typography.fonts.length === 0) {
94
+ v2.typography.fonts = structuredClone(defaultDesignTokensV2.typography.fonts);
95
+ }
96
+ if (!v2.typography.monoFont) {
97
+ v2.typography.monoFont = structuredClone(defaultDesignTokensV2.typography.monoFont);
98
+ }
99
+
100
+ // Text Styles
101
+ if (!v2.typography.textStyles) v2.typography.textStyles = structuredClone(defaultDesignTokensV2.typography.textStyles);
102
+ for (const voice of TEXT_VOICES) {
103
+ if (!v2.typography.textStyles[voice]) {
104
+ v2.typography.textStyles[voice] = structuredClone(defaultDesignTokensV2.typography.textStyles[voice]);
105
+ }
106
+ for (const level of TEXT_LEVELS) {
107
+ if (!v2.typography.textStyles[voice][level]) {
108
+ v2.typography.textStyles[voice][level] = structuredClone(defaultDesignTokensV2.typography.textStyles[voice][level]);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Aliases
114
+ if (!v2.typography.aliases) v2.typography.aliases = { ...DEFAULT_TEXT_ALIASES };
115
+
116
+ // ── Buttons validieren ──
117
+ if (!v2.buttons) v2.buttons = structuredClone(defaultDesignTokensV2.buttons);
118
+ if (!v2.buttons.sizes) v2.buttons.sizes = structuredClone(defaultDesignTokensV2.buttons.sizes);
119
+ if (!v2.buttons.variants) v2.buttons.variants = structuredClone(defaultDesignTokensV2.buttons.variants);
120
+ if (!v2.buttons.global) v2.buttons.global = structuredClone(defaultDesignTokensV2.buttons.global);
121
+
122
+ for (const size of BUTTON_SIZES) {
123
+ if (!v2.buttons.sizes[size]) {
124
+ v2.buttons.sizes[size] = structuredClone(defaultDesignTokensV2.buttons.sizes[size]);
125
+ }
126
+ }
127
+ for (const variant of BUTTON_VARIANTS) {
128
+ if (!v2.buttons.variants[variant]) {
129
+ v2.buttons.variants[variant] = structuredClone(defaultDesignTokensV2.buttons.variants[variant]);
130
+ }
131
+ if (!v2.buttons.variants[variant].normal) {
132
+ v2.buttons.variants[variant].normal = structuredClone(defaultDesignTokensV2.buttons.variants[variant].normal);
133
+ }
134
+ if (!v2.buttons.variants[variant].onSurface) {
135
+ v2.buttons.variants[variant].onSurface = structuredClone(defaultDesignTokensV2.buttons.variants[variant].onSurface);
136
+ }
137
+ }
138
+
139
+ // ── Layout validieren ──
140
+ if (!v2.spacing) v2.spacing = structuredClone(defaultDesignTokensV2.spacing);
141
+ if (!v2.borderRadius) v2.borderRadius = defaultDesignTokensV2.borderRadius;
142
+ if (!v2.borderWidth) v2.borderWidth = defaultDesignTokensV2.borderWidth;
143
+ if (!v2.container) v2.container = structuredClone(defaultDesignTokensV2.container);
144
+ if (!v2.breakpoints) v2.breakpoints = structuredClone(defaultDesignTokensV2.breakpoints);
145
+
146
+ return v2;
147
+ }
148
+
149
+
150
+ // ─── Farb-Migration ─────────────────────────────────────────────────────────
151
+
152
+ function migrateColors(v1, v2) {
153
+ if (!v1.colors) return;
154
+
155
+ // V1 hat flache Farben → V2 braucht 12-Stufen-Skalen
156
+
157
+ // Primary: v1.colors.primary → v2.colors.primary.base + Skala
158
+ migrateColorWorld(v1, v2, 'primary');
159
+ migrateColorWorld(v1, v2, 'secondary');
160
+ migrateColorWorld(v1, v2, 'success');
161
+ migrateColorWorld(v1, v2, 'warning');
162
+ migrateColorWorld(v1, v2, 'error');
163
+ migrateColorWorld(v1, v2, 'info');
164
+
165
+ // Accent: V1 hat bgAccentBase als Hauptfarbe
166
+ if (v1.colors.bgAccentBase) {
167
+ v2.colors.accent = {
168
+ base: v1.colors.bgAccentBase,
169
+ scale: generateColorScale(v1.colors.bgAccentBase)
170
+ };
171
+ }
172
+
173
+ // Neutral: V1 hat keine Neutral-Skala → Defaults behalten
174
+ // Aber wir können die V1 Hintergrund-/Text-Werte als Hinweis nehmen
175
+ if (v1.colors.bgBody && v1.colors.textBase) {
176
+ // Prüfe ob das ein helles oder dunkles Theme ist
177
+ const bgIsLight = isLightColor(v1.colors.bgBody);
178
+ if (!bgIsLight) {
179
+ // Dunkles Theme → invertierte Neutral-Skala
180
+ v2.colors.neutral.scale = {
181
+ 1: v1.colors.bgBody || '#0F172A',
182
+ 2: v1.colors.bgBase || '#1E293B',
183
+ 3: v1.colors.bgElevated || '#334155',
184
+ 4: '#475569',
185
+ 5: '#64748B',
186
+ 6: '#78859B',
187
+ 7: '#94A3B8',
188
+ 8: v1.colors.textMuted || '#A8B5C7',
189
+ 9: '#CBD5E1',
190
+ 10: '#D5DDE7',
191
+ 11: v1.colors.textSubtle || '#E2E8F0',
192
+ 12: v1.colors.textBase || '#F8FAFC'
193
+ };
194
+ }
195
+ }
196
+ }
197
+
198
+ function migrateColorWorld(v1, v2, world) {
199
+ if (v1.colors[world]) {
200
+ v2.colors[world] = {
201
+ base: v1.colors[world],
202
+ scale: generateColorScale(v1.colors[world])
203
+ };
204
+ }
205
+ }
206
+
207
+ function isLightColor(hex) {
208
+ if (!hex) return true;
209
+ const r = parseInt(hex.slice(1, 3), 16);
210
+ const g = parseInt(hex.slice(3, 5), 16);
211
+ const b = parseInt(hex.slice(5, 7), 16);
212
+ // Relative luminance
213
+ return (0.299 * r + 0.587 * g + 0.114 * b) > 128;
214
+ }
215
+
216
+
217
+ // ─── Font-Migration ─────────────────────────────────────────────────────────
218
+
219
+ function migrateFonts(v1, v2) {
220
+ if (!v1.typography) return;
221
+
222
+ const fonts = [];
223
+ const seen = new Set();
224
+
225
+ // Sammle einzigartige Fonts aus V1-Slots
226
+ const slots = ['heading', 'body', 'accent'];
227
+ const fontIds = {
228
+ heading: v1.typography.fontHeadingId,
229
+ body: v1.typography.fontBodyId,
230
+ accent: v1.typography.fontAccentId
231
+ };
232
+
233
+ for (const slot of slots) {
234
+ const familyString = v1.typography.fontFamily?.[slot];
235
+ if (!familyString) continue;
236
+
237
+ // Extrahiere den ersten Font-Namen aus dem CSS font-family string
238
+ const name = familyString.split(',')[0].trim().replace(/['"]/g, '');
239
+ if (seen.has(name)) continue;
240
+ seen.add(name);
241
+
242
+ fonts.push({
243
+ name,
244
+ source: fontIds[slot] ? 'fontsource' : 'system',
245
+ id: fontIds[slot] || null,
246
+ fallback: familyString.split(',').slice(1).map(s => s.trim()).join(', ') || 'sans-serif'
247
+ });
248
+ }
249
+
250
+ if (fonts.length > 0) v2.typography.fonts = fonts;
251
+
252
+ // Mono
253
+ if (v1.typography.fontFamily?.mono) {
254
+ const monoName = v1.typography.fontFamily.mono.split(',')[0].trim().replace(/['"]/g, '');
255
+ const monoFallback = v1.typography.fontFamily.mono.split(',').slice(1).map(s => s.trim()).join(', ') || 'monospace';
256
+ v2.typography.monoFont = {
257
+ name: monoName,
258
+ source: v1.typography.fontMonoId ? 'fontsource' : 'system',
259
+ id: v1.typography.fontMonoId || null,
260
+ fallback: monoFallback
261
+ };
262
+ }
263
+ }
264
+
265
+
266
+ // ─── Text-Stil-Migration ────────────────────────────────────────────────────
267
+
268
+ function migrateTextStyles(v1, v2) {
269
+ if (!v1.textStyles) return;
270
+
271
+ // Finde Font-Namen für V1-Slots
272
+ const headingFont = v2.typography.fonts.find(f =>
273
+ v1.typography?.fontFamily?.heading?.includes(f.name))?.name || v2.typography.fonts[0]?.name || 'Inter';
274
+ const bodyFont = v2.typography.fonts.find(f =>
275
+ v1.typography?.fontFamily?.body?.includes(f.name))?.name || v2.typography.fonts[0]?.name || 'Inter';
276
+ const accentFont = v2.typography.fonts.find(f =>
277
+ v1.typography?.fontFamily?.accent?.includes(f.name))?.name || bodyFont;
278
+
279
+ // Display: V1 display/heading1-5 → V2 display.1-5
280
+ const displayMap = {
281
+ display: 1,
282
+ heading1: 2,
283
+ heading2: 3,
284
+ heading3: 4,
285
+ heading4: 4, // heading4 und heading5 werden zu display.4 und display.5
286
+ heading5: 5,
287
+ heading6: 5
288
+ };
289
+
290
+ for (const [v1Name, v2Level] of Object.entries(displayMap)) {
291
+ if (v1.textStyles[v1Name]) {
292
+ const s = v1.textStyles[v1Name];
293
+ v2.typography.textStyles.display[v2Level] = {
294
+ font: headingFont,
295
+ fontSize: s.fontSize || v2.typography.textStyles.display[v2Level].fontSize,
296
+ fontWeight: s.fontWeight || 600,
297
+ lineHeight: s.lineHeight || '1.2',
298
+ letterSpacing: s.letterSpacing || '0em',
299
+ textTransform: s.textTransform || 'none'
300
+ };
301
+ }
302
+ }
303
+
304
+ // Body: V1 lead/body/bodySmall/caption → V2 body.1-5
305
+ const bodyMap = {
306
+ lead: 1,
307
+ body: 3,
308
+ bodySmall: 4,
309
+ caption: 5
310
+ };
311
+
312
+ for (const [v1Name, v2Level] of Object.entries(bodyMap)) {
313
+ if (v1.textStyles[v1Name]) {
314
+ const s = v1.textStyles[v1Name];
315
+ v2.typography.textStyles.body[v2Level] = {
316
+ font: bodyFont,
317
+ fontSize: s.fontSize || v2.typography.textStyles.body[v2Level].fontSize,
318
+ fontWeight: s.fontWeight || 400,
319
+ lineHeight: s.lineHeight || '1.5',
320
+ letterSpacing: s.letterSpacing || '0em',
321
+ textTransform: s.textTransform || 'none'
322
+ };
323
+ }
324
+ }
325
+
326
+ // UI: V1 label → V2 ui.3
327
+ if (v1.textStyles.label) {
328
+ const s = v1.textStyles.label;
329
+ v2.typography.textStyles.ui[3] = {
330
+ font: bodyFont,
331
+ fontSize: s.fontSize || '0.875rem',
332
+ fontWeight: s.fontWeight || 500,
333
+ lineHeight: s.lineHeight || '1.4',
334
+ letterSpacing: s.letterSpacing || '0em',
335
+ textTransform: s.textTransform || 'none'
336
+ };
337
+ }
338
+
339
+ // Accent: V1 overlines → V2 accent.4-5
340
+ const overlineMap = {
341
+ overlineDisplay: 3,
342
+ overlineHeading1: 4,
343
+ overlineHeading2: 4,
344
+ overlineHeading3: 5,
345
+ overlineHeading4: 5,
346
+ overlineHeading5: 5,
347
+ overlineHeading6: 5
348
+ };
349
+
350
+ for (const [v1Name, v2Level] of Object.entries(overlineMap)) {
351
+ if (v1.textStyles[v1Name]) {
352
+ const s = v1.textStyles[v1Name];
353
+ v2.typography.textStyles.accent[v2Level] = {
354
+ font: accentFont,
355
+ fontSize: s.fontSize || v2.typography.textStyles.accent[v2Level].fontSize,
356
+ fontWeight: s.fontWeight || 600,
357
+ lineHeight: s.lineHeight || '1.4',
358
+ letterSpacing: s.letterSpacing || '0.08em',
359
+ textTransform: s.textTransform || 'uppercase'
360
+ };
361
+ }
362
+ }
363
+ }
364
+
365
+
366
+ // ─── Button-Migration ───────────────────────────────────────────────────────
367
+
368
+ function migrateButtons(v1, v2) {
369
+ if (!v1.buttons) return;
370
+
371
+ // Sizes: V1 small/medium/large → V2 sm/md/lg
372
+ const sizeMap = { small: 'sm', medium: 'md', large: 'lg' };
373
+ if (v1.buttons.sizes) {
374
+ for (const [v1Size, v2Size] of Object.entries(sizeMap)) {
375
+ const s = v1.buttons.sizes[v1Size];
376
+ if (!s) continue;
377
+
378
+ v2.buttons.sizes[v2Size] = {
379
+ paddingX: typeof s.paddingX === 'number' ? `${s.paddingX / 16}rem` : s.paddingX,
380
+ paddingY: typeof s.paddingY === 'number' ? `${s.paddingY / 16}rem` : s.paddingY,
381
+ fontSize: typeof s.fontSize === 'number' ? `${s.fontSize / 16}rem` : s.fontSize,
382
+ fontWeight: 500,
383
+ lineHeight: '1.4',
384
+ letterSpacing: '0em',
385
+ borderRadius: typeof s.borderRadius === 'number' ? `${s.borderRadius / 16}rem` : s.borderRadius,
386
+ minHeight: typeof s.minHeight === 'number' ? `${s.minHeight / 16}rem` : s.minHeight,
387
+ gap: typeof s.gap === 'number' ? `${s.gap / 16}rem` : s.gap,
388
+ iconSize: typeof s.iconSize === 'number' ? `${s.iconSize / 16}rem` : s.iconSize
389
+ };
390
+ }
391
+ }
392
+
393
+ // Variants: V1 primary → V2 filled, V1 secondary → V2 tonal, etc.
394
+ const variantMap = {
395
+ primary: 'filled',
396
+ secondary: 'tonal',
397
+ ghost: 'ghost',
398
+ accent: 'tonal', // Fallback: accent → tonal
399
+ inverted: null // Wird als filled.onSurface migriert
400
+ };
401
+
402
+ for (const [v1Name, v2Name] of Object.entries(variantMap)) {
403
+ const variant = v1.buttons[v1Name];
404
+ if (!variant || !v2Name) continue;
405
+
406
+ // V1 Farbreferenzen zu V2 Stufen konvertieren
407
+ v2.buttons.variants[v2Name].normal = {
408
+ bg: mapV1ColorToV2Ref(variant.bgColor, v1),
409
+ bgHover: mapV1ColorToV2Ref(variant.bgColorHover, v1),
410
+ text: mapV1ColorToV2Ref(variant.textColor, v1),
411
+ textHover: mapV1ColorToV2Ref(variant.textColorHover || variant.textColor, v1),
412
+ border: variant.borderColor === 'transparent' ? 'none' : mapV1ColorToV2Ref(variant.borderColor, v1),
413
+ borderHover: variant.borderColorHover === 'transparent' ? 'none' : mapV1ColorToV2Ref(variant.borderColorHover, v1),
414
+ shadow: 'none',
415
+ shadowHover: 'none',
416
+ focusRingColor: 'primary-8',
417
+ customCSS: null
418
+ };
419
+ }
420
+
421
+ // inverted → filled.onSurface
422
+ if (v1.buttons.inverted) {
423
+ const inv = v1.buttons.inverted;
424
+ v2.buttons.variants.filled.onSurface = {
425
+ bg: mapV1ColorToV2Ref(inv.bgColor, v1),
426
+ bgHover: mapV1ColorToV2Ref(inv.bgColorHover, v1),
427
+ text: mapV1ColorToV2Ref(inv.textColor, v1),
428
+ textHover: mapV1ColorToV2Ref(inv.textColorHover || inv.textColor, v1),
429
+ border: 'none',
430
+ borderHover: 'none',
431
+ customCSS: null
432
+ };
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Konvertiert V1-Farbreferenzen (Token-Name oder Hex) zu V2-Stufenreferenzen.
438
+ * V1: "primary" → V2: "primary-9"
439
+ * V1: "primaryDark" → V2: "primary-10"
440
+ * V1: "white" → V2: "neutral-1"
441
+ * V1: "textBase" → V2: "neutral-12"
442
+ * V1: "transparent" → V2: "transparent"
443
+ */
444
+ function mapV1ColorToV2Ref(v1Ref, v1) {
445
+ if (!v1Ref) return 'none';
446
+ if (v1Ref === 'transparent') return 'transparent';
447
+
448
+ // Direkte Mappings V1 → V2
449
+ const mapping = {
450
+ // Brand
451
+ 'primary': 'primary-9',
452
+ 'primaryDark': 'primary-10',
453
+ 'primaryLight': 'primary-7',
454
+ 'secondary': 'secondary-9',
455
+ 'secondaryDark': 'secondary-10',
456
+ 'secondaryLight': 'secondary-7',
457
+
458
+ // Status
459
+ 'success': 'success-9',
460
+ 'successDark': 'success-10',
461
+ 'successLight': 'success-3',
462
+ 'warning': 'warning-9',
463
+ 'warningDark': 'warning-10',
464
+ 'warningLight': 'warning-3',
465
+ 'error': 'error-9',
466
+ 'errorDark': 'error-10',
467
+ 'errorLight': 'error-3',
468
+ 'info': 'info-9',
469
+ 'infoDark': 'info-10',
470
+ 'infoLight': 'info-3',
471
+
472
+ // Neutral
473
+ 'white': 'neutral-1',
474
+ 'black': 'neutral-12',
475
+
476
+ // Background
477
+ 'bgBody': 'neutral-1',
478
+ 'bgBase': 'neutral-1',
479
+ 'bgElevated': 'neutral-2',
480
+ 'bgLifted': 'neutral-3',
481
+
482
+ // Text
483
+ 'textBase': 'neutral-12',
484
+ 'textSubtle': 'neutral-11',
485
+ 'textMuted': 'neutral-8',
486
+
487
+ // Border
488
+ 'borderBase': 'neutral-7',
489
+ 'borderSubtle': 'neutral-6',
490
+ 'borderMuted': 'neutral-5',
491
+
492
+ // On-colors
493
+ 'textOnPrimary': 'on-primary',
494
+ 'textOnSecondary': 'on-secondary',
495
+ 'textOnAccent': 'on-accent',
496
+
497
+ // Accent backgrounds
498
+ 'bgAccentBase': 'accent-9',
499
+ 'bgAccentElevated': 'accent-10',
500
+ 'bgAccentLifted': 'accent-3'
501
+ };
502
+
503
+ if (mapping[v1Ref]) return mapping[v1Ref];
504
+
505
+ // Wenn es ein Hex-Wert ist, gib ihn als Stufe 9 der nächsten Welt zurück
506
+ // (Fallback — sollte selten vorkommen)
507
+ if (v1Ref.startsWith('#')) return v1Ref;
508
+
509
+ return 'primary-9'; // Letzter Fallback
510
+ }