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

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

2 

3from contextlib import suppress 

4from typing import Any 

5from uuid import UUID 

6 

7from acb.config import Settings 

8from acb.depends import depends 

9 

10from ._base import StylesBase 

11 

12 

13class VanillaSettings(Settings): # type: ignore[misc] 

14 """Vanilla CSS-specific settings.""" 

15 

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

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

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

19 

20 

21class VanillaAdapter(StylesBase): 

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

23 

24 # Required ACB 0.19.0+ metadata 

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

26 MODULE_STATUS = "stable" 

27 

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 } 

72 

73 def __init__(self) -> None: 

74 """Initialize Vanilla CSS adapter.""" 

75 super().__init__() 

76 self.settings = VanillaSettings() 

77 

78 # Register with ACB dependency system 

79 with suppress(Exception): 

80 depends.set(self) 

81 

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 ] 

88 

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

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

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

92 

93 def get_css_variables(self) -> str: 

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

95 if not self.settings.css_variables: 

96 return "" 

97 

98 variables = [ 

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

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

101 ] 

102 

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

104 

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 } 

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

170 

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} 

185 

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

190 

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} 

201 

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