Coverage for fastblocks / adapters / icons / heroicons.py: 27%

156 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-19 12:49 -0800

1"""Heroicons adapter for FastBlocks with outline/solid variants.""" 

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 

10from ._utils import ( 

11 add_accessibility_attributes, 

12 build_attr_string, 

13 process_animations, 

14 process_semantic_colors, 

15 process_state_attributes, 

16 process_transformations, 

17) 

18 

19 

20class HeroiconsIconsSettings(IconsBaseSettings): 

21 """Settings for Heroicons adapter.""" 

22 

23 # Required ACB 0.19.0+ metadata 

24 MODULE_ID: UUID = UUID("01937d86-ad8f-c29a-eb1c-0a1b2c3d4e5f") # Static UUID7 

25 MODULE_STATUS: str = "stable" 

26 

27 # Heroicons configuration 

28 version: str = "2.0.18" 

29 cdn_url: str = "https://cdn.jsdelivr.net/npm/heroicons" 

30 default_variant: str = "outline" # outline, solid, mini 

31 default_size: str = "24" # 20 (mini), 24 (outline/solid) 

32 

33 # Variant settings 

34 enabled_variants: list[str] = ["outline", "solid", "mini"] 

35 

36 # Icon mapping for common names and aliases 

37 icon_aliases: dict[str, str] = { 

38 "home": "home", 

39 "user": "user", 

40 "settings": "cog-6-tooth", 

41 "search": "magnifying-glass", 

42 "menu": "bars-3", 

43 "close": "x-mark", 

44 "check": "check", 

45 "error": "exclamation-triangle", 

46 "info": "information-circle", 

47 "success": "check-circle", 

48 "warning": "exclamation-triangle", 

49 "edit": "pencil", 

50 "delete": "trash", 

51 "save": "document-arrow-down", 

52 "download": "arrow-down-tray", 

53 "upload": "arrow-up-tray", 

54 "email": "envelope", 

55 "phone": "phone", 

56 "location": "map-pin", 

57 "calendar": "calendar-days", 

58 "clock": "clock", 

59 "heart": "heart", 

60 "star": "star", 

61 "share": "share", 

62 "link": "link", 

63 "copy": "document-duplicate", 

64 "cut": "scissors", 

65 "paste": "clipboard", 

66 "undo": "arrow-uturn-left", 

67 "redo": "arrow-uturn-right", 

68 "refresh": "arrow-path", 

69 "logout": "arrow-right-on-rectangle", 

70 "login": "arrow-left-on-rectangle", 

71 "plus": "plus", 

72 "minus": "minus", 

73 "eye": "eye", 

74 "eye-off": "eye-slash", 

75 "lock": "lock-closed", 

76 "unlock": "lock-open", 

77 } 

78 

79 # Size presets 

80 size_presets: dict[str, str] = { 

81 "xs": "16", 

82 "sm": "20", 

83 "md": "24", 

84 "lg": "28", 

85 "xl": "32", 

86 "2xl": "40", 

87 "3xl": "48", 

88 } 

89 

90 

91class HeroiconsIcons(IconsBase): 

92 """Heroicons adapter with outline/solid/mini variants.""" 

93 

94 # Required ACB 0.19.0+ metadata 

95 MODULE_ID: UUID = UUID("01937d86-ad8f-c29a-eb1c-0a1b2c3d4e5f") # Static UUID7 

96 MODULE_STATUS: str = "stable" 

97 

98 def __init__(self) -> None: 

99 """Initialize Heroicons adapter.""" 

100 super().__init__() 

101 self.settings: HeroiconsIconsSettings | None = None 

102 

103 # Register with ACB dependency system 

104 with suppress(Exception): 

105 depends.set(self) 

106 

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

108 """Get Heroicons stylesheet links.""" 

109 if not self.settings: 

110 self.settings = HeroiconsIconsSettings() 

111 

112 links = [] 

113 

114 # Heroicons base CSS 

