Coverage for fastblocks/adapters/icons/lucide.py: 62%

84 statements  

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

1"""Lucide icons 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 IconsBase 

11 

12 

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

14 """Lucide-specific settings.""" 

15 

16 version: str = "0.263.1" 

17 cdn_url: str = "https://unpkg.com/lucide@{version}/dist/umd/lucide.js" 

18 css_url: str = "https://unpkg.com/lucide-static@{version}/font/lucide.css" 

19 use_svg: bool = True # Use SVG icons vs icon font 

20 

21 

22class LucideAdapter(IconsBase): 

23 """Lucide icons adapter implementation.""" 

24 

25 # Required ACB 0.19.0+ metadata 

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

27 MODULE_STATUS = "stable" 

28 

29 # Icon mapping for common icons (Lucide naming) 

30 ICON_MAPPINGS = { 

31 "home": "home", 

32 "user": "user", 

33 "users": "users", 

34 "settings": "settings", 

35 "edit": "edit", 

36 "delete": "trash-2", 

37 "save": "save", 

38 "search": "search", 

39 "add": "plus", 

40 "remove": "minus", 

41 "check": "check", 

42 "close": "x", 

43 "arrow_up": "arrow-up", 

44 "arrow_down": "arrow-down", 

45 "arrow_left": "arrow-left", 

46 "arrow_right": "arrow-right", 

47 "chevron_up": "chevron-up", 

48 "chevron_down": "chevron-down", 

49 "chevron_left": "chevron-left", 

50 "chevron_right": "chevron-right", 

51 "heart": "heart", 

52 "star": "star", 

53 "bookmark": "bookmark", 

54 "share": "share", 

55 "download": "download", 

56 "upload": "upload", 

57 "file": "file", 

58 "folder": "folder", 

59 "image": "image", 

60 "video": "video", 

61 "music": "music", 

62 "calendar": "calendar", 

63 "clock": "clock", 

64 "bell": "bell", 

65 "email": "mail", 

66 "phone": "phone", 

67 "location": "map-pin", 

68 "link": "link", 

69 "external_link": "external-link", 

70 "info": "info", 

71 "warning": "alert-triangle", 

72 "error": "alert-circle", 

73 "success": "check-circle", 

74 "menu": "menu", 

75 "grid": "grid-3x3", 

76 "list": "list", 

77 "lock": "lock", 

78 "unlock": "unlock", 

79 "eye": "eye", 

80 "eye_slash": "eye-off", 

81 "shopping_cart": "shopping-cart", 

82 "credit_card": "credit-card", 

83 "print": "printer", 

84 "question": "help-circle", 

85 "help": "help-circle", 

86 "refresh": "refresh-cw", 

87 "copy": "copy", 

88 "cut": "scissors", 

89 "paste": "clipboard", 

90 "undo": "undo", 

91 "redo": "redo", 

92 "maximize": "maximize", 

93 "minimize": "minimize", 

94 "filter": "filter", 

95 "sort": "arrow-up-down", 

96 "play": "play", 

97 "pause": "pause", 

98 "stop": "square", 

99 "volume": "volume-2", 

100 "volume_off": "volume-x", 

101 "fullscreen": "maximize", 

102 "zoom_in": "zoom-in", 

103 "zoom_out": "zoom-out", 

104 } 

105 

106 def __init__(self) -> None: 

107 """Initialize Lucide adapter.""" 

108 super().__init__() 

109 self.settings = LucideSettings() 

110 

111 # Register with ACB dependency system 

112 with suppress(Exception): 

113 depends.set(self) 

114 

115 def get_stylesheet_links(self) -> list[str]: 

116 """Generate Lucide stylesheet/script link tags.""" 

117 links = [] 

118 

119 if self.settings.use_svg: 

120 # Use JavaScript library for SVG icons 

121 js_url = self.settings.cdn_url.format(version=self.settings.version) 

122 links.append(f'<script src="{js_url}"></script>') 

123 else: 

124 # Use icon font CSS 

125 css_url = self.settings.css_url.format(version=self.settings.version) 

126 links.append(f'<link rel="stylesheet" href="{css_url}">') 

127 

128 return links 

129 

130 def get_icon_class(self, icon_name: str) -> str: 

131 """Get Lucide-specific class names for icons.""" 

132 # Map common names to Lucide names 

133 lucide_name = self.ICON_MAPPINGS.get(icon_name, icon_name) 

134 

135 if self.settings.use_svg: 

136 # For SVG mode, return data attribute for JavaScript initialization 

137 return f"lucide-{lucide_name}" 

138 # For icon font mode 

139 return f"lucide lucide-{lucide_name}" 

140 

141 def get_icon_tag(self, icon_name: str, **attributes: Any) -> str: 

142 """Generate complete icon tags with Lucide classes.""" 

143 # Map common names to Lucide names 

144 lucide_name = self.ICON_MAPPINGS.get(icon_name, icon_name) 

145 

146 if self.settings.use_svg: 

147 return self._get_svg_icon_tag(lucide_name, **attributes) 

148 

149 return self._get_font_icon_tag(lucide_name, **attributes) 

150 

151 def _get_svg_icon_tag(self, icon_name: str, **attributes: Any) -> str: 

152 """Generate SVG icon tag for JavaScript initialization.""" 

153 # Build attributes string 

154 attr_parts = [f'data-lucide="{icon_name}"'] 

155 

156 # Handle size attributes 

157 size = attributes.pop("size", None) 

158 if size: 

159 attr_parts.extend((f'width="{size}"', f'height="{size}"')) 

160 

161 # Handle other attributes 

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

163 if key in ("class", "id", "style", "stroke-width", "color"): 

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

165 elif key.startswith(("data-", "aria-")): 

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

167 

168 # Add accessibility 

169 if "aria-label" not in attributes: 

170 attr_parts.append(f'aria-label="{icon_name} icon"') 

171 

172 attrs_str = " ".join(attr_parts) 

173 return f"<i {attrs_str}></i>" 

174 

175 def _get_font_icon_tag(self, icon_name: str, **attributes: Any) -> str: 

176 """Generate font icon tag.""" 

177 icon_class = f"lucide lucide-{icon_name}" 

178 

179 # Add any additional classes 

180 if "class" in attributes: 

181 icon_class = f"{icon_class} {attributes.pop('class')}" 

182 

183 # Build attributes string 

184 attr_parts = [f'class="{icon_class}"'] 

185 

186 # Handle common attributes 

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

188 if key in ("id", "style", "title"): 

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

190 elif key.startswith(("data-", "aria-")): 

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

192 

193 # Add accessibility 

194 if "aria-label" not in attributes: 

195 attr_parts.append(f'aria-label="{icon_name} icon"') 

196 

197 attrs_str = " ".join(attr_parts) 

198 return f"<i {attrs_str}></i>" 

199 

200 def get_initialization_script(self) -> str: 

201 """Generate JavaScript initialization script for SVG mode.""" 

202 if not self.settings.use_svg: 

203 return "" 

204 

205 return """ 

