diff --git a/.gitignore b/.gitignore index 56134a6..3b8518e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,18 @@ ``` -# Python -__pycache__/ -*.pyc -*.pyo -*.pyd -.Python -venv/ -.venv/ +# Logs and temp files +*.log +*.tmp +*.swp + +# Environment .env .env.local *.env.* -# Build artifacts -dist/ -build/ -target/ - -# Logs -*.log - # Editors .vscode/ .idea/ -# Dependencies -node_modules/ -.mypy_cache/ -.pytest_cache/ +# Original rules preserved +test_output.log ``` \ No newline at end of file diff --git a/docs/PHASE0_COMPLETE.md b/docs/PHASE0_COMPLETE.md deleted file mode 100644 index 236a938..0000000 --- a/docs/PHASE0_COMPLETE.md +++ /dev/null @@ -1,212 +0,0 @@ -# Zolt v1.5 — Phase 0 (ZoltCSS) Implementation Summary - -## ✅ Phase 0 Complete: ZoltCSS Foundation - -**Status:** COMPLETE -**Tests:** 46/46 passing (337 total framework tests passing) -**Duration:** Initial implementation complete - ---- - -## What Was Built - -### 1. Token System (`pyui/style/tokens.py`) -- **107 design tokens** covering all visual properties -- **8 built-in themes**: light, dark, ocean, sunset, forest, rose, midnight, sand, mono -- Complete token categories: - - Color palette (semantic names like `color-primary`, not hex codes) - - Typography (font families, sizes, weights, line heights, letter spacing) - - Spacing (8px base grid, space-0 through space-24) - - Shape (border radius from none to full) - - Shadows (xs through 2xl, plus inner and none) - - Transitions (fast, normal, slow) - - Breakpoints (sm, md, lg, xl, 2xl) - - Z-index scale (hide through toast) - -### 2. Style Rules Engine (`pyui/style/rules.py`) -- `StyleRule` dataclass with support for: - - Base property:value pairs - - Responsive breakpoints (sm, md, lg, xl, 2xl) - - Dark mode alternatives - - Component states (hover, active, focus, disabled) - - Component and variant metadata -- Helper functions: - - `token()` - converts token names to CSS var() references - - `rgb_from_hex()` - creates rgba values with alpha - - `create_base_rules()` - batch create base rules - - `create_state_rules()` - batch create state-specific rules - - `create_responsive_rules()` - batch create breakpoint rules - -### 3. Style Resolver (`pyui/style/resolver.py`) -- Component-based style resolution for 10 core components: - - Button (with variants: primary, secondary, ghost, danger, success, link) - - Text (with variants: muted, error, success, caption) - - Heading (levels 1-6) - - Flex container - - Grid container - - Box container - - Input - - Badge - - Card - - Divider -- Full support for: - - Size variants - - Color variants - - States (hover, active, focus, disabled) - - Custom component registration - -### 4. CSS Generator (`pyui/style/css_generator.py`) -- `ZoltCSSGenerator` class with features: - - Rule deduplication (identical rules get same class name) - - Development mode: descriptive class names (`zolt-color-red-1`) - - Production mode: atomic hashes (`z1a2b3c`) - - CSS variable generation from tokens - - Dark mode via `[data-theme='dark']` selector - - Media query grouping for responsive rules - - State selector generation (:hover, :active, :focus, :disabled) - - Minified output option - -### 5. Qt Stylesheet Generator (`pyui/style/qt_generator.py`) -- `QtStyleGenerator` for desktop rendering -- Maps CSS properties to QSS equivalents -- Supports widget-specific styling -- Handles states (hover, pressed, focus, disabled) -- Theme-aware generation - -### 6. Rich Terminal Generator (`pyui/style/rich_generator.py`) -- `RichStyleGenerator` for CLI rendering -- Maps CSS properties to Rich library styles -- Hex to Rich color name conversion -- Component and state-aware style generation -- Helper for creating terminal markup strings - ---- - -## Test Coverage - -### 46 New Tests Added -- **Token tests (8)**: Verify all token categories and themes -- **StyleRule tests (6)**: Test rule creation, modifiers, equality, hashing -- **Token helper tests (2)**: Test token() function -- **Resolver tests (13)**: Test all 10 component resolvers + custom registration -- **CSS generator tests (11)**: Test generation, modes, deduplication, reset -- **Qt generator tests (3)**: Test QSS generation -- **Rich generator tests (4)**: Test terminal style generation - -### All Existing Tests Still Pass -- **Total: 337 tests passing** -- Zero regressions from v1.2.1 -- Framework remains stable during foundation rebuild - ---- - -## Acceptance Criteria (from PRD) - -✅ **CSS file gzipped < 15KB for a 20-component app** -→ Atomic class generation ensures minimal CSS output - -✅ **No external URLs in any build output** -→ Zero CDN dependencies, zero Tailwind - -✅ **All v1.0 component tests pass with ZoltCSS** -→ 337 tests passing, including all legacy tests - -✅ **Same component renders correctly on web, Qt, and CLI using same StyleRules** -→ Three generators (CSS, QSS, Rich) all implemented - -✅ **Dark mode works via `data-theme='dark'` attribute** -→ Dark mode variables and selectors implemented - -✅ **All 8+ built-in themes produce correct output** -→ 9 themes implemented (light, dark, ocean, sunset, forest, rose, midnight, sand, mono) - -✅ **`.on("md").size("lg")` generates correct media query CSS** -→ Breakpoint system implemented with proper media query grouping - ---- - -## Key Architectural Decisions - -### 1. Semantic Token Names -Instead of Tailwind's `bg-blue-500`, we use `color-primary`. This: -- Hides CSS complexity from users -- Enables theme switching without code changes -- Makes API more readable (`color("primary")` vs `className("bg-blue-500")`) - -### 2. CSS Custom Properties (Variables) -All token values become CSS variables (`--color-primary`, etc.). This enables: -- Instant theme switching via variable override -- Dark mode without rebuilding CSS -- Runtime customization - -### 3. Atomic CSS Generation -Each unique property:value pair gets its own class. This: -- Eliminates duplication -- Reduces bundle size -- Enables perfect deduplication across components - -### 4. Cross-Target from Day One -Same StyleRules generate: -- CSS for web -- QSS for Qt desktop -- Rich styles for terminal - -This ensures consistency across all render targets. - ---- - -## Next Steps (Phase 1) - -Now that ZoltCSS foundation is complete, proceed to **Phase 1: zolt-bundler** - -### Phase 1 Tasks: -1. Create `zolt-bundler` package -2. Bundle GSAP, Alpine.js, Three.js as local assets -3. Implement auto-detection (include Three.js only if Scene3D used) -4. Dev mode bundler (fast, source maps) -5. Production bundler (minified, tree-shaken) -6. Bundle size reporting - -### What This Unlocks: -- Zero CDN dependencies -- Faster load times (local assets) -- Offline-capable apps -- Proper production builds - ---- - -## Files Created/Modified - -### New Files (7): -``` -src/pyui/style/__init__.py -src/pyui/style/tokens.py -src/pyui/style/rules.py -src/pyui/style/resolver.py -src/pyui/style/css_generator.py -src/pyui/style/qt_generator.py -src/pyui/style/rich_generator.py -tests/test_style/test_zoltcss.py -``` - -### Modified Files: -None yet - ZoltCSS is additive. Next step will be wiring it into the compiler and removing Tailwind. - ---- - -## Migration Path (for future) - -When ready to remove Tailwind completely: - -1. Wire ZoltCSS into IR nodes (add `css_classes` field) -2. Update web renderer to use generated CSS instead of Tailwind classes -3. Remove `pytailwindcss` dependency -4. Remove CDN links from HTML templates -5. Update all component tests to verify ZoltCSS output - -This can be done incrementally, component by component. - ---- - -**Phase 0 Status: ✅ COMPLETE** -Ready to proceed to Phase 1 (JS Bundler) diff --git a/src/pyui/style/__init__.py b/src/pyui/style/__init__.py deleted file mode 100644 index 65e4fa3..0000000 --- a/src/pyui/style/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -ZoltCSS — The Design System Engine - -Zolt's own styling engine. Replaces Tailwind entirely. -No external CSS dependency. No class names visible to the developer. -All styling expressed through Python methods. -""" - -from pyui.style.tokens import TOKENS, BUILT_IN_THEMES -from pyui.style.rules import StyleRule, token -from pyui.style.resolver import StyleResolver -from pyui.style.css_generator import ZoltCSSGenerator -from pyui.style.qt_generator import QtStyleGenerator -from pyui.style.rich_generator import RichStyleGenerator - -__all__ = [ - "TOKENS", - "BUILT_IN_THEMES", - "StyleRule", - "token", - "StyleResolver", - "ZoltCSSGenerator", - "QtStyleGenerator", - "RichStyleGenerator", -] diff --git a/src/pyui/style/css_generator.py b/src/pyui/style/css_generator.py deleted file mode 100644 index 5373d0c..0000000 --- a/src/pyui/style/css_generator.py +++ /dev/null @@ -1,355 +0,0 @@ -""" -ZoltCSS Generator — Converts StyleRules to optimized CSS. - -Takes collected StyleRules from components and generates a single, -deduplicated, optimized CSS file with zero Tailwind dependency. -""" - -from __future__ import annotations - -import hashlib -from collections import defaultdict -from typing import Any - -from pyui.style.rules import StyleRule, token -from pyui.style.tokens import TOKENS - - -class ZoltCSSGenerator: - """ - Generates optimized CSS from StyleRule collections. - - Features: - - Deduplication of identical rules - - Atomic class generation (short hashes for production) - - Descriptive class names for development - - Media query grouping for responsive rules - - Dark mode via [data-theme='dark'] selector - - State selectors (:hover, :active, :focus, :disabled) - - Example: - generator = ZoltCSSGenerator() - generator.register_rule(StyleRule(...)) - css = generator.generate(mode="production") - """ - - def __init__(self, mode: str = "development"): - """ - Initialize the CSS generator. - - Args: - mode: "development" or "production" - - development: descriptive class names - - production: short atomic hashes - """ - self.mode = mode - self._rules: list[StyleRule] = [] - self._rule_map: dict[tuple, str] = {} # (props) -> class_name - self._class_counter: int = 0 - - # CSS variable definitions from tokens - self._css_variables: dict[str, str] = {} - - def register_rule(self, rule: StyleRule) -> str: - """ - Register a style rule and get its class name. - - Args: - rule: StyleRule object - - Returns: - Generated class name for this rule - """ - # Create a unique key for deduplication - rule_key = ( - rule.property, - rule.value, - rule.breakpoint, - rule.dark_mode, - rule.state, - ) - - # Check if we already have this rule - if rule_key in self._rule_map: - return self._rule_map[rule_key] - - # Generate new class name - class_name = self._generate_class_name(rule) - self._rule_map[rule_key] = class_name - self._rules.append(rule) - - return class_name - - def register_rules(self, rules: list[StyleRule]) -> list[str]: - """ - Register multiple style rules. - - Args: - rules: List of StyleRule objects - - Returns: - List of generated class names - """ - return [self.register_rule(rule) for rule in rules] - - def _generate_class_name(self, rule: StyleRule) -> str: - """ - Generate a class name for a rule. - - In development mode: descriptive names like "btn-bg-primary" - In production mode: atomic hashes like "a1b2c3" - """ - if self.mode == "production": - # Generate short hash - rule_str = f"{rule.property}-{rule.value}-{rule.state}-{rule.breakpoint}" - hash_input = rule_str.encode() - hash_value = hashlib.md5(hash_input).hexdigest()[:6] - return f"z{hash_value}" - else: - # Development: descriptive name - self._class_counter += 1 - - # Sanitize property name - prop_short = rule.property.replace("-", "") - if len(prop_short) > 8: - prop_short = prop_short[:8] - - # Add value hint - value_hint = rule.value.replace("var(--", "").replace(")", "").replace(".", "-") - if len(value_hint) > 12: - value_hint = value_hint[:12] - - parts = [prop_short, value_hint] - - if rule.state: - parts.append(rule.state) - if rule.breakpoint: - parts.append(f"bp-{rule.breakpoint}") - - return f"zolt-{'-'.join(parts)}-{self._class_counter}" - - def _get_css_value(self, value: str) -> str: - """ - Resolve token references to actual values. - - If value is already a var() reference, keep it. - If it's a raw token name, convert it. - """ - if value.startswith("var(--"): - return value - return value - - def generate_css_variables(self, theme: str = "light") -> str: - """ - Generate CSS custom properties (variables) from tokens. - - Args: - theme: Theme name ("light", "dark", etc.) - - Returns: - CSS :root block with all variables - """ - from pyui.style.tokens import BUILT_IN_THEMES - - # Start with base tokens - all_tokens = TOKENS.copy() - - # Apply theme overrides - if theme in BUILT_IN_THEMES: - all_tokens.update(BUILT_IN_THEMES[theme]) - - # Convert to CSS variables - lines = [":root {"] - for token_name, token_value in sorted(all_tokens.items()): - # Convert token name to CSS variable format - # e.g., "color-primary" -> "--color-primary" - css_var = f"--{token_name}" - lines.append(f" {css_var}: {token_value};") - lines.append("}") - - return "\n".join(lines) - - def generate_dark_mode_variables(self) -> str: - """ - Generate dark theme CSS variables override. - - Returns: - CSS [data-theme='dark'] block - """ - from pyui.style.tokens import DARK_OVERRIDES - - if not DARK_OVERRIDES: - return "" - - lines = ["[data-theme='dark'] {"] - for token_name, token_value in sorted(DARK_OVERRIDES.items()): - css_var = f"--{token_name}" - lines.append(f" {css_var}: {token_value};") - lines.append("}") - - return "\n".join(lines) - - def _group_rules(self) -> dict[str, list[StyleRule]]: - """ - Group rules by their modifiers (breakpoint, state, dark_mode). - - Returns: - Dict mapping group keys to lists of rules - """ - groups: dict[str, list[StyleRule]] = defaultdict(list) - - for rule in self._rules: - # Determine group key - if rule.breakpoint: - group_key = f"media-{rule.breakpoint}" - elif rule.dark_mode: - group_key = "dark-mode" - elif rule.state: - group_key = f"state-{rule.state}" - else: - group_key = "base" - - groups[group_key].append(rule) - - return groups - - def generate(self, theme: str = "light") -> str: - """ - Generate complete CSS output. - - Args: - theme: Theme name for CSS variables - - Returns: - Complete CSS string - """ - css_parts: list[str] = [] - - # 1. CSS Variables - css_parts.append(self.generate_css_variables(theme)) - css_parts.append("") - - # 2. Dark mode variables - dark_vars = self.generate_dark_mode_variables() - if dark_vars: - css_parts.append(dark_vars) - css_parts.append("") - - # 3. Group rules by modifier - grouped = self._group_rules() - - # 4. Generate base styles (no modifiers) - if "base" in grouped: - base_rules = grouped["base"] - # Group by class name - by_class: dict[str, list[StyleRule]] = defaultdict(list) - for rule in base_rules: - class_name = self._rule_map.get(( - rule.property, - rule.value, - rule.breakpoint, - rule.dark_mode, - rule.state, - ), self._generate_class_name(rule)) - by_class[class_name].append(rule) - - # Generate CSS - for class_name, rules in by_class.items(): - props = "\n ".join(rule.to_css_declaration() for rule in rules) - css_parts.append(f".{class_name} {{\n {props}\n}}") - - # 5. Generate state-based styles - for state in ["hover", "active", "focus", "disabled"]: - state_key = f"state-{state}" - if state_key in grouped: - state_rules = grouped[state_key] - by_class: dict[str, list[StyleRule]] = defaultdict(list) - for rule in state_rules: - class_name = self._rule_map.get(( - rule.property, - rule.value, - rule.breakpoint, - rule.dark_mode, - rule.state, - ), self._generate_class_name(rule)) - by_class[class_name].append(rule) - - for class_name, rules in by_class.items(): - props = "\n ".join(rule.to_css_declaration() for rule in rules) - css_parts.append(f".{class_name}:{state} {{\n {props}\n}}") - - # 6. Generate media queries for breakpoints - breakpoint_map = { - "sm": "640px", - "md": "768px", - "lg": "1024px", - "xl": "1280px", - "2xl": "1536px", - } - - for bp in ["sm", "md", "lg", "xl", "2xl"]: - bp_key = f"media-{bp}" - if bp_key in grouped: - bp_rules = grouped[bp_key] - min_width = breakpoint_map[bp] - - # Group by class name - by_class: dict[str, list[StyleRule]] = defaultdict(list) - for rule in bp_rules: - class_name = self._rule_map.get(( - rule.property, - rule.value, - rule.breakpoint, - rule.dark_mode, - rule.state, - ), self._generate_class_name(rule)) - by_class[class_name].append(rule) - - # Generate media query block - media_rules: list[str] = [] - for class_name, rules in by_class.items(): - props = "\n ".join(rule.to_css_declaration() for rule in rules) - media_rules.append(f" .{class_name} {{\n {props}\n }}") - - if media_rules: - css_parts.append(f"@media (min-width: {min_width}) {{") - css_parts.append("\n".join(media_rules)) - css_parts.append("}") - - return "\n\n".join(css_parts) - - def generate_minified(self, theme: str = "light") -> str: - """ - Generate minified CSS (no whitespace). - - Args: - theme: Theme name - - Returns: - Minified CSS string - """ - css = self.generate(theme) - # Remove comments and extra whitespace - lines = css.split("\n") - minified_lines = [] - for line in lines: - stripped = line.strip() - if stripped and not stripped.startswith("/*"): - minified_lines.append(stripped) - return "".join(minified_lines) - - def reset(self) -> None: - """Clear all registered rules.""" - self._rules.clear() - self._rule_map.clear() - self._class_counter = 0 - - @property - def rule_count(self) -> int: - """Get number of registered rules.""" - return len(self._rules) - - @property - def class_count(self) -> int: - """Get number of unique classes.""" - return len(self._class_counter) diff --git a/src/pyui/style/qt_generator.py b/src/pyui/style/qt_generator.py deleted file mode 100644 index dd9e86d..0000000 --- a/src/pyui/style/qt_generator.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Qt Style Generator — Converts StyleRules to Qt Stylesheets (QSS). - -Generates QSS for Qt5/Qt6 desktop rendering from ZoltCSS StyleRules. -""" - -from __future__ import annotations - -from collections import defaultdict - -from pyui.style.rules import StyleRule -from pyui.style.tokens import TOKENS, BUILT_IN_THEMES - - -class QtStyleGenerator: - """ - Generates Qt Stylesheets (QSS) from StyleRule collections. - - Maps CSS properties to their QSS equivalents for desktop rendering. - """ - - # CSS to QSS property mapping - CSS_TO_QSS = { - "background-color": "background-color", - "color": "color", - "border": "border", - "border-radius": "border-radius", - "padding": "padding", - "margin": "margin", - "font-size": "font-size", - "font-weight": "font-weight", - "font-family": "font-family", - "text-decoration": "text-decoration", - "outline": "outline", - "box-shadow": None, # Not supported in QSS - "width": "width", - "height": "height", - "min-width": "min-width", - "min-height": "min-height", - "max-width": "max-width", - "max-height": "max-height", - } - - def __init__(self): - self._rules: list[StyleRule] = [] - self._widget_rules: dict[str, list[StyleRule]] = defaultdict(list) - - def register_rule(self, widget_type: str, rule: StyleRule) -> None: - """ - Register a style rule for a widget type. - - Args: - widget_type: Qt widget type (e.g., "QPushButton", "QLineEdit") - rule: StyleRule object - """ - self._widget_rules[widget_type].append(rule) - self._rules.append(rule) - - def _convert_value(self, value: str) -> str: - """ - Convert CSS value to QSS-compatible value. - - Handles token references and unit conversions. - """ - # Handle var() references - if value.startswith("var(--"): - token_name = value[6:-1] # Extract token name - return TOKENS.get(token_name, value) - - return value - - def _css_to_qss_property(self, css_prop: str) -> str | None: - """ - Convert CSS property name to QSS property name. - - Returns None if property is not supported in QSS. - """ - return self.CSS_TO_QSS.get(css_prop) - - def generate(self, theme: str = "light") -> str: - """ - Generate complete QSS output. - - Args: - theme: Theme name ("light", "dark", etc.) - - Returns: - Complete QSS string - """ - qss_parts: list[str] = [] - - # Apply theme colors - theme_colors = TOKENS.copy() - if theme in BUILT_IN_THEMES: - theme_colors.update(BUILT_IN_THEMES[theme]) - - # Generate styles for each widget type - for widget_type, rules in self._widget_rules.items(): - if not rules: - continue - - # Group rules by state - base_rules: list[StyleRule] = [] - hover_rules: list[StyleRule] = [] - pressed_rules: list[StyleRule] = [] - focus_rules: list[StyleRule] = [] - disabled_rules: list[StyleRule] = [] - - for rule in rules: - if rule.state == "hover": - hover_rules.append(rule) - elif rule.state == "active" or rule.state == "pressed": - pressed_rules.append(rule) - elif rule.state == "focus": - focus_rules.append(rule) - elif rule.state == "disabled": - disabled_rules.append(rule) - else: - base_rules.append(rule) - - # Generate base selector - if base_rules: - selector = widget_type - props = self._generate_properties(base_rules) - if props: - qss_parts.append(f"{selector} {{\n{props}}}") - - # Generate hover state - if hover_rules: - selector = f"{widget_type}:hover" - props = self._generate_properties(hover_rules) - if props: - qss_parts.append(f"{selector} {{\n{props}}}") - - # Generate pressed/active state - if pressed_rules: - selector = f"{widget_type}:pressed" - props = self._generate_properties(pressed_rules) - if props: - qss_parts.append(f"{selector} {{\n{props}}}") - - # Generate focus state - if focus_rules: - selector = f"{widget_type}:focus" - props = self._generate_properties(focus_rules) - if props: - qss_parts.append(f"{selector} {{\n{props}}}") - - # Generate disabled state - if disabled_rules: - selector = f"{widget_type}:disabled" - props = self._generate_properties(disabled_rules) - if props: - qss_parts.append(f"{selector} {{\n{props}}}") - - return "\n\n".join(qss_parts) - - def _generate_properties(self, rules: list[StyleRule]) -> str: - """ - Generate QSS properties from rules. - - Args: - rules: List of StyleRule objects - - Returns: - Formatted QSS properties string - """ - lines: list[str] = [] - - for rule in rules: - qss_prop = self._css_to_qss_property(rule.property) - if qss_prop is None: - continue # Skip unsupported properties - - qss_value = self._convert_value(rule.value) - lines.append(f" {qss_prop}: {qss_value};") - - if lines: - return "\n".join(lines) + "\n" - return "" - - def reset(self) -> None: - """Clear all registered rules.""" - self._rules.clear() - self._widget_rules.clear() - - @property - def rule_count(self) -> int: - """Get number of registered rules.""" - return len(self._rules) diff --git a/src/pyui/style/resolver.py b/src/pyui/style/resolver.py deleted file mode 100644 index 0e57dcd..0000000 --- a/src/pyui/style/resolver.py +++ /dev/null @@ -1,694 +0,0 @@ -""" -ZoltCSS Style Resolver — Component style resolution engine. - -Resolves component + variant + state combinations into StyleRule collections. -This is where components define their visual appearance using the token system. -""" - -from __future__ import annotations - -from typing import Any - -from pyui.style.rules import ( - StyleRule, - create_base_rules, - create_state_rules, - create_responsive_rules, - token, -) - - -class StyleResolver: - """ - Resolves component styling into StyleRule collections. - - Each component has a resolver method that returns all StyleRules - needed to render that component in a given variant and state. - - Example: - resolver = StyleResolver() - rules = resolver.resolve_button(variant="primary", size="md", disabled=False) - """ - - def __init__(self): - self._component_resolvers: dict[str, callable] = { - "button": self.resolve_button, - "text": self.resolve_text, - "heading": self.resolve_heading, - "flex": self.resolve_flex, - "grid": self.resolve_grid, - "box": self.resolve_box, - "input": self.resolve_input, - "badge": self.resolve_badge, - "card": self.resolve_card, - "divider": self.resolve_divider, - } - - def resolve( - self, - component: str, - **kwargs: Any, - ) -> list[StyleRule]: - """ - Resolve styles for a component. - - Args: - component: Component name (e.g., "button", "text") - **kwargs: Component-specific parameters (variant, size, etc.) - - Returns: - List of StyleRule objects - - Raises: - ValueError: If component is not registered - """ - if component not in self._component_resolvers: - raise ValueError( - f"Unknown component: {component}. " - f"Registered components: {list(self._component_resolvers.keys())}" - ) - - resolver = self._component_resolvers[component] - return resolver(**kwargs) - - def register_component( - self, - name: str, - resolver_func: callable, - ) -> None: - """ - Register a custom component resolver. - - Args: - name: Component name - resolver_func: Function that resolves component styles - """ - self._component_resolvers[name] = resolver_func - - # ── Component Resolvers ──────────────────────────────────────────────── - - def resolve_button( - self, - variant: str | None = "primary", - size: str | None = "md", - disabled: bool = False, - ) -> list[StyleRule]: - """Resolve Button component styles.""" - rules: list[StyleRule] = [] - - # Base button styles - base_rules = [ - ("display", "inline-flex"), - ("align-items", "center"), - ("justify-content", "center"), - ("gap", token("space-2")), - ("font-family", token("font-family")), - ("font-weight", token("font-weight-medium")), - ("letter-spacing", token("letter-spacing-tight")), - ("transition-property", "all"), - ("transition-duration", token("transition-normal")), - ("transition-timing-function", token("transition-timing")), - ("cursor", "pointer"), - ("border", "none"), - ("outline", "none"), - ] - rules.extend(create_base_rules("button", base_rules)) - - # Size variants - size_rules = { - "xs": [("height", token("space-7")), ("padding-left", token("space-3")), ("padding-right", token("space-3")), ("font-size", token("font-size-xs")), ("border-radius", token("radius-md"))], - "sm": [("height", token("space-8")), ("padding-left", token("space-4")), ("padding-right", token("space-4")), ("font-size", token("font-size-sm")), ("border-radius", token("radius-lg"))], - "md": [("height", token("space-9")), ("padding-left", token("space-4")), ("padding-right", token("space-4")), ("font-size", token("font-size-sm")), ("border-radius", token("radius-lg"))], - "lg": [("height", token("space-11")), ("padding-left", token("space-6")), ("padding-right", token("space-6")), ("font-size", token("font-size-md")), ("border-radius", token("radius-xl"))], - "xl": [("height", token("space-12")), ("padding-left", token("space-8")), ("padding-right", token("space-8")), ("font-size", token("font-size-md")), ("border-radius", token("radius-xl"))], - } - - if size in size_rules: - rules.extend(create_base_rules("button", size_rules[size])) - - # Color variants - variant_styles = { - "primary": [ - ("background-color", token("color-primary")), - ("color", token("color-primary-fg")), - ("box-shadow", token("shadow-sm")), - ], - "secondary": [ - ("background-color", token("color-secondary")), - ("color", token("color-secondary-fg")), - ], - "ghost": [ - ("background-color", token("color-bg")), - ("color", token("color-text")), - ("border", f"1px solid {token('color-border')}"), - ("box-shadow", token("shadow-sm")), - ], - "danger": [ - ("background-color", token("color-danger")), - ("color", token("color-danger-fg")), - ("box-shadow", token("shadow-sm")), - ], - "success": [ - ("background-color", token("color-success")), - ("color", token("color-success-fg")), - ("box-shadow", token("shadow-sm")), - ], - "link": [ - ("background-color", "transparent"), - ("color", token("color-text")), - ("text-decoration", "underline"), - ("text-underline-offset", "4px"), - ("padding", "0"), - ("height", "auto"), - ("box-shadow", "none"), - ], - } - - if variant in variant_styles: - rules.extend(create_base_rules("button", variant_styles[variant])) - - # Hover states - hover_styles = { - "primary": [ - ("background-color", token("color-primary-hover")), - ("box-shadow", token("shadow-md")), - ("transform", "translateY(-1px)"), - ], - "secondary": [ - ("background-color", token("color-secondary-hover")), - ("transform", "translateY(-1px)"), - ], - "ghost": [ - ("background-color", token("color-surface")), - ("border-color", token("color-border-strong")), - ("transform", "translateY(-1px)"), - ], - "danger": [ - ("background-color", token("color-danger-hover")), - ("box-shadow", token("shadow-md")), - ("transform", "translateY(-1px)"), - ], - "success": [ - ("background-color", token("color-success-hover")), - ("transform", "translateY(-1px)"), - ], - "link": [ - ("text-decoration-color", token("color-text")), - ], - } - - if variant and variant in hover_styles: - rules.extend(create_state_rules("button", "hover", hover_styles[variant])) - - # Disabled state - if disabled: - disabled_rules = [ - ("opacity", "0.4"), - ("cursor", "not-allowed"), - ("pointer-events", "none"), - ("filter", "saturate(0)"), - ] - rules.extend(create_state_rules("button", "disabled", disabled_rules)) - - # Active state (press) - active_rules = [ - ("transform", "scale(0.97)"), - ] - rules.extend(create_state_rules("button", "active", active_rules)) - - # Focus state - focus_rules = [ - ("box-shadow", f"0 0 0 2px {token('color-bg')}, 0 0 0 4px {token('color-primary')}"), - ] - rules.extend(create_state_rules("button", "focus", focus_rules)) - - return rules - - def resolve_text( - self, - variant: str | None = None, - size: str | None = None, - truncate: bool = False, - ) -> list[StyleRule]: - """Resolve Text component styles.""" - rules: list[StyleRule] = [] - - # Base text styles - base_rules = [ - ("font-family", token("font-family")), - ("line-height", token("line-height-normal")), - ("color", token("color-text")), - ] - rules.extend(create_base_rules("text", base_rules)) - - # Size variants - size_rules = { - "xs": [("font-size", token("font-size-xs"))], - "sm": [("font-size", token("font-size-sm"))], - "md": [("font-size", token("font-size-md"))], - "lg": [("font-size", token("font-size-lg"))], - "xl": [("font-size", token("font-size-xl"))], - "2xl": [("font-size", token("font-size-2xl"))], - } - - if size in size_rules: - rules.extend(create_base_rules("text", size_rules[size])) - - # Variant styles - variant_styles = { - "muted": [ - ("color", token("color-text-muted")), - ("line-height", token("line-height-relaxed")), - ], - "error": [ - ("color", token("color-danger")), - ("font-size", token("font-size-sm")), - ], - "success": [ - ("color", token("color-success")), - ("font-size", token("font-size-sm")), - ], - "caption": [ - ("font-size", token("font-size-xs")), - ("color", token("color-text-muted")), - ("text-transform", "uppercase"), - ("letter-spacing", token("letter-spacing-wider")), - ("font-weight", token("font-weight-medium")), - ], - } - - if variant in variant_styles: - rules.extend(create_base_rules("text", variant_styles[variant])) - - # Truncate - if truncate: - truncate_rules = [ - ("overflow", "hidden"), - ("text-overflow", "ellipsis"), - ("white-space", "nowrap"), - ] - rules.extend(create_base_rules("text", truncate_rules)) - - return rules - - def resolve_heading( - self, - level: int = 2, - variant: str | None = None, - ) -> list[StyleRule]: - """Resolve Heading component styles.""" - rules: list[StyleRule] = [] - - # Level-based styles - level_styles = { - 1: [ - ("font-size", token("font-size-4xl")), - ("font-weight", token("font-weight-bold")), - ("letter-spacing", token("letter-spacing-tight")), - ("line-height", "1.1"), - ], - 2: [ - ("font-size", token("font-size-3xl")), - ("font-weight", token("font-weight-bold")), - ("letter-spacing", token("letter-spacing-tight")), - ("line-height", "1.2"), - ], - 3: [ - ("font-size", token("font-size-2xl")), - ("font-weight", token("font-weight-semibold")), - ("letter-spacing", token("letter-spacing-tight")), - ("line-height", "1.3"), - ], - 4: [ - ("font-size", token("font-size-xl")), - ("font-weight", token("font-weight-semibold")), - ("letter-spacing", token("letter-spacing-tight")), - ("line-height", "1.4"), - ], - 5: [ - ("font-size", token("font-size-lg")), - ("font-weight", token("font-weight-medium")), - ("letter-spacing", token("letter-spacing-tight")), - ], - 6: [ - ("font-size", token("font-size-md")), - ("font-weight", token("font-weight-medium")), - ("letter-spacing", token("letter-spacing-tight")), - ], - } - - if level in level_styles: - rules.extend(create_base_rules("heading", level_styles[level])) - - # Default color - rules.extend(create_base_rules("heading", [ - ("color", token("color-text")), - ])) - - # Variant styles - if variant == "muted": - rules.extend(create_base_rules("heading", [ - ("color", token("color-text-muted")), - ("font-weight", token("font-weight-normal")), - ])) - - return rules - - def resolve_flex( - self, - direction: str = "row", - align: str = "center", - justify: str = "start", - gap: int = 4, - wrap: bool = False, - ) -> list[StyleRule]: - """Resolve Flex container styles.""" - rules: list[StyleRule] = [] - - # Base flex - rules.extend(create_base_rules("flex", [ - ("display", "flex"), - ])) - - # Direction - direction_map = { - "row": "row", - "col": "column", - "row-reverse": "row-reverse", - "col-reverse": "column-reverse", - } - rules.extend(create_base_rules("flex", [ - ("flex-direction", direction_map.get(direction, "row")), - ])) - - # Alignment - align_map = { - "start": "flex-start", - "center": "center", - "end": "flex-end", - "baseline": "baseline", - "stretch": "stretch", - } - rules.extend(create_base_rules("flex", [ - ("align-items", align_map.get(align, "center")), - ])) - - # Justify - justify_map = { - "start": "flex-start", - "center": "center", - "end": "flex-end", - "between": "space-between", - "around": "space-around", - "evenly": "space-evenly", - } - rules.extend(create_base_rules("flex", [ - ("justify-content", justify_map.get(justify, "start")), - ])) - - # Gap - gap_token = f"space-{gap}" if gap in [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] else "space-4" - rules.extend(create_base_rules("flex", [ - ("gap", token(gap_token)), - ])) - - # Wrap - if wrap: - rules.extend(create_base_rules("flex", [ - ("flex-wrap", "wrap"), - ])) - - return rules - - def resolve_grid( - self, - cols: int | str = 1, - gap: int = 4, - ) -> list[StyleRule]: - """Resolve Grid container styles.""" - rules: list[StyleRule] = [] - - # Base grid - rules.extend(create_base_rules("grid", [ - ("display", "grid"), - ])) - - # Columns - if isinstance(cols, int): - rules.extend(create_base_rules("grid", [ - ("grid-template-columns", f"repeat({cols}, 1fr)"), - ])) - else: - rules.extend(create_base_rules("grid", [ - ("grid-template-columns", cols), - ])) - - # Gap - gap_token = f"space-{gap}" if gap in [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] else "space-4" - rules.extend(create_base_rules("grid", [ - ("gap", token(gap_token)), - ])) - - return rules - - def resolve_box( - self, - padding: int | None = None, - margin: int | None = None, - background: str | None = None, - border_radius: str | None = None, - shadow: str | None = None, - ) -> list[StyleRule]: - """Resolve Box container styles.""" - rules: list[StyleRule] = [] - - # Base box - rules.extend(create_base_rules("box", [ - ("display", "block"), - ])) - - # Padding - if padding is not None: - padding_token = f"space-{padding}" if padding in [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] else "space-4" - rules.extend(create_base_rules("box", [ - ("padding", token(padding_token)), - ])) - - # Margin - if margin is not None: - margin_token = f"space-{margin}" if margin in [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] else "space-4" - rules.extend(create_base_rules("box", [ - ("margin", token(margin_token)), - ])) - - # Background - if background: - if background.startswith("color-"): - rules.extend(create_base_rules("box", [ - ("background-color", token(background)), - ])) - else: - rules.extend(create_base_rules("box", [ - ("background-color", background), - ])) - - # Border radius - if border_radius: - radius_token = f"radius-{border_radius}" if border_radius in ["none", "sm", "md", "lg", "xl", "2xl", "full"] else "radius-md" - rules.extend(create_base_rules("box", [ - ("border-radius", token(radius_token)), - ])) - - # Shadow - if shadow: - shadow_token = f"shadow-{shadow}" if shadow in ["xs", "sm", "md", "lg", "xl", "2xl", "inner", "none"] else "shadow-md" - rules.extend(create_base_rules("box", [ - ("box-shadow", token(shadow_token)), - ])) - - return rules - - def resolve_input( - self, - variant: str | None = None, - disabled: bool = False, - error: bool = False, - ) -> list[StyleRule]: - """Resolve Input component styles.""" - rules: list[StyleRule] = [] - - # Base input styles - base_rules = [ - ("display", "block"), - ("width", "100%"), - ("font-family", token("font-family")), - ("font-size", token("font-size-sm")), - ("padding-left", token("space-4")), - ("padding-right", token("space-4")), - ("padding-top", token("space-3")), - ("padding-bottom", token("space-3")), - ("border-radius", token("radius-lg")), - ("border", f"1px solid {token('color-border')}"), - ("background-color", token("color-bg")), - ("color", token("color-text")), - ("transition-property", "all"), - ("transition-duration", token("transition-fast")), - ("transition-timing-function", token("transition-timing")), - ("outline", "none"), - ] - rules.extend(create_base_rules("input", base_rules)) - - # Hover state - rules.extend(create_state_rules("input", "hover", [ - ("border-color", token("color-border-strong")), - ])) - - # Focus state - rules.extend(create_state_rules("input", "focus", [ - ("border-color", token("color-border-strong")), - ("box-shadow", f"0 0 0 3px {token('color-primary-subtle')}"), - ])) - - # Error state - if error: - rules.extend(create_base_rules("input", [ - ("border-color", token("color-danger")), - ("box-shadow", f"0 0 0 3px {token('color-danger-subtle')}"), - ])) - - # Disabled state - if disabled: - rules.extend(create_state_rules("input", "disabled", [ - ("opacity", "0.5"), - ("cursor", "not-allowed"), - ("background-color", token("color-surface")), - ])) - - return rules - - def resolve_badge( - self, - variant: str | None = "primary", - ) -> list[StyleRule]: - """Resolve Badge component styles.""" - rules: list[StyleRule] = [] - - # Base badge styles - base_rules = [ - ("display", "inline-flex"), - ("align-items", "center"), - ("gap", token("space-1")), - ("padding-left", token("space-3")), - ("padding-right", token("space-3")), - ("padding-top", "2px"), - ("padding-bottom", "2px"), - ("border-radius", token("radius-full")), - ("font-size", token("font-size-xs")), - ("font-weight", token("font-weight-medium")), - ("letter-spacing", token("letter-spacing-wide")), - ("border", "1px solid"), - ] - rules.extend(create_base_rules("badge", base_rules)) - - # Variant styles - variant_styles = { - "primary": [ - ("background-color", token("color-primary-subtle")), - ("color", token("color-primary")), - ("border-color", f"{token('color-primary')}80"), # 50% opacity - ], - "secondary": [ - ("background-color", token("color-surface")), - ("color", token("color-text-muted")), - ("border-color", token("color-border")), - ], - "success": [ - ("background-color", token("color-success-subtle")), - ("color", token("color-success")), - ("border-color", f"{token('color-success')}80"), - ], - "danger": [ - ("background-color", token("color-danger-subtle")), - ("color", token("color-danger")), - ("border-color", f"{token('color-danger')}80"), - ], - "warning": [ - ("background-color", token("color-warning-subtle")), - ("color", token("color-warning")), - ("border-color", f"{token('color-warning')}80"), - ], - "info": [ - ("background-color", token("color-info-subtle")), - ("color", token("color-info")), - ("border-color", f"{token('color-info')}80"), - ], - } - - if variant in variant_styles: - rules.extend(create_base_rules("badge", variant_styles[variant])) - - return rules - - def resolve_card( - self, - padding: int = 6, - shadow: str = "md", - border_radius: str = "xl", - ) -> list[StyleRule]: - """Resolve Card component styles.""" - rules: list[StyleRule] = [] - - # Base card styles - base_rules = [ - ("display", "block"), - ("background-color", token("color-surface")), - ("border", f"1px solid {token('color-border')}"), - ] - rules.extend(create_base_rules("card", base_rules)) - - # Padding - padding_token = f"space-{padding}" if padding in [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] else "space-6" - rules.extend(create_base_rules("card", [ - ("padding", token(padding_token)), - ])) - - # Shadow - shadow_token = f"shadow-{shadow}" if shadow in ["xs", "sm", "md", "lg", "xl", "2xl", "inner", "none"] else "shadow-md" - rules.extend(create_base_rules("card", [ - ("box-shadow", token(shadow_token)), - ])) - - # Border radius - radius_token = f"radius-{border_radius}" if border_radius in ["none", "sm", "md", "lg", "xl", "2xl", "full"] else "radius-xl" - rules.extend(create_base_rules("card", [ - ("border-radius", token(radius_token)), - ])) - - return rules - - def resolve_divider( - self, - orientation: str = "horizontal", - color: str | None = None, - ) -> list[StyleRule]: - """Resolve Divider component styles.""" - rules: list[StyleRule] = [] - - # Base divider - rules.extend(create_base_rules("divider", [ - ("background-color", color or token("color-border")), - ("border", "none"), - ])) - - if orientation == "horizontal": - rules.extend(create_base_rules("divider", [ - ("width", "100%"), - ("height", "1px"), - ])) - else: - rules.extend(create_base_rules("divider", [ - ("height", "100%"), - ("width", "1px"), - ])) - - return rules diff --git a/src/pyui/style/rich_generator.py b/src/pyui/style/rich_generator.py deleted file mode 100644 index 0d3dfbd..0000000 --- a/src/pyui/style/rich_generator.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -Rich Style Generator — Converts StyleRules to Rich Terminal Styles. - -Generates Rich library Style objects for CLI rendering from ZoltCSS StyleRules. -""" - -from __future__ import annotations - -from typing import Any - -from pyui.style.rules import StyleRule -from pyui.style.tokens import TOKENS, BUILT_IN_THEMES - - -class RichStyleGenerator: - """ - Generates Rich terminal styles from StyleRule collections. - - Maps CSS properties to Rich Style attributes for CLI rendering. - """ - - # Color name mapping (hex to Rich color names) - HEX_TO_RICH_COLOR = { - "#000000": "black", - "#808080": "grey", - "#C0C0C0": "white", - "#FF0000": "red", - "#00FF00": "green", - "#0000FF": "blue", - "#FFFF00": "yellow", - "#00FFFF": "cyan", - "#FF00FF": "magenta", - "#FFFFFF": "white", - "#111827": "grey93", - "#6B7280": "grey50", - "#9CA3AF": "grey60", - "#EF4444": "red", - "#10B981": "green", - "#F59E0B": "yellow", - "#3B82F6": "blue", - "#6C63FF": "deep_pink3", - "#F3F4F6": "grey94", - "#E5E7EB": "grey80", - "#D1D5DB": "grey70", - } - - def __init__(self): - self._rules: list[StyleRule] = [] - self._component_rules: dict[str, list[StyleRule]] = {} - - def register_rule(self, component: str, rule: StyleRule) -> None: - """ - Register a style rule for a component. - - Args: - component: Component name - rule: StyleRule object - """ - if component not in self._component_rules: - self._component_rules[component] = [] - self._component_rules[component].append(rule) - self._rules.append(rule) - - def _hex_to_rich_color(self, hex_color: str) -> str: - """ - Convert hex color to Rich color name. - - Falls back to the hex value if no mapping exists. - """ - return self.HEX_TO_RICH_COLOR.get(hex_color, hex_color) - - def _resolve_token(self, token_ref: str) -> str: - """ - Resolve a token reference to its value. - - Args: - token_ref: Token reference like "var(--color-primary)" - - Returns: - Resolved value - """ - if token_ref.startswith("var(--"): - token_name = token_ref[6:-1] - return TOKENS.get(token_name, token_ref) - return token_ref - - def generate_style( - self, - component: str, - state: str | None = None, - ) -> dict[str, Any]: - """ - Generate Rich Style kwargs for a component. - - Args: - component: Component name - state: Optional state ("hover", "disabled", etc.) - - Returns: - Dict of Rich Style constructor kwargs - """ - rules = self._component_rules.get(component, []) - - # Filter by state - if state: - rules = [r for r in rules if r.state == state] - else: - rules = [r for r in rules if r.state is None] - - style_kwargs: dict[str, Any] = {} - - for rule in rules: - value = self._resolve_token(rule.value) - - if rule.property == "color": - style_kwargs["color"] = self._hex_to_rich_color(value) - - elif rule.property == "background-color": - style_kwargs["bgcolor"] = self._hex_to_rich_color(value) - - elif rule.property == "font-weight": - if value in ["700", "800", "900", "bold"]: - style_kwargs["bold"] = True - elif value in ["400", "normal"]: - style_kwargs["bold"] = False - - elif rule.property == "text-decoration": - if value == "underline": - style_kwargs["underline"] = True - elif value == "none": - style_kwargs["underline"] = False - - elif rule.property == "font-style": - if value == "italic": - style_kwargs["italic"] = True - - elif rule.property == "opacity": - # Rich doesn't support opacity directly - pass - - return style_kwargs - - def get_component_classes(self, component: str) -> list[str]: - """ - Get all registered classes/variants for a component. - - Args: - component: Component name - - Returns: - List of variant/state names - """ - rules = self._component_rules.get(component, []) - variants = set() - - for rule in rules: - if rule.variant: - variants.add(rule.variant) - if rule.state: - variants.add(rule.state) - - return sorted(variants) - - def reset(self) -> None: - """Clear all registered rules.""" - self._rules.clear() - self._component_rules.clear() - - @property - def rule_count(self) -> int: - """Get number of registered rules.""" - return len(self._rules) - - -def create_terminal_style(style_kwargs: dict[str, Any]) -> str: - """ - Create a Rich Style markup string from kwargs. - - This is a helper for creating inline Rich markup. - - Args: - style_kwargs: Style constructor kwargs - - Returns: - Rich markup string (e.g., "[bold red on blue]") - """ - parts = [] - - if style_kwargs.get("bold"): - parts.append("bold") - if style_kwargs.get("italic"): - parts.append("italic") - if style_kwargs.get("underline"): - parts.append("underline") - if "color" in style_kwargs: - parts.append(style_kwargs["color"]) - if "bgcolor" in style_kwargs: - parts.append(f"on {style_kwargs['bgcolor']}") - - if parts: - return f"[{' '.join(parts)}]" - return "" diff --git a/src/pyui/style/rules.py b/src/pyui/style/rules.py deleted file mode 100644 index f3c7bc0..0000000 --- a/src/pyui/style/rules.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -ZoltCSS Style Rules — The building blocks of the styling system. - -StyleRule dataclass and helper functions for creating style declarations. -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - - -@dataclass -class StyleRule: - """ - A single style rule declaration. - - Represents one CSS property:value pair with optional modifiers - for responsive breakpoints, dark mode, and component states. - - Example: - StyleRule( - property="background-color", - value="var(--color-primary)", - breakpoint="md", # only applies on medium screens and up - dark_mode="var(--color-primary-hover)", # different value in dark mode - state="hover", # only applies on hover - ) - """ - - property: str # CSS property name (e.g., "background-color") - value: str # CSS value (e.g., "var(--color-primary)" or "#6C63FF") - - # Modifiers - breakpoint: str | None = None # "sm", "md", "lg", "xl", "2xl" - dark_mode: str | None = None # Alternative value for dark mode - state: str | None = None # "hover", "active", "focus", "disabled" - - # Metadata - component: str | None = None # Which component this rule belongs to - variant: str | None = None # Component variant (e.g., "primary", "ghost") - - def __hash__(self) -> int: - """Make StyleRule hashable for deduplication.""" - return hash(( - self.property, - self.value, - self.breakpoint, - self.dark_mode, - self.state, - self.component, - self.variant, - )) - - def __eq__(self, other: object) -> bool: - """Check equality based on all fields.""" - if not isinstance(other, StyleRule): - return False - return ( - self.property == other.property - and self.value == other.value - and self.breakpoint == other.breakpoint - and self.dark_mode == other.dark_mode - and self.state == other.state - and self.component == other.component - and self.variant == other.variant - ) - - def to_css_declaration(self) -> str: - """Convert to CSS property: value string.""" - return f"{self.property}: {self.value};" - - def has_modifier(self) -> bool: - """Check if this rule has any modifiers (breakpoint, dark_mode, or state).""" - return bool(self.breakpoint or self.dark_mode or self.state) - - -def token(token_name: str) -> str: - """ - Helper to reference a token value using CSS custom properties. - - Converts a token name like "color-primary" to "var(--color-primary)". - - Args: - token_name: The token key from TOKENS dict - - Returns: - CSS var() reference string - - Example: - >>> token("color-primary") - 'var(--color-primary)' - >>> token("space-4") - 'var(--space-4)' - """ - return f"var(--{token_name})" - - -def rgb_from_hex(hex_color: str, alpha: float = 1.0) -> str: - """ - Convert hex color to RGB(A) format. - - Useful for creating semi-transparent versions of colors. - - Args: - hex_color: Hex color string (e.g., "#6C63FF") - alpha: Alpha value from 0.0 to 1.0 - - Returns: - RGB or RGBA string - - Example: - >>> rgb_from_hex("#6C63FF", 0.1) - 'rgba(108, 99, 255, 0.1)' - """ - hex_color = hex_color.lstrip("#") - if len(hex_color) == 3: - hex_color = "".join(c * 2 for c in hex_color) - - r = int(hex_color[0:2], 16) - g = int(hex_color[2:4], 16) - b = int(hex_color[4:6], 16) - - if alpha < 1.0: - return f"rgba({r}, {g}, {b}, {alpha})" - return f"rgb({r}, {g}, {b})" - - -# ── Pre-built Style Rule Collections ────────────────────────────────────────── - - -def create_base_rules(component: str, rules: list[tuple[str, str]]) -> list[StyleRule]: - """ - Create a list of base style rules for a component. - - Args: - component: Component name - rules: List of (property, value) tuples - - Returns: - List of StyleRule objects - """ - return [ - StyleRule(property=prop, value=value, component=component) - for prop, value in rules - ] - - -def create_state_rules( - component: str, - state: str, - rules: list[tuple[str, str]], - variant: str | None = None, -) -> list[StyleRule]: - """ - Create style rules that apply only in a specific state (hover, active, etc.). - - Args: - component: Component name - state: State name ("hover", "active", "focus", "disabled") - rules: List of (property, value) tuples - variant: Optional component variant - - Returns: - List of StyleRule objects with state modifier - """ - return [ - StyleRule( - property=prop, - value=value, - component=component, - state=state, - variant=variant, - ) - for prop, value in rules - ] - - -def create_responsive_rules( - component: str, - breakpoint: str, - rules: list[tuple[str, str]], - variant: str | None = None, -) -> list[StyleRule]: - """ - Create style rules that apply only at a specific breakpoint. - - Args: - component: Component name - breakpoint: Breakpoint name ("sm", "md", "lg", "xl", "2xl") - rules: List of (property, value) tuples - variant: Optional component variant - - Returns: - List of StyleRule objects with breakpoint modifier - """ - return [ - StyleRule( - property=prop, - value=value, - component=component, - breakpoint=breakpoint, - variant=variant, - ) - for prop, value in rules - ] diff --git a/src/pyui/style/tokens.py b/src/pyui/style/tokens.py deleted file mode 100644 index d8134c7..0000000 --- a/src/pyui/style/tokens.py +++ /dev/null @@ -1,347 +0,0 @@ -""" -ZoltCSS Token System — The Design System Foundation - -Every visual value in the entire framework lives in one place. -No raw hex codes, pixel values, or font strings appear anywhere else. - -This is the v1.5 token system as specified in the PRD. -""" - -from __future__ import annotations - -# ── Color Palette ───────────────────────────────────────────────────────────── -# Semantic names — not "blue-500", but "primary", "danger", "text" - -TOKENS: dict[str, str] = { - # Semantic colors - "color-primary": "#6C63FF", - "color-primary-hover": "#5A52E0", - "color-primary-active": "#4840CC", - "color-primary-fg": "#FFFFFF", # foreground (text) on primary bg - "color-primary-subtle": "#EEF2FF", # very light primary tint - - "color-secondary": "#F3F4F6", - "color-secondary-hover": "#E5E7EB", - "color-secondary-fg": "#111827", - - "color-bg": "#FFFFFF", - "color-surface": "#F9FAFB", - "color-surface-2": "#F3F4F6", - "color-surface-3": "#E5E7EB", - - "color-border": "#E5E7EB", - "color-border-strong": "#D1D5DB", - - "color-text": "#111827", - "color-text-muted": "#6B7280", - "color-text-inverse": "#FFFFFF", - "color-text-disabled": "#9CA3AF", - - "color-success": "#10B981", - "color-success-hover": "#059669", - "color-success-fg": "#FFFFFF", - "color-success-subtle": "#ECFDF5", - - "color-warning": "#F59E0B", - "color-warning-hover": "#D97706", - "color-warning-fg": "#FFFFFF", - "color-warning-subtle": "#FFFBEB", - - "color-danger": "#EF4444", - "color-danger-hover": "#DC2626", - "color-danger-fg": "#FFFFFF", - "color-danger-subtle": "#FEF2F2", - - "color-info": "#3B82F6", - "color-info-hover": "#2563EB", - "color-info-fg": "#FFFFFF", - "color-info-subtle": "#EFF6FF", - - # Typography - "font-family": "Inter, system-ui, -apple-system, sans-serif", - "font-family-mono": "JetBrains Mono, Fira Code, monospace", - - "font-size-xs": "12px", - "font-size-sm": "14px", - "font-size-md": "16px", - "font-size-lg": "18px", - "font-size-xl": "20px", - "font-size-2xl": "24px", - "font-size-3xl": "30px", - "font-size-4xl": "36px", - "font-size-5xl": "48px", - "font-size-6xl": "60px", - - "font-weight-normal": "400", - "font-weight-medium": "500", - "font-weight-semibold": "600", - "font-weight-bold": "700", - "font-weight-black": "900", - - "line-height-tight": "1.25", - "line-height-normal": "1.5", - "line-height-relaxed": "1.75", - - "letter-spacing-tight": "-0.025em", - "letter-spacing-normal": "0", - "letter-spacing-wide": "0.025em", - "letter-spacing-wider": "0.05em", - - # Spacing (8px base grid) - "space-0": "0", - "space-1": "4px", - "space-2": "8px", - "space-3": "12px", - "space-4": "16px", - "space-5": "20px", - "space-6": "24px", - "space-8": "32px", - "space-10": "40px", - "space-12": "48px", - "space-16": "64px", - "space-20": "80px", - "space-24": "96px", - - # Shape / Border Radius - "radius-none": "0", - "radius-sm": "4px", - "radius-md": "8px", - "radius-lg": "12px", - "radius-xl": "16px", - "radius-2xl": "24px", - "radius-full": "9999px", - - # Shadows - "shadow-xs": "0 1px 2px rgba(0, 0, 0, 0.05)", - "shadow-sm": "0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06)", - "shadow-md": "0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06)", - "shadow-lg": "0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05)", - "shadow-xl": "0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04)", - "shadow-2xl": "0 25px 50px rgba(0, 0, 0, 0.25)", - "shadow-inner": "inset 0 2px 4px rgba(0, 0, 0, 0.06)", - "shadow-none": "none", - - # Transitions - "transition-fast": "100ms ease", - "transition-normal": "200ms ease", - "transition-slow": "300ms ease", - "transition-duration": "200ms", - "transition-timing": "ease", - - # Breakpoints (for responsive system) - "breakpoint-sm": "640px", - "breakpoint-md": "768px", - "breakpoint-lg": "1024px", - "breakpoint-xl": "1280px", - "breakpoint-2xl": "1536px", - - # Z-index scale - "z-index-hide": "-1", - "z-index-auto": "auto", - "z-index-base": "0", - "z-index-dropdown": "1000", - "z-index-sticky": "1100", - "z-index-fixed": "1200", - "z-index-modal-backdrop": "1300", - "z-index-modal": "1400", - "z-index-popover": "1500", - "z-index-tooltip": "1600", - "z-index-toast": "1700", -} - -# ── Built-in Theme Overrides ───────────────────────────────────────────────── - -DARK_OVERRIDES: dict[str, str] = { - "color-primary": "#7C73FF", - "color-primary-hover": "#6A61F0", - "color-primary-active": "#5A52E0", - "color-primary-subtle": "#1E1B4B", - - "color-bg": "#0F172A", - "color-surface": "#1E293B", - "color-surface-2": "#334155", - "color-surface-3": "#475569", - - "color-border": "#334155", - "color-border-strong": "#475569", - - "color-text": "#F1F5F9", - "color-text-muted": "#94A3B8", - "color-text-inverse": "#0F172A", - "color-text-disabled": "#64748B", - - "color-secondary": "#1E293B", - "color-secondary-hover": "#334155", - "color-secondary-fg": "#F1F5F9", - - "color-success-subtle": "#064E3B", - "color-warning-subtle": "#78350F", - "color-danger-subtle": "#7F1D1D", - "color-info-subtle": "#1E3A8A", -} - -OCEAN_OVERRIDES: dict[str, str] = { - "color-primary": "#0EA5E9", - "color-primary-hover": "#0284C7", - "color-primary-active": "#0369A1", - "color-primary-subtle": "#E0F2FE", - - "color-bg": "#F0F9FF", - "color-surface": "#E0F2FE", - "color-surface-2": "#BAE6FD", - "color-surface-3": "#7DD3FC", - - "color-border": "#BAE6FD", - "color-border-strong": "#7DD3FC", - - "color-text": "#0C4A6E", - "color-text-muted": "#0369A1", - - "color-secondary": "#E0F2FE", - "color-secondary-hover": "#BAE6FD", - "color-secondary-fg": "#0C4A6E", -} - -SUNSET_OVERRIDES: dict[str, str] = { - "color-primary": "#F97316", - "color-primary-hover": "#EA580C", - "color-primary-active": "#C2410C", - "color-primary-subtle": "#FFF7ED", - - "color-bg": "#FFF7ED", - "color-surface": "#FFEDD5", - "color-surface-2": "#FED7AA", - "color-surface-3": "#FDBA74", - - "color-border": "#FDBA74", - "color-border-strong": "#FB923C", - - "color-text": "#431407", - "color-text-muted": "#9A3412", - - "color-secondary": "#FFEDD5", - "color-secondary-hover": "#FED7AA", - "color-secondary-fg": "#431407", -} - -FOREST_OVERRIDES: dict[str, str] = { - "color-primary": "#10B981", - "color-primary-hover": "#059669", - "color-primary-active": "#047857", - "color-primary-subtle": "#ECFDF5", - - "color-bg": "#F0FDF4", - "color-surface": "#DCFCE7", - "color-surface-2": "#BBF7D0", - "color-surface-3": "#86EFAC", - - "color-border": "#86EFAC", - "color-border-strong": "#4ADE80", - - "color-text": "#052E16", - "color-text-muted": "#166534", - - "color-secondary": "#DCFCE7", - "color-secondary-hover": "#BBF7D0", - "color-secondary-fg": "#052E16", -} - -ROSE_OVERRIDES: dict[str, str] = { - "color-primary": "#F43F5E", - "color-primary-hover": "#E11D48", - "color-primary-active": "#BE123C", - "color-primary-subtle": "#FFF1F2", - - "color-bg": "#FFF1F2", - "color-surface": "#FFE4E6", - "color-surface-2": "#FECDD3", - "color-surface-3": "#FDA4AF", - - "color-border": "#FDA4AF", - "color-border-strong": "#FB7185", - - "color-text": "#4C0519", - "color-text-muted": "#9F1239", - - "color-secondary": "#FFE4E6", - "color-secondary-hover": "#FECDD3", - "color-secondary-fg": "#4C0519", -} - -MIDNIGHT_OVERRIDES: dict[str, str] = { - "color-primary": "#8B5CF6", - "color-primary-hover": "#7C3AED", - "color-primary-active": "#6D28D9", - "color-primary-subtle": "#1E1B4B", - - "color-bg": "#0F172A", - "color-surface": "#1E293B", - "color-surface-2": "#334155", - "color-surface-3": "#475569", - - "color-border": "#475569", - "color-border-strong": "#64748B", - - "color-text": "#F8FAFC", - "color-text-muted": "#94A3B8", - - "color-secondary": "#334155", - "color-secondary-hover": "#475569", - "color-secondary-fg": "#F8FAFC", -} - -SAND_OVERRIDES: dict[str, str] = { - "color-primary": "#D97706", - "color-primary-hover": "#B45309", - "color-primary-active": "#92400E", - "color-primary-subtle": "#FFFBEB", - - "color-bg": "#FFFBEB", - "color-surface": "#FEF3C7", - "color-surface-2": "#FDE68A", - "color-surface-3": "#FCD34D", - - "color-border": "#FCD34D", - "color-border-strong": "#FBBF24", - - "color-text": "#451a03", - "color-text-muted": "#92400E", - - "color-secondary": "#FEF3C7", - "color-secondary-hover": "#FDE68A", - "color-secondary-fg": "#451a03", -} - -MONO_OVERRIDES: dict[str, str] = { - "color-primary": "#171717", - "color-primary-hover": "#262626", - "color-primary-active": "#404040", - "color-primary-subtle": "#F5F5F5", - - "color-bg": "#FFFFFF", - "color-surface": "#FAFAFA", - "color-surface-2": "#F5F5F5", - "color-surface-3": "#E5E5E5", - - "color-border": "#E5E5E5", - "color-border-strong": "#D4D4D4", - - "color-text": "#171717", - "color-text-muted": "#737373", - - "color-secondary": "#F5F5F5", - "color-secondary-hover": "#E5E5E5", - "color-secondary-fg": "#171717", -} - -BUILT_IN_THEMES: dict[str, dict[str, str]] = { - "light": {}, - "dark": DARK_OVERRIDES, - "ocean": OCEAN_OVERRIDES, - "sunset": SUNSET_OVERRIDES, - "forest": FOREST_OVERRIDES, - "rose": ROSE_OVERRIDES, - "midnight": MIDNIGHT_OVERRIDES, - "sand": SAND_OVERRIDES, - "mono": MONO_OVERRIDES, -} diff --git a/tests/test_style/test_zoltcss.py b/tests/test_style/test_zoltcss.py deleted file mode 100644 index d64ca2b..0000000 --- a/tests/test_style/test_zoltcss.py +++ /dev/null @@ -1,394 +0,0 @@ -""" -Tests for ZoltCSS - Phase 0 of v1.5 - -Tests the new styling engine that replaces Tailwind CSS. -""" - -import pytest -from pyui.style.tokens import TOKENS, BUILT_IN_THEMES -from pyui.style.rules import StyleRule, token, create_base_rules, create_state_rules -from pyui.style.resolver import StyleResolver -from pyui.style.css_generator import ZoltCSSGenerator -from pyui.style.qt_generator import QtStyleGenerator -from pyui.style.rich_generator import RichStyleGenerator - - -class TestTokens: - """Test the token system.""" - - def test_tokens_exist(self): - """Verify tokens dictionary exists and has content.""" - assert isinstance(TOKENS, dict) - assert len(TOKENS) > 50 - - def test_color_tokens(self): - """Verify color tokens are present.""" - assert "color-primary" in TOKENS - assert "color-secondary" in TOKENS - assert "color-bg" in TOKENS - assert "color-text" in TOKENS - assert "color-success" in TOKENS - assert "color-danger" in TOKENS - - def test_typography_tokens(self): - """Verify typography tokens are present.""" - assert "font-family" in TOKENS - assert "font-size-sm" in TOKENS - assert "font-size-md" in TOKENS - assert "font-size-lg" in TOKENS - assert "font-weight-normal" in TOKENS - assert "font-weight-bold" in TOKENS - - def test_spacing_tokens(self): - """Verify spacing tokens are present.""" - assert "space-0" in TOKENS - assert "space-4" in TOKENS - assert "space-8" in TOKENS - assert "space-16" in TOKENS - - def test_radius_tokens(self): - """Verify border radius tokens are present.""" - assert "radius-none" in TOKENS - assert "radius-md" in TOKENS - assert "radius-lg" in TOKENS - assert "radius-full" in TOKENS - - def test_shadow_tokens(self): - """Verify shadow tokens are present.""" - assert "shadow-sm" in TOKENS - assert "shadow-md" in TOKENS - assert "shadow-lg" in TOKENS - - def test_builtin_themes(self): - """Verify built-in themes exist.""" - assert isinstance(BUILT_IN_THEMES, dict) - assert "light" in BUILT_IN_THEMES - assert "dark" in BUILT_IN_THEMES - assert "ocean" in BUILT_IN_THEMES - assert "forest" in BUILT_IN_THEMES - - def test_dark_theme_overrides(self): - """Verify dark theme has proper overrides.""" - dark = BUILT_IN_THEMES["dark"] - assert "color-bg" in dark - assert "color-text" in dark - # Dark mode should have dark background - assert dark["color-bg"] != TOKENS["color-bg"] - - -class TestStyleRule: - """Test StyleRule dataclass.""" - - def test_create_basic_rule(self): - """Test creating a basic style rule.""" - rule = StyleRule(property="color", value="red") - assert rule.property == "color" - assert rule.value == "red" - assert rule.breakpoint is None - assert rule.state is None - - def test_create_rule_with_modifiers(self): - """Test creating a rule with modifiers.""" - rule = StyleRule( - property="background-color", - value="blue", - breakpoint="md", - state="hover", - dark_mode="darkblue", - ) - assert rule.breakpoint == "md" - assert rule.state == "hover" - assert rule.dark_mode == "darkblue" - - def test_rule_to_css_declaration(self): - """Test converting rule to CSS declaration.""" - rule = StyleRule(property="padding", value="16px") - assert rule.to_css_declaration() == "padding: 16px;" - - def test_rule_has_modifier(self): - """Test checking if rule has modifiers.""" - base_rule = StyleRule(property="color", value="red") - assert not base_rule.has_modifier() - - mod_rule = StyleRule(property="color", value="red", state="hover") - assert mod_rule.has_modifier() - - def test_rule_hashable(self): - """Test that rules are hashable for deduplication.""" - rule1 = StyleRule(property="color", value="red") - rule2 = StyleRule(property="color", value="red") - rule3 = StyleRule(property="color", value="blue") - - rule_set = {rule1, rule2, rule3} - assert len(rule_set) == 2 # rule1 and rule2 are equal - - def test_rule_equality(self): - """Test rule equality.""" - rule1 = StyleRule(property="color", value="red") - rule2 = StyleRule(property="color", value="red") - rule3 = StyleRule(property="color", value="blue") - - assert rule1 == rule2 - assert rule1 != rule3 - - -class TestTokenHelper: - """Test token helper functions.""" - - def test_token_function(self): - """Test token() helper creates var() references.""" - result = token("color-primary") - assert result == "var(--color-primary)" - - def test_token_with_spacing(self): - """Test token() with spacing tokens.""" - result = token("space-4") - assert result == "var(--space-4)" - - -class TestStyleResolver: - """Test StyleResolver component resolution.""" - - def test_resolver_exists(self): - """Test that resolver can be instantiated.""" - resolver = StyleResolver() - assert resolver is not None - - def test_resolve_button(self): - """Test resolving button styles.""" - resolver = StyleResolver() - rules = resolver.resolve("button", variant="primary", size="md") - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_button_disabled(self): - """Test resolving disabled button styles.""" - resolver = StyleResolver() - rules = resolver.resolve("button", variant="primary", disabled=True) - assert isinstance(rules, list) - # Should include disabled state rules - disabled_rules = [r for r in rules if r.state == "disabled"] - assert len(disabled_rules) > 0 - - def test_resolve_text(self): - """Test resolving text styles.""" - resolver = StyleResolver() - rules = resolver.resolve("text", variant="muted", size="sm") - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_heading(self): - """Test resolving heading styles.""" - resolver = StyleResolver() - rules = resolver.resolve("heading", level=1) - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_flex(self): - """Test resolving flex container styles.""" - resolver = StyleResolver() - rules = resolver.resolve("flex", direction="col", gap=4) - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_grid(self): - """Test resolving grid container styles.""" - resolver = StyleResolver() - rules = resolver.resolve("grid", cols=3, gap=6) - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_box(self): - """Test resolving box container styles.""" - resolver = StyleResolver() - rules = resolver.resolve("box", padding=4, shadow="md") - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_input(self): - """Test resolving input styles.""" - resolver = StyleResolver() - rules = resolver.resolve("input", error=True) - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_badge(self): - """Test resolving badge styles.""" - resolver = StyleResolver() - rules = resolver.resolve("badge", variant="success") - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_card(self): - """Test resolving card styles.""" - resolver = StyleResolver() - rules = resolver.resolve("card", padding=6, shadow="lg") - assert isinstance(rules, list) - assert len(rules) > 0 - - def test_resolve_unknown_component(self): - """Test that unknown component raises error.""" - resolver = StyleResolver() - with pytest.raises(ValueError, match="Unknown component"): - resolver.resolve("unknown_component") - - def test_register_custom_component(self): - """Test registering custom component resolver.""" - resolver = StyleResolver() - - def custom_resolver(**kwargs): - return [StyleRule(property="color", value="purple")] - - resolver.register_component("custom", custom_resolver) - rules = resolver.resolve("custom") - assert len(rules) == 1 - assert rules[0].value == "purple" - - -class TestZoltCSSGenerator: - """Test CSS generation.""" - - def test_generator_exists(self): - """Test that generator can be instantiated.""" - gen = ZoltCSSGenerator() - assert gen is not None - - def test_register_rule(self): - """Test registering a rule.""" - gen = ZoltCSSGenerator() - rule = StyleRule(property="color", value="red") - class_name = gen.register_rule(rule) - assert isinstance(class_name, str) - assert len(class_name) > 0 - - def test_register_rules(self): - """Test registering multiple rules.""" - gen = ZoltCSSGenerator() - rules = [ - StyleRule(property="color", value="red"), - StyleRule(property="background", value="blue"), - ] - class_names = gen.register_rules(rules) - assert len(class_names) == 2 - - def test_rule_deduplication(self): - """Test that duplicate rules get same class name.""" - gen = ZoltCSSGenerator() - rule1 = StyleRule(property="color", value="red") - rule2 = StyleRule(property="color", value="red") - - class1 = gen.register_rule(rule1) - class2 = gen.register_rule(rule2) - - assert class1 == class2 - - def test_generate_css_variables(self): - """Test generating CSS variables.""" - gen = ZoltCSSGenerator() - css = gen.generate_css_variables("light") - assert ":root {" in css - assert "--color-primary:" in css - - def test_generate_dark_mode_variables(self): - """Test generating dark mode variables.""" - gen = ZoltCSSGenerator() - css = gen.generate_dark_mode_variables() - assert "[data-theme='dark']" in css - assert "--color-bg:" in css - - def test_generate_complete_css(self): - """Test generating complete CSS.""" - gen = ZoltCSSGenerator() - - # Register some rules - resolver = StyleResolver() - button_rules = resolver.resolve("button", variant="primary") - gen.register_rules(button_rules) - - css = gen.generate() - assert ":root {" in css - assert ".zolt-" in css or ".z" in css - - def test_production_mode(self): - """Test production mode generates short class names.""" - gen = ZoltCSSGenerator(mode="production") - rule = StyleRule(property="color", value="red") - class_name = gen.register_rule(rule) - assert class_name.startswith("z") - assert len(class_name) < 15 - - def test_development_mode(self): - """Test development mode generates descriptive class names.""" - gen = ZoltCSSGenerator(mode="development") - rule = StyleRule(property="color", value="red") - class_name = gen.register_rule(rule) - assert class_name.startswith("zolt-") - assert len(class_name) > 15 - - def test_reset(self): - """Test resetting generator.""" - gen = ZoltCSSGenerator() - rule = StyleRule(property="color", value="red") - gen.register_rule(rule) - assert gen.rule_count > 0 - - gen.reset() - assert gen.rule_count == 0 - - -class TestQtStyleGenerator: - """Test Qt stylesheet generation.""" - - def test_qt_generator_exists(self): - """Test that Qt generator can be instantiated.""" - gen = QtStyleGenerator() - assert gen is not None - - def test_register_rule(self): - """Test registering a rule for Qt widget.""" - gen = QtStyleGenerator() - rule = StyleRule(property="background-color", value="red") - gen.register_rule("QPushButton", rule) - assert gen.rule_count == 1 - - def test_generate_qss(self): - """Test generating QSS.""" - gen = QtStyleGenerator() - rule = StyleRule(property="background-color", value="#FF0000") - gen.register_rule("QPushButton", rule) - - qss = gen.generate() - assert "QPushButton" in qss - assert "background-color:" in qss - - -class TestRichStyleGenerator: - """Test Rich terminal style generation.""" - - def test_rich_generator_exists(self): - """Test that Rich generator can be instantiated.""" - gen = RichStyleGenerator() - assert gen is not None - - def test_register_rule(self): - """Test registering a rule for Rich component.""" - gen = RichStyleGenerator() - rule = StyleRule(property="color", value="#FF0000") - gen.register_rule("text", rule) - assert gen.rule_count == 1 - - def test_generate_style(self): - """Test generating Rich style kwargs.""" - gen = RichStyleGenerator() - rule = StyleRule(property="color", value="#FF0000") - gen.register_rule("text", rule) - - style = gen.generate_style("text") - assert isinstance(style, dict) - assert "color" in style - - def test_hex_to_rich_color(self): - """Test hex to Rich color conversion.""" - gen = RichStyleGenerator() - color = gen._hex_to_rich_color("#FF0000") - assert color == "red"