115 heroicons_css = self._generate_heroicons_css() 

116 links.append(f"<style>{heroicons_css}</style>") 

117 

118 return links 

119 

120 def _generate_heroicons_css(self) -> str: 

121 """Generate Heroicons-specific CSS.""" 

122 if not self.settings: 

123 self.settings = HeroiconsIconsSettings() 

124 

125 return f""" 

126/* Heroicons Base Styles */ 

127.heroicon {{ 

128 display: inline-block; 

129 vertical-align: -0.125em; 

130 width: {self.settings.default_size}px; 

131 height: {self.settings.default_size}px; 

132 flex-shrink: 0; 

133}} 

134 

135/* Size variants */ 

136.heroicon-xs {{ width: 16px; height: 16px; }} 

137.heroicon-sm {{ width: 20px; height: 20px; }} 

138.heroicon-md {{ width: 24px; height: 24px; }} 

139.heroicon-lg {{ width: 28px; height: 28px; }} 

140.heroicon-xl {{ width: 32px; height: 32px; }} 

141.heroicon-2xl {{ width: 40px; height: 40px; }} 

142.heroicon-3xl {{ width: 48px; height: 48px; }} 

143 

144/* Variant-specific styles */ 

145.heroicon-outline {{ 

146 stroke: currentColor; 

147 fill: none; 

148 stroke-width: 1.5; 

149}} 

150 

151.heroicon-solid {{ 

152 fill: currentColor; 

153}} 

154 

155.heroicon-mini {{ 

156 fill: currentColor; 

157 width: 20px; 

158 height: 20px; 

159}} 

160 

161/* Rotation and transformation */ 

162.heroicon-rotate-90 {{ transform: rotate(90deg); }} 

163.heroicon-rotate-180 {{ transform: rotate(180deg); }} 

164.heroicon-rotate-270 {{ transform: rotate(270deg); }} 

165.heroicon-flip-horizontal {{ transform: scaleX(-1); }} 

166.heroicon-flip-vertical {{ transform: scaleY(-1); }} 

167 

168/* Animation support */ 

169.heroicon-spin {{ 

170 animation: heroicon-spin 2s linear infinite; 

171}} 

172 

173.heroicon-pulse {{ 

174 animation: heroicon-pulse 2s ease-in-out infinite alternate; 

175}} 

176 

177.heroicon-bounce {{ 

178 animation: heroicon-bounce 1s ease-in-out infinite; 

179}} 

180 

181@keyframes heroicon-spin {{ 

182 0% {{ transform: rotate(0deg); }} 

183 100% {{ transform: rotate(360deg); }} 

184}} 

185 

186@keyframes heroicon-pulse {{ 

187 from {{ opacity: 1; }} 

188 to {{ opacity: 0.25; }} 

189}} 

190 

191@keyframes heroicon-bounce {{ 

192 0%, 100% {{ transform: translateY(0); }} 

193 50% {{ transform: translateY(-25%); }} 

194}} 

195 

196/* Color utilities */ 

197.heroicon-primary {{ color: var(--primary-color, #3b82f6); }} 

198.heroicon-secondary {{ color: var(--secondary-color, #6b7280); }} 

199.heroicon-success {{ color: var(--success-color, #10b981); }} 

200.heroicon-warning {{ color: var(--warning-color, #f59e0b); }} 

201.heroicon-danger {{ color: var(--danger-color, #ef4444); }} 

202.heroicon-info {{ color: var(--info-color, #3b82f6); }} 

203.heroicon-gray {{ color: var(--gray-color, #6b7280); }} 

204.heroicon-white {{ color: white; }} 

205.heroicon-black {{ color: black; }} 

206 

207/* Interactive states */ 

208.heroicon-interactive {{ 

209 cursor: pointer; 

210 transition: all 0.2s ease; 

211}} 

212 

213.heroicon-interactive:hover {{ 

214 transform: scale(1.1); 

215 opacity: 0.8; 

216}} 

217 

218.heroicon-interactive:active {{ 

219 transform: scale(0.95); 

220}} 

221 

222/* States */ 

223.heroicon-disabled {{ 

224 opacity: 0.5; 

225 cursor: not-allowed; 

226}} 

227 

228.heroicon-loading {{ 

229 opacity: 0.6; 

230}} 

231 

232/* Button integration */ 

233.btn .heroicon {{ 

234 margin-right: 0.5rem; 

235}} 

236 

237.btn .heroicon:last-child {{ 

238 margin-right: 0; 

239 margin-left: 0.5rem; 

240}} 

241 

242.btn .heroicon:only-child {{ 

243 margin: 0; 

244}} 

245 

246/* Badge integration */ 

247.badge .heroicon {{ 

248 width: 1em; 

249 height: 1em; 

250 margin-right: 0.25rem; 

251}} 

252 

253/* Navigation integration */ 

254.nav-link .heroicon {{ 

255 width: 1.25rem; 

256 height: 1.25rem; 

257 margin-right: 0.5rem; 

258}} 

259""" 

