riazmo commited on
Commit
e2a2bc6
·
verified ·
1 Parent(s): b1b0b33

Upload preview_generator.py

Browse files
Files changed (1) hide show
  1. core/preview_generator.py +237 -62
core/preview_generator.py CHANGED
@@ -21,151 +21,224 @@ import re
21
 
22
  def generate_colors_asis_preview_html(
23
  color_tokens: dict,
24
- background: str = "#FAFAFA"
 
25
  ) -> str:
26
  """
27
  Generate HTML preview for AS-IS colors (Stage 1).
28
 
29
  Shows simple color swatches without generated ramps.
 
30
 
31
  Args:
32
  color_tokens: Dict of colors {name: {value: "#hex", ...}}
33
  background: Background color
 
34
 
35
  Returns:
36
  HTML string for Gradio HTML component
37
  """
38
 
 
 
 
 
 
 
 
 
 
 
 
39
  rows_html = ""
40
 
41
- for name, token in list(color_tokens.items())[:20]: # Limit to 20 colors
42
  # Get hex value
43
  if isinstance(token, dict):
44
  hex_val = token.get("value", "#888888")
45
  frequency = token.get("frequency", 0)
46
  contexts = token.get("contexts", [])
47
  contrast_white = token.get("contrast_white", 0)
 
48
  else:
49
  hex_val = str(token)
50
  frequency = 0
51
  contexts = []
52
  contrast_white = 0
 
53
 
54
  # Clean up hex
55
  if not hex_val.startswith("#"):
56
  hex_val = f"#{hex_val}"
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  # Clean name
59
- display_name = name.replace("_", " ").replace("-", " ").title()
60
- if len(display_name) > 20:
61
- display_name = display_name[:17] + "..."
62
 
63
  # AA compliance check
64
  aa_status = "✓ AA" if contrast_white and contrast_white >= 4.5 else "✗ AA" if contrast_white else ""
65
  aa_class = "aa-pass" if contrast_white and contrast_white >= 4.5 else "aa-fail"
66
 
67
- # Context badges
68
  context_html = ""
69
- for ctx in contexts[:2]:
70
- context_html += f'<span class="context-badge">{ctx}</span>'
 
71
 
72
  rows_html += f'''
73
  <div class="color-row-asis">
74
- <div class="color-swatch-large" style="background-color: {hex_val};"></div>
 
 
75
  <div class="color-info-asis">
76
  <div class="color-name-asis">{display_name}</div>
77
- <div class="color-hex-asis">{hex_val}</div>
78
  <div class="color-meta-asis">
79
  <span class="frequency">Used {frequency}x</span>
80
- {context_html}
81
  <span class="{aa_class}">{aa_status}</span>
82
  </div>
 
 
 
83
  </div>
84
  </div>
85
  '''
