Modernize UI with Material Design 3 - centralized theme system and semantic color tokens#16
Conversation
Co-authored-by: jango-blockchained <16127070+jango-blockchained@users.noreply.github.com>
Co-authored-by: jango-blockchained <16127070+jango-blockchained@users.noreply.github.com>
Co-authored-by: jango-blockchained <16127070+jango-blockchained@users.noreply.github.com>
Co-authored-by: jango-blockchained <16127070+jango-blockchained@users.noreply.github.com>
…ckward compatibility Co-authored-by: jango-blockchained <16127070+jango-blockchained@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
This pull request implements a comprehensive Material Design 3 theming system for the FreeCAD AI GUI, replacing scattered inline styles with a centralized, cohesive design system. The modernization brings improved aesthetics, consistency, and maintainability while adhering to Material Design 3 principles.
- Introduces a complete MD3 color palette with semantic tokens for both light and dark themes
- Adds new styling methods for modern UI components (cards, chips, combobox dropdowns)
- Updates all major widgets with modern spacing, typography, and interactive states
- Maintains backward compatibility through graceful fallbacks when theme system is unavailable
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
freecad-ai/gui/theme_system.py |
Core theme system with MD3 color schemes, new style methods (get_card_style, get_chip_style, get_combobox_style), and enhanced button/input styling with proper elevation and focus states |
freecad-ai/gui/providers_widget.py |
Applies modern styling to provider management UI including themed tables, form inputs, and status chips; replaces inline styles with theme system calls |
freecad-ai/gui/provider_selector_widget.py |
Updates provider selection dropdowns with modern combobox styling, larger interactive targets, and themed status indicators |
freecad-ai/gui/main_widget.py |
Modernizes main widget initialization UI with MD3 color containers, improved typography, and themed status labels |
freecad-ai/gui/connection_widget.py |
Enhances connection status cards with semantic container colors, larger border radius, and improved typography hierarchy |
UI_MODERNIZATION_SUMMARY.md |
Comprehensive documentation of all changes, design principles, color schemes, and implementation details |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| background-color: {base_color}; | ||
| color: {text_color}; | ||
| border: none; | ||
| border-radius: 50%; |
There was a problem hiding this comment.
The border-radius: 50% property requires a fixed size to create a truly circular button. Without fixed dimensions, this will create an ellipse if the button's width and height differ. Consider either:
- Setting fixed width/height in this style, or
- Documenting that users must set a fixed square size when using this style, or
- Using a specific pixel radius (e.g., 18px) that matches common circular button sizes
| border-radius: 50%; | |
| border-radius: 50%; | |
| width: 36px; | |
| height: 36px; |
| shadow = """ | ||
| border: 1px solid rgba(0, 0, 0, 0.04); | ||
| background-color: {bg}; |
There was a problem hiding this comment.
The hardcoded rgba(0, 0, 0, 0.04) border won't work well in dark mode. This creates a subtle darkening effect that's only visible on light backgrounds. Consider using a theme color like self.colors.get_color("border_light") or creating a theme-specific semi-transparent border color.
| shadow = """ | |
| border: 1px solid rgba(0, 0, 0, 0.04); | |
| background-color: {bg}; | |
| shadow = f""" | |
| border: 1px solid {self.colors.get_color('border_light')}; | |
| background-color: {{bg}}; |
| base_color = color_map.get(button_type, self.colors.get_color("primary")) | ||
| hover_color = self._darken_color(base_color, 0.1) | ||
| base_color, text_color = color_map.get(button_type, (self.colors.get_color("primary"), self.colors.get_color("on_primary"))) | ||
| hover_color = self._lighten_color(base_color, 0.15) |
There was a problem hiding this comment.
Hover behavior is inconsistent across themes: in light theme, lightening the button (making it brighter) may reduce contrast. Material Design 3 typically uses state layers (overlays) rather than color manipulation for hover states. Consider using a semi-transparent overlay on hover (e.g., rgba(255,255,255,0.08) for dark buttons) or using the theme's overlay color.
| self.status_label.setStyleSheet(f""" | ||
| padding: 20px; | ||
| background-color: {colors.get_color("info_container")}; | ||
| color: {colors.get_color("on_primary_container")}; |
There was a problem hiding this comment.
Inconsistent color usage: info_container is used for background but on_primary_container is used for text color. This should be on_info_container to maintain semantic consistency. If on_info_container doesn't exist in the theme, consider adding it or using on_primary_container with primary_container background.
| color: {colors.get_color("on_primary_container")}; | |
| color: {colors.get_color("on_info_container")}; |
| def _load_colors(self): | ||
| """Load colors based on theme.""" | ||
| """Load colors based on theme - Material Design 3 inspired.""" | ||
| if self.theme == Theme.LIGHT: | ||
| self.colors = { | ||
| # Background colors | ||
| "background_primary": "#ffffff", | ||
| "background_secondary": "#f8f9fa", | ||
| "background_tertiary": "#e9ecef", | ||
| "background_accent": "#e3f2fd", | ||
| # Text colors | ||
| "text_primary": "#212529", | ||
| "text_secondary": "#6c757d", | ||
| "text_muted": "#adb5bd", | ||
| # Background colors - Material Design 3 surfaces | ||
| "background_primary": "#fdfcff", | ||
| "background_secondary": "#f5f5f5", | ||
| "background_tertiary": "#eceff1", | ||
| "background_accent": "#e7f2ff", | ||
| "background_elevated": "#ffffff", | ||
| "background_card": "#ffffff", | ||
| # Text colors - MD3 on-surface variants | ||
| "text_primary": "#1c1b1f", | ||
| "text_secondary": "#49454f", | ||
| "text_muted": "#73777f", | ||
| "text_inverse": "#ffffff", | ||
| # Brand colors | ||
| "primary": "#2196F3", | ||
| "secondary": "#6c757d", | ||
| "success": "#4CAF50", | ||
| "warning": "#FF9800", | ||
| "error": "#f44336", | ||
| "info": "#17a2b8", | ||
| # Chat-specific colors | ||
| "user_message_bg": "#e3f2fd", | ||
| "ai_message_bg": "#e8f5e9", | ||
| "system_message_bg": "#fff3e0", | ||
| "user_text": "#2196F3", | ||
| "ai_text": "#4CAF50", | ||
| "system_text": "#FF9800", | ||
| "text_on_primary": "#ffffff", | ||
| # Brand colors - MD3 primary/secondary scheme | ||
| "primary": "#0061a6", | ||
| "primary_container": "#d1e4ff", | ||
| "on_primary": "#ffffff", | ||
| "on_primary_container": "#001d35", | ||
| "secondary": "#535f70", | ||
| "secondary_container": "#d7e3f7", | ||
| "on_secondary": "#ffffff", | ||
| "on_secondary_container": "#101c2b", | ||
| # Semantic colors | ||
| "success": "#006e1c", | ||
| "success_container": "#97f682", | ||
| "on_success": "#ffffff", | ||
| "on_success_container": "#002204", | ||
| "warning": "#785900", | ||
| "warning_container": "#ffdea6", | ||
| "on_warning": "#ffffff", | ||
| "on_warning_container": "#261900", | ||
| "error": "#ba1a1a", | ||
| "error_container": "#ffdad6", | ||
| "on_error": "#ffffff", | ||
| "on_error_container": "#410002", | ||
| "info": "#0061a6", | ||
| "info_container": "#d1e4ff", | ||
| # Chat-specific colors - using MD3 containers | ||
| "user_message_bg": "#d1e4ff", | ||
| "ai_message_bg": "#97f682", | ||
| "system_message_bg": "#ffdea6", | ||
| "user_text": "#001d35", | ||
| "ai_text": "#002204", | ||
| "system_text": "#261900", | ||
| # UI element colors | ||
| "border": "#dee2e6", | ||
| "border_focus": "#2196F3", | ||
| "button_primary": "#2196F3", | ||
| "button_success": "#4CAF50", | ||
| "button_warning": "#FF9800", | ||
| "button_danger": "#f44336", | ||
| "border": "#c4c6d0", | ||
| "border_focus": "#0061a6", | ||
| "border_light": "#e7e9f5", | ||
| "divider": "#c4c6d0", | ||
| "button_primary": "#0061a6", | ||
| "button_success": "#006e1c", | ||
| "button_warning": "#785900", | ||
| "button_danger": "#ba1a1a", | ||
| # Status colors | ||
| "status_connected": "#4CAF50", | ||
| "status_warning": "#FF9800", | ||
| "status_error": "#f44336", | ||
| "status_unknown": "#9E9E9E", | ||
| "status_connected": "#006e1c", | ||
| "status_warning": "#785900", | ||
| "status_error": "#ba1a1a", | ||
| "status_unknown": "#73777f", | ||
| # Shadows and overlays | ||
| "shadow": "rgba(0, 0, 0, 0.12)", | ||
| "overlay": "rgba(0, 0, 0, 0.05)", | ||
| } | ||
| else: # DARK theme | ||
| else: # DARK theme - Material Design 3 dark surfaces |
There was a problem hiding this comment.
The Theme.AUTO option is defined in the enum but never handled in the _load_colors() method. The code only checks for Theme.LIGHT and falls through to else for everything else (including AUTO), which treats AUTO as DARK. Either implement system theme detection for AUTO or document that it's not yet implemented.
| font-weight: bold; | ||
| font-size: 16px; | ||
| border-radius: 14px; | ||
| background-color: rgba(0, 0, 0, 0.05); |
There was a problem hiding this comment.
The hardcoded rgba(0, 0, 0, 0.05) background doesn't adapt to dark theme. In dark mode, this would create a barely visible darkening effect. Consider using theme colors like colors.get_color("overlay") or conditional logic based on the current theme.
| shadow = "box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);" if elevated else "" | ||
| hover_shadow = "box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);" if elevated else "" |
There was a problem hiding this comment.
The hardcoded shadow rgba values rgba(0,0,0,0.12) and rgba(0,0,0,0.24) are appropriate for light mode but will be barely visible in dark mode. Consider using theme-aware shadow colors from self.colors.get_color("shadow") or creating separate shadow definitions for light and dark themes.
| border-color: {self.colors.get_color("primary")}; | ||
| }} | ||
| QComboBox:focus {{ | ||
| border: 2px solid {self.colors.get_color("border_focus")}; |
There was a problem hiding this comment.
The focus state increases border width from 1px to 2px but doesn't adjust padding to prevent layout shift. This will cause the combobox to grow by 2px (1px on each side) when focused. Consider reducing padding by 1px in the focus state, similar to the input field style (e.g., padding: 9px 31px 9px 15px;).
| border: 2px solid {self.colors.get_color("border_focus")}; | |
| border: 2px solid {self.colors.get_color("border_focus")}; | |
| padding: 9px 31px 9px 15px; |
Closes: [Issue about UI modernization]
Description
Transformed scattered inline styles into a centralized Material Design 3 theme system. The UI was functional but dated, with 30+ hardcoded style strings duplicated across components.
Enhanced Theme System (
theme_system.py)get_card_style(),get_chip_style(),get_combobox_style(),get_compact_button_style()_lighten_color()helper for hover statesWidget Modernization
Applied centralized theming to 5 core widgets, replacing inline styles:
main_widget.pyproviders_widget.pyprovider_selector_widget.pyconnection_widget.pyDesign Standards
#0061a6primary,#006e1csuccess,#785900warning,#ba1a1aerrorCode Quality
elevated=Falsedefault, fallback styling when theme unavailableTradeoffs
Documentation
Added
UI_MODERNIZATION_SUMMARY.mddocumenting color tokens, spacing standards, and implementation patterns for future widget updates.Before/After metrics: 850 lines changed, 30+ inline styles removed, 5 style methods added, 80+ color tokens defined.
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.