riazmo commited on
Commit
b81141e
·
verified ·
1 Parent(s): 9f73abe

Delete core/preview_generator.py

Browse files
Files changed (1) hide show
  1. core/preview_generator.py +0 -1534
core/preview_generator.py DELETED
@@ -1,1534 +0,0 @@
1
- """
2
- Preview Generator for Typography and Color Previews
3
-
4
- Generates HTML previews for:
5
- 1. Typography - Actual font rendering with detected styles
6
- 2. Colors AS-IS - Simple swatches showing extracted colors (Stage 1)
7
- 3. Color Ramps - 11 shades (50-950) with AA compliance (Stage 2)
8
- 4. Spacing AS-IS - Visual spacing blocks
9
- 5. Radius AS-IS - Rounded corner examples
10
- 6. Shadows AS-IS - Shadow examples
11
- """
12
-
13
- from typing import Optional
14
- import colorsys
15
- import re
16
-
17
-
18
- # =============================================================================
19
- # STAGE 1: AS-IS PREVIEWS (No enhancements, just raw extracted values)
20
- # =============================================================================
21
-
22
- def generate_colors_asis_preview_html(
23
- color_tokens: dict,
24
- background: str = "#FAFAFA",
25
- max_colors: int = 50
26
- ) -> str:
27
- """
28
- Generate HTML preview for AS-IS colors (Stage 1).
29
-
30
- Shows simple color swatches without generated ramps.
31
- Sorted by frequency (most used first).
32
-
33
- Args:
34
- color_tokens: Dict of colors {name: {value: "#hex", ...}}
35
- background: Background color
36
- max_colors: Maximum colors to display (default 50)
37
-
38
- Returns:
39
- HTML string for Gradio HTML component
40
- """
41
-
42
- # Sort by frequency (highest first)
43
- sorted_tokens = []
44
- for name, token in color_tokens.items():
45
- if isinstance(token, dict):
46
- freq = token.get("frequency", 0)
47
- else:
48
- freq = 0
49
- sorted_tokens.append((name, token, freq))
50
-
51
- sorted_tokens.sort(key=lambda x: -x[2]) # Descending by frequency
52
-
53
- rows_html = ""
54
-
55
- for name, token, freq in sorted_tokens[:max_colors]:
56
- # Get hex value
57
- if isinstance(token, dict):
58
- hex_val = token.get("value", "#888888")
59
- frequency = token.get("frequency", 0)
60
- contexts = token.get("contexts", [])
61
- contrast_white = token.get("contrast_white", 0)
62
- contrast_black = token.get("contrast_black", 0)
63
- else:
64
- hex_val = str(token)
65
- frequency = 0
66
- contexts = []
67
- contrast_white = 0
68
- contrast_black = 0
69
-
70
- # Clean up hex
71
- if not hex_val.startswith("#"):
72
- hex_val = f"#{hex_val}"
73
-
74
- # Determine text color based on background luminance
75
- # Use contrast ratios to pick best text color
76
- text_color = "#1a1a1a" if contrast_white and contrast_white < 4.5 else "#ffffff"
77
- if not contrast_white:
78
- # Fallback: calculate from hex
79
- try:
80
- r = int(hex_val[1:3], 16)
81
- g = int(hex_val[3:5], 16)
82
- b = int(hex_val[5:7], 16)
83
- luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
84
- text_color = "#1a1a1a" if luminance > 0.5 else "#ffffff"
85
- except:
86
- text_color = "#1a1a1a"
87
-
88
- # Clean name
89
- display_name = name.replace("_", " ").replace("-", " ").replace(".", " ").title()
90
- if len(display_name) > 25:
91
- display_name = display_name[:22] + "..."
92
-
93
- # AA compliance check
94
- aa_status = "✓ AA" if contrast_white and contrast_white >= 4.5 else "✗ AA" if contrast_white else ""
95
- aa_class = "aa-pass" if contrast_white and contrast_white >= 4.5 else "aa-fail"
96
-
97
- # Context badges (limit to 3)
98
- context_html = ""
99
- for ctx in contexts[:3]:
100
- ctx_display = ctx[:12] + "..." if len(ctx) > 12 else ctx
101
- context_html += f'<span class="context-badge">{ctx_display}</span>'
102
-
103
- rows_html += f'''
104
- <div class="color-row-asis">
105
- <div class="color-swatch-large" style="background-color: {hex_val};">
106
- <span class="swatch-hex" style="color: {text_color};">{hex_val}</span>
107
- </div>
108
- <div class="color-info-asis">
109
- <div class="color-name-asis">{display_name}</div>
110
- <div class="color-meta-asis">
111
- <span class="frequency">Used {frequency}x</span>
112
- <span class="{aa_class}">{aa_status}</span>
113
- </div>
114
- <div class="context-row">
115
- {context_html}
116
- </div>
117
- </div>
118
- </div>
119
- '''
120
-
121
- # Show count info
122
- total_colors = len(color_tokens)
123
- showing = min(max_colors, total_colors)
124
- count_info = f"Showing {showing} of {total_colors} colors (sorted by frequency)"
125
-
126
- html = f'''
127
- <style>
128
- .colors-asis-header {{
129
- font-family: system-ui, -apple-system, sans-serif;
130
- font-size: 14px;
131
- color: #333 !important;
132
- margin-bottom: 16px;
133
- padding: 8px 12px;
134
- background: #e8e8e8 !important;
135
- border-radius: 6px;
136
- }}
137
-
138
- .colors-asis-preview {{
139
- font-family: system-ui, -apple-system, sans-serif;
140
- background: {background} !important;
141
- border-radius: 12px;
142
- padding: 20px;
143
- display: grid;
144
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
145
- gap: 16px;
146
- max-height: 800px;
147
- overflow-y: auto;
148
- }}
149
-
150
- .color-row-asis {{
151
- display: flex;
152
- align-items: center;
153
- background: #ffffff !important;
154
- border-radius: 8px;
155
- padding: 12px;
156
- border: 1px solid #d0d0d0 !important;
157
- box-shadow: 0 1px 3px rgba(0,0,0,0.08);
158
- }}
159
-
160
- .color-swatch-large {{
161
- width: 80px;
162
- height: 80px;
163
- border-radius: 8px;
164
- border: 2px solid rgba(0,0,0,0.15) !important;
165
- margin-right: 16px;
166
- flex-shrink: 0;
167
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
168
- display: flex;
169
- align-items: center;
170
- justify-content: center;
171
- }}
172
-
173
- .swatch-hex {{
174
- font-size: 11px;
175
- font-family: 'SF Mono', Monaco, monospace;
176
- font-weight: 600;
177
- text-shadow: 0 1px 2px rgba(0,0,0,0.4);
178
- }}
179
-
180
- .color-info-asis {{
181
- flex: 1;
182
- min-width: 0;
183
- }}
184
-
185
- .color-name-asis {{
186
- font-weight: 700;
187
- font-size: 14px;
188
- color: #1a1a1a !important;
189
- margin-bottom: 6px;
190
- white-space: nowrap;
191
- overflow: hidden;
192
- text-overflow: ellipsis;
193
- }}
194
-
195
- .color-meta-asis {{
196
- display: flex;
197
- gap: 12px;
198
- align-items: center;
199
- margin-bottom: 6px;
200
- }}
201
-
202
- .frequency {{
203
- font-size: 12px;
204
- color: #333 !important;
205
- font-weight: 500;
206
- }}
207
-
208
- .context-row {{
209
- display: flex;
210
- gap: 6px;
211
- flex-wrap: wrap;
212
- }}
213
-
214
- .context-badge {{
215
- font-size: 10px;
216
- background: #d0d0d0 !important;
217
- padding: 2px 8px;
218
- border-radius: 4px;
219
- color: #222 !important;
220
- }}
221
-
222
- .aa-pass {{
223
- font-size: 11px;
224
- color: #166534 !important;
225
- font-weight: 700;
226
- background: #dcfce7 !important;
227
- padding: 2px 6px;
228
- border-radius: 4px;
229
- }}
230
-
231
- .aa-fail {{
232
- font-size: 11px;
233
- color: #991b1b !important;
234
- font-weight: 700;
235
- background: #fee2e2 !important;
236
- padding: 2px 6px;
237
- border-radius: 4px;
238
- }}
239
-
240
- /* Dark mode overrides */
241
- .dark .colors-asis-header {{ color: #e2e8f0 !important; background: #1e293b !important; }}
242
- .dark .colors-asis-preview {{ background: #0f172a !important; }}
243
- .dark .color-row-asis {{ background: #1e293b !important; border-color: #475569 !important; }}
244
- .dark .color-name-asis {{ color: #f1f5f9 !important; }}
245
- .dark .frequency {{ color: #cbd5e1 !important; }}
246
- .dark .context-badge {{ background: #334155 !important; color: #e2e8f0 !important; }}
247
- .dark .aa-pass {{ color: #22c55e !important; background: #14532d !important; }}
248
- .dark .aa-fail {{ color: #f87171 !important; background: #450a0a !important; }}
249
- </style>
250
-
251
- <div class="colors-asis-header">{count_info}</div>
252
- <div class="colors-asis-preview">
253
- {rows_html}
254
- </div>
255
- '''
256
-
257
- return html
258
-
259
-
260
- def generate_spacing_asis_preview_html(
261
- spacing_tokens: dict,
262
- background: str = "#FAFAFA"
263
- ) -> str:
264
- """
265
- Generate HTML preview for AS-IS spacing (Stage 1).
266
-
267
- Shows visual blocks representing each spacing value.
268
- """
269
-
270
- rows_html = ""
271
-
272
- # Sort by pixel value
273
- sorted_tokens = []
274
- for name, token in spacing_tokens.items():
275
- if isinstance(token, dict):
276
- value_px = token.get("value_px", 0)
277
- value = token.get("value", "0px")
278
- else:
279
- value = str(token)
280
- value_px = float(re.sub(r'[^0-9.]', '', value) or 0)
281
- sorted_tokens.append((name, token, value_px, value))
282
-
283
- sorted_tokens.sort(key=lambda x: x[2])
284
-
285
- for name, token, value_px, value in sorted_tokens[:15]:
286
- # Cap visual width at 200px
287
- visual_width = min(value_px, 200)
288
-
289
- rows_html += f'''
290
- <div class="spacing-row-asis">
291
- <div class="spacing-label">{value}</div>
292
- <div class="spacing-bar" style="width: {visual_width}px;"></div>
293
- </div>
294
- '''
295
-
296
- html = f'''
297
- <style>
298
- .spacing-asis-preview {{
299
- font-family: system-ui, -apple-system, sans-serif;
300
- background: #f5f5f5 !important;
301
- border-radius: 12px;
302
- padding: 20px;
303
- }}
304
-
305
- .spacing-row-asis {{
306
- display: flex;
307
- align-items: center;
308
- margin-bottom: 12px;
309
- background: #ffffff !important;
310
- padding: 8px 12px;
311
- border-radius: 6px;
312
- }}
313
-
314
- .spacing-label {{
315
- width: 80px;
316
- font-size: 14px;
317
- font-weight: 600;
318
- color: #1a1a1a !important;
319
- font-family: 'SF Mono', Monaco, monospace;
320
- }}
321
-
322
- .spacing-bar {{
323
- height: 24px;
324
- background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%) !important;
325
- border-radius: 4px;
326
- min-width: 4px;
327
- }}
328
-
329
- /* Dark mode */
330
- .dark .spacing-asis-preview {{ background: #0f172a !important; }}
331
- .dark .spacing-row-asis {{ background: #1e293b !important; }}
332
- .dark .spacing-label {{ color: #f1f5f9 !important; }}
333
- </style>
334
-
335
- <div class="spacing-asis-preview">
336
- {rows_html}
337
- </div>
338
- '''
339
-
340
- return html
341
-
342
-
343
- def generate_radius_asis_preview_html(
344
- radius_tokens: dict,
345
- background: str = "#FAFAFA"
346
- ) -> str:
347
- """
348
- Generate HTML preview for AS-IS border radius (Stage 1).
349
-
350
- Shows boxes with each radius value applied.
351
- """
352
-
353
- rows_html = ""
354
-
355
- for name, token in list(radius_tokens.items())[:12]:
356
- if isinstance(token, dict):
357
- value = token.get("value", "0px")
358
- else:
359
- value = str(token)
360
-
361
- rows_html += f'''
362
- <div class="radius-item">
363
- <div class="radius-box" style="border-radius: {value};"></div>
364
- <div class="radius-label">{value}</div>
365
- </div>
366
- '''
367
-
368
- html = f'''
369
- <style>
370
- .radius-asis-preview {{
371
- font-family: system-ui, -apple-system, sans-serif;
372
- background: #f5f5f5 !important;
373
- border-radius: 12px;
374
- padding: 20px;
375
- display: flex;
376
- flex-wrap: wrap;
377
- gap: 20px;
378
- }}
379
-
380
- .radius-item {{
381
- display: flex;
382
- flex-direction: column;
383
- align-items: center;
384
- background: #ffffff !important;
385
- padding: 12px;
386
- border-radius: 8px;
387
- }}
388
-
389
- .radius-box {{
390
- width: 60px;
391
- height: 60px;
392
- background: #3b82f6 !important;
393
- margin-bottom: 8px;
394
- }}
395
-
396
- .radius-label {{
397
- font-size: 13px;
398
- font-weight: 600;
399
- color: #1a1a1a !important;
400
- font-family: 'SF Mono', Monaco, monospace;
401
- }}
402
-
403
- /* Dark mode */
404
- .dark .radius-asis-preview {{ background: #0f172a !important; }}
405
- .dark .radius-item {{ background: #1e293b !important; }}
406
- .dark .radius-label {{ color: #f1f5f9 !important; }}
407
- </style>
408
-
409
- <div class="radius-asis-preview">
410
- {rows_html}
411
- </div>
412
- '''
413
-
414
- return html
415
-
416
-
417
- def generate_shadows_asis_preview_html(
418
- shadow_tokens: dict,
419
- background: str = "#FAFAFA"
420
- ) -> str:
421
- """
422
- Generate HTML preview for AS-IS shadows (Stage 1).
423
-
424
- Shows cards with each shadow value applied.
425
- """
426
-
427
- rows_html = ""
428
-
429
- for name, token in list(shadow_tokens.items())[:8]:
430
- if isinstance(token, dict):
431
- value = token.get("value", "none")
432
- else:
433
- value = str(token)
434
-
435
- # Clean name for display
436
- display_name = name.replace("_", " ").replace("-", " ").title()
437
- if len(display_name) > 15:
438
- display_name = display_name[:12] + "..."
439
-
440
- rows_html += f'''
441
- <div class="shadow-item">
442
- <div class="shadow-box" style="box-shadow: {value};"></div>
443
- <div class="shadow-label">{display_name}</div>
444
- <div class="shadow-value">{value[:40]}...</div>
445
- </div>
446
- '''
447
-
448
- html = f'''
449
- <style>
450
- .shadows-asis-preview {{
451
- font-family: system-ui, -apple-system, sans-serif;
452
- background: #f5f5f5 !important;
453
- border-radius: 12px;
454
- padding: 20px;
455
- display: grid;
456
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
457
- gap: 24px;
458
- }}
459
-
460
- .shadow-item {{
461
- display: flex;
462
- flex-direction: column;
463
- align-items: center;
464
- background: #e8e8e8 !important;
465
- padding: 16px;
466
- border-radius: 8px;
467
- }}
468
-
469
- .shadow-box {{
470
- width: 100px;
471
- height: 100px;
472
- background: #ffffff !important;
473
- border-radius: 8px;
474
- margin-bottom: 12px;
475
- }}
476
-
477
- .shadow-label {{
478
- font-size: 13px;
479
- font-weight: 600;
480
- color: #1a1a1a !important;
481
- margin-bottom: 4px;
482
- }}
483
-
484
- .shadow-value {{
485
- font-size: 10px;
486
- color: #444 !important;
487
- font-family: 'SF Mono', Monaco, monospace;
488
- text-align: center;
489
- word-break: break-all;
490
- }}
491
-
492
- /* Dark mode */
493
- .dark .shadows-asis-preview {{ background: #0f172a !important; }}
494
- .dark .shadow-item {{ background: #1e293b !important; }}
495
- .dark .shadow-box {{ background: #334155 !important; }}
496
- .dark .shadow-label {{ color: #f1f5f9 !important; }}
497
- .dark .shadow-value {{ color: #94a3b8 !important; }}
498
- </style>
499
-
500
- <div class="shadows-asis-preview">
501
- {rows_html}
502
- </div>
503
- '''
504
-
505
- return html
506
-
507
-
508
- # =============================================================================
509
- # STAGE 2: TYPOGRAPHY PREVIEW (with rendered font)
510
- # =============================================================================
511
-
512
- def generate_typography_preview_html(
513
- typography_tokens: dict,
514
- font_family: str = "Open Sans",
515
- background: str = "#FAFAFA",
516
- sample_text: str = "The quick brown fox jumps over the lazy dog"
517
- ) -> str:
518
- """
519
- Generate HTML preview for typography tokens.
520
-
521
- Args:
522
- typography_tokens: Dict of typography styles {name: {font_size, font_weight, line_height, letter_spacing}}
523
- font_family: Primary font family detected
524
- background: Background color (neutral)
525
- sample_text: Text to render for preview
526
-
527
- Returns:
528
- HTML string for Gradio HTML component
529
- """
530
-
531
- # Sort tokens by font size (largest first)
532
- sorted_tokens = []
533
- for name, token in typography_tokens.items():
534
- size_str = str(token.get("font_size", "16px"))
535
- size_num = float(re.sub(r'[^0-9.]', '', size_str) or 16)
536
- sorted_tokens.append((name, token, size_num))
537
-
538
- sorted_tokens.sort(key=lambda x: -x[2]) # Descending by size
539
-
540
- # Generate rows
541
- rows_html = ""
542
- for name, token, size_num in sorted_tokens[:15]: # Limit to 15 styles
543
- font_size = token.get("font_size", "16px")
544
- font_weight = token.get("font_weight", "400")
545
- line_height = token.get("line_height", "1.5")
546
- letter_spacing = token.get("letter_spacing", "0")
547
-
548
- # Convert weight names to numbers
549
- weight_map = {
550
- "thin": 100, "extralight": 200, "light": 300, "regular": 400,
551
- "medium": 500, "semibold": 600, "bold": 700, "extrabold": 800, "black": 900
552
- }
553
- if isinstance(font_weight, str) and font_weight.lower() in weight_map:
554
- font_weight = weight_map[font_weight.lower()]
555
-
556
- # Weight label
557
- weight_labels = {
558
- 100: "Thin", 200: "ExtraLight", 300: "Light", 400: "Regular",
559
- 500: "Medium", 600: "SemiBold", 700: "Bold", 800: "ExtraBold", 900: "Black"
560
- }
561
- weight_label = weight_labels.get(int(font_weight) if str(font_weight).isdigit() else 400, "Regular")
562
-
563
- # Clean up name for display
564
- display_name = name.replace("_", " ").replace("-", " ").title()
565
- if len(display_name) > 15:
566
- display_name = display_name[:15] + "..."
567
-
568
- # Truncate sample text for large sizes
569
- display_text = sample_text
570
- if size_num > 48:
571
- display_text = sample_text[:30] + "..."
572
- elif size_num > 32:
573
- display_text = sample_text[:40] + "..."
574
-
575
- rows_html += f'''
576
- <tr class="meta-row">
577
- <td class="scale-name">
578
- <div class="scale-label">{display_name}</div>
579
- </td>
580
- <td class="meta">{font_family}</td>
581
- <td class="meta">{weight_label}</td>
582
- <td class="meta">{int(size_num)}</td>
583
- <td class="meta">Sentence</td>
584
- <td class="meta">{letter_spacing}</td>
585
- </tr>
586
- <tr>
587
- <td colspan="6" class="preview-cell">
588
- <div class="preview-text" style="
589
- font-family: '{font_family}', sans-serif;
590
- font-size: {font_size};
591
- font-weight: {font_weight};
592
- line-height: {line_height};
593
- letter-spacing: {letter_spacing}px;
594
- ">{display_text}</div>
595
- </td>
596
- </tr>
597
- '''
598
-
599
- html = f'''
600
- <style>
601
- @import url('https://fonts.googleapis.com/css2?family={font_family.replace(" ", "+")}:wght@100;200;300;400;500;600;700;800;900&display=swap');
602
-
603
- .typography-preview {{
604
- font-family: system-ui, -apple-system, sans-serif;
605
- background: {background};
606
- border-radius: 12px;
607
- padding: 20px;
608
- overflow-x: auto;
609
- }}
610
-
611
- .typography-preview table {{
612
- width: 100%;
613
- border-collapse: collapse;
614
- }}
615
-
616
- .typography-preview th {{
617
- text-align: left;
618
- padding: 12px 16px;
619
- font-size: 12px;
620
- font-weight: 600;
621
- color: #333;
622
- text-transform: uppercase;
623
- letter-spacing: 0.5px;
624
- border-bottom: 2px solid #E0E0E0;
625
- background: #F5F5F5;
626
- }}
627
-
628
- .typography-preview td {{
629
- padding: 8px 16px;
630
- vertical-align: middle;
631
- }}
632
-
633
- .typography-preview .meta-row {{
634
- background: #F8F8F8;
635
- border-top: 1px solid #E8E8E8;
636
- }}
637
-
638
- .typography-preview .scale-name {{
639
- font-weight: 700;
640
- color: #1A1A1A;
641
- min-width: 120px;
642
- }}
643
-
644
- .typography-preview .scale-label {{
645
- font-size: 13px;
646
- font-weight: 600;
647
- color: #1A1A1A;
648
- background: #E8E8E8;
649
- padding: 4px 8px;
650
- border-radius: 4px;
651
- display: inline-block;
652
- }}
653
-
654
- .typography-preview .meta {{
655
- font-size: 13px;
656
- color: #444;
657
- white-space: nowrap;
658
- }}
659
-
660
- .typography-preview .preview-cell {{
661
- padding: 16px;
662
- background: #FFFFFF;
663
- border-bottom: 1px solid #E8E8E8;
664
- }}
665
-
666
- .typography-preview .preview-text {{
667
- color: #1A1A1A;
668
- margin: 0;
669
- word-break: break-word;
670
- }}
671
-
672
- .typography-preview tr:hover .preview-cell {{
673
- background: #F5F5F5;
674
- }}
675
-
676
- /* Dark mode */
677
- .dark .typography-preview {{ background: #1e293b !important; }}
678
- .dark .typography-preview th {{ background: #334155 !important; color: #e2e8f0 !important; border-bottom-color: #475569 !important; }}
679
- .dark .typography-preview td {{ color: #e2e8f0 !important; }}
680
- .dark .typography-preview .meta-row {{ background: #1e293b !important; border-top-color: #334155 !important; }}
681
- .dark .typography-preview .scale-name,
682
- .dark .typography-preview .scale-label {{ color: #f1f5f9 !important; background: #475569 !important; }}
683
- .dark .typography-preview .meta {{ color: #cbd5e1 !important; }}
684
- .dark .typography-preview .preview-cell {{ background: #0f172a !important; border-bottom-color: #334155 !important; }}
685
- .dark .typography-preview .preview-text {{ color: #f1f5f9 !important; }}
686
- .dark .typography-preview tr:hover .preview-cell {{ background: #1e293b !important; }}
687
- </style>
688
-
689
- <div class="typography-preview">
690
- <table>
691
- <thead>
692
- <tr>
693
- <th>Scale Category</th>
694
- <th>Typeface</th>
695
- <th>Weight</th>
696
- <th>Size</th>
697
- <th>Case</th>
698
- <th>Letter Spacing</th>
699
- </tr>
700
- </thead>
701
- <tbody>
702
- {rows_html}
703
- </tbody>
704
- </table>
705
- </div>
706
- '''
707
-
708
- return html
709
-
710
-
711
- # =============================================================================
712
- # COLOR RAMP PREVIEW
713
- # =============================================================================
714
-
715
- def hex_to_rgb(hex_color: str) -> tuple:
716
- """Convert hex color to RGB tuple."""
717
- hex_color = hex_color.lstrip('#')
718
- if len(hex_color) == 3:
719
- hex_color = ''.join([c*2 for c in hex_color])
720
- return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
721
-
722
-
723
- def rgb_to_hex(rgb: tuple) -> str:
724
- """Convert RGB tuple to hex string."""
725
- return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]), int(rgb[1]), int(rgb[2]))
726
-
727
-
728
- def get_luminance(rgb: tuple) -> float:
729
- """Calculate relative luminance for contrast ratio."""
730
- def adjust(c):
731
- c = c / 255
732
- return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
733
-
734
- r, g, b = rgb
735
- return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)
736
-
737
-
738
- def get_contrast_ratio(color1: tuple, color2: tuple) -> float:
739
- """Calculate contrast ratio between two colors."""
740
- l1 = get_luminance(color1)
741
- l2 = get_luminance(color2)
742
- lighter = max(l1, l2)
743
- darker = min(l1, l2)
744
- return (lighter + 0.05) / (darker + 0.05)
745
-
746
-
747
- def generate_color_ramp(base_hex: str) -> list[dict]:
748
- """
749
- Generate 11 shades (50-950) from a base color.
750
-
751
- Uses OKLCH-like approach for perceptually uniform steps.
752
- """
753
- try:
754
- rgb = hex_to_rgb(base_hex)
755
- except:
756
- return []
757
-
758
- # Convert to HLS for easier manipulation
759
- r, g, b = [x / 255 for x in rgb]
760
- h, l, s = colorsys.rgb_to_hls(r, g, b)
761
-
762
- # Define lightness levels for each shade
763
- # 50 = very light (0.95), 500 = base, 950 = very dark (0.05)
764
- shade_lightness = {
765
- 50: 0.95,
766
- 100: 0.90,
767
- 200: 0.80,
768
- 300: 0.70,
769
- 400: 0.60,
770
- 500: l, # Keep original lightness for 500
771
- 600: 0.45,
772
- 700: 0.35,
773
- 800: 0.25,
774
- 900: 0.15,
775
- 950: 0.08,
776
- }
777
-
778
- # Adjust saturation for light/dark shades
779
- ramp = []
780
- for shade, target_l in shade_lightness.items():
781
- # Reduce saturation for very light colors
782
- if target_l > 0.8:
783
- adjusted_s = s * 0.6
784
- elif target_l < 0.2:
785
- adjusted_s = s * 0.8
786
- else:
787
- adjusted_s = s
788
-
789
- # Generate new RGB
790
- new_r, new_g, new_b = colorsys.hls_to_rgb(h, target_l, adjusted_s)
791
- new_rgb = (int(new_r * 255), int(new_g * 255), int(new_b * 255))
792
- new_hex = rgb_to_hex(new_rgb)
793
-
794
- # Check AA compliance
795
- white = (255, 255, 255)
796
- black = (0, 0, 0)
797
- contrast_white = get_contrast_ratio(new_rgb, white)
798
- contrast_black = get_contrast_ratio(new_rgb, black)
799
-
800
- # AA requires 4.5:1 for normal text
801
- aa_on_white = contrast_white >= 4.5
802
- aa_on_black = contrast_black >= 4.5
803
-
804
- ramp.append({
805
- "shade": shade,
806
- "hex": new_hex,
807
- "rgb": new_rgb,
808
- "contrast_white": round(contrast_white, 2),
809
- "contrast_black": round(contrast_black, 2),
810
- "aa_on_white": aa_on_white,
811
- "aa_on_black": aa_on_black,
812
- })
813
-
814
- return ramp
815
-
816
-
817
- def generate_color_ramps_preview_html(
818
- color_tokens: dict,
819
- background: str = "#FAFAFA",
820
- max_colors: int = 20
821
- ) -> str:
822
- """
823
- Generate HTML preview for color ramps.
824
-
825
- Sorts colors by frequency and filters out near-white/near-black
826
- to prioritize showing actual brand colors.
827
-
828
- Args:
829
- color_tokens: Dict of colors {name: {value: "#hex", ...}}
830
- background: Background color
831
- max_colors: Maximum colors to show ramps for
832
-
833
- Returns:
834
- HTML string for Gradio HTML component
835
- """
836
-
837
- def get_color_priority(name, token):
838
- """Calculate priority score for a color (higher = more important)."""
839
- if isinstance(token, dict):
840
- hex_val = token.get("value", "#888888")
841
- frequency = token.get("frequency", 0)
842
- else:
843
- hex_val = str(token)
844
- frequency = 0
845
-
846
- # Clean hex
847
- if not hex_val.startswith("#"):
848
- hex_val = f"#{hex_val}"
849
-
850
- # Calculate luminance
851
- try:
852
- r = int(hex_val[1:3], 16)
853
- g = int(hex_val[3:5], 16)
854
- b = int(hex_val[5:7], 16)
855
- luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
856
-
857
- # Calculate saturation (simplified)
858
- max_c = max(r, g, b)
859
- min_c = min(r, g, b)
860
- saturation = (max_c - min_c) / 255 if max_c > 0 else 0
861
- except:
862
- luminance = 0.5
863
- saturation = 0
864
-
865
- # Priority scoring:
866
- # - Penalize near-white (luminance > 0.9)
867
- # - Penalize near-black (luminance < 0.1)
868
- # - Penalize low saturation (grays)
869
- # - Reward high frequency
870
- # - Reward colors with "primary", "brand", "accent" in name
871
-
872
- score = frequency * 10 # Base score from frequency
873
-
874
- # Penalize extremes
875
- if luminance > 0.9:
876
- score -= 500 # Near white
877
- if luminance < 0.1:
878
- score -= 300 # Near black
879
-
880
- # Reward saturated colors (actual brand colors)
881
- score += saturation * 200
882
-
883
- # Reward named brand colors
884
- name_lower = name.lower()
885
- if any(kw in name_lower for kw in ['primary', 'brand', 'accent', 'cyan', 'blue', 'green', 'red', 'orange', 'purple']):
886
- score += 100
887
-
888
- # Penalize "background", "border", "text" colors
889
- if any(kw in name_lower for kw in ['background', 'border', 'neutral', 'gray', 'grey']):
890
- score -= 50
891
-
892
- return score
893
-
894
- # Sort colors by priority
895
- sorted_colors = []
896
- for name, token in color_tokens.items():
897
- priority = get_color_priority(name, token)
898
- sorted_colors.append((name, token, priority))
899
-
900
- sorted_colors.sort(key=lambda x: -x[2]) # Descending by priority
901
-
902
- rows_html = ""
903
- shown_count = 0
904
-
905
- for name, token, priority in sorted_colors:
906
- if shown_count >= max_colors:
907
- break
908
-
909
- # Get hex value
910
- if isinstance(token, dict):
911
- hex_val = token.get("value", "#888888")
912
- else:
913
- hex_val = str(token)
914
-
915
- # Clean up hex
916
- if not hex_val.startswith("#"):
917
- hex_val = f"#{hex_val}"
918
-
919
- # Skip invalid hex
920
- if len(hex_val) < 7:
921
- continue
922
-
923
- # Generate ramp
924
- ramp = generate_color_ramp(hex_val)
925
- if not ramp:
926
- continue
927
-
928
- # Clean name
929
- display_name = name.replace("_", " ").replace("-", " ").replace(".", " ").title()
930
- if len(display_name) > 18:
931
- display_name = display_name[:15] + "..."
932
-
933
- # Generate shade cells
934
- shades_html = ""
935
- for shade_info in ramp:
936
- shade = shade_info["shade"]
937
- hex_color = shade_info["hex"]
938
- aa_white = shade_info["aa_on_white"]
939
- aa_black = shade_info["aa_on_black"]
940
-
941
- # Determine text color for label
942
- text_color = "#000" if shade < 500 else "#FFF"
943
-
944
- # AA indicator
945
- if aa_white or aa_black:
946
- aa_indicator = "✓"
947
- aa_class = "aa-pass"
948
- else:
949
- aa_indicator = ""
950
- aa_class = ""
951
-
952
- shades_html += f'''
953
- <div class="shade-cell" style="background-color: {hex_color};" title="{hex_color} | AA: {'Pass' if aa_white or aa_black else 'Fail'}">
954
- <span class="shade-label" style="color: {text_color};">{shade}</span>
955
- <span class="aa-badge {aa_class}">{aa_indicator}</span>
956
- </div>
957
- '''
958
-
959
- rows_html += f'''
960
- <div class="color-row">
961
- <div class="color-info">
962
- <div class="color-swatch" style="background-color: {hex_val};"></div>
963
- <div class="color-meta">
964
- <div class="color-name">{display_name}</div>
965
- <div class="color-hex">{hex_val}</div>
966
- </div>
967
- </div>
968
- <div class="color-ramp">
969
- {shades_html}
970
- </div>
971
- </div>
972
- '''
973
- shown_count += 1
974
-
975
- # Count info
976
- total_colors = len(color_tokens)
977
- count_info = f"Showing {shown_count} of {total_colors} colors (sorted by brand priority)"
978
-
979
- html = f'''
980
- <style>
981
- .color-ramps-preview {{
982
- font-family: system-ui, -apple-system, sans-serif;
983
- background: #f5f5f5 !important;
984
- border-radius: 12px;
985
- padding: 20px;
986
- overflow-x: auto;
987
- }}
988
-
989
- .color-row {{
990
- display: flex;
991
- align-items: center;
992
- margin-bottom: 16px;
993
- padding: 12px;
994
- background: #ffffff !important;
995
- border-radius: 8px;
996
- border: 1px solid #d0d0d0 !important;
997
- }}
998
-
999
- .color-row:last-child {{
1000
- margin-bottom: 0;
1001
- }}
1002
-
1003
- .color-info {{
1004
- display: flex;
1005
- align-items: center;
1006
- min-width: 160px;
1007
- margin-right: 20px;
1008
- }}
1009
-
1010
- .color-swatch {{
1011
- width: 44px;
1012
- height: 44px;
1013
- border-radius: 8px;
1014
- border: 2px solid rgba(0,0,0,0.15) !important;
1015
- margin-right: 12px;
1016
- flex-shrink: 0;
1017
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1018
- }}
1019
-
1020
- .color-meta {{
1021
- flex: 1;
1022
- min-width: 100px;
1023
- }}
1024
-
1025
- .color-name {{
1026
- font-weight: 700;
1027
- font-size: 13px;
1028
- color: #1a1a1a !important;
1029
- margin-bottom: 4px;
1030
- background: #e0e0e0 !important;
1031
- padding: 4px 10px;
1032
- border-radius: 4px;
1033
- display: inline-block;
1034
- }}
1035
-
1036
- .color-hex {{
1037
- font-size: 12px;
1038
- color: #333 !important;
1039
- font-family: 'SF Mono', Monaco, monospace;
1040
- margin-top: 4px;
1041
- font-weight: 500;
1042
- }}
1043
-
1044
- .color-ramp {{
1045
- display: flex;
1046
- gap: 4px;
1047
- flex: 1;
1048
- }}
1049
-
1050
- .shade-cell {{
1051
- width: 48px;
1052
- height: 48px;
1053
- border-radius: 6px;
1054
- display: flex;
1055
- flex-direction: column;
1056
- align-items: center;
1057
- justify-content: center;
1058
- position: relative;
1059
- cursor: pointer;
1060
- transition: transform 0.15s;
1061
- border: 1px solid rgba(0,0,0,0.1) !important;
1062
- }}
1063
-
1064
- .shade-cell:hover {{
1065
- transform: scale(1.1);
1066
- z-index: 10;
1067
- box-shadow: 0 4px 12px rgba(0,0,0,0.2);
1068
- }}
1069
-
1070
- .shade-label {{
1071
- font-size: 10px;
1072
- font-weight: 700;
1073
- }}
1074
-
1075
- .aa-badge {{
1076
- font-size: 12px;
1077
- margin-top: 2px;
1078
- font-weight: 700;
1079
- }}
1080
-
1081
- .aa-pass {{
1082
- color: #166534 !important;
1083
- }}
1084
-
1085
- .aa-fail {{
1086
- color: #991b1b !important;
1087
- }}
1088
-
1089
- .shade-cell:hover .shade-label,
1090
- .shade-cell:hover .aa-badge {{
1091
- opacity: 1;
1092
- }}
1093
-
1094
- /* Header row */
1095
- .ramp-header {{
1096
- display: flex;
1097
- margin-bottom: 12px;
1098
- padding-left: 180px;
1099
- background: #e8e8e8 !important;
1100
- padding-top: 8px;
1101
- padding-bottom: 8px;
1102
- border-radius: 6px;
1103
- }}
1104
-
1105
- .ramp-header-label {{
1106
- width: 48px;
1107
- text-align: center;
1108
- font-size: 12px;
1109
- font-weight: 700;
1110
- color: #333 !important;
1111
- margin-right: 4px;
1112
- }}
1113
-
1114
- .ramps-header-info {{
1115
- font-size: 14px;
1116
- color: #333 !important;
1117
- margin-bottom: 16px;
1118
- padding: 10px 14px;
1119
- background: #e0e0e0 !important;
1120
- border-radius: 6px;
1121
- font-weight: 500;
1122
- }}
1123
-
1124
- /* Dark mode */
1125
- .dark .color-ramps-preview {{ background: #0f172a !important; }}
1126
- .dark .ramps-header-info {{ color: #e2e8f0 !important; background: #1e293b !important; }}
1127
- .dark .ramp-header {{ background: #1e293b !important; }}
1128
- .dark .ramp-header-label {{ color: #cbd5e1 !important; }}
1129
- .dark .color-row {{ background: #1e293b !important; border-color: #475569 !important; }}
1130
- .dark .color-name {{ color: #f1f5f9 !important; background: #475569 !important; }}
1131
- .dark .color-hex {{ color: #cbd5e1 !important; }}
1132
- </style>
1133
-
1134
- <div class="color-ramps-preview">
1135
- <div class="ramps-header-info">{count_info}</div>
1136
- <div class="ramp-header">
1137
- <span class="ramp-header-label">50</span>
1138
- <span class="ramp-header-label">100</span>
1139
- <span class="ramp-header-label">200</span>
1140
- <span class="ramp-header-label">300</span>
1141
- <span class="ramp-header-label">400</span>
1142
- <span class="ramp-header-label">500</span>
1143
- <span class="ramp-header-label">600</span>
1144
- <span class="ramp-header-label">700</span>
1145
- <span class="ramp-header-label">800</span>
1146
- <span class="ramp-header-label">900</span>
1147
- <span class="ramp-header-label">950</span>
1148
- </div>
1149
- {rows_html}
1150
- </div>
1151
- '''
1152
-
1153
- return html
1154
-
1155
-
1156
- # =============================================================================
1157
- # SEMANTIC COLOR RAMPS WITH LLM RECOMMENDATIONS (Stage 2)
1158
- # =============================================================================
1159
-
1160
- def generate_semantic_color_ramps_html(
1161
- semantic_analysis: dict,
1162
- color_tokens: dict,
1163
- llm_recommendations: dict = None,
1164
- background: str = "#F5F5F5"
1165
- ) -> str:
1166
- """
1167
- Generate HTML preview for colors organized by semantic role with LLM recommendations.
1168
-
1169
- Args:
1170
- semantic_analysis: Output from SemanticColorAnalyzer
1171
- color_tokens: Dict of all color tokens
1172
- llm_recommendations: LLM suggestions for color improvements
1173
- background: Background color
1174
-
1175
- Returns:
1176
- HTML string for Gradio HTML component
1177
- """
1178
-
1179
- def generate_single_ramp(hex_val: str) -> str:
1180
- """Generate a single color ramp HTML."""
1181
- ramp = generate_color_ramp(hex_val)
1182
- if not ramp:
1183
- return ""
1184
-
1185
- shades_html = ""
1186
- for shade_info in ramp:
1187
- shade = shade_info["shade"]
1188
- hex_color = shade_info["hex"]
1189
- aa_white = shade_info["aa_on_white"]
1190
- aa_black = shade_info["aa_on_black"]
1191
-
1192
- text_color = "#000" if shade < 500 else "#FFF"
1193
- aa_indicator = "✓" if aa_white or aa_black else ""
1194
-
1195
- shades_html += f'''
1196
- <div class="sem-shade" style="background-color: {hex_color};">
1197
- <span class="sem-shade-num" style="color: {text_color};">{shade}</span>
1198
- <span class="sem-shade-aa" style="color: {text_color};">{aa_indicator}</span>
1199
- </div>
1200
- '''
1201
- return shades_html
1202
-
1203
- def color_row_with_recommendation(hex_val: str, role: str, role_display: str, recommendation: dict = None) -> str:
1204
- """Generate a color row with optional LLM recommendation."""
1205
- ramp_html = generate_single_ramp(hex_val)
1206
-
1207
- # Calculate contrast
1208
- try:
1209
- from core.color_utils import get_contrast_with_white
1210
- contrast = get_contrast_with_white(hex_val)
1211
- aa_status = "✓ AA" if contrast >= 4.5 else f"⚠️ {contrast:.1f}:1"
1212
- aa_class = "aa-ok" if contrast >= 4.5 else "aa-warn"
1213
- except:
1214
- aa_status = ""
1215
- aa_class = ""
1216
-
1217
- # LLM recommendation display
1218
- rec_html = ""
1219
- if recommendation:
1220
- suggested = recommendation.get("suggested", "")
1221
- issue = recommendation.get("issue", "")
1222
- if suggested and suggested != hex_val:
1223
- rec_html = f'''
1224
- <div class="llm-rec">
1225
- <span class="rec-label">💡 LLM:</span>
1226
- <span class="rec-issue">{issue}</span>
1227
- <span class="rec-arrow">→</span>
1228
- <span class="rec-suggested" style="background-color: {suggested};">{suggested}</span>
1229
- </div>
1230
- '''
1231
-
1232
- return f'''
1233
- <div class="sem-color-row">
1234
- <div class="sem-color-info">
1235
- <div class="sem-swatch" style="background-color: {hex_val};"></div>
1236
- <div class="sem-details">
1237
- <div class="sem-role">{role_display}</div>
1238
- <div class="sem-hex">{hex_val} <span class="{aa_class}">{aa_status}</span></div>
1239
- </div>
1240
- </div>
1241
- <div class="sem-ramp">{ramp_html}</div>
1242
- {rec_html}
1243
- </div>
1244
- '''
1245
-
1246
- def category_section(title: str, icon: str, colors: dict, category_key: str) -> str:
1247
- """Generate a category section with color rows."""
1248
- if not colors:
1249
- return ""
1250
-
1251
- rows_html = ""
1252
- for role, data in colors.items():
1253
- if data and isinstance(data, dict) and "hex" in data:
1254
- # Get LLM recommendation for this role
1255
- rec = None
1256
- if llm_recommendations:
1257
- color_recs = llm_recommendations.get("color_recommendations", {})
1258
- rec = color_recs.get(f"{category_key}.{role}", {})
1259
-
1260
- role_display = role.replace("_", " ").title()
1261
- rows_html += color_row_with_recommendation(
1262
- data["hex"],
1263
- f"{category_key}.{role}",
1264
- role_display,
1265
- rec
1266
- )
1267
-
1268
- if not rows_html:
1269
- return ""
1270
-
1271
- return f'''
1272
- <div class="sem-category">
1273
- <h3 class="sem-cat-title">{icon} {title}</h3>
1274
- {rows_html}
1275
- </div>
1276
- '''
1277
-
1278
- # Handle empty analysis
1279
- if not semantic_analysis:
1280
- return '''
1281
- <div class="sem-warning-box" style="padding: 40px; text-align: center; background: #fff3cd; border-radius: 8px;">
1282
- <p style="color: #856404; font-size: 14px;">⚠️ No semantic analysis available.</p>
1283
- </div>
1284
- <style>
1285
- .dark .sem-warning-box { background: #422006 !important; border-color: #b45309 !important; }
1286
- .dark .sem-warning-box p { color: #fde68a !important; }
1287
- </style>
1288
- '''
1289
-
1290
- # Build sections
1291
- sections_html = ""
1292
- sections_html += category_section("Brand Colors", "🎨", semantic_analysis.get("brand", {}), "brand")
1293
- sections_html += category_section("Text Colors", "📝", semantic_analysis.get("text", {}), "text")
1294
- sections_html += category_section("Background Colors", "🖼️", semantic_analysis.get("background", {}), "background")
1295
- sections_html += category_section("Border Colors", "📏", semantic_analysis.get("border", {}), "border")
1296
- sections_html += category_section("Feedback Colors", "🚨", semantic_analysis.get("feedback", {}), "feedback")
1297
-
1298
- # LLM Impact Summary
1299
- llm_summary = ""
1300
- if llm_recommendations:
1301
- changes = llm_recommendations.get("changes_made", [])
1302
- if changes:
1303
- changes_html = "".join([f"<li>{c}</li>" for c in changes[:5]])
1304
- llm_summary = f'''
1305
- <div class="llm-summary">
1306
- <h4>🤖 LLM Recommendations Applied:</h4>
1307
- <ul>{changes_html}</ul>
1308
- </div>
1309
- '''
1310
-
1311
- html = f'''
1312
- <style>
1313
- .sem-ramps-preview {{
1314
- font-family: system-ui, -apple-system, sans-serif;
1315
- background: #f5f5f5 !important;
1316
- border-radius: 12px;
1317
- padding: 20px;
1318
- }}
1319
-
1320
- .sem-category {{
1321
- background: #ffffff !important;
1322
- border-radius: 8px;
1323
- padding: 16px;
1324
- margin-bottom: 20px;
1325
- border: 1px solid #d0d0d0 !important;
1326
- }}
1327
-
1328
- .sem-cat-title {{
1329
- font-size: 16px;
1330
- font-weight: 700;
1331
- color: #1a1a1a !important;
1332
- margin: 0 0 16px 0;
1333
- padding-bottom: 8px;
1334
- border-bottom: 2px solid #e0e0e0 !important;
1335
- }}
1336
-
1337
- .sem-color-row {{
1338
- display: flex;
1339
- flex-wrap: wrap;
1340
- align-items: center;
1341
- padding: 12px;
1342
- background: #f8f8f8 !important;
1343
- border-radius: 6px;
1344
- margin-bottom: 12px;
1345
- border: 1px solid #e0e0e0 !important;
1346
- }}
1347
-
1348
- .sem-color-row:last-child {{
1349
- margin-bottom: 0;
1350
- }}
1351
-
1352
- .sem-color-info {{
1353
- display: flex;
1354
- align-items: center;
1355
- min-width: 180px;
1356
- margin-right: 16px;
1357
- }}
1358
-
1359
- .sem-swatch {{
1360
- width: 48px;
1361
- height: 48px;
1362
- border-radius: 8px;
1363
- border: 2px solid rgba(0,0,0,0.15) !important;
1364
- margin-right: 12px;
1365
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1366
- }}
1367
-
1368
- .sem-details {{
1369
- flex: 1;
1370
- }}
1371
-
1372
- .sem-role {{
1373
- font-weight: 700;
1374
- font-size: 14px;
1375
- color: #1a1a1a !important;
1376
- margin-bottom: 4px;
1377
- }}
1378
-
1379
- .sem-hex {{
1380
- font-size: 12px;
1381
- font-family: 'SF Mono', Monaco, monospace;
1382
- color: #333 !important;
1383
- }}
1384
-
1385
- .aa-ok {{
1386
- color: #166534 !important;
1387
- font-weight: 600;
1388
- }}
1389
-
1390
- .aa-warn {{
1391
- color: #b45309 !important;
1392
- font-weight: 600;
1393
- }}
1394
-
1395
- .sem-ramp {{
1396
- display: flex;
1397
- gap: 3px;
1398
- flex: 1;
1399
- min-width: 400px;
1400
- }}
1401
-
1402
- .sem-shade {{
1403
- width: 36px;
1404
- height: 36px;
1405
- border-radius: 4px;
1406
- display: flex;
1407
- flex-direction: column;
1408
- align-items: center;
1409
- justify-content: center;
1410
- border: 1px solid rgba(0,0,0,0.1) !important;
1411
- }}
1412
-
1413
- .sem-shade-num {{
1414
- font-size: 9px;
1415
- font-weight: 700;
1416
- }}
1417
-
1418
- .sem-shade-aa {{
1419
- font-size: 10px;
1420
- }}
1421
-
1422
- .llm-rec {{
1423
- width: 100%;
1424
- margin-top: 10px;
1425
- padding: 8px 12px;
1426
- background: #fef3c7 !important;
1427
- border-radius: 4px;
1428
- display: flex;
1429
- align-items: center;
1430
- gap: 8px;
1431
- border: 1px solid #f59e0b !important;
1432
- }}
1433
-
1434
- .rec-label {{
1435
- font-weight: 600;
1436
- color: #92400e !important;
1437
- }}
1438
-
1439
- .rec-issue {{
1440
- color: #78350f !important;
1441
- font-size: 13px;
1442
- }}
1443
-
1444
- .rec-arrow {{
1445
- color: #92400e !important;
1446
- }}
1447
-
1448
- .rec-suggested {{
1449
- padding: 4px 10px;
1450
- border-radius: 4px;
1451
- font-family: 'SF Mono', Monaco, monospace;
1452
- font-size: 12px;
1453
- font-weight: 600;
1454
- color: #fff !important;
1455
- text-shadow: 0 1px 2px rgba(0,0,0,0.3);
1456
- }}
1457
-
1458
- .llm-summary {{
1459
- background: #dbeafe !important;
1460
- border: 1px solid #3b82f6 !important;
1461
- border-radius: 8px;
1462
- padding: 16px;
1463
- margin-top: 20px;
1464
- }}
1465
-
1466
- .llm-summary h4 {{
1467
- color: #1e40af !important;
1468
- margin: 0 0 12px 0;
1469
- font-size: 14px;
1470
- }}
1471
-
1472
- .llm-summary ul {{
1473
- margin: 0;
1474
- padding-left: 20px;
1475
- color: #1e3a8a !important;
1476
- }}
1477
-
1478
- .llm-summary li {{
1479
- margin-bottom: 4px;
1480
- font-size: 13px;
1481
- }}
1482
-
1483
- /* Dark mode */
1484
- .dark .sem-ramps-preview {{ background: #0f172a !important; }}
1485
- .dark .sem-category {{ background: #1e293b !important; border-color: #475569 !important; }}
1486
- .dark .sem-cat-title {{ color: #f1f5f9 !important; border-bottom-color: #475569 !important; }}
1487
- .dark .sem-color-row {{ background: #0f172a !important; border-color: #334155 !important; }}
1488
- .dark .sem-role {{ color: #f1f5f9 !important; }}
1489
- .dark .sem-hex {{ color: #cbd5e1 !important; }}
1490
- .dark .llm-rec {{ background: #422006 !important; border-color: #b45309 !important; }}
1491
- .dark .rec-label {{ color: #fbbf24 !important; }}
1492
- .dark .rec-issue {{ color: #fde68a !important; }}
1493
- .dark .rec-arrow {{ color: #fbbf24 !important; }}
1494
- .dark .llm-summary {{ background: #1e3a5f !important; border-color: #3b82f6 !important; }}
1495
- .dark .llm-summary h4 {{ color: #93c5fd !important; }}
1496
- .dark .llm-summary ul, .dark .llm-summary li {{ color: #bfdbfe !important; }}
1497
- </style>
1498
-
1499
- <div class="sem-ramps-preview">
1500
- {sections_html}
1501
- {llm_summary}
1502
- </div>
1503
- '''
1504
-
1505
- return html
1506
-
1507
-
1508
- # =============================================================================
1509
- # COMBINED PREVIEW
1510
- # =============================================================================
1511
-
1512
- def generate_design_system_preview_html(
1513
- typography_tokens: dict,
1514
- color_tokens: dict,
1515
- font_family: str = "Open Sans",
1516
- sample_text: str = "The quick brown fox jumps over the lazy dog"
1517
- ) -> tuple[str, str]:
1518
- """
1519
- Generate both typography and color ramp previews.
1520
-
1521
- Returns:
1522
- Tuple of (typography_html, color_ramps_html)
1523
- """
1524
- typography_html = generate_typography_preview_html(
1525
- typography_tokens=typography_tokens,
1526
- font_family=font_family,
1527
- sample_text=sample_text,
1528
- )
1529
-
1530
- color_ramps_html = generate_color_ramps_preview_html(
1531
- color_tokens=color_tokens,
1532
- )
1533
-
1534
- return typography_html, color_ramps_html