86
 
 
 
 
 
 
87
  html = f'''
88
  <style>
 
 
 
 
 
 
 
 
 
 
89
  .colors-asis-preview {{
90
  font-family: system-ui, -apple-system, sans-serif;
91
- background: {background};
92
  border-radius: 12px;
93
  padding: 20px;
94
  display: grid;
95
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
96
  gap: 16px;
 
 
97
  }}
98
 
99
  .color-row-asis {{
100
  display: flex;
101
  align-items: center;
102
- background: #fff;
103
  border-radius: 8px;
104
  padding: 12px;
105
- border: 1px solid #e0e0e0;
 
106
  }}
107
 
108
  .color-swatch-large {{
109
- width: 60px;
110
- height: 60px;
111
  border-radius: 8px;
112
- border: 2px solid rgba(0,0,0,0.1);
113
  margin-right: 16px;
114
  flex-shrink: 0;
115
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 
 
 
 
 
 
 
 
 
 
116
  }}
117
 
118
  .color-info-asis {{
119
  flex: 1;
 
120
  }}
121
 
122
  .color-name-asis {{
123
  font-weight: 700;
124
  font-size: 14px;
125
- color: #1a1a1a;
126
- margin-bottom: 4px;
127
- }}
128
-
129
- .color-hex-asis {{
130
- font-size: 13px;
131
- color: #444;
132
- font-family: 'SF Mono', Monaco, monospace;
133
  margin-bottom: 6px;
 
 
 
134
  }}
135
 
136
  .color-meta-asis {{
137
  display: flex;
138
- gap: 8px;
139
- flex-wrap: wrap;
140
  align-items: center;
 
141
  }}
142
 
143
  .frequency {{
144
- font-size: 11px;
145
- color: #666;
 
 
 
 
 
 
 
146
  }}
147
 
148
  .context-badge {{
149
  font-size: 10px;
150
- background: #e8e8e8;
151
- padding: 2px 6px;
152
  border-radius: 4px;
153
- color: #555;
154
  }}
155
 
156
  .aa-pass {{
157
  font-size: 11px;
158
- color: #16a34a;
159
- font-weight: 600;
 
 
 
160
  }}
161
 
162
  .aa-fail {{
163
  font-size: 11px;
164
- color: #dc2626;
165
- font-weight: 600;
 
 
 
166
  }}
167
  </style>
168
 
 
169
  <div class="colors-asis-preview">
170
  {rows_html}
171
  </div>
@@ -214,7 +287,7 @@ def generate_spacing_asis_preview_html(
214
  <style>
215
  .spacing-asis-preview {{
216
  font-family: system-ui, -apple-system, sans-serif;
217
- background: {background};
218
  border-radius: 12px;
219
  padding: 20px;
220
  }}
@@ -223,19 +296,22 @@ def generate_spacing_asis_preview_html(
223
  display: flex;
224
  align-items: center;
225
  margin-bottom: 12px;
 
 
 
226
  }}
227
 
228
  .spacing-label {{
229
- width: 60px;
230
- font-size: 13px;
231
  font-weight: 600;
232
- color: #333;
233
- font-family: monospace;
234
  }}
235
 
236
  .spacing-bar {{
237
  height: 24px;
238
- background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%);
239
  border-radius: 4px;
240
  min-width: 4px;
241
  }}
@@ -278,32 +354,35 @@ def generate_radius_asis_preview_html(
278
  <style>
279
  .radius-asis-preview {{
280
  font-family: system-ui, -apple-system, sans-serif;
281
- background: {background};
282
  border-radius: 12px;
283
  padding: 20px;
284
  display: flex;
285
  flex-wrap: wrap;
286
- gap: 16px;
287
  }}
288
 
289
  .radius-item {{
290
  display: flex;
291
  flex-direction: column;
292
  align-items: center;
 
 
 
293
  }}
294
 
295
  .radius-box {{
296
  width: 60px;
297
  height: 60px;
298
- background: #3b82f6;
299
  margin-bottom: 8px;
300
  }}
301
 
302
  .radius-label {{
303
- font-size: 12px;
304
  font-weight: 600;
305
- color: #333;
306
- font-family: monospace;
307
  }}
308
  </style>
309
 
@@ -350,7 +429,7 @@ def generate_shadows_asis_preview_html(
350
  <style>
351
  .shadows-asis-preview {{
352
  font-family: system-ui, -apple-system, sans-serif;
353
- background: {background};
354
  border-radius: 12px;
355
  padding: 20px;
356
  display: grid;
@@ -362,27 +441,30 @@ def generate_shadows_asis_preview_html(
362
  display: flex;
363
  flex-direction: column;
364
  align-items: center;
 
 
 
365
  }}
366
 
367
  .shadow-box {{
368
  width: 100px;
369
  height: 100px;
370
- background: #fff;
371
  border-radius: 8px;
372
  margin-bottom: 12px;
373
  }}
374
 
375
  .shadow-label {{
376
- font-size: 12px;
377
  font-weight: 600;
378
- color: #333;
379
  margin-bottom: 4px;
380
  }}
381
 
382
  .shadow-value {{
383
  font-size: 10px;
384
- color: #666;
385
- font-family: monospace;
386
  text-align: center;
387
  word-break: break-all;
388
  }}
@@ -695,22 +777,96 @@ def generate_color_ramp(base_hex: str) -> list[dict]:
695
 
696
  def generate_color_ramps_preview_html(
697
  color_tokens: dict,
698
- background: str = "#FAFAFA"
 
699
  ) -> str:
700
  """
701
  Generate HTML preview for color ramps.
702
 
 
 
 
703
  Args:
704
  color_tokens: Dict of colors {name: {value: "#hex", ...}}
705
  background: Background color
 
706
 
707
  Returns:
708
  HTML string for Gradio HTML component
709
  """
710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  rows_html = ""
 
712
 
713
- for name, token in list(color_tokens.items())[:10]: # Limit to 10 colors
 
 
 
714
  # Get hex value
715
  if isinstance(token, dict):
716
  hex_val = token.get("value", "#888888")
@@ -721,15 +877,19 @@ def generate_color_ramps_preview_html(
721
  if not hex_val.startswith("#"):
722
  hex_val = f"#{hex_val}"
723
 
 
 
 
 
724
  # Generate ramp
725
  ramp = generate_color_ramp(hex_val)
726
  if not ramp:
727
  continue
728
 
729
  # Clean name
730
- display_name = name.replace("_", " ").replace("-", " ").title()
731
- if len(display_name) > 15:
732
- display_name = display_name[:12] + "..."
733
 
734
  # Generate shade cells
735
  shades_html = ""
@@ -747,8 +907,8 @@ def generate_color_ramps_preview_html(
747
  aa_indicator = "✓"
748
  aa_class = "aa-pass"
749
  else:
750
- aa_indicator = ""
751
- aa_class = "aa-fail"
752
 
753
  shades_html += f'''
