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

1"""Bulma CSS framework adapter implementation.""" 

2 

3from contextlib import suppress 

4from typing import Any 

5from uuid import UUID 

6 

7from acb.depends import depends 

8 

9from ._base import StyleBase, StyleBaseSettings 

10 

11 

12class BulmaStyleSettings(StyleBaseSettings): 

13 """Bulma-specific settings.""" 

14 

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 

19 

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 

25 

26 @property 

27 def cdn(self) -> bool: 

28 """Check if using CDN (True if local_path is None).""" 

29 return self._cdn 

30 

31 @cdn.setter 

32 def cdn(self, value: bool) -> None: 

33 """Set CDN status.""" 

34 self._cdn = value 

35 

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 } 

83 

84 

85class BulmaStyle(StyleBase): 

86 """Bulma CSS framework adapter implementation.""" 

87 

88 # Required ACB 0.19.0+ metadata 

89 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2c1a1") # Static UUID7 

90 MODULE_STATUS = "stable" 

91 

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 } 

136 

137 def __init__(self) -> None: 

138 """Initialize Bulma adapter.""" 

139 super().__init__() 

140 self.settings = BulmaStyleSettings() 

141 

142 # Register with ACB dependency system 

143 with suppress(Exception): 

144 depends.set(self) 

145 

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}">'] 

159 

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) 

163 

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 } 

203 

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) 

209 

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}" 

217 

218 # Add any additional classes 

219 if "class" in attributes: 

220 additional_class = attributes.pop("class") 

221 css_class = f"{css_class} {additional_class}" 

222 

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}"') 

228 

229 attrs_str = " ".join(attr_parts) 

230 

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>" 

238 

239 return f"<div {attrs_str}>{content}</div>" 

240 

241 

242StyleSettings = BulmaStyleSettings 

243Style = BulmaStyle 

244 

245depends.set(Style, "bulma") 

246 

247__all__ = ["BulmaStyle", "Style", "StyleSettings", "BulmaStyleSettings"]