260 

261 def get_icon_class(self, icon_name: str, variant: str | None = None) -> str: 

262 if not self.settings: 

263 self.settings = HeroiconsIconsSettings() 

264 

265 # Resolve icon aliases 

266 if icon_name in self.settings.icon_aliases: 

267 icon_name = self.settings.icon_aliases[icon_name] 

268 

269 # Use default variant if not specified 

270 if not variant: 

271 variant = self.settings.default_variant 

272 

273 # Validate variant 

274 if variant not in self.settings.enabled_variants: 

275 variant = self.settings.default_variant 

276 

277 return f"heroicon heroicon-{variant}" 

278 

279 def _get_icon_size(self, size: str | None, variant: str) -> str: 

280 """Determine icon size based on input and variant.""" 

281 if size and size in self.settings.size_presets: 

282 return self.settings.size_presets[size] 

283 elif size and size.isdigit(): 

284 return size 

285 else: 

286 # Default size based on variant 

287 return "20" if variant == "mini" else self.settings.default_size 

288 

289 def _build_icon_class( 

290 self, icon_name: str, variant: str, size: str | None, attributes: dict[str, Any] 

291 ) -> str: 

292 """Build the complete icon class string.""" 

293 # Build base icon class 

294 icon_class = self.get_icon_class(icon_name, variant) 

295 

296 # Add size class if using preset 

297 if size and size in self.settings.size_presets: 

298 icon_class += f" heroicon-{size}" 

299 

300 # Add custom classes 

301 if "class" in attributes: 

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

303 

304 # Process attributes using shared utilities 

305 transform_classes, attributes = process_transformations(attributes, "heroicon") 

306 animation_classes, attributes = process_animations( 

307 attributes, ["spin", "pulse", "bounce"], "heroicon" 

308 ) 

309 semantic_colors = [ 

310 "primary", 

311 "secondary", 

312 "success", 

313 "warning", 

314 "danger", 

315 "info", 

316 "gray", 

317 "white", 

318 "black", 

319 ] 

320 color_class, attributes = process_semantic_colors( 

321 attributes, semantic_colors, "heroicon" 

322 ) 

323 state_classes, attributes = process_state_attributes(attributes, "heroicon") 

324 

325 # Combine all classes 

326 return icon_class + ( 

327 transform_classes + animation_classes + color_class + state_classes 

328 ) 

329 

330 def _build_svg_attributes( 

331 self, icon_class: str, icon_size: str, variant: str, attributes: dict[str, Any] 

332 ) -> dict[str, Any]: 

333 """Build SVG attributes with variant-specific settings.""" 

334 # Build SVG attributes 

335 svg_attrs = { 

336 "class": icon_class, 

337 "width": icon_size, 

338 "height": icon_size, 

339 "viewBox": f"0 0 {icon_size} {icon_size}", 

340 } | attributes 

341 

342 # Add accessibility and variant-specific attributes 

