Coverage for fastblocks / adapters / style / bulma.py: 84%
69 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"""Bulma CSS framework adapter implementation."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.depends import depends
9from ._base import StyleBase, StyleBaseSettings
12class BulmaStyleSettings(StyleBaseSettings):
13 """Bulma-specific settings."""
15 version: str = "0.9.4"
16 cdn_url: str = "https://cdn.jsdelivr.net/npm/bulma@{version}/css/bulma.min.css"
17 custom_variables: dict[str, str] = {}
18 local_path: str | None = None # For local CSS files
20 def __init__(self, **data):
21 """Initialize settings with support for cdn property."""
22 super().__init__(**data)
23 # Store cdn override if passed in, otherwise default to True
24 self._cdn = data.get("cdn", True) # Default to True as per test
26 @property
27 def cdn(self) -> bool:
28 """Check if using CDN (True if local_path is None)."""
29 return self._cdn
31 @cdn.setter
32 def cdn(self, value: bool) -> None:
33 """Set CDN status."""
34 self._cdn = value
36 @property
37 def components(self):
38 """Return component mappings for Bulma framework."""
39 # Return the same mappings as defined in BulmaStyle.COMPONENT_CLASSES
40 return {
41 "button": "button",
42 "button_primary": "button is-primary",
43 "button_secondary": "button is-light",
44 "button_success": "button is-success",
45 "button_danger": "button is-danger",
46 "button_warning": "button is-warning",
47 "button_info": "button is-info",
48 "button_small": "button is-small",
49 "button_medium": "button is-medium",
50 "button_large": "button is-large",
51 "input": "input",
52 "textarea": "textarea",
53 "select": "select",
54 "checkbox": "checkbox",
55 "radio": "radio",
56 "field": "field",
57 "label": "label",
58 "control": "control",
59 "card": "card",
60 "card_header": "card-header",
61 "card_content": "card-content",
62 "card_footer": "card-footer",
63 "hero": "hero",
64 "hero_body": "hero-body",
65 "section": "section",
66 "container": "container",
67 "columns": "columns",
68 "column": "column",
69 "navbar": "navbar",
70 "navbar_brand": "navbar-brand",
71 "navbar_menu": "navbar-menu",
72 "navbar_item": "navbar-item",
73 "footer": "footer",
74 "modal": "modal",
75 "modal_background": "modal-background",
76 "modal_content": "modal-content",
77 "modal_close": "modal-close",
78 "notification": "notification",
79 "tag": "tag",
80 "title": "title",
81 "subtitle": "subtitle",
82 }
85class BulmaStyle(StyleBase):
86 """Bulma CSS framework adapter implementation."""
88 # Required ACB 0.19.0+ metadata
89 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2c1a1") # Static UUID7
90 MODULE_STATUS = "stable"
92 # Component class mappings for Bulma
93 COMPONENT_CLASSES = {
94 "button": "button",
95 "button_primary": "button is-primary",
96 "button_secondary": "button is-light",
97 "button_success": "button is-success",
98 "button_danger": "button is-danger",
99 "button_warning": "button is-warning",
100 "button_info": "button is-info",
101 "button_small": "button is-small",
102 "button_medium": "button is-medium",
103 "button_large": "button is-large",
104 "input": "input",
105 "textarea": "textarea",
106 "select": "select",
107 "checkbox": "checkbox",
108 "radio": "radio",
109 "field": "field",
110 "label": "label",
111 "control": "control",
112 "card": "card",
113 "card_header": "card-header",
114 "card_content": "card-content",
115 "card_footer": "card-footer",
116 "hero": "hero",
117 "hero_body": "hero-body",
118 "section": "section",
119 "container": "container",
120 "columns": "columns",
121 "column": "column",
122 "navbar": "navbar",
123 "navbar_brand": "navbar-brand",
124 "navbar_menu": "navbar-menu",
125 "navbar_item": "navbar-item",
126 "footer": "footer",
127 "modal": "modal",
128 "modal_background": "modal-background",
129 "modal_content": "modal-content",
130 "modal_close": "modal-close",
131 "notification": "notification",
132 "tag": "tag",
133 "title": "title",
134 "subtitle": "subtitle",
135 }
137 def __init__(self) -> None:
138 """Initialize Bulma adapter."""
139 super().__init__()
140 self.settings = BulmaStyleSettings()
142 # Register with ACB dependency system
143 with suppress(Exception):
144 depends.set(self)
146 def get_stylesheet_links(self) -> list[str]:
147 """Generate Bulma stylesheet link tags."""
148 if self.settings.cdn and self.settings.local_path is None:
149 # Use CDN
150 cdn_url = self.settings.cdn_url.format(version=self.settings.version)
151 return [f'<link rel="stylesheet" href="{cdn_url}">']
152 elif not self.settings.cdn and self.settings.local_path:
153 # Use local path
154 return [f'<link rel="stylesheet" href="{self.settings.local_path}">']
155 else:
156 # Fallback to CDN
157 cdn_url = self.settings.cdn_url.format(version=self.settings.version)
158 return [f'<link rel="stylesheet" href="{cdn_url}">']
160 def get_component_class(self, component: str) -> str:
161 """Get Bulma-specific class names for components."""
162 return self.COMPONENT_CLASSES.get(component, component)
164 def get_utility_classes(self) -> dict[str, str]:
165 """Get common Bulma utility classes."""
166 return {
167 "primary": "is-primary",
168 "secondary": "is-secondary", # This might not be standard Bulma, but we'll include it
169 "success": "is-success",
170 "danger": "is-danger",
171 "warning": "is-warning",
172 "info": "is-info",
173 "light": "is-light",
174 "dark": "is-dark",
175 "small": "is-small",
176 "medium": "is-medium",
177 "large": "is-large",
178 # Responsive utilities
179 "mobile": "is-hidden-tablet-only",
180 "tablet": "is-hidden-mobile",
181 "desktop": "is-hidden-widescreen-only",
182 "widescreen": "is-hidden-fullhd-only",
183 "fullhd": "is-hidden-touch",
184 "text_center": "has-text-centered",
185 "text_left": "has-text-left",
186 "text_right": "has-text-right",
187 "text_weight_bold": "has-text-weight-bold",
188 "text_weight_light": "has-text-weight-light",
189 "background_primary": "has-background-primary",
190 "background_secondary": "has-background-light",
191 "text_primary": "has-text-primary",
192 "text_secondary": "has-text-dark",
193 "margin_small": "m-2",
194 "margin_medium": "m-4",
195 "margin_large": "m-6",
196 "padding_small": "p-2",
197 "padding_medium": "p-4",
198 "padding_large": "p-6",
199 "is_hidden": "is-hidden",
200 "is_visible": "is-visible",
201 "is_responsive": "is-responsive",
202 }
204 def build_component_html(
205 self, component: str, content: str = "", **attributes: Any
206 ) -> str:
207 """Build complete HTML component with Bulma classes."""
208 css_class = self.get_component_class(component)
210 # Handle variant attribute by converting it to appropriate Bulma class
211 if "variant" in attributes:
212 variant = attributes.pop("variant")
213 # Add variant class to the css_class
214 utility_classes = self.get_utility_classes()
215 variant_class = utility_classes.get(variant, f"is-{variant}")
216 css_class = f"{css_class} {variant_class}"
218 # Add any additional classes
219 if "class" in attributes:
220 additional_class = attributes.pop("class")
221 css_class = f"{css_class} {additional_class}"
223 # Build attributes string
224 attr_parts = [f'class="{css_class}"']
225 for key, value in attributes.items():
226 if key not in ("transformations"): # Skip internal attributes
227 attr_parts.append(f'{key}="{value}"')
229 attrs_str = " ".join(attr_parts)
231 # Determine the appropriate HTML tag based on component type
232 if component.startswith("button"):
233 return f"<button {attrs_str}>{content}</button>"
234 elif component in ("input", "textarea", "select"):
235 return f"<{component} {attrs_str}>"
236 elif component == "field":
237 return f"<div {attrs_str}>{content}</div>"
239 return f"<div {attrs_str}>{content}</div>"
242StyleSettings = BulmaStyleSettings
243Style = BulmaStyle
245depends.set(Style, "bulma")
247__all__ = ["BulmaStyle", "Style", "StyleSettings", "BulmaStyleSettings"]