Coverage for fastblocks/adapters/styles/vanilla.py: 83%
48 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""Vanilla CSS adapter implementation for custom stylesheets."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.config import Settings
8from acb.depends import depends
10from ._base import StylesBase
13class VanillaSettings(Settings): # type: ignore[misc]
14 """Vanilla CSS-specific settings."""
16 css_paths: list[str] = ["/static/css/base.css"]
17 custom_properties: dict[str, str] = {}
18 css_variables: dict[str, str] = {}
21class VanillaAdapter(StylesBase):
22 """Vanilla CSS adapter for custom stylesheets."""
24 # Required ACB 0.19.0+ metadata
25 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2c1a2") # Static UUID7
26 MODULE_STATUS = "stable"
28 # Default component class mappings for semantic naming
29 COMPONENT_CLASSES = {
30 "button": "btn",
31 "button_primary": "btn btn--primary",
32 "button_secondary": "btn btn--secondary",
33 "button_success": "btn btn--success",
34 "button_danger": "btn btn--danger",
35 "button_warning": "btn btn--warning",
36 "button_info": "btn btn--info",
37 "button_small": "btn btn--small",
38 "button_medium": "btn btn--medium",
39 "button_large": "btn btn--large",
40 "input": "form__input",
41 "textarea": "form__textarea",
42 "select": "form__select",
43 "checkbox": "form__checkbox",
44 "radio": "form__radio",
45 "field": "form__field",
46 "label": "form__label",
47 "control": "form__control",
48 "card": "card",
49 "card_header": "card__header",
50 "card_content": "card__content",
51 "card_footer": "card__footer",
52 "hero": "hero",
53 "hero_body": "hero__body",
54 "section": "section",
55 "container": "container",
56 "columns": "grid",
57 "column": "grid__item",
58 "navbar": "navbar",
59 "navbar_brand": "navbar__brand",
60 "navbar_menu": "navbar__menu",
61 "navbar_item": "navbar__item",
62 "footer": "footer",
63 "modal": "modal",
64 "modal_background": "modal__background",
65 "modal_content": "modal__content",
66 "modal_close": "modal__close",
67 "notification": "notification",
68 "tag": "tag",
69 "title": "title",
70 "subtitle": "subtitle",
71 }
73 def __init__(self) -> None:
74 """Initialize Vanilla CSS adapter."""
75 super().__init__()
76 self.settings = VanillaSettings()
78 # Register with ACB dependency system
79 with suppress(Exception):
80 depends.set(self)
82 def get_stylesheet_links(self) -> list[str]:
83 """Generate link tags for custom CSS files."""
84 return [
85 f'<link rel="stylesheet" href="{css_path}">'
86 for css_path in self.settings.css_paths
87 ]
89 def get_component_class(self, component: str) -> str:
90 """Get semantic class names for components."""
91 return self.COMPONENT_CLASSES.get(component, component)
93 def get_css_variables(self) -> str:
94 """Generate CSS custom properties (variables) style block."""
95 if not self.settings.css_variables:
96 return ""
98 variables = [
99 f" --{prop}: {value};"
100 for prop, value in self.settings.css_variables.items()
101 ]
103 return ":root {\n" + "\n".join(variables) + "\n}"
105 def get_utility_classes(self) -> 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 def generate_base_css(self) -> str:
157 """Generate a basic CSS foundation for vanilla styling."""
158 return """
159/* FastBlocks Vanilla CSS Base */
160:root {
161 --primary-color: #007bff;
162 --secondary-color: #6c757d;
163 --success-color: #28a745;
164 --danger-color: #dc3545;
165 --warning-color: #ffc107;
166 --info-color: #17a2b8;
167 --light-color: #f8f9fa;
168 --dark-color: #343a40;
169}
171.btn {
172 display: inline-block;
173 padding: 0.375rem 0.75rem;
174 margin-bottom: 0;
175 font-size: 1rem;
176 line-height: 1.5;
177 text-align: center;
178 text-decoration: none;
179 vertical-align: middle;
180 cursor: pointer;
181 border: 1px solid transparent;
182 border-radius: 0.25rem;
183 transition: all 0.15s ease-in-out;
184}
186.btn--primary { background-color: var(--primary-color); color: white; }
187.btn--secondary { background-color: var(--secondary-color); color: white; }
188.btn--success { background-color: var(--success-color); color: white; }
189.btn--danger { background-color: var(--danger-color); color: white; }
191.form__field { margin-bottom: 1rem; }
192.form__input, .form__textarea, .form__select {
193 display: block;
194 width: 100%;
195 padding: 0.375rem 0.75rem;
196 font-size: 1rem;
197 line-height: 1.5;
198 border: 1px solid #ced4da;
199 border-radius: 0.25rem;
200}
202.container { max-width: 1200px; margin: 0 auto; padding: 0 15px; }
203.grid { display: grid; gap: 1rem; }
204.card { border: 1px solid #dee2e6; border-radius: 0.25rem; }
205"""