206<script> 

207 document.addEventListener('DOMContentLoaded', function() { 

208 if (typeof lucide !== 'undefined') { 

209 lucide.createIcons(); 

210 } 

211 }); 

212</script> 

213""" 

214 

215 def get_icon_with_text( 

216 self, icon_name: str, text: str, position: str = "left", **attributes: Any 

217 ) -> str: 

218 """Generate icon with text combination.""" 

219 icon_tag = self.get_icon_tag(icon_name, **attributes) 

220 

221 if position == "right": 

222 return f"{text} {icon_tag}" 

223 

224 return f"{icon_tag} {text}" 

225 

226 def get_icon_button(self, icon_name: str, **attributes: Any) -> str: 

227 """Generate button with icon.""" 

228 icon_tag = self.get_icon_tag(icon_name) 

229 

230 # Extract button-specific attributes 

231 button_class = attributes.pop("button_class", "btn") 

232 button_attrs = { 

233 k: v 

234 for k, v in attributes.items() 

235 if k in ("id", "style", "onclick", "type", "disabled") 

236 } 

237 

238 # Build button attributes 

239 attr_parts = [f'class="{button_class}"'] 

240 for key, value in button_attrs.items(): 

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

242 

243 attrs_str = " ".join(attr_parts) 

244 return f"<button {attrs_str}>{icon_tag}</button>"