riazmo commited on
Commit
fb953a1
·
verified ·
1 Parent(s): e3a838d

Delete core/preview_generator.py

Browse files
Files changed (1) hide show
  1. core/preview_generator.py +0 -568
core/preview_generator.py DELETED
@@ -1,568 +0,0 @@
1
- """
2
- Preview Generator for Typography and Color Ramps
3
-
4
- Generates HTML previews for:
5
- 1. Typography - Actual font rendering with detected styles
6
- 2. Color Ramps - 11 shades (50-950) with AA compliance indicators
7
- """
8
-
9
- from typing import Optional
10
- import colorsys
11
- import re
12
-
13
-
14
- # =============================================================================
15
- # TYPOGRAPHY PREVIEW
16
- # =============================================================================
17
-
18
- def generate_typography_preview_html(
19
- typography_tokens: dict,
20
- font_family: str = "Open Sans",
21
- background: str = "#FAFAFA",
22
- sample_text: str = "The quick brown fox jumps over the lazy dog"
23
- ) -> str:
24
- """
25
- Generate HTML preview for typography tokens.
26
-
27
- Args:
28
- typography_tokens: Dict of typography styles {name: {font_size, font_weight, line_height, letter_spacing}}
29
- font_family: Primary font family detected
30
- background: Background color (neutral)
31
- sample_text: Text to render for preview
32
-
33
- Returns:
34
- HTML string for Gradio HTML component
35
- """
36
-
37
- # Sort tokens by font size (largest first)
38
- sorted_tokens = []
39
- for name, token in typography_tokens.items():
40
- size_str = str(token.get("font_size", "16px"))
41
- size_num = float(re.sub(r'[^0-9.]', '', size_str) or 16)
42
- sorted_tokens.append((name, token, size_num))
43
-
44
- sorted_tokens.sort(key=lambda x: -x[2]) # Descending by size
45
-
46
- # Generate rows
47
- rows_html = ""
48
- for name, token, size_num in sorted_tokens[:15]: # Limit to 15 styles
49
- font_size = token.get("font_size", "16px")
50
- font_weight = token.get("font_weight", "400")
51
- line_height = token.get("line_height", "1.5")
52
- letter_spacing = token.get("letter_spacing", "0")
53
-
54
- # Convert weight names to numbers
55
- weight_map = {
56
- "thin": 100, "extralight": 200, "light": 300, "regular": 400,
57
- "medium": 500, "semibold": 600, "bold": 700, "extrabold": 800, "black": 900
58
- }
59
- if isinstance(font_weight, str) and font_weight.lower() in weight_map:
60
- font_weight = weight_map[font_weight.lower()]
61
-
62
- # Weight label
63
- weight_labels = {
64
- 100: "Thin", 200: "ExtraLight", 300: "Light", 400: "Regular",
65
- 500: "Medium", 600: "SemiBold", 700: "Bold", 800: "ExtraBold", 900: "Black"
66
- }
67
- weight_label = weight_labels.get(int(font_weight) if str(font_weight).isdigit() else 400, "Regular")
68
-
69
- # Clean up name for display
70
- display_name = name.replace("_", " ").replace("-", " ").title()
71
- if len(display_name) > 15:
72
- display_name = display_name[:15] + "..."
73
-
74
- # Truncate sample text for large sizes
75
- display_text = sample_text
76
- if size_num > 48:
77
- display_text = sample_text[:30] + "..."
78
- elif size_num > 32:
79
- display_text = sample_text[:40] + "..."
80
-
81
- rows_html += f'''
82
- <tr class="meta-row">
83
- <td class="scale-name">
84
- <div class="scale-label">{display_name}</div>
85
- </td>
86
- <td class="meta">{font_family}</td>
87
- <td class="meta">{weight_label}</td>
88
- <td class="meta">{int(size_num)}</td>
89
- <td class="meta">Sentence</td>
90
- <td class="meta">{letter_spacing}</td>
91
- </tr>
92
- <tr>
93
- <td colspan="6" class="preview-cell">
94
- <div class="preview-text" style="
95
- font-family: '{font_family}', sans-serif;
96
- font-size: {font_size};
97
- font-weight: {font_weight};
98
- line-height: {line_height};
99
- letter-spacing: {letter_spacing}px;
100
- ">{display_text}</div>
101
- </td>
102
- </tr>
103
- '''
104
-
105
- html = f'''
106
- <style>
107
- @import url('https://fonts.googleapis.com/css2?family={font_family.replace(" ", "+")}:wght@100;200;300;400;500;600;700;800;900&display=swap');
108
-
109
- .typography-preview {{
110
- font-family: system-ui, -apple-system, sans-serif;
111
- background: {background};
112
- border-radius: 12px;
113
- padding: 20px;
114
- overflow-x: auto;
115
- }}
116
-
117
- .typography-preview table {{
118
- width: 100%;
119
- border-collapse: collapse;
120
- }}
121
-
122
- .typography-preview th {{
123
- text-align: left;
124
- padding: 12px 16px;
125
- font-size: 12px;
126
- font-weight: 600;
127
- color: #333;
128
- text-transform: uppercase;
129
- letter-spacing: 0.5px;
130
- border-bottom: 2px solid #E0E0E0;
131
- background: #F5F5F5;
132
- }}
133
-
134
- .typography-preview td {{
135
- padding: 8px 16px;
136
- vertical-align: middle;
137
- }}
138
-
139
- .typography-preview .meta-row {{
140
- background: #F8F8F8;
141
- border-top: 1px solid #E8E8E8;
142
- }}
143
-
144
- .typography-preview .scale-name {{
145
- font-weight: 700;
146
- color: #1A1A1A;
147
- min-width: 120px;
148
- }}
149
-
150
- .typography-preview .scale-label {{
151
- font-size: 13px;
152
- font-weight: 600;
153
- color: #1A1A1A;
154
- background: #E8E8E8;
155
- padding: 4px 8px;
156
- border-radius: 4px;
157
- display: inline-block;
158
- }}
159
-
160
- .typography-preview .meta {{
161
- font-size: 13px;
162
- color: #444;
163
- white-space: nowrap;
164
- }}
165
-
166
- .typography-preview .preview-cell {{
167
- padding: 16px;
168
- background: #FFFFFF;
169
- border-bottom: 1px solid #E8E8E8;
170
- }}
171
-
172
- .typography-preview .preview-text {{
173
- color: #1A1A1A;
174
- margin: 0;
175
- word-break: break-word;
176
- }}
177
-
178
- .typography-preview tr:hover .preview-cell {{
179
- background: #F5F5F5;
180
- }}
181
- </style>
182
-
183
- <div class="typography-preview">
184
- <table>
185
- <thead>
186
- <tr>
187
- <th>Scale Category</th>
188
- <th>Typeface</th>
189
- <th>Weight</th>
190
- <th>Size</th>
191
- <th>Case</th>
192
- <th>Letter Spacing</th>
193
- </tr>
194
- </thead>
195
- <tbody>
196
- {rows_html}
197
- </tbody>
198
- </table>
199
- </div>
200
- '''
201
-
202
- return html
203
-
204
-
205
- # =============================================================================
206
- # COLOR RAMP PREVIEW
207
- # =============================================================================
208
-
209
- def hex_to_rgb(hex_color: str) -> tuple:
210
- """Convert hex color to RGB tuple."""
211
- hex_color = hex_color.lstrip('#')
212
- if len(hex_color) == 3:
213
- hex_color = ''.join([c*2 for c in hex_color])
214
- return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
215
-
216
-
217
- def rgb_to_hex(rgb: tuple) -> str:
218
- """Convert RGB tuple to hex string."""
219
- return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]), int(rgb[1]), int(rgb[2]))
220
-
221
-
222
- def get_luminance(rgb: tuple) -> float:
223
- """Calculate relative luminance for contrast ratio."""
224
- def adjust(c):
225
- c = c / 255
226
- return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
227
-
228
- r, g, b = rgb
229
- return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)
230
-
231
-
232
- def get_contrast_ratio(color1: tuple, color2: tuple) -> float:
233
- """Calculate contrast ratio between two colors."""
234
- l1 = get_luminance(color1)
235
- l2 = get_luminance(color2)
236
- lighter = max(l1, l2)
237
- darker = min(l1, l2)
238
- return (lighter + 0.05) / (darker + 0.05)
239
-
240
-
241
- def generate_color_ramp(base_hex: str) -> list[dict]:
242
- """
243
- Generate 11 shades (50-950) from a base color.
244
-
245
- Uses OKLCH-like approach for perceptually uniform steps.
246
- """
247
- try:
248
- rgb = hex_to_rgb(base_hex)
249
- except:
250
- return []
251
-
252
- # Convert to HLS for easier manipulation
253
- r, g, b = [x / 255 for x in rgb]
254
- h, l, s = colorsys.rgb_to_hls(r, g, b)
255
-
256
- # Define lightness levels for each shade
257
- # 50 = very light (0.95), 500 = base, 950 = very dark (0.05)
258
- shade_lightness = {
259
- 50: 0.95,
260
- 100: 0.90,
261
- 200: 0.80,
262
- 300: 0.70,
263
- 400: 0.60,
264
- 500: l, # Keep original lightness for 500
265
- 600: 0.45,
266
- 700: 0.35,
267
- 800: 0.25,
268
- 900: 0.15,
269
- 950: 0.08,
270
- }
271
-
272
- # Adjust saturation for light/dark shades
273
- ramp = []
274
- for shade, target_l in shade_lightness.items():
275
- # Reduce saturation for very light colors
276
- if target_l > 0.8:
277
- adjusted_s = s * 0.6
278
- elif target_l < 0.2:
279
- adjusted_s = s * 0.8
280
- else:
281
- adjusted_s = s
282
-
283
- # Generate new RGB
284
- new_r, new_g, new_b = colorsys.hls_to_rgb(h, target_l, adjusted_s)
285
- new_rgb = (int(new_r * 255), int(new_g * 255), int(new_b * 255))
286
- new_hex = rgb_to_hex(new_rgb)
287
-
288
- # Check AA compliance
289
- white = (255, 255, 255)
290
- black = (0, 0, 0)
291
- contrast_white = get_contrast_ratio(new_rgb, white)
292
- contrast_black = get_contrast_ratio(new_rgb, black)
293
-
294
- # AA requires 4.5:1 for normal text
295
- aa_on_white = contrast_white >= 4.5
296
- aa_on_black = contrast_black >= 4.5
297
-
298
- ramp.append({
299
- "shade": shade,
300
- "hex": new_hex,
301
- "rgb": new_rgb,
302
- "contrast_white": round(contrast_white, 2),
303
- "contrast_black": round(contrast_black, 2),
304
- "aa_on_white": aa_on_white,
305
- "aa_on_black": aa_on_black,
306
- })
307
-
308
- return ramp
309
-
310
-
311
- def generate_color_ramps_preview_html(
312
- color_tokens: dict,
313
- background: str = "#FAFAFA"
314
- ) -> str:
315
- """
316
- Generate HTML preview for color ramps.
317
-
318
- Args:
319
- color_tokens: Dict of colors {name: {value: "#hex", ...}}
320
- background: Background color
321
-
322
- Returns:
323
- HTML string for Gradio HTML component
324
- """
325
-
326
- rows_html = ""
327
-
328
- for name, token in list(color_tokens.items())[:10]: # Limit to 10 colors
329
- # Get hex value
330
- if isinstance(token, dict):
331
- hex_val = token.get("value", "#888888")
332
- else:
333
- hex_val = str(token)
334
-
335
- # Clean up hex
336
- if not hex_val.startswith("#"):
337
- hex_val = f"#{hex_val}"
338
-
339
- # Generate ramp
340
- ramp = generate_color_ramp(hex_val)
341
- if not ramp:
342
- continue
343
-
344
- # Clean name
345
- display_name = name.replace("_", " ").replace("-", " ").title()
346
- if len(display_name) > 15:
347
- display_name = display_name[:12] + "..."
348
-
349
- # Generate shade cells
350
- shades_html = ""
351
- for shade_info in ramp:
352
- shade = shade_info["shade"]
353
- hex_color = shade_info["hex"]
354
- aa_white = shade_info["aa_on_white"]
355
- aa_black = shade_info["aa_on_black"]
356
-
357
- # Determine text color for label
358
- text_color = "#000" if shade < 500 else "#FFF"
359
-
360
- # AA indicator
361
- if aa_white or aa_black:
362
- aa_indicator = "✓"
363
- aa_class = "aa-pass"
364
- else:
365
- aa_indicator = "✗"
366
- aa_class = "aa-fail"
367
-
368
- shades_html += f'''
369
- <div class="shade-cell" style="background-color: {hex_color};" title="{hex_color} | AA: {'Pass' if aa_white or aa_black else 'Fail'}">
370
- <span class="shade-label" style="color: {text_color};">{shade}</span>
371
- <span class="aa-badge {aa_class}">{aa_indicator}</span>
372
- </div>
373
- '''
374
-
375
- rows_html += f'''
376
- <div class="color-row">
377
- <div class="color-info">
378
- <div class="color-swatch" style="background-color: {hex_val};"></div>
379
- <div class="color-meta">
380
- <div class="color-name">{display_name}</div>
381
- <div class="color-hex">{hex_val}</div>
382
- </div>
383
- </div>
384
- <div class="color-ramp">
385
- {shades_html}
386
- </div>
387
- </div>
388
- '''
389
-
390
- html = f'''
391
- <style>
392
- .color-ramps-preview {{
393
- font-family: system-ui, -apple-system, sans-serif;
394
- background: {background};
395
- border-radius: 12px;
396
- padding: 20px;
397
- overflow-x: auto;
398
- }}
399
-
400
- .color-row {{
401
- display: flex;
402
- align-items: center;
403
- margin-bottom: 16px;
404
- padding-bottom: 16px;
405
- border-bottom: 1px solid #E8E8E8;
406
- }}
407
-
408
- .color-row:last-child {{
409
- border-bottom: none;
410
- margin-bottom: 0;
411
- padding-bottom: 0;
412
- }}
413
-
414
- .color-info {{
415
- display: flex;
416
- align-items: center;
417
- min-width: 140px;
418
- margin-right: 20px;
419
- }}
420
-
421
- .color-swatch {{
422
- width: 40px;
423
- height: 40px;
424
- border-radius: 8px;
425
- border: 2px solid rgba(0,0,0,0.15);
426
- margin-right: 12px;
427
- flex-shrink: 0;
428
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
429
- }}
430
-
431
- .color-meta {{
432
- flex: 1;
433
- min-width: 100px;
434
- }}
435
-
436
- .color-name {{
437
- font-weight: 700;
438
- font-size: 13px;
439
- color: #1A1A1A;
440
- margin-bottom: 2px;
441
- background: #E8E8E8;
442
- padding: 3px 8px;
443
- border-radius: 4px;
444
- display: inline-block;
445
- }}
446
-
447
- .color-hex {{
448
- font-size: 11px;
449
- color: #444;
450
- font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
451
- margin-top: 4px;
452
- }}
453
-
454
- .color-ramp {{
455
- display: flex;
456
- gap: 4px;
457
- flex: 1;
458
- }}
459
-
460
- .shade-cell {{
461
- width: 48px;
462
- height: 48px;
463
- border-radius: 6px;
464
- display: flex;
465
- flex-direction: column;
466
- align-items: center;
467
- justify-content: center;
468
- position: relative;
469
- cursor: pointer;
470
- transition: transform 0.15s;
471
- border: 1px solid rgba(0,0,0,0.1);
472
- }}
473
-
474
- .shade-cell:hover {{
475
- transform: scale(1.1);
476
- z-index: 10;
477
- box-shadow: 0 4px 12px rgba(0,0,0,0.2);
478
- }}
479
-
480
- .shade-label {{
481
- font-size: 10px;
482
- font-weight: 700;
483
- }}
484
-
485
- .aa-badge {{
486
- font-size: 12px;
487
- margin-top: 2px;
488
- font-weight: 700;
489
- }}
490
-
491
- .aa-pass {{
492
- color: #16A34A;
493
- }}
494
-
495
- .aa-fail {{
496
- color: #DC2626;
497
- }}
498
-
499
- .shade-cell:hover .shade-label,
500
- .shade-cell:hover .aa-badge {{
501
- opacity: 1;
502
- }}
503
-
504
- /* Header row */
505
- .ramp-header {{
506
- display: flex;
507
- margin-bottom: 12px;
508
- padding-left: 160px;
509
- }}
510
-
511
- .ramp-header-label {{
512
- width: 48px;
513
- text-align: center;
514
- font-size: 11px;
515
- font-weight: 600;
516
- color: #666;
517
- margin-right: 4px;
518
- }}
519
- </style>
520
-
521
- <div class="color-ramps-preview">
522
- <div class="ramp-header">
523
- <span class="ramp-header-label">50</span>
524
- <span class="ramp-header-label">100</span>
525
- <span class="ramp-header-label">200</span>
526
- <span class="ramp-header-label">300</span>
527
- <span class="ramp-header-label">400</span>
528
- <span class="ramp-header-label">500</span>
529
- <span class="ramp-header-label">600</span>
530
- <span class="ramp-header-label">700</span>
531
- <span class="ramp-header-label">800</span>
532
- <span class="ramp-header-label">900</span>
533
- <span class="ramp-header-label">950</span>
534
- </div>
535
- {rows_html}
536
- </div>
537
- '''
538
-
539
- return html
540
-
541
-
542
- # =============================================================================
543
- # COMBINED PREVIEW
544
- # =============================================================================
545
-
546
- def generate_design_system_preview_html(
547
- typography_tokens: dict,
548
- color_tokens: dict,
549
- font_family: str = "Open Sans",
550
- sample_text: str = "The quick brown fox jumps over the lazy dog"
551
- ) -> tuple[str, str]:
552
- """
553
- Generate both typography and color ramp previews.
554
-
555
- Returns:
556
- Tuple of (typography_html, color_ramps_html)
557
- """
558
- typography_html = generate_typography_preview_html(
559
- typography_tokens=typography_tokens,
560
- font_family=font_family,
561
- sample_text=sample_text,
562
- )
563
-
564
- color_ramps_html = generate_color_ramps_preview_html(
565
- color_tokens=color_tokens,
566
- )
567
-
568
- return typography_html, color_ramps_html