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
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""Lucide icons adapter implementation."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.config import Settings
8from acb.depends import depends
10from ._base import IconsBase
13class LucideSettings(Settings): # type: ignore[misc]
14 """Lucide-specific settings."""
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
22class LucideAdapter(IconsBase):
23 """Lucide icons adapter implementation."""
25 # Required ACB 0.19.0+ metadata
26 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2d1a2") # Static UUID7
27 MODULE_STATUS = "stable"
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 }
106 def __init__(self) -> None:
107 """Initialize Lucide adapter."""
108 super().__init__()
109 self.settings = LucideSettings()
111 # Register with ACB dependency system
112 with suppress(Exception):
113 depends.set(self)
115 def get_stylesheet_links(self) -> list[str]:
116 """Generate Lucide stylesheet/script link tags."""
117 links = []
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}">')
128 return links
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)
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}"
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)
146 if self.settings.use_svg:
147 return self._get_svg_icon_tag(lucide_name, **attributes)
149 return self._get_font_icon_tag(lucide_name, **attributes)
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}"']
156 # Handle size attributes
157 size = attributes.pop("size", None)
158 if size:
159 attr_parts.extend((f'width="{size}"', f'height="{size}"'))
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}"')
168 # Add accessibility
169 if "aria-label" not in attributes:
170 attr_parts.append(f'aria-label="{icon_name} icon"')
172 attrs_str = " ".join(attr_parts)
173 return f"<i {attrs_str}></i>"
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}"
179 # Add any additional classes
180 if "class" in attributes:
181 icon_class = f"{icon_class} {attributes.pop('class')}"
183 # Build attributes string
184 attr_parts = [f'class="{icon_class}"']
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}"')
193 # Add accessibility
194 if "aria-label" not in attributes:
195 attr_parts.append(f'aria-label="{icon_name} icon"')
197 attrs_str = " ".join(attr_parts)
198 return f"<i {attrs_str}></i>"
200 def get_initialization_script(self) -> str:
201 """Generate JavaScript initialization script for SVG mode."""
202 if not self.settings.use_svg:
203 return ""
205 return """
206<script>
207 document.addEventListener('DOMContentLoaded', function() {
208 if (typeof lucide !== 'undefined') {
209 lucide.createIcons();
210 }
211 });
212</script>
213"""
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)
221 if position == "right":
222 return f"{text} {icon_tag}"
224 return f"{icon_tag} {text}"
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)
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 }
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}"')
243 attrs_str = " ".join(attr_parts)
244 return f"<button {attrs_str}>{icon_tag}</button>"