Coverage for fastblocks / adapters / style / vanilla.py: 85%
53 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:30 -0800
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:30 -0800
1"""Vanilla CSS adapter implementation for custom stylesheets."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.depends import depends
9from ._base import StyleBase, StyleBaseSettings
12class VanillaStyleSettings(StyleBaseSettings):
13 """Vanilla CSS-specific settings."""
15 css_paths: list[str] = ["/static/css/base.css"]
16 custom_properties: dict[str, str] = {}
17 css_variables: dict[str, str] = {}
20class VanillaStyle(StyleBase):
21 """Vanilla CSS adapter for custom stylesheets."""
23 # Required ACB 0.19.0+ metadata
24 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2c1a2") # Static UUID7
25 MODULE_STATUS = "stable"
27 # Default component class mappings for semantic naming
28 COMPONENT_CLASSES = {
29 "button": "btn",
30 "button_primary": "btn btn--primary",
31 "button_secondary": "btn btn--secondary",
32 "button_success": "btn btn--success",
33 "button_danger": "btn btn--danger",
34 "button_warning": "btn btn--warning",
35 "button_info": "btn btn--info",
36 "button_small": "btn btn--small",
37 "button_medium": "btn btn--medium",
38 "button_large": "btn btn--large",
39 "input": "form__input",
40 "textarea": "form__textarea",
41 "select": "form__select",
42 "checkbox": "form__checkbox",
43 "radio": "form__radio",
44 "field": "form__field",
45 "label": "form__label",
46 "control": "form__control",
47 "card": "card",
48 "card_header": "card__header",
49 "card_content": "card__content",
50 "card_footer": "card__footer",
51 "hero": "hero",
52 "hero_body": "hero__body",
53 "section": "section",
54 "container": "container",
55 "columns": "grid",
56 "column": "grid__item",
57 "navbar": "navbar",
58 "navbar_brand": "navbar__brand",
59 "navbar_menu": "navbar__menu",
60 "navbar_item": "navbar__item",
61 "footer": "footer",
62 "modal": "modal",
63 "modal_background": "modal__background",
64 "modal_content": "modal__content",
65 "modal_close": "modal__close",
66 "notification": "notification",
67 "tag": "tag",
68 "title": "title",
69 "subtitle": "subtitle",
70 }
72 def __init__(self) -> None:
73 """Initialize Vanilla CSS adapter."""
74 super().__init__()
75 self.settings = VanillaStyleSettings()
77 # Register with ACB dependency system
78 with suppress(Exception):
79 depends.set(self)
81 def get_stylesheet_links(self) -> list[str]:
82 """Generate link tags for custom CSS files."""
83 return [
84 f'<link rel="stylesheet" href="{css_path}">'
85 for css_path in self.settings.css_paths
86 ]
88 def get_component_class(self, component: str) -> str:
89 """Get semantic class names for components."""
90 return self.COMPONENT_CLASSES.get(component, component)
92 def get_css_variables(self) -> str:
93 """Generate CSS custom properties (variables) style block."""
94 if not self.settings.css_variables:
95 return ""
97 variables = [
98 f" --{prop}: {value};"
99 for prop, value in self.settings.css_variables.items()
100 ]
102 return ":root {\n" + "\n".join(variables) + "\n}"
104 @staticmethod
105 def get_utility_classes() -> dict[str, str]:
106 """Get semantic utility classes for common patterns."""
107 return {
108 "text_center": "text--center",
109 "text_left": "text--left",
110 "text_right": "text--right",
111 "text_weight_bold": "text--bold",
112 "text_weight_light": "text--light",
113 "background_primary": "bg--primary",
114 "background_secondary": "bg--secondary",
115 "text_primary": "text--primary",
116 "text_secondary": "text--secondary",
117 "margin_small": "m--sm",
118 "margin_medium": "m--md",
119 "margin_large": "m--lg",
120 "padding_small": "p--sm",
121 "padding_medium": "p--md",
122 "padding_large": "p--lg",
123 "is_hidden": "hidden",
124 "is_visible": "visible",
125 "is_responsive": "responsive",
126 }
128 def build_component_html(
129 self, component: str, content: str = "", **attributes: Any
130 ) -> str:
131 """Build complete HTML component with semantic classes."""
132 css_class = self.get_component_class(component)
134 # Add any additional classes
135 if "class" in attributes:
136 css_class = f"{css_class} {attributes.pop('class')}"
138 # Build attributes string
139 attr_parts = [f'class="{css_class}"']
140 for key, value in attributes.items():
141 if key not in ("transformations"): # Skip internal attributes
142 attr_parts.append(f'{key}="{value}"')
144 attrs_str = " ".join(attr_parts)
146 # Determine the appropriate HTML tag based on component type
147 if component.startswith("button"):
148 return f"<button {attrs_str}>{content}</button>"
149 elif component in ("input", "textarea", "select"):
150 return f"<{component} {attrs_str}>"
151 elif component == "field":
152 return f"<div {attrs_str}>{content}</div>"
154 return f"<div {attrs_str}>{content}</div>"
156 @staticmethod
157 def generate_base_css() -> str:
158 """Generate a basic CSS foundation for vanilla styling."""
159 return """
160/* FastBlocks Vanilla CSS Base */
161:root {
162 --primary-color: #007bff;
163 --secondary-color: #6c757d;
164 --success-color: #28a745;
165 --danger-color: #dc3545;
166 --warning-color: #ffc107;
167 --info-color: #17a2b8;
168 --light-color: #f8f9fa;
169 --dark-color: #343a40;
170}
172.btn {
173 display: inline-block;
174 padding: 0.375rem 0.75rem;
175 margin-bottom: 0;
176 font-size: 1rem;
177 line-height: 1.5;
178 text-align: center;
179 text-decoration: none;
180 vertical-align: middle;
181 cursor: pointer;
182 border: 1px solid transparent;
183 border-radius: 0.25rem;
184 transition: all 0.15s ease-in-out;
185}
187.btn--primary { background-color: var(--primary-color); color: white; }
188.btn--secondary { background-color: var(--secondary-color); color: white; }
189.btn--success { background-color: var(--success-color); color: white; }
190.btn--danger { background-color: var(--danger-color); color: white; }
192.form__field { margin-bottom: 1rem; }
193.form__input, .form__textarea, .form__select {
194 display: block;
195 width: 100%;
196 padding: 0.375rem 0.75rem;
197 font-size: 1rem;
198 line-height: 1.5;
199 border: 1px solid #ced4da;
200 border-radius: 0.25rem;
201}
203.container { max-width: 1200px; margin: 0 auto; padding: 0 15px; }
204.grid { display: grid; gap: 1rem; }
205.card { border: 1px solid #dee2e6; border-radius: 0.25rem; }
206"""
209StyleSettings = VanillaStyleSettings
210Style = VanillaStyle
212depends.set(Style, "vanilla")
214__all__ = ["VanillaStyle", "VanillaStyleSettings", "Style", "StyleSettings"]