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

89 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-26 03:30 -0800

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

10 

11 

12class LucideIconsSettings(IconsBaseSettings): 

13 """Lucide-specific settings.""" 

14 

15 version: str = "0.263.1" 

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

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

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

19 

20 

21class LucideIcons(IconsBase): 

22 """Lucide icons adapter implementation.""" 

23 

24 # Required ACB 0.19.0+ metadata 

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

26 MODULE_STATUS = "stable" 

27 

28 # Icon mapping for common icons (Lucide naming) 

29 ICON_MAPPINGS = { 

30 "home": "home", 

31 "user": "user", 

32 "users": "users", 

33 "settings": "settings", 

34 "edit": "edit", 

35 "delete": "trash-2", 

36 "save": "save", 

37 "search": "search", 

38 "add": "plus", 

39 "remove": "minus", 

40 "check": "check", 

41 "close": "x", 

42 "arrow_up": "arrow-up", 

43 "arrow_down": "arrow-down", 

44 "arrow_left": "arrow-left", 

45 "arrow_right": "arrow-right", 

46 "chevron_up": "chevron-up", 

47 "chevron_down": "chevron-down", 

48 "chevron_left": "chevron-left", 

49 "chevron_right": "chevron-right", 

50 "heart": "heart", 

51 "star": "star", 

52 "bookmark": "bookmark", 

53 "share": "share", 

54 "download": "download", 

55 "upload": "upload", 

56 "file": "file", 

57 "folder": "folder", 

58 "image": "image", 

59 "video": "video", 

60 "music": "music", 

61 "calendar": "calendar", 

62 "clock": "clock", 

63 "bell": "bell", 

64 "email": "mail", 

65 "phone": "phone", 

66 "location": "map-pin", 

67 "link": "link", 

68 "external_link": "external-link", 

69 "info": "info", 

70 "warning": "alert-triangle", 

71 "error": "alert-circle", 

72 "success": "check-circle", 

73 "menu": "menu", 

74 "grid": "grid-3x3", 

75 "list": "list", 

76 "lock": "lock", 

77 "unlock": "unlock", 

78 "eye": "eye", 

79 "eye_slash": "eye-off", 

80 "shopping_cart": "shopping-cart", 

81 "credit_card": "credit-card", 

82 "print": "printer", 

83 "question": "help-circle", 

84 "help": "help-circle", 

85 "refresh": "refresh-cw", 

86 "copy": "copy", 

87 "cut": "scissors", 

88 "paste": "clipboard", 

89 "undo": "undo", 

90 "redo": "redo", 

91 "maximize": "maximize", 

92 "minimize": "minimize", 

93 "filter": "filter", 

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

95 "play": "play", 

96 "pause": "pause", 

97 "stop": "square", 

98 "volume": "volume-2", 

99 "volume_off": "volume-x", 

100 "fullscreen": "maximize", 

101 "zoom_in": "zoom-in", 

102 "zoom_out": "zoom-out", 

103 } 

104 

105 def __init__(self) -> None: 

106 """Initialize Lucide adapter.""" 

107 super().__init__() 

108 self.settings = LucideIconsSettings() 

109 

110 # Register with ACB dependency system 

111 with suppress(Exception): 

112 depends.set(self) 

113 

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

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

116 links = [] 

117 

118 if self.settings.use_svg: 

119 # Use JavaScript library for SVG icons 

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

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

122 else: 

123 # Use icon font CSS 

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

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

126 

127 return links 

128 

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

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

131 # Map common names to Lucide names 

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

133 

134 if self.settings.use_svg: 

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

136 return f"lucide-{lucide_name}" 

137 # For icon font mode 

138 return f"lucide lucide-{lucide_name}" 

139 

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

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

142 # Map common names to Lucide names 

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

144 

145 if self.settings.use_svg: 

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

147 

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

149 

150 @staticmethod 

151 def _get_svg_icon_tag(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 @staticmethod 

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

177 """Generate font icon tag.""" 

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

179 

180 # Add any additional classes 

181 if "class" in attributes: 

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

183 

184 # Build attributes string 

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

186 

187 # Handle common attributes 

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

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

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

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

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

193 

194 # Add accessibility 

195 if "aria-label" not in attributes: 

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

197 

198 attrs_str = " ".join(attr_parts) 

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

200 

201 def get_initialization_script(self) -> str: 

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

203 if not self.settings.use_svg: 

204 return "" 

205 

206 return """ 

207<script> 

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

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

210 lucide.createIcons(); 

211 } 

212 }); 

213</script> 

214""" 

215 

216 def get_icon_with_text( 

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

218 ) -> str: 

219 """Generate icon with text combination.""" 

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

221 

222 if position == "right": 

223 return f"{text} {icon_tag}" 

224 

225 return f"{icon_tag} {text}" 

226 

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

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

245 

246 

247IconsSettings = LucideIconsSettings 

248Icons = LucideIcons 

249 

250depends.set(Icons, "lucide") 

251 

252__all__ = ["LucideIcons", "LucideIconsSettings", "IconsSettings", "Icons"]