754
  <div class="shade-cell" style="background-color: {hex_color};" title="{hex_color} | AA: {'Pass' if aa_white or aa_black else 'Fail'}">
@@ -771,6 +931,11 @@ def generate_color_ramps_preview_html(
771
  </div>
772
  </div>
773
  '''
 
 
 
 
 
774
 
775
  html = f'''
776
  <style>
@@ -901,9 +1066,19 @@ def generate_color_ramps_preview_html(
901
  color: #666;
902
  margin-right: 4px;
903
  }}
 
 
 
 
 
 
 
 
 
904
  </style>
905
 
906
  <div class="color-ramps-preview">
 
907
  <div class="ramp-header">
908
  <span class="ramp-header-label">50</span>
909
  <span class="ramp-header-label">100</span>
 
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
  </style>
240
 
241
+ <div class="colors-asis-header">{count_info}</div>
242
  <div class="colors-asis-preview">
243
  {rows_html}
244
  </div>
 
287
  <style>
288
  .spacing-asis-preview {{
289
  font-family: system-ui, -apple-system, sans-serif;
290
+ background: #f5f5f5 !important;
291
  border-radius: 12px;
292
  padding: 20px;
293
  }}
 
296
  display: flex;
297
  align-items: center;
298
  margin-bottom: 12px;
299
+ background: #ffffff !important;
300
+ padding: 8px 12px;
301
+ border-radius: 6px;
302
  }}
303
 
304
  .spacing-label {{
305
+ width: 80px;
306
+ font-size: 14px;
307
  font-weight: 600;
308
+ color: #1a1a1a !important;
309
+ font-family: 'SF Mono', Monaco, monospace;
310
  }}
311
 
312
  .spacing-bar {{
313
  height: 24px;
314
+ background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%) !important;
315
  border-radius: 4px;
316
  min-width: 4px;
317
  }}
 
354
  <style>
355
  .radius-asis-preview {{
356
  font-family: system-ui, -apple-system, sans-serif;
357
+ background: #f5f5f5 !important;
358
  border-radius: 12px;
359
  padding: 20px;
360
  display: flex;
361
  flex-wrap: wrap;
362
+ gap: 20px;
363
  }}
364
 
365
  .radius-item {{
366
  display: flex;
367
  flex-direction: column;
368
  align-items: center;
369
+ background: #ffffff !important;
370
+ padding: 12px;
371
+ border-radius: 8px;
372
  }}
373
 
374
  .radius-box {{
375
  width: 60px;
376
  height: 60px;
377
+ background: #3b82f6 !important;
378
  margin-bottom: 8px;
379
  }}
380
 
381
  .radius-label {{
382
+ font-size: 13px;
383
  font-weight: 600;
384
+ color: #1a1a1a !important;
385
+ font-family: 'SF Mono', Monaco, monospace;
386
  }}
387
  </style>
388
 
 
429
  <style>
430
  .shadows-asis-preview {{
431
  font-family: system-ui, -apple-system, sans-serif;
432
+ background: #f5f5f5 !important;
433
  border-radius: 12px;
434
  padding: 20px;
435
  display: grid;
 
441
  display: flex;
442
  flex-direction: column;
443
  align-items: center;
444
+ background: #e8e8e8 !important;
445
+ padding: 16px;
446
+ border-radius: 8px;
447
  }}
448
 
449
  .shadow-box {{
450
  width: 100px;
451
  height: 100px;
452
+ background: #ffffff !important;
453
  border-radius: 8px;
454
  margin-bottom: 12px;
455
  }}
456
 
457
  .shadow-label {{
458
+ font-size: 13px;
459
  font-weight: 600;
460
+ color: #1a1a1a !important;
461
  margin-bottom: 4px;
462
  }}
463
 
464
  .shadow-value {{
465
  font-size: 10px;
466
+ color: #444 !important;
467
+ font-family: 'SF Mono', Monaco, monospace;
468
  text-align: center;
469
  word-break: break-all;
470
  }}
 
777
 
778
  def generate_color_ramps_preview_html(
779
  color_tokens: dict,
780
+ background: str = "#FAFAFA",
781
+ max_colors: int = 20
782
  ) -> str:
783
  """
784
  Generate HTML preview for color ramps.
785
 
786
+ Sorts colors by frequency and filters out near-white/near-black
787
+ to prioritize showing actual brand colors.
788
+
789
  Args:
790
  color_tokens: Dict of colors {name: {value: "#hex", ...}}
791
  background: Background color
792
+ max_colors: Maximum colors to show ramps for
793
 
794
  Returns:
795
  HTML string for Gradio HTML component
796
  """