343 svg_attrs = add_accessibility_attributes(svg_attrs) 

344 if variant == "outline": 

345 svg_attrs.setdefault("stroke-width", "1.5") 

346 svg_attrs.setdefault("stroke", "currentColor") 

347 svg_attrs.setdefault("fill", "none") 

348 else: 

349 svg_attrs.setdefault("fill", "currentColor") 

350 

351 return svg_attrs 

352 

353 def get_icon_tag( 

354 self, 

355 icon_name: str, 

356 variant: str | None = None, 

357 size: str | None = None, 

358 **attributes: Any, 

359 ) -> str: 

360 if not self.settings: 

361 self.settings = HeroiconsIconsSettings() 

362 

363 # Resolve icon aliases 

364 if icon_name in self.settings.icon_aliases: 

365 icon_name = self.settings.icon_aliases[icon_name] 

366 

367 # Use default variant if not specified 

368 if not variant: 

369 variant = self.settings.default_variant 

370 

371 # Validate variant 

372 if variant not in self.settings.enabled_variants: 

373 variant = self.settings.default_variant 

374 

375 # Determine size 

376 icon_size = self._get_icon_size(size, variant) 

377 

378 # Build icon class 

379 icon_class = self._build_icon_class(icon_name, variant, size, attributes) 

380 

381 # Build SVG attributes 

382 svg_attrs = self._build_svg_attributes( 

383 icon_class, icon_size, variant, attributes 

384 ) 

385 

386 # Generate SVG content and build tag 

387 svg_content = self._get_icon_svg_content(icon_name, variant) 

388 attr_string = build_attr_string(svg_attrs) 

389 return f"<svg {attr_string}>{svg_content}</svg>" 

390 

391 def _get_icon_svg_content(self, icon_name: str, variant: str) -> str: 

392 """Get SVG content for specific icon and variant.""" 

393 # This would typically come from the Heroicons icon registry 

394 # For now, return placeholder content for common icons 

395 

396 # Common icon paths (simplified examples) 

397 icon_paths = { 

398 "home": { 

399 "outline": '<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />', 

400 "solid": '<path d="M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 101.06-1.06l-8.689-8.69a2.25 2.25 0 00-3.182 0l-8.69 8.69a.75.75 0 001.061 1.06l8.69-8.69z"/><path d="M12 5.432l8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 01-.75-.75v-4.5a.75.75 0 00-.75-.75h-3a.75.75 0 00-.75.75V21a.75.75 0 01-.75.75H5.625a1.875 1.875 0 01-1.875-1.875v-6.198a2.29 2.29 0 00.091-.086L12 5.432z"/>', 

401 "mini": '<path d="M9.293 2.293a1 1 0 011.414 0l7 7A1 1 0 0117 11h-1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3a1 1 0 00-1-1H9a1 1 0 00-1 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-6H3a1 1 0 01-.707-1.707l7-7z"/>', 

402 }, 

403 "user": { 

404 "outline": '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />', 

405 "solid": '<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd" />', 

406 "mini": '<path d="M10 8a3 3 0 100-6 3 3 0 000 6zM3.465 14.493a1.23 1.23 0 00.41 1.412A9.957 9.957 0 0010 18c2.31 0 4.438-.784 6.131-2.1.43-.333.604-.903.408-1.41a7.002 7.002 0 00-13.074.003z"/>', 

407 }, 

408 "x-mark": { 

409 "outline": '<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />', 

410 "solid": '<path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 011.06 0L12 10.94l5.47-5.47a.75.75 0 111.06 1.06L13.06 12l5.47 5.47a.75.75 0 11-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 01-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 010-1.06z" clip-rule="evenodd" />', 

411 "mini": '<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>', 

412 }, 

413 "check": { 

414 "outline": '<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />', 

415 "solid": '<path fill-rule="evenodd" d="M19.916 4.626a.75.75 0 01.208 1.04l-9 13.5a.75.75 0 01-1.154.114l-6-6a.75.75 0 011.06-1.06l5.353 5.353 8.493-12.739a.75.75 0 011.04-.208z" clip-rule="evenodd" />', 

416 "mini": '<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />', 

417 }, 

418 } 

