Coverage for fastblocks/adapters/styles/bulma.py: 86%

42 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-09 00:47 -0700

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

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 BulmaSettings(Settings): # type: ignore[misc] 

14 """Bulma-specific settings.""" 

15 

16 version: str = "0.9.4" 

17 cdn_url: str = "https://cdn.jsdelivr.net/npm/bulma@{version}/css/bulma.min.css" 

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

19 

20 

21class BulmaAdapter(StylesBase): 

22 """Bulma CSS framework adapter implementation.""" 

23 

24 # Required ACB 0.19.0+ metadata 

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

26 MODULE_STATUS = "stable" 

27 

28 # Component class mappings for Bulma 

29 COMPONENT_CLASSES = { 

30 "button": "button", 

31 "button_primary": "button is-primary", 

32 "button_secondary": "button is-light", 

33 "button_success": "button is-success", 

34 "button_danger": "button is-danger", 

35 "button_warning": "button is-warning", 

36 "button_info": "button is-info", 

37 "button_small": "button is-small", 

38 "button_medium": "button is-medium", 

39 "button_large": "button is-large", 

40 "input": "input", 

41 "textarea": "textarea", 

42 "select": "select", 

43 "checkbox": "checkbox", 

44 "radio": "radio", 

45 "field": "field", 

46 "label": "label", 

47 "control": "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": "columns", 

57 "column": "column", 

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 Bulma adapter.""" 

75 super().__init__() 

76 self.settings = BulmaSettings() 

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 Bulma stylesheet link tags.""" 

84 cdn_url = self.settings.cdn_url.format(version=self.settings.version) 

85 return [f'<link rel="stylesheet" href="{cdn_url}">'] 

86 

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

88 """Get Bulma-specific class names for components.""" 

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

90 

91 def get_utility_classes(self) -> dict[str, str]: 

92 """Get common Bulma utility classes.""" 

93 return { 

94 "text_center": "has-text-centered", 

95 "text_left": "has-text-left", 

96 "text_right": "has-text-right", 

97 "text_weight_bold": "has-text-weight-bold", 

98 "text_weight_light": "has-text-weight-light", 

99 "background_primary": "has-background-primary", 

100 "background_secondary": "has-background-light", 

101 "text_primary": "has-text-primary", 

102 "text_secondary": "has-text-dark", 

103 "margin_small": "m-2", 

104 "margin_medium": "m-4", 

105 "margin_large": "m-6", 

106 "padding_small": "p-2", 

107 "padding_medium": "p-4", 

108 "padding_large": "p-6", 

109 "is_hidden": "is-hidden", 

110 "is_visible": "is-visible", 

111 "is_responsive": "is-responsive", 

112 } 

113 

114 def build_component_html( 

115 self, component: str, content: str = "", **attributes: Any 

116 ) -> str: 

117 """Build complete HTML component with Bulma classes.""" 

118 css_class = self.get_component_class(component) 

119 

120 # Add any additional classes 

121 if "class" in attributes: 

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

123 

124 # Build attributes string 

125 attr_parts = [f'class="{css_class}"'] 

126 for key, value in attributes.items(): 

127 if key not in ("transformations"): # Skip internal attributes 

128 attr_parts.append(f'{key}="{value}"') 

129 

130 attrs_str = " ".join(attr_parts) 

131 

132 # Determine the appropriate HTML tag based on component type 

133 if component.startswith("button"): 

134 return f"<button {attrs_str}>{content}</button>" 

135 elif component in ("input", "textarea", "select"): 

136 return f"<{component} {attrs_str}>" 

137 elif component == "field": 

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

139 

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