797
 
798
+ def get_color_priority(name, token):
799
+ """Calculate priority score for a color (higher = more important)."""
800
+ if isinstance(token, dict):
801
+ hex_val = token.get("value", "#888888")
802
+ frequency = token.get("frequency", 0)
803
+ else:
804
+ hex_val = str(token)
805
+ frequency = 0
806
+
807
+ # Clean hex
808
+ if not hex_val.startswith("#"):
809
+ hex_val = f"#{hex_val}"
810
+
811
+ # Calculate luminance
812
+ try:
813
+ r = int(hex_val[1:3], 16)
814
+ g = int(hex_val[3:5], 16)
815
+ b = int(hex_val[5:7], 16)
816
+ luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
817
+
818
+ # Calculate saturation (simplified)
819
+ max_c = max(r, g, b)
820
+ min_c = min(r, g, b)
821
+ saturation = (max_c - min_c) / 255 if max_c > 0 else 0
822
+ except:
823
+ luminance = 0.5
824
+ saturation = 0
825
+
826
+ # Priority scoring:
827
+ # - Penalize near-white (luminance > 0.9)
828
+ # - Penalize near-black (luminance < 0.1)
829
+ # - Penalize low saturation (grays)
830
+ # - Reward high frequency
831
+ # - Reward colors with "primary", "brand", "accent" in name
832
+
833
+ score = frequency * 10 # Base score from frequency
834
+
835
+ # Penalize extremes
836
+ if luminance > 0.9:
837
+ score -= 500 # Near white
838
+ if luminance < 0.1:
839
+ score -= 300 # Near black
840
+
841
+ # Reward saturated colors (actual brand colors)
842
+ score += saturation * 200
843
+
844
+ # Reward named brand colors
845
+ name_lower = name.lower()
846
+ if any(kw in name_lower for kw in ['primary', 'brand', 'accent', 'cyan', 'blue', 'green', 'red', 'orange', 'purple']):
847
+ score += 100
848
+
849
+ # Penalize "background", "border", "text" colors
850
+ if any(kw in name_lower for kw in ['background', 'border', 'neutral', 'gray', 'grey']):
851
+ score -= 50
852
+
853
+ return score
854
+
855
+ # Sort colors by priority
856
+ sorted_colors = []
857
+ for name, token in color_tokens.items():
858
+ priority = get_color_priority(name, token)
859
+ sorted_colors.append((name, token, priority))
860
+
861
+ sorted_colors.sort(key=lambda x: -x[2]) # Descending by priority
862
+
863
  rows_html = ""
864
+ shown_count = 0
865
 
866
+ for name, token, priority in sorted_colors:
867
+ if shown_count >= max_colors:
868
+ break
869
+
870
  # Get hex value
871
  if isinstance(token, dict):
872
  hex_val = token.get("value", "#888888")
 
877
  if not hex_val.startswith("#"):
878
  hex_val = f"#{hex_val}"
879
 
880
+ # Skip invalid hex
881
+ if len(hex_val) < 7:
882
+ continue
883
+
884
  # Generate ramp
885
  ramp = generate_color_ramp(hex_val)
886
  if not ramp:
887
  continue
888
 
889
  # Clean name
890
+ display_name = name.replace("_", " ").replace("-", " ").replace(".", " ").title()
891
+ if len(display_name) > 18:
892
+ display_name = display_name[:15] + "..."
893
 
894
  # Generate shade cells
895
  shades_html = ""
 
907
  aa_indicator = "✓"
908
  aa_class = "aa-pass"
909
  else:
910
+ aa_indicator = ""
911
+ aa_class = ""
912
 
913
  shades_html += f'''
914
  <div class="shade-cell" style="background-color: {hex_color};" title="{hex_color} | AA: {'Pass' if aa_white or aa_black else 'Fail'}">
 
931
  </div>
932
  </div>
933
  '''
934
+ shown_count += 1
935
+
936
+ # Count info
937
+ total_colors = len(color_tokens)
938
+ count_info = f"Showing {shown_count} of {total_colors} colors (sorted by brand priority)"
939
 
940
  html = f'''
941
  <style>
 
1066
  color: #666;
1067
  margin-right: 4px;
1068
  }}
1069
+
1070
+ .ramps-header-info {{
1071
+ font-size: 14px;
1072
+ color: #666;
1073
+ margin-bottom: 16px;
1074
+ padding: 8px 12px;
1075
+ background: #f0f0f0;
1076
+ border-radius: 6px;
1077
+ }}
1078
  </style>
1079
 
1080
  <div class="color-ramps-preview">
1081
+ <div class="ramps-header-info">{count_info}</div>
1082
  <div class="ramp-header">
1083
  <span class="ramp-header-label">50</span>
1084
  <span class="ramp-header-label">100</span>