419 

420 # Return path for the requested icon and variant 

421 if icon_name in icon_paths and variant in icon_paths[icon_name]: 

422 return icon_paths[icon_name][variant] 

423 

424 # Fallback for unknown icons 

425 return f"<!-- {icon_name} ({variant}) not found -->" 

426 

427 def get_icon_sprite_url(self, variant: str = "outline") -> str: 

428 """Get URL for Heroicons sprite file.""" 

429 if not self.settings: 

430 self.settings = HeroiconsIconsSettings() 

431 

432 return f"{self.settings.cdn_url}@{self.settings.version}/{variant}.svg" 

433 

434 @staticmethod 

435 def get_available_icons() -> dict[str, list[str]]: 

436 """Get list of available icons by category.""" 

437 return { 

438 "general": [ 

439 "home", 

440 "user", 

441 "cog-6-tooth", 

442 "magnifying-glass", 

443 "bars-3", 

444 "x-mark", 

445 "check", 

446 "plus", 

447 "minus", 

448 "ellipsis-horizontal", 

449 ], 

450 "navigation": [ 

451 "arrow-left", 

452 "arrow-right", 

453 "arrow-up", 

454 "arrow-down", 

455 "chevron-left", 

456 "chevron-right", 

457 "chevron-up", 

458 "chevron-down", 

459 "arrow-path", 

460 "arrow-uturn-left", 

461 "arrow-uturn-right", 

462 ], 

463 "communication": [ 

464 "envelope", 

465 "phone", 

466 "chat-bubble-left", 

467 "paper-airplane", 

468 "bell", 

469 "speaker-wave", 

470 "microphone", 

471 "video-camera", 

472 ], 

473 "media": [ 

474 "play", 

475 "pause", 

476 "stop", 

477 "backward", 

478 "forward", 

479 "speaker-wave", 

480 "speaker-x-mark", 

481 "musical-note", 

482 ], 

483 "file": [ 

484 "document", 

485 "folder", 

486 "arrow-down-tray", 

487 "arrow-up-tray", 

488 "document-arrow-down", 

489 "document-text", 

490 "photo", 

491 "film", 

492 ], 

493 "editing": [ 

494 "pencil", 

495 "trash", 

496 "document-duplicate", 

497 "scissors", 

498 "clipboard", 

499 "eye", 

500 "eye-slash", 

501 "lock-closed", 

502 "lock-open", 

503 ], 

504 "status": [ 

505 "check-circle", 

506 "x-circle", 

507 "exclamation-triangle", 

508 "information-circle", 

509 "question-mark-circle", 

510 "light-bulb", 

511 ], 

512 } 

513 

514 

515# Template filter registration for FastBlocks 

516def _create_hero_button( 

517 text: str, 

518 icon: str | None, 

519 variant: str, 

520 icon_position: str, 

521 icons: HeroiconsIcons, 

522 **attributes: Any, 

523) -> str: 

524 """Build button HTML with Heroicons icon.""" 

525 btn_class = attributes.pop("class", "btn btn-primary") 

526 

527 # Build button content 

528 if icon: 

529 icon_tag = icons.get_icon_tag(icon, variant, size="sm") 

530 if icon_position == "left": 

531 content = f"{icon_tag} {text}" 

532 elif icon_position == "right": 

533 content = f"{text} {icon_tag}" 

534 else: 

535 content = text 

536 else: 

537 content = text 

538 

539 # Build button attributes 

540 btn_attrs = {"class": btn_class} | attributes 

541 attr_string = " ".join(f'{k}="{v}"' for k, v in btn_attrs.items()) 

542 

543 return f"<button {attr_string}>{content}</button>" 

544 

545 

