Files
metabuilder/python/fakemui/atoms.py
johndoe6345789 c406b8df96 refactor: Reorganize FakeMUI by implementation type
Move FakeMUI content to appropriate root-level folders by implementation:

**React Components → components/fakemui/**
- 537 components (inputs, surfaces, layout, data-display, feedback,
  navigation, utils, atoms, lab, x, email, workflows)
- 416 SVG icons
- Full barrel exports in components/fakemui/index.ts

**QML Components → qml/**
- 104 Material Design 3 components (11 categories)
- 7 hybrid application views
- 8 desktop widgets
- qmldir module registration

**Python Bindings → python/fakemui/**
- 15 PyQt6 modules (120+ components)
- Full Python package structure with pyproject.toml

**SCSS/Styles → fakemui/** (renamed purpose)
- scss/ - Material Design 3 stylesheets
- styles/ - Component SCSS modules
- src/utils/ - Accessibility utilities
- index.ts now re-exports from components/fakemui/

This separation allows:
- React: import { Button } from '@metabuilder/components/fakemui'
- QML: import QmlComponents 1.0
- Python: from fakemui import Button, Card
- Backward compat: import { Button } from '@metabuilder/fakemui'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:59:24 +00:00

428 lines
11 KiB
Python

"""Atom components for FakeMUI - Higher-level composed components."""
from PyQt6.QtWidgets import (
QWidget, QFrame, QLabel, QVBoxLayout, QHBoxLayout,
QGridLayout, QPushButton, QSizePolicy
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont, QIcon
from .base import FakeMUIWidget, FakeMUIContainer, StyleMixin
from .feedback import Spinner
class Title(QLabel, StyleMixin):
"""Large title text."""
_base_class = 'title'
def __init__(
self,
text: str = '',
parent=None,
level: int = 1, # 1-6, maps to h1-h6
):
super().__init__(text, parent)
self.set_style_class(f'h{level}')
# Font sizes for heading levels
sizes = {1: 32, 2: 28, 3: 24, 4: 20, 5: 18, 6: 16}
font = self.font()
font.setPointSize(sizes.get(level, 24))
font.setBold(True)
self.setFont(font)
class Subtitle(QLabel, StyleMixin):
"""Subtitle text."""
_base_class = 'subtitle'
def __init__(self, text: str = '', parent=None, level: int = 1):
super().__init__(text, parent)
self.set_style_class(f'level-{level}')
font = self.font()
font.setPointSize(16 if level == 1 else 14)
self.setFont(font)
class Label(QLabel, StyleMixin):
"""Form label or small text label."""
_base_class = 'label'
def __init__(
self,
text: str = '',
parent=None,
required: bool = False,
color: str = 'default',
):
display_text = f"{text} *" if required else text
super().__init__(display_text, parent)
modifiers = [color]
if required:
modifiers.append('required')
self.set_style_class(*modifiers)
class Text(QLabel, StyleMixin):
"""General purpose text component."""
_base_class = 'text'
def __init__(
self,
text: str = '',
parent=None,
variant: str = 'body', # body, caption, overline
color: str = 'primary',
align: str = 'left',
wrap: bool = True,
):
super().__init__(text, parent)
self.set_style_class(variant, f'color-{color}')
# Variant font sizes
sizes = {'body': 14, 'caption': 12, 'overline': 10}
font = self.font()
font.setPointSize(sizes.get(variant, 14))
self.setFont(font)
# Alignment
alignments = {
'left': Qt.AlignmentFlag.AlignLeft,
'center': Qt.AlignmentFlag.AlignCenter,
'right': Qt.AlignmentFlag.AlignRight,
}
self.setAlignment(alignments.get(align, Qt.AlignmentFlag.AlignLeft))
self.setWordWrap(wrap)
class StatBadge(FakeMUIContainer):
"""Badge for displaying statistics."""
_base_class = 'stat-badge'
def __init__(
self,
label: str = '',
value: str = '',
parent=None,
color: str = 'primary',
size: str = 'medium',
):
super().__init__(parent, 'vertical')
self.set_style_class(color, size)
self.set_spacing(4)
# Value
value_label = QLabel(str(value))
value_label.setObjectName('stat-value')
font = value_label.font()
font.setPointSize(24 if size == 'large' else 18)
font.setBold(True)
value_label.setFont(font)
value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.add_widget(value_label)
# Label
label_widget = QLabel(label)
label_widget.setObjectName('stat-label')
label_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.add_widget(label_widget)
class Section(FakeMUIContainer):
"""Section container with optional header."""
_base_class = 'section'
def __init__(
self,
parent=None,
title: str = '',
collapsible: bool = False,
):
super().__init__(parent, 'vertical')
self._collapsible = collapsible
self._expanded = True
self.set_spacing(16)
if title:
self._header = SectionHeader(title, collapsible=collapsible)
if collapsible:
self._header.clicked.connect(self._toggle)
self.add_widget(self._header)
else:
self._header = None
self._content = SectionContent()
self.add_widget(self._content)
def _toggle(self):
"""Toggle section visibility."""
self._expanded = not self._expanded
self._content.setVisible(self._expanded)
def add_content(self, widget: QWidget):
"""Add widget to section content."""
self._content.add_widget(widget)
class SectionHeader(FakeMUIContainer):
"""Header for a section."""
_base_class = 'section-header'
clicked = pyqtSignal()
def __init__(
self,
title: str = '',
parent=None,
collapsible: bool = False,
action: QWidget = None,
):
super().__init__(parent, 'horizontal')
self._collapsible = collapsible
# Title
self._title = SectionTitle(title)
self.add_widget(self._title, stretch=1)
# Action
if action:
self.add_widget(action)
if collapsible:
self.setCursor(Qt.CursorShape.PointingHandCursor)
def mousePressEvent(self, event):
"""Handle click."""
if self._collapsible:
self.clicked.emit()
super().mousePressEvent(event)
class SectionTitle(QLabel, StyleMixin):
"""Title for a section."""
_base_class = 'section-title'
def __init__(self, text: str = '', parent=None):
super().__init__(text, parent)
font = self.font()
font.setPointSize(16)
font.setBold(True)
self.setFont(font)
class SectionContent(FakeMUIContainer):
"""Content area of a section."""
_base_class = 'section-content'
def __init__(self, parent=None):
super().__init__(parent, 'vertical')
self.set_spacing(8)
class EmptyState(FakeMUIContainer):
"""Empty state placeholder."""
_base_class = 'empty-state'
def __init__(
self,
parent=None,
icon: QIcon = None,
title: str = 'No data',
description: str = '',
action: QWidget = None,
):
super().__init__(parent, 'vertical')
self.set_spacing(16)
self._layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Icon
if icon:
icon_label = QLabel()
icon_label.setPixmap(icon.pixmap(64, 64))
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.add_widget(icon_label)
# Title
title_label = Title(title, level=3)
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.add_widget(title_label)
# Description
if description:
desc_label = Text(description, color='secondary', align='center')
self.add_widget(desc_label)
# Action
if action:
self.add_widget(action)
class LoadingState(FakeMUIContainer):
"""Loading state placeholder."""
_base_class = 'loading-state'
def __init__(
self,
parent=None,
message: str = 'Loading...',
size: int = 40,
):
super().__init__(parent, 'vertical')
self.set_spacing(16)
self._layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Spinner
spinner = Spinner(size=size)
self.add_widget(spinner)
# Message
if message:
msg_label = Text(message, color='secondary', align='center')
self.add_widget(msg_label)
class ErrorState(FakeMUIContainer):
"""Error state placeholder."""
_base_class = 'error-state'
retryClicked = pyqtSignal()
def __init__(
self,
parent=None,
title: str = 'Error',
message: str = 'Something went wrong',
retryText: str = 'Retry',
showRetry: bool = True,
):
super().__init__(parent, 'vertical')
self.set_spacing(16)
self._layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Title
title_label = Title(title, level=3)
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.add_widget(title_label)
# Message
msg_label = Text(message, color='secondary', align='center')
self.add_widget(msg_label)
# Retry button
if showRetry:
from .inputs import Button
retry_btn = Button(retryText, primary=True)
retry_btn.clicked.connect(self.retryClicked.emit)
self.add_widget(retry_btn)
class Panel(FakeMUIContainer):
"""Panel container with optional header and footer."""
_base_class = 'panel'
def __init__(
self,
parent=None,
title: str = '',
elevation: int = 1,
):
super().__init__(parent, 'vertical')
self.set_style_class(f'elevation-{elevation}')
# Header
if title:
header = FakeMUIContainer(layout_type='horizontal')
header.setObjectName('panel-header')
title_label = Title(title, level=4)
header.add_widget(title_label)
self.add_widget(header)
# Content area
self._content = FakeMUIContainer(layout_type='vertical')
self._content.setObjectName('panel-content')
self.add_widget(self._content, stretch=1)
def add_content(self, widget: QWidget, stretch: int = 0):
"""Add widget to panel content."""
self._content.add_widget(widget, stretch)
class AutoGrid(QFrame, StyleMixin):
"""Auto-sizing grid container."""
_base_class = 'auto-grid'
def __init__(
self,
parent=None,
minChildWidth: int = 200,
gap: int = 16,
columns: int = None, # If set, override auto calculation
):
super().__init__(parent)
self._min_child_width = minChildWidth
self._gap = gap
self._columns = columns
self._items = []
self._layout = QGridLayout(self)
self._layout.setSpacing(gap)
self._layout.setContentsMargins(0, 0, 0, 0)
def add_widget(self, widget: QWidget):
"""Add a widget to the grid."""
self._items.append(widget)
self._relayout()
def _relayout(self):
"""Recalculate grid layout."""
# Clear current layout
while self._layout.count():
self._layout.takeAt(0)
# Calculate columns
if self._columns:
cols = self._columns
else:
width = self.width() or 600
cols = max(1, width // (self._min_child_width + self._gap))
# Add items
for i, item in enumerate(self._items):
row = i // cols
col = i % cols
self._layout.addWidget(item, row, col)
def resizeEvent(self, event):
"""Handle resize to recalculate grid."""
super().resizeEvent(event)
self._relayout()