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
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:30 -0800
1"""Lucide icons adapter implementation."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.depends import depends
9from ._base import IconsBase, IconsBaseSettings
12class LucideIconsSettings(IconsBaseSettings):
13 """Lucide-specific settings."""
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
21class LucideIcons(IconsBase):
22 """Lucide icons adapter implementation."""
24 # Required ACB 0.19.0+ metadata
25 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2d1a2") # Static UUID7
26 MODULE_STATUS = "stable"
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 }
105 def __init__(self) -> None:
106 """Initialize Lucide adapter."""
107 super().__init__()
108 self.settings = LucideIconsSettings()
110 # Register with ACB dependency system
111 with suppress(Exception):
112 depends.set(self)
114 def get_stylesheet_links(self) -> list[str]:
115 """Generate Lucide stylesheet/script link tags."""
116 links = []
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}">')
127 return links
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)
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}"
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)
145 if self.settings.use_svg:
146 return self._get_svg_icon_tag(lucide_name, **attributes)
148 return self._get_font_icon_tag(lucide_name, **attributes)
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}"']
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 @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}"
180 # Add any additional classes
181 if "class" in attributes:
182 icon_class = f"{icon_class} {attributes.pop('class')}"
184 # Build attributes string
185 attr_parts = [f'class="{icon_class}"']
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}"')
194 # Add accessibility
195 if "aria-label" not in attributes:
196 attr_parts.append(f'aria-label="{icon_name} icon"')
198 attrs_str = " ".join(attr_parts)
199 return f"<i {attrs_str}></i>"
201 def get_initialization_script(self) -> str:
202 """Generate JavaScript initialization script for SVG mode."""
203 if not self.settings.use_svg:
204 return ""
206 return """
207<script>
208 document.addEventListener('DOMContentLoaded', function() {
209 if (typeof lucide !== 'undefined') {
210 lucide.createIcons();
211 }
212 });
213</script>
214"""
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)
222 if position == "right":
223 return f"{text} {icon_tag}"
225 return f"{icon_tag} {text}"
227 def get_icon_button(self, icon_name: str, **attributes: Any) -> str:
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>"
247IconsSettings = LucideIconsSettings
248Icons = LucideIcons
250depends.set(Icons, "lucide")
252__all__ = ["LucideIcons", "LucideIconsSettings", "IconsSettings", "Icons"]