mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
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>
395 lines
10 KiB
Python
395 lines
10 KiB
Python
"""Surface components for FakeMUI."""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QFrame, QLabel, QVBoxLayout, QHBoxLayout,
|
|
QPushButton, QScrollArea, QSizePolicy
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QSize
|
|
from PyQt6.QtGui import QPixmap, QIcon
|
|
|
|
from .base import FakeMUIWidget, FakeMUIContainer, StyleMixin
|
|
|
|
|
|
class Paper(QFrame, StyleMixin):
|
|
"""Base surface component with elevation."""
|
|
|
|
_base_class = 'paper'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
elevation: int = 1,
|
|
square: bool = False,
|
|
variant: str = 'elevation', # elevation, outlined
|
|
):
|
|
super().__init__(parent)
|
|
|
|
modifiers = [f'elevation-{elevation}', variant]
|
|
if square:
|
|
modifiers.append('square')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self._layout = QVBoxLayout(self)
|
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
def add_widget(self, widget: QWidget):
|
|
"""Add a widget to the paper."""
|
|
self._layout.addWidget(widget)
|
|
|
|
|
|
class Card(Paper):
|
|
"""Card component for displaying content."""
|
|
|
|
_base_class = 'card'
|
|
|
|
clicked = pyqtSignal()
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
clickable: bool = False,
|
|
raised: bool = False,
|
|
):
|
|
super().__init__(parent, elevation=2 if raised else 1)
|
|
|
|
modifiers = []
|
|
if clickable:
|
|
modifiers.append('clickable')
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
if raised:
|
|
modifiers.append('raised')
|
|
self.set_style_class(*modifiers)
|
|
|
|
self._clickable = clickable
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Handle click."""
|
|
if self._clickable:
|
|
self.clicked.emit()
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
class CardHeader(FakeMUIContainer):
|
|
"""Header section of a card."""
|
|
|
|
_base_class = 'card-header'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
title: str = '',
|
|
subheader: str = '',
|
|
avatar: QWidget = None,
|
|
action: QWidget = None,
|
|
):
|
|
super().__init__(parent, 'horizontal')
|
|
|
|
self.set_spacing(16)
|
|
self.set_padding(16)
|
|
|
|
# Avatar
|
|
if avatar:
|
|
avatar.setObjectName('card-header-avatar')
|
|
self.add_widget(avatar)
|
|
|
|
# Content
|
|
content = FakeMUIContainer(layout_type='vertical')
|
|
content.setObjectName('card-header-content')
|
|
if title:
|
|
title_label = QLabel(title)
|
|
title_label.setObjectName('card-header-title')
|
|
content.add_widget(title_label)
|
|
if subheader:
|
|
sub_label = QLabel(subheader)
|
|
sub_label.setObjectName('card-header-subheader')
|
|
content.add_widget(sub_label)
|
|
self.add_widget(content, stretch=1)
|
|
|
|
# Action
|
|
if action:
|
|
action.setObjectName('card-header-action')
|
|
self.add_widget(action)
|
|
|
|
|
|
class CardContent(FakeMUIContainer):
|
|
"""Content section of a card."""
|
|
|
|
_base_class = 'card-content'
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent, 'vertical')
|
|
self.set_padding(16)
|
|
|
|
|
|
class CardActions(FakeMUIContainer):
|
|
"""Actions section of a card."""
|
|
|
|
_base_class = 'card-actions'
|
|
|
|
def __init__(self, parent=None, disableSpacing: bool = False):
|
|
super().__init__(parent, 'horizontal')
|
|
|
|
if not disableSpacing:
|
|
self.set_spacing(8)
|
|
self.set_padding(8)
|
|
|
|
if disableSpacing:
|
|
self.add_modifier('no-spacing')
|
|
|
|
|
|
class CardActionArea(QPushButton, StyleMixin):
|
|
"""Clickable area within a card."""
|
|
|
|
_base_class = 'card-action-area'
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self.setFlat(True)
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
self._layout = QVBoxLayout(self)
|
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
def add_widget(self, widget: QWidget):
|
|
"""Add a widget to the action area."""
|
|
self._layout.addWidget(widget)
|
|
|
|
|
|
class CardMedia(QLabel, StyleMixin):
|
|
"""Media section of a card."""
|
|
|
|
_base_class = 'card-media'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
image: str = '',
|
|
alt: str = '',
|
|
height: int = 140,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.setFixedHeight(height)
|
|
self.setScaledContents(True)
|
|
|
|
if image:
|
|
self.set_image(image)
|
|
|
|
if alt:
|
|
self.setAccessibleName(alt)
|
|
|
|
def set_image(self, image: str):
|
|
"""Set the media image."""
|
|
pixmap = QPixmap(image)
|
|
if not pixmap.isNull():
|
|
self.setPixmap(pixmap)
|
|
|
|
|
|
class Accordion(FakeMUIContainer):
|
|
"""Expandable panel component."""
|
|
|
|
_base_class = 'accordion'
|
|
|
|
expanded_changed = pyqtSignal(bool)
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
expanded: bool = False,
|
|
disabled: bool = False,
|
|
):
|
|
super().__init__(parent, 'vertical')
|
|
|
|
self._expanded = expanded
|
|
self._summary = None
|
|
self._details = None
|
|
|
|
modifiers = []
|
|
if expanded:
|
|
modifiers.append('expanded')
|
|
if disabled:
|
|
modifiers.append('disabled')
|
|
self.set_style_class(*modifiers)
|
|
|
|
def set_summary(self, summary: 'AccordionSummary'):
|
|
"""Set the accordion summary."""
|
|
self._summary = summary
|
|
self.add_widget(summary)
|
|
summary.clicked.connect(self.toggle)
|
|
|
|
def set_details(self, details: 'AccordionDetails'):
|
|
"""Set the accordion details."""
|
|
self._details = details
|
|
self.add_widget(details)
|
|
details.setVisible(self._expanded)
|
|
|
|
def toggle(self):
|
|
"""Toggle expanded state."""
|
|
self._expanded = not self._expanded
|
|
if self._details:
|
|
self._details.setVisible(self._expanded)
|
|
if self._expanded:
|
|
self.add_modifier('expanded')
|
|
else:
|
|
self.remove_modifier('expanded')
|
|
self.expanded_changed.emit(self._expanded)
|
|
|
|
def is_expanded(self) -> bool:
|
|
"""Get expanded state."""
|
|
return self._expanded
|
|
|
|
|
|
class AccordionSummary(QPushButton, StyleMixin):
|
|
"""Summary/header of an accordion."""
|
|
|
|
_base_class = 'accordion-summary'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
expandIcon: QIcon = None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.setFlat(True)
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
self._layout = QHBoxLayout(self)
|
|
self._layout.setContentsMargins(16, 12, 16, 12)
|
|
|
|
self._content = QLabel()
|
|
self._layout.addWidget(self._content, stretch=1)
|
|
|
|
if expandIcon:
|
|
self._icon = QLabel()
|
|
self._icon.setPixmap(expandIcon.pixmap(24, 24))
|
|
self._layout.addWidget(self._icon)
|
|
|
|
def set_content(self, text: str):
|
|
"""Set summary text."""
|
|
self._content.setText(text)
|
|
|
|
|
|
class AccordionDetails(FakeMUIContainer):
|
|
"""Details/content of an accordion."""
|
|
|
|
_base_class = 'accordion-details'
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent, 'vertical')
|
|
self.set_padding(16)
|
|
|
|
|
|
class AccordionActions(FakeMUIContainer):
|
|
"""Actions section of an accordion."""
|
|
|
|
_base_class = 'accordion-actions'
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent, 'horizontal')
|
|
self.set_spacing(8)
|
|
self.set_padding(8)
|
|
|
|
|
|
class AppBar(QFrame, StyleMixin):
|
|
"""Top application bar."""
|
|
|
|
_base_class = 'app-bar'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
position: str = 'fixed', # fixed, absolute, sticky, static
|
|
color: str = 'primary',
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.set_style_class(position, color)
|
|
|
|
self._layout = QHBoxLayout(self)
|
|
self._layout.setContentsMargins(16, 0, 16, 0)
|
|
|
|
self.setFixedHeight(64)
|
|
|
|
def add_widget(self, widget: QWidget, stretch: int = 0):
|
|
"""Add a widget to the app bar."""
|
|
self._layout.addWidget(widget, stretch)
|
|
|
|
|
|
class Toolbar(FakeMUIContainer):
|
|
"""Toolbar component."""
|
|
|
|
_base_class = 'toolbar'
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
variant: str = 'regular', # regular, dense
|
|
disableGutters: bool = False,
|
|
):
|
|
super().__init__(parent, 'horizontal')
|
|
|
|
modifiers = [variant]
|
|
if disableGutters:
|
|
modifiers.append('no-gutters')
|
|
self.set_style_class(*modifiers)
|
|
|
|
if variant == 'dense':
|
|
self.setMinimumHeight(48)
|
|
else:
|
|
self.setMinimumHeight(64)
|
|
|
|
if not disableGutters:
|
|
self.set_padding(16)
|
|
|
|
|
|
class Drawer(QFrame, StyleMixin):
|
|
"""Side navigation drawer."""
|
|
|
|
_base_class = 'drawer'
|
|
|
|
onClose = pyqtSignal()
|
|
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
anchor: str = 'left', # left, right, top, bottom
|
|
variant: str = 'temporary', # permanent, persistent, temporary
|
|
open: bool = False,
|
|
width: int = 256,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self._anchor = anchor
|
|
self._variant = variant
|
|
self._width = width
|
|
|
|
self.set_style_class(anchor, variant)
|
|
|
|
if anchor in ('left', 'right'):
|
|
self.setFixedWidth(width)
|
|
else:
|
|
self.setFixedHeight(width)
|
|
|
|
self._layout = QVBoxLayout(self)
|
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
if variant == 'temporary':
|
|
self.setVisible(open)
|
|
|
|
def add_widget(self, widget: QWidget):
|
|
"""Add a widget to the drawer."""
|
|
self._layout.addWidget(widget)
|
|
|
|
def set_open(self, open: bool):
|
|
"""Set drawer visibility."""
|
|
if self._variant == 'temporary':
|
|
self.setVisible(open)
|
|
if not open:
|
|
self.onClose.emit()
|
|
|
|
def toggle(self):
|
|
"""Toggle drawer visibility."""
|
|
self.set_open(not self.isVisible())
|