546def _create_hero_badge( 

547 text: str, 

548 icon: str | None, 

549 variant: str, 

550 icons: HeroiconsIcons, 

551 **attributes: Any, 

552) -> str: 

553 """Build badge HTML with Heroicons icon.""" 

554 badge_class = attributes.pop("class", "badge badge-primary") 

555 

556 # Build badge content 

557 if icon: 

558 icon_tag = icons.get_icon_tag(icon, variant, size="xs") 

559 content = f"{icon_tag} {text}" 

560 else: 

561 content = text 

562 

563 # Build badge attributes 

564 badge_attrs = {"class": badge_class} | attributes 

565 attr_string = " ".join(f'{k}="{v}"' for k, v in badge_attrs.items()) 

566 

567 return f"<span {attr_string}>{content}</span>" 

568 

569 

570def register_heroicons_filters(env: Any) -> None: 

571 """Register Heroicons filters for Jinja2 templates.""" 

572 

573 @env.filter("heroicon") # type: ignore[misc] # Jinja2 decorator preserves signature 

574 def heroicon_filter( 

575 icon_name: str, 

576 variant: str = "outline", 

577 size: str | None = None, 

578 **attributes: Any, 

579 ) -> str: 

580 """Template filter for Heroicons.""" 

581 icons = depends.get_sync("icons") 

582 if isinstance(icons, HeroiconsIcons): 

583 return icons.get_icon_tag(icon_name, variant, size, **attributes) 

584 return f"<!-- {icon_name} -->" 

585 

586 @env.filter("heroicon_class") # type: ignore[misc] # Jinja2 decorator preserves signature 

587 def heroicon_class_filter(icon_name: str, variant: str = "outline") -> str: 

588 """Template filter for Heroicons classes.""" 

589 icons = depends.get_sync("icons") 

590 if isinstance(icons, HeroiconsIcons): 

591 return icons.get_icon_class(icon_name, variant) 

592 return f"heroicon-{icon_name}" 

593 

594 @env.global_("heroicons_stylesheet_links") # type: ignore[misc] # Jinja2 decorator preserves signature 

595 def heroicons_stylesheet_links() -> str: 

596 """Global function for Heroicons stylesheet links.""" 

597 icons = depends.get_sync("icons") 

598 if isinstance(icons, HeroiconsIcons): 

599 return "\n".join(icons.get_stylesheet_links()) 

600 return "" 

601 

602 @env.global_("hero_button") # type: ignore[misc] # Jinja2 decorator preserves signature 

603 def hero_button( 

604 text: str, 

605 icon: str | None = None, 

606 variant: str = "outline", 

607 icon_position: str = "left", 

608 **attributes: Any, 

609 ) -> str: 

610 """Generate button with Heroicons icon.""" 

611 icons = depends.get_sync("icons") 

612 if isinstance(icons, HeroiconsIcons): 

613 return _create_hero_button( 

614 text, icon, variant, icon_position, icons, **attributes 

615 ) 

616 return f"<button>{text}</button>" 

617 

618 @env.global_("hero_badge") # type: ignore[misc] # Jinja2 decorator preserves signature 

619 def hero_badge( 

620 text: str, icon: str | None = None, variant: str = "outline", **attributes: Any 

621 ) -> str: 

622 """Generate badge with Heroicons icon.""" 

623 icons = depends.get_sync("icons") 

624 if isinstance(icons, HeroiconsIcons): 

625 return _create_hero_badge(text, icon, variant, icons, **attributes) 

626 return f"<span class='badge'>{text}</span>" 

627 

628 

629IconsSettings = HeroiconsIconsSettings 

630Icons = HeroiconsIcons 

631 

632depends.set(Icons, "heroicons") 

633 

634 

635# ACB 0.19.0+ compatibility 

636__all__ = [ 

637 "HeroiconsIcons", 

638 "HeroiconsIconsSettings", 

639 "register_heroicons_filters", 

640 "Icons", 

641 "IconsSettings", 

642]