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

1"""Vanilla CSS adapter implementation for custom stylesheets.""" 

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 VanillaStyleSettings(StyleBaseSettings): 

13 """Vanilla CSS-specific settings.""" 

14 

15 css_paths: list[str] = ["/static/css/base.css"] 

16 custom_properties: dict[str, str] = {} 

17 css_variables: dict[str, str] = {} 

18 

19 

20class VanillaStyle(StyleBase): 

21 """Vanilla CSS adapter for custom stylesheets.""" 

22 

23 # Required ACB 0.19.0+ metadata 

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

25 MODULE_STATUS = "stable" 

26 

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 } 

71 

72 def __init__(self) -> None: 

73 """Initialize Vanilla CSS adapter.""" 

74 super().__init__() 

75 self.settings = VanillaStyleSettings() 

76 

77 # Register with ACB dependency system 

78 with suppress(Exception): 

79 depends.set(self) 

80 

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 ] 

87 

88 def get_component_class(self, component: str) -> str: 

89 """Get semantic class names for components.""" 

90 return self.COMPONENT_CLASSES.get(component, component) 

91 

92 def get_css_variables(self) -> str: 

93 """Generate CSS custom properties (variables) style block.""" 

94 if not self.settings.css_variables: 

95 return "" 

96 

97 variables = [ 

98 f" --{prop}: {value};" 

99 for prop, value in self.settings.css_variables.items() 

100 ] 

101 

102 return ":root {\n" + "\n".join(variables) + "\n}" 

103 

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 } 

127 

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) 

133 

134 # Add any additional classes 

135 if "class" in attributes: 

136 css_class = f"{css_class} {attributes.pop('class')}" 

137 

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

143 

144 attrs_str = " ".join(attr_parts) 

145 

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

153 

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

155 

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} 

171 

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} 

186 

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

191 

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} 

202 

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

207 

208 

209StyleSettings = VanillaStyleSettings 

210Style = VanillaStyle 

211 

212depends.set(Style, "vanilla") 

213 

214__all__ = ["VanillaStyle", "VanillaStyleSettings", "Style", "StyleSettings"]