@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,513 @@
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 explizite Neutral-Basisfarbe.
174
+ // Wir leiten sie aus den V1-Hintergrund-/Text-Farben ab.
175
+ if (v1.colors.bgBody && v1.colors.textBase) {
176
+ const bgIsLight = isLightColor(v1.colors.bgBody);
177
+ if (!bgIsLight) {
178
+ // Dunkles Theme → invertierte Neutral-Skala (manuell, da generateColorScale für Light optimiert ist)
179
+ v2.colors.neutral = {
180
+ base: '#94A3B8', // Slate-400 als Basis für dunkle Themes
181
+ scale: {
182
+ 1: v1.colors.bgBody || '#0F172A',
183
+ 2: v1.colors.bgBase || '#1E293B',
184
+ 3: v1.colors.bgElevated || '#334155',
185
+ 4: '#475569',
186
+ 5: '#64748B',
187
+ 6: '#78859B',
188
+ 7: '#94A3B8',
189
+ 8: v1.colors.textMuted || '#A8B5C7',
190
+ 9: '#CBD5E1',
191
+ 10: '#D5DDE7',
192
+ 11: v1.colors.textSubtle || '#E2E8F0',
193
+ 12: v1.colors.textBase || '#F8FAFC'
194
+ }
195
+ };
196
+ }
197
+ // Helles Theme → Default-Neutral (auto-generiert aus #6B7280) bleibt
198
+ }
199
+ }
200
+
201
+ function migrateColorWorld(v1, v2, world) {
202
+ if (v1.colors[world]) {
203
+ v2.colors[world] = {
204
+ base: v1.colors[world],
205
+ scale: generateColorScale(v1.colors[world])
206
+ };
207
+ }
208
+ }
209
+
210
+ function isLightColor(hex) {
211
+ if (!hex) return true;
212
+ const r = parseInt(hex.slice(1, 3), 16);
213
+ const g = parseInt(hex.slice(3, 5), 16);
214
+ const b = parseInt(hex.slice(5, 7), 16);
215
+ // Relative luminance
216
+ return (0.299 * r + 0.587 * g + 0.114 * b) > 128;
217
+ }
218
+
219
+
220
+ // ─── Font-Migration ─────────────────────────────────────────────────────────
221
+
222
+ function migrateFonts(v1, v2) {
223
+ if (!v1.typography) return;
224
+
225
+ const fonts = [];
226
+ const seen = new Set();
227
+
228
+ // Sammle einzigartige Fonts aus V1-Slots
229
+ const slots = ['heading', 'body', 'accent'];
230
+ const fontIds = {
231
+ heading: v1.typography.fontHeadingId,
232
+ body: v1.typography.fontBodyId,
233
+ accent: v1.typography.fontAccentId
234
+ };
235
+
236
+ for (const slot of slots) {
237
+ const familyString = v1.typography.fontFamily?.[slot];
238
+ if (!familyString) continue;
239
+
240
+ // Extrahiere den ersten Font-Namen aus dem CSS font-family string
241
+ const name = familyString.split(',')[0].trim().replace(/['"]/g, '');
242
+ if (seen.has(name)) continue;
243
+ seen.add(name);
244
+
245
+ fonts.push({
246
+ name,
247
+ source: fontIds[slot] ? 'fontsource' : 'system',
248
+ id: fontIds[slot] || null,
249
+ fallback: familyString.split(',').slice(1).map(s => s.trim()).join(', ') || 'sans-serif'
250
+ });
251
+ }
252
+
253
+ if (fonts.length > 0) v2.typography.fonts = fonts;
254
+
255
+ // Mono
256
+ if (v1.typography.fontFamily?.mono) {
257
+ const monoName = v1.typography.fontFamily.mono.split(',')[0].trim().replace(/['"]/g, '');
258
+ const monoFallback = v1.typography.fontFamily.mono.split(',').slice(1).map(s => s.trim()).join(', ') || 'monospace';
259
+ v2.typography.monoFont = {
260
+ name: monoName,
261
+ source: v1.typography.fontMonoId ? 'fontsource' : 'system',
262
+ id: v1.typography.fontMonoId || null,
263
+ fallback: monoFallback
264
+ };
265
+ }
266
+ }
267
+
268
+
269
+ // ─── Text-Stil-Migration ────────────────────────────────────────────────────
270
+
271
+ function migrateTextStyles(v1, v2) {
272
+ if (!v1.textStyles) return;
273
+
274
+ // Finde Font-Namen für V1-Slots
275
+ const headingFont = v2.typography.fonts.find(f =>
276
+ v1.typography?.fontFamily?.heading?.includes(f.name))?.name || v2.typography.fonts[0]?.name || 'Inter';
277
+ const bodyFont = v2.typography.fonts.find(f =>
278
+ v1.typography?.fontFamily?.body?.includes(f.name))?.name || v2.typography.fonts[0]?.name || 'Inter';
279
+ const accentFont = v2.typography.fonts.find(f =>
280
+ v1.typography?.fontFamily?.accent?.includes(f.name))?.name || bodyFont;
281
+
282
+ // Display: V1 display/heading1-5 → V2 display.1-5
283
+ const displayMap = {
284
+ display: 1,
285
+ heading1: 2,
286
+ heading2: 3,
287
+ heading3: 4,
288
+ heading4: 4, // heading4 und heading5 werden zu display.4 und display.5
289
+ heading5: 5,
290
+ heading6: 5
291
+ };
292
+
293
+ for (const [v1Name, v2Level] of Object.entries(displayMap)) {
294
+ if (v1.textStyles[v1Name]) {
295
+ const s = v1.textStyles[v1Name];
296
+ v2.typography.textStyles.display[v2Level] = {
297
+ font: headingFont,
298
+ fontSize: s.fontSize || v2.typography.textStyles.display[v2Level].fontSize,
299
+ fontWeight: s.fontWeight || 600,
300
+ lineHeight: s.lineHeight || '1.2',
301
+ letterSpacing: s.letterSpacing || '0em',
302
+ textTransform: s.textTransform || 'none'
303
+ };
304
+ }
305
+ }
306
+
307
+ // Body: V1 lead/body/bodySmall/caption → V2 body.1-5
308
+ const bodyMap = {
309
+ lead: 1,
310
+ body: 3,
311
+ bodySmall: 4,
312
+ caption: 5
313
+ };
314
+
315
+ for (const [v1Name, v2Level] of Object.entries(bodyMap)) {
316
+ if (v1.textStyles[v1Name]) {
317
+ const s = v1.textStyles[v1Name];
318
+ v2.typography.textStyles.body[v2Level] = {
319
+ font: bodyFont,
320
+ fontSize: s.fontSize || v2.typography.textStyles.body[v2Level].fontSize,
321
+ fontWeight: s.fontWeight || 400,
322
+ lineHeight: s.lineHeight || '1.5',
323
+ letterSpacing: s.letterSpacing || '0em',
324
+ textTransform: s.textTransform || 'none'
325
+ };
326
+ }
327
+ }
328
+
329
+ // UI: V1 label → V2 ui.3
330
+ if (v1.textStyles.label) {
331
+ const s = v1.textStyles.label;
332
+ v2.typography.textStyles.ui[3] = {
333
+ font: bodyFont,
334
+ fontSize: s.fontSize || '0.875rem',
335
+ fontWeight: s.fontWeight || 500,
336
+ lineHeight: s.lineHeight || '1.4',
337
+ letterSpacing: s.letterSpacing || '0em',
338
+ textTransform: s.textTransform || 'none'
339
+ };
340
+ }
341
+
342
+ // Accent: V1 overlines → V2 accent.4-5
343
+ const overlineMap = {
344
+ overlineDisplay: 3,
345
+ overlineHeading1: 4,
346
+ overlineHeading2: 4,
347
+ overlineHeading3: 5,
348
+ overlineHeading4: 5,
349
+ overlineHeading5: 5,
350
+ overlineHeading6: 5
351
+ };
352
+
353
+ for (const [v1Name, v2Level] of Object.entries(overlineMap)) {
354
+ if (v1.textStyles[v1Name]) {
355
+ const s = v1.textStyles[v1Name];
356
+ v2.typography.textStyles.accent[v2Level] = {
357
+ font: accentFont,
358
+ fontSize: s.fontSize || v2.typography.textStyles.accent[v2Level].fontSize,
359
+ fontWeight: s.fontWeight || 600,
360
+ lineHeight: s.lineHeight || '1.4',
361
+ letterSpacing: s.letterSpacing || '0.08em',
362
+ textTransform: s.textTransform || 'uppercase'
363
+ };
364
+ }
365
+ }
366
+ }
367
+
368
+
369
+ // ─── Button-Migration ───────────────────────────────────────────────────────
370
+
371
+ function migrateButtons(v1, v2) {
372
+ if (!v1.buttons) return;
373
+
374
+ // Sizes: V1 small/medium/large → V2 sm/md/lg
375
+ const sizeMap = { small: 'sm', medium: 'md', large: 'lg' };
376
+ if (v1.buttons.sizes) {
377
+ for (const [v1Size, v2Size] of Object.entries(sizeMap)) {
378
+ const s = v1.buttons.sizes[v1Size];
379
+ if (!s) continue;
380
+
381
+ v2.buttons.sizes[v2Size] = {
382
+ paddingX: typeof s.paddingX === 'number' ? `${s.paddingX / 16}rem` : s.paddingX,
383
+ paddingY: typeof s.paddingY === 'number' ? `${s.paddingY / 16}rem` : s.paddingY,
384
+ fontSize: typeof s.fontSize === 'number' ? `${s.fontSize / 16}rem` : s.fontSize,
385
+ fontWeight: 500,
386
+ lineHeight: '1.4',
387
+ letterSpacing: '0em',
388
+ borderRadius: typeof s.borderRadius === 'number' ? `${s.borderRadius / 16}rem` : s.borderRadius,
389
+ minHeight: typeof s.minHeight === 'number' ? `${s.minHeight / 16}rem` : s.minHeight,
390
+ gap: typeof s.gap === 'number' ? `${s.gap / 16}rem` : s.gap,
391
+ iconSize: typeof s.iconSize === 'number' ? `${s.iconSize / 16}rem` : s.iconSize
392
+ };
393
+ }
394
+ }
395
+
396
+ // Variants: V1 primary → V2 filled, V1 secondary → V2 tonal, etc.
397
+ const variantMap = {
398
+ primary: 'filled',
399
+ secondary: 'tonal',
400
+ ghost: 'ghost',
401
+ accent: 'tonal', // Fallback: accent → tonal
402
+ inverted: null // Wird als filled.onSurface migriert
403
+ };
404
+
405
+ for (const [v1Name, v2Name] of Object.entries(variantMap)) {
406
+ const variant = v1.buttons[v1Name];
407
+ if (!variant || !v2Name) continue;
408
+
409
+ // V1 Farbreferenzen zu V2 Stufen konvertieren
410
+ v2.buttons.variants[v2Name].normal = {
411
+ bg: mapV1ColorToV2Ref(variant.bgColor, v1),
412
+ bgHover: mapV1ColorToV2Ref(variant.bgColorHover, v1),
413
+ text: mapV1ColorToV2Ref(variant.textColor, v1),
414
+ textHover: mapV1ColorToV2Ref(variant.textColorHover || variant.textColor, v1),
415
+ border: variant.borderColor === 'transparent' ? 'none' : mapV1ColorToV2Ref(variant.borderColor, v1),
416
+ borderHover: variant.borderColorHover === 'transparent' ? 'none' : mapV1ColorToV2Ref(variant.borderColorHover, v1),
417
+ shadow: 'none',
418
+ shadowHover: 'none',
419
+ focusRingColor: 'primary-8',
420
+ customCSS: null
421
+ };
422
+ }
423
+
424
+ // inverted → filled.onSurface
425
+ if (v1.buttons.inverted) {
426
+ const inv = v1.buttons.inverted;
427
+ v2.buttons.variants.filled.onSurface = {
428
+ bg: mapV1ColorToV2Ref(inv.bgColor, v1),
429
+ bgHover: mapV1ColorToV2Ref(inv.bgColorHover, v1),
430
+ text: mapV1ColorToV2Ref(inv.textColor, v1),
431
+ textHover: mapV1ColorToV2Ref(inv.textColorHover || inv.textColor, v1),
432
+ border: 'none',
433
+ borderHover: 'none',
434
+ customCSS: null
435
+ };
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Konvertiert V1-Farbreferenzen (Token-Name oder Hex) zu V2-Stufenreferenzen.
441
+ * V1: "primary" → V2: "primary-9"
442
+ * V1: "primaryDark" → V2: "primary-10"
443
+ * V1: "white" → V2: "neutral-1"
444
+ * V1: "textBase" → V2: "neutral-12"
445
+ * V1: "transparent" → V2: "transparent"
446
+ */
447
+ function mapV1ColorToV2Ref(v1Ref, v1) {
448
+ if (!v1Ref) return 'none';
449
+ if (v1Ref === 'transparent') return 'transparent';
450
+
451
+ // Direkte Mappings V1 → V2
452
+ const mapping = {
453
+ // Brand
454
+ 'primary': 'primary-9',
455
+ 'primaryDark': 'primary-10',
456
+ 'primaryLight': 'primary-7',
457
+ 'secondary': 'secondary-9',
458
+ 'secondaryDark': 'secondary-10',
459
+ 'secondaryLight': 'secondary-7',
460
+
461
+ // Status
462
+ 'success': 'success-9',
463
+ 'successDark': 'success-10',
464
+ 'successLight': 'success-3',
465
+ 'warning': 'warning-9',
466
+ 'warningDark': 'warning-10',
467
+ 'warningLight': 'warning-3',
468
+ 'error': 'error-9',
469
+ 'errorDark': 'error-10',
470
+ 'errorLight': 'error-3',
471
+ 'info': 'info-9',
472
+ 'infoDark': 'info-10',
473
+ 'infoLight': 'info-3',
474
+
475
+ // Neutral
476
+ 'white': 'neutral-1',
477
+ 'black': 'neutral-12',
478
+
479
+ // Background
480
+ 'bgBody': 'neutral-1',
481
+ 'bgBase': 'neutral-1',
482
+ 'bgElevated': 'neutral-2',
483
+ 'bgLifted': 'neutral-3',
484
+
485
+ // Text
486
+ 'textBase': 'neutral-12',
487
+ 'textSubtle': 'neutral-11',
488
+ 'textMuted': 'neutral-8',
489
+
490
+ // Border
491
+ 'borderBase': 'neutral-7',
492
+ 'borderSubtle': 'neutral-6',
493
+ 'borderMuted': 'neutral-5',
494
+
495
+ // On-colors
496
+ 'textOnPrimary': 'on-primary',
497
+ 'textOnSecondary': 'on-secondary',
498
+ 'textOnAccent': 'on-accent',
499
+
500
+ // Accent backgrounds
501
+ 'bgAccentBase': 'accent-9',
502
+ 'bgAccentElevated': 'accent-10',
503
+ 'bgAccentLifted': 'accent-3'
504
+ };
505
+
506
+ if (mapping[v1Ref]) return mapping[v1Ref];
507
+
508
+ // Wenn es ein Hex-Wert ist, gib ihn als Stufe 9 der nächsten Welt zurück
509
+ // (Fallback — sollte selten vorkommen)
510
+ if (v1Ref.startsWith('#')) return v1Ref;
511
+
512
+ return 'primary-9'; // Letzter Fallback
513
+ }