uxoxo/eb2ab
0
1"""2Design System Components - Reusable UI building blocks for Gradio3 4This module provides factory functions to create consistent UI components5following the OOUX-based design system principles.6 7Author: Claude Code8Date: 2025-10-219"""10 11import gradio as gr12from typing import Optional, List, Dict, Any, Callable13from .design_tokens import DesignTokens as DT14 15 16class Components:17 """Factory class for creating design-system-compliant Gradio components"""18 19 @staticmethod20 def _merge_elem_classes(base_classes: List[str], kwargs: dict) -> List[str]:21 """22 Helper to merge elem_classes from kwargs into base classes.23 Removes elem_classes from kwargs to prevent duplicate argument error.24 25 Args:26 base_classes: List of CSS classes from component defaults27 kwargs: Keyword arguments that may contain elem_classes28 29 Returns:30 Merged list of CSS classes31 """32 elem_classes = base_classes.copy()33 34 if 'elem_classes' in kwargs:35 custom_classes = kwargs.pop('elem_classes')36 if isinstance(custom_classes, list):37 elem_classes.extend(custom_classes)38 elif isinstance(custom_classes, str):39 elem_classes.append(custom_classes)40 41 return elem_classes42 43 # ========== ATOMS ==========44 45 @staticmethod46 def button(47 label: str,48 variant: str = "primary",49 size: str = "md",50 icon: Optional[str] = None,51 **kwargs52 ) -> gr.Button:53 """54 Create a styled button following design system55 56 Args:57 label: Button text58 variant: primary | secondary | success | destructive | outline | ghost59 size: sm | md | lg60 icon: Optional icon name/emoji61 **kwargs: Additional Gradio button parameters62 63 Returns:64 gr.Button component65 """66 # Build CSS class based on variant and size67 base_classes = ["btn", f"btn-{variant}"]68 if size != "md":69 base_classes.append(f"btn-{size}")70 71 # Merge with any custom elem_classes from kwargs72 elem_classes = Components._merge_elem_classes(base_classes, kwargs)73 74 # Add icon to label if provided75 display_label = f"{icon} {label}" if icon else label76 77 return gr.Button(78 value=display_label,79 elem_classes=elem_classes,80 **kwargs81 )82 83 @staticmethod84 def progress_bar(85 value: float = 0,86 state: str = "default",87 **kwargs88 ) -> gr.Slider:89 """90 Create a progress bar91 92 Args:93 value: Progress value (0-100)94 state: default | processing | complete | error95 **kwargs: Additional parameters96 97 Returns:98 gr.Slider configured as progress bar99 """100 state_class = f"progress-bar-fill {state}" if state != "default" else "progress-bar-fill"101 102 # Merge with any custom elem_classes from kwargs103 elem_classes = Components._merge_elem_classes(["progress-bar"], kwargs)104 105 return gr.Slider(106 minimum=0,107 maximum=100,108 value=value,109 label=None,110 interactive=False,111 elem_classes=elem_classes,112 **kwargs113 )114 115 @staticmethod116 def tag(117 text: str,118 state: str = "pending",119 **kwargs120 ) -> gr.HTML:121 """122 Create a state tag/chip123 124 Args:125 text: Tag text126 state: pending | processing | modernized | approved | error127 **kwargs: Additional parameters128 129 Returns:130 gr.HTML component rendering tag131 """132 colors = DT.get_state_colors(state)133 134 html = f"""135 <span class="tag tag-{state}" style="136 background: {colors['bg']};137 color: {colors['text']};138 border: 1px solid {colors['border']};139 ">140 {text}141 </span>142 """143 144 return gr.HTML(html, **kwargs)145 146 @staticmethod147 def spinner(size: int = 20, **kwargs) -> gr.HTML:148 """149 Create a loading spinner150 151 Args:152 size: Spinner size in pixels153 **kwargs: Additional parameters154 155 Returns:156 gr.HTML component with animated spinner157 """158 html = f"""159 <div class="spinner" style="160 width: {size}px;161 height: {size}px;162 "></div>163 """164 165 return gr.HTML(html, **kwargs)166 167 # ========== MOLECULES ==========168 169 @staticmethod170 def card(171 title: str,172 content: Optional[gr.Component] = None,173 actions: Optional[List[gr.Button]] = None,174 **kwargs175 ) -> gr.Column:176 """177 Create a card container178 179 Args:180 title: Card title181 content: Card body content (Gradio component)182 actions: List of action buttons for footer183 **kwargs: Additional parameters184 185 Returns:186 gr.Column configured as card187 """188 with gr.Column(elem_classes=["card"], **kwargs) as card:189 # Header190 with gr.Row(elem_classes=["card-header"]):191 gr.Markdown(f"### {title}", elem_classes=["card-title"])192 193 # Body194 if content:195 with gr.Column(elem_classes=["card-body"]):196 content197 198 # Footer with actions199 if actions:200 with gr.Row(elem_classes=["card-footer"]):201 for action in actions:202 action203 204 return card205 206 @staticmethod207 def estimate_box(208 cost: str,209 time: str,210 details: Optional[str] = None,211 **kwargs212 ) -> gr.Column:213 """214 Create an estimate/summary box215 216 Args:217 cost: Cost estimate string218 time: Time estimate string219 details: Optional detailed breakdown220 **kwargs: Additional parameters221 222 Returns:223 gr.Column styled as estimate box224 """225 with gr.Column(elem_classes=["estimate-box"], **kwargs) as box:226 gr.Markdown(227 f'<div class="cost-estimate">{cost}</div>',228 elem_classes=["cost-estimate"]229 )230 gr.Markdown(231 f'<div class="time-estimate">{time}</div>',232 elem_classes=["time-estimate"]233 )234 if details:235 gr.Markdown(details)236 237 return box238 239 @staticmethod240 def alert(241 message: str,242 variant: str = "info",243 icon: Optional[str] = None,244 **kwargs245 ) -> gr.HTML:246 """247 Create an alert/notification248 249 Args:250 message: Alert message251 variant: info | success | warning | error252 icon: Optional icon/emoji253 **kwargs: Additional parameters254 255 Returns:256 gr.HTML component257 """258 icons = {259 "info": "ℹ️",260 "success": "✅",261 "warning": "⚠️",262 "error": "❌"263 }264 265 display_icon = icon or icons.get(variant, "")266 267 html = f"""268 <div class="alert alert-{variant}">269 <span style="font-size: 20px;">{display_icon}</span>270 <div>{message}</div>271 </div>272 """273 274 return gr.HTML(html, **kwargs)275 276 @staticmethod277 def status_indicator(278 label: str,279 state: str,280 progress: Optional[float] = None,281 **kwargs282 ) -> gr.Column:283 """284 Create a status indicator with optional progress285 286 Args:287 label: Status label288 state: Current state289 progress: Optional progress value (0-100)290 **kwargs: Additional parameters291 292 Returns:293 gr.Column with status display294 """295 with gr.Column(**kwargs) as status:296 with gr.Row():297 Components.tag(label, state)298 if progress is not None:299 gr.Markdown(f"{progress:.0f}%")300 301 if progress is not None:302 Components.progress_bar(progress, state)303 304 return status305 306 # ========== ORGANISMS ==========307 308 @staticmethod309 def project_card(310 title: str,311 progress: float,312 state: str,313 metadata: Optional[Dict[str, Any]] = None,314 on_click: Optional[Callable] = None,315 **kwargs316 ) -> gr.Column:317 """318 Create a project card319 320 Args:321 title: Project title322 progress: Progress percentage (0-100)323 state: Project state324 metadata: Optional metadata dict325 on_click: Optional click handler326 **kwargs: Additional parameters327 328 Returns:329 gr.Column configured as project card330 """331 with gr.Column(elem_classes=["project-card"], **kwargs) as card:332 # Header333 with gr.Row(elem_classes=["project-card-header"]):334 gr.Markdown(f"### {title}", elem_classes=["project-card-title"])335 Components.tag(state.upper(), state)336 337 # Metadata338 if metadata:339 with gr.Row():340 for key, value in metadata.items():341 gr.Markdown(f"**{key}:** {value}", elem_classes=["body-small"])342 343 # Progress344 with gr.Column(elem_classes=["project-card-progress"]):345 gr.Markdown(f"Progress: {progress:.0f}%", elem_classes=["body-small"])346 Components.progress_bar(progress, state)347 348 # Actions349 with gr.Row(elem_classes=["project-card-footer"]):350 Components.button("Resume", variant="primary", size="sm")351 Components.button("Export", variant="outline", size="sm")352 353 return card354 355 @staticmethod356 def chunk_row_table(357 chunks: List[Dict[str, Any]],358 on_row_click: Optional[Callable] = None,359 **kwargs360 ) -> gr.DataFrame:361 """362 Create a data table for chunks363 364 Args:365 chunks: List of chunk dictionaries366 on_row_click: Optional row click handler367 **kwargs: Additional parameters368 369 Returns:370 gr.DataFrame styled as data table371 """372 # Convert chunks to DataFrame format373 import pandas as pd374 375 df = pd.DataFrame(chunks)376 377 return gr.DataFrame(378 value=df,379 interactive=False,380 elem_classes=["data-table"],381 **kwargs382 )383 384 @staticmethod385 def split_pane_editor(386 left_content: str,387 right_content: str,388 left_label: str = "Original",389 right_label: str = "Modernized",390 **kwargs391 ) -> gr.Row:392 """393 Create a split-pane comparison editor394 395 Args:396 left_content: Left pane content397 right_content: Right pane content398 left_label: Left pane label399 right_label: Right pane label400 **kwargs: Additional parameters401 402 Returns:403 gr.Row with split panes404 """405 with gr.Row(elem_classes=["split-pane"], **kwargs) as pane:406 with gr.Column(elem_classes=["split-pane-panel"]):407 gr.Markdown(f"### {left_label}")408 gr.Textbox(409 value=left_content,410 lines=20,411 interactive=False412 )413 414 with gr.Column(elem_classes=["split-pane-panel"]):415 gr.Markdown(f"### {right_label}")416 gr.Textbox(417 value=right_content,418 lines=20,419 interactive=True420 )421 422 return pane423 424 @staticmethod425 def voice_selector(426 voices: List[Dict[str, str]],427 selected_voice: Optional[str] = None,428 on_select: Optional[Callable] = None,429 **kwargs430 ) -> gr.Column:431 """432 Create a voice selector grid433 434 Args:435 voices: List of voice dicts with 'name' and 'preview' keys436 selected_voice: Currently selected voice name437 on_select: Selection handler438 **kwargs: Additional parameters439 440 Returns:441 gr.Column with voice cards442 """443 with gr.Column(**kwargs) as selector:444 gr.Markdown("### Select Voice")445 446 with gr.Row():447 for voice in voices:448 is_selected = voice['name'] == selected_voice449 classes = ["voice-card"]450 if is_selected:451 classes.append("selected")452 453 with gr.Column(elem_classes=classes):454 gr.Markdown(f"**{voice['name']}**")455 if 'preview' in voice:456 gr.Audio(voice['preview'], label=None)457 Components.button(458 "Select" if not is_selected else "Selected ✓",459 variant="primary" if not is_selected else "success",460 size="sm"461 )462 463 return selector464 465 @staticmethod466 def processing_queue_bar(467 current_job: str,468 progress: float,469 visible: bool = True,470 **kwargs471 ) -> gr.Column:472 """473 Create global processing queue bar474 475 Args:476 current_job: Current job description477 progress: Progress value (0-100)478 visible: Whether bar is visible479 **kwargs: Additional parameters480 481 Returns:482 gr.Column styled as queue bar483 """484 classes = ["processing-queue-bar"]485 if visible:486 classes.append("visible")487 488 with gr.Column(elem_classes=classes, **kwargs) as bar:489 with gr.Row(elem_classes=["processing-queue-content"]):490 with gr.Column():491 Components.spinner()492 gr.Markdown(current_job)493 494 gr.Markdown(f"{progress:.0f}%")495 496 Components.button("Pause", variant="ghost", size="sm", icon="⏸️")497 498 return bar499 500 501# Convenience exports502button = Components.button503progress_bar = Components.progress_bar504tag = Components.tag505spinner = Components.spinner506card = Components.card507estimate_box = Components.estimate_box508alert = Components.alert509status_indicator = Components.status_indicator510project_card = Components.project_card511chunk_row_table = Components.chunk_row_table512split_pane_editor = Components.split_pane_editor513voice_selector = Components.voice_selector514processing_queue_bar = Components.processing_queue_bar515 