Coverage for fastblocks / adapters / icons / materialicons.py: 22%

187 statements  

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

1"""Material Icons adapter for FastBlocks with multiple themes.""" 

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 MaterialIconsSettings(IconsBaseSettings): 

21 """Settings for Material Icons adapter.""" 

22 

23 # Required ACB 0.19.0+ metadata 

24 MODULE_ID: UUID = UUID("01937d86-cf0b-e4bc-0d3e-2c3d4e5f6071") # Static UUID7 

25 MODULE_STATUS: str = "stable" 

26 

27 # Material Icons configuration 

28 version: str = "latest" 

29 base_url: str = "https://fonts.googleapis.com" 

30 default_theme: str = "filled" # filled, outlined, round, sharp, two-tone 

31 default_size: str = "24px" 

32 

33 # Available themes 

34 enabled_themes: list[str] = ["filled", "outlined", "round", "sharp", "two-tone"] 

35 

36 # Icon mapping for common names 

37 icon_aliases: dict[str, str] = { 

38 "home": "home", 

39 "user": "person", 

40 "settings": "settings", 

41 "search": "search", 

42 "menu": "menu", 

43 "close": "close", 

44 "check": "check", 

45 "error": "error", 

46 "info": "info", 

47 "success": "check_circle", 

48 "warning": "warning", 

49 "edit": "edit", 

50 "delete": "delete", 

51 "save": "save", 

52 "download": "download", 

53 "upload": "upload", 

54 "email": "email", 

55 "phone": "phone", 

56 "location": "location_on", 

57 "calendar": "event", 

58 "clock": "schedule", 

59 "heart": "favorite", 

60 "star": "star", 

61 "share": "share", 

62 "link": "link", 

63 "copy": "content_copy", 

64 "cut": "content_cut", 

65 "paste": "content_paste", 

66 "undo": "undo", 

67 "redo": "redo", 

68 "refresh": "refresh", 

69 "logout": "logout", 

70 "login": "login", 

71 "plus": "add", 

72 "minus": "remove", 

73 "eye": "visibility", 

74 "eye-off": "visibility_off", 

75 "lock": "lock", 

76 "unlock": "lock_open", 

77 "arrow-up": "keyboard_arrow_up", 

78 "arrow-down": "keyboard_arrow_down", 

79 "arrow-left": "keyboard_arrow_left", 

80 "arrow-right": "keyboard_arrow_right", 

81 } 

82 

83 # Size presets 

84 size_presets: dict[str, str] = { 

85 "xs": "16px", 

86 "sm": "20px", 

87 "md": "24px", 

88 "lg": "28px", 

89 "xl": "32px", 

90 "2xl": "40px", 

91 "3xl": "48px", 

92 "4xl": "56px", 

93 "5xl": "64px", 

94 } 

95 

96 # Color palette 

97 material_colors: dict[str, str] = { 

98 "red": "#f44336", 

99 "pink": "#e91e63", 

100 "purple": "#9c27b0", 

101 "deep-purple": "#673ab7", 

102 "indigo": "#3f51b5", 

103 "blue": "#2196f3", 

104 "light-blue": "#03a9f4", 

105 "cyan": "#00bcd4", 

106 "teal": "#009688", 

107 "green": "#4caf50", 

108 "light-green": "#8bc34a", 

109 "lime": "#cddc39", 

110 "yellow": "#ffeb3b", 

111 "amber": "#ffc107", 

112 "orange": "#ff9800", 

113 "deep-orange": "#ff5722", 

114 "brown": "#795548", 

115 "grey": "#9e9e9e", 

116 "blue-grey": "#607d8b", 

117 } 

118 

119 

120class MaterialIcons(IconsBase): 

121 """Material Icons adapter with multiple themes and comprehensive icon set.""" 

122 

123 # Required ACB 0.19.0+ metadata 

124 MODULE_ID: UUID = UUID("01937d86-cf0b-e4bc-0d3e-2c3d4e5f6071") # Static UUID7 

125 MODULE_STATUS: str = "stable" 

126 

127 def __init__(self) -> None: 

128 """Initialize Material Icons adapter.""" 

129 super().__init__() 

130 self.settings: MaterialIconsSettings | None = None 

131 

132 # Register with ACB dependency system 

133 with suppress(Exception): 

134 depends.set(self) 

135 

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

137 """Get Material Icons stylesheet links.""" 

138 if not self.settings: 

139 self.settings = MaterialIconsSettings() 

140 

141 links = [] 

142 

143 # Material Icons CSS from Google Fonts 

144 for theme in self.settings.enabled_themes: 

145 if theme == "filled": 

146 # Base Material Icons (filled is default) 

147 css_url = f"{self.settings.base_url}/icon?family=Material+Icons" 

148 else: 

149 # Themed variants 

150 theme_name = theme.replace("-", "+").title() 

151 css_url = ( 

152 f"{self.settings.base_url}/icon?family=Material+Icons+{theme_name}" 

153 ) 

154 

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

156 

157 # Custom Material Icons CSS 

158 material_css = self._generate_material_css() 

159 links.append(f"<style>{material_css}</style>") 

160 

161 return links 

162 

163 def _generate_material_css(self) -> str: 

164 """Generate Material Icons-specific CSS.""" 

165 if not self.settings: 

166 self.settings = MaterialIconsSettings() 

167 

168 return f""" 

169/* Material Icons Base Styles */ 

170.material-icons {{ 

171 font-family: 'Material Icons'; 

172 font-weight: normal; 

173 font-style: normal; 

174 font-size: {self.settings.default_size}; 

175 line-height: 1; 

176 letter-spacing: normal; 

177 text-transform: none; 

178 display: inline-block; 

179 white-space: nowrap; 

180 word-wrap: normal; 

181 direction: ltr; 

182 -webkit-font-feature-settings: 'liga'; 

183 -webkit-font-smoothing: antialiased; 

184 vertical-align: -0.125em; 

185}} 

186 

187/* Theme-specific font families */ 

188.material-icons-outlined {{ 

189 font-family: 'Material Icons Outlined'; 

190}} 

191 

192.material-icons-round {{ 

193 font-family: 'Material Icons Round'; 

194}} 

195 

196.material-icons-sharp {{ 

197 font-family: 'Material Icons Sharp'; 

198}} 

199 

200.material-icons-two-tone {{ 

201 font-family: 'Material Icons Two Tone'; 

202}} 

203 

204/* Size variants */ 

205.material-icons-xs {{ font-size: 16px; }} 

206.material-icons-sm {{ font-size: 20px; }} 

207.material-icons-md {{ font-size: 24px; }} 

208.material-icons-lg {{ font-size: 28px; }} 

209.material-icons-xl {{ font-size: 32px; }} 

210.material-icons-2xl {{ font-size: 40px; }} 

211.material-icons-3xl {{ font-size: 48px; }} 

212.material-icons-4xl {{ font-size: 56px; }} 

213.material-icons-5xl {{ font-size: 64px; }} 

214 

215/* Density variants */ 

216.material-icons-dense {{ 

217 font-size: 20px; 

218}} 

219 

220.material-icons-comfortable {{ 

221 font-size: 24px; 

222}} 

223 

224.material-icons-compact {{ 

225 font-size: 18px; 

226}} 

227 

228/* Rotation and transformation */ 

229.material-icons-rotate-90 {{ transform: rotate(90deg); }} 

230.material-icons-rotate-180 {{ transform: rotate(180deg); }} 

231.material-icons-rotate-270 {{ transform: rotate(270deg); }} 

232.material-icons-flip-horizontal {{ transform: scaleX(-1); }} 

233.material-icons-flip-vertical {{ transform: scaleY(-1); }} 

234 

235/* Animation support */ 

236.material-icons-spin {{ 

237 animation: material-spin 2s linear infinite; 

238}} 

239 

240.material-icons-pulse {{ 

241 animation: material-pulse 2s ease-in-out infinite alternate; 

242}} 

243 

244.material-icons-bounce {{ 

245 animation: material-bounce 1s ease-in-out infinite; 

246}} 

247 

248.material-icons-shake {{ 

249 animation: material-shake 0.82s cubic-bezier(.36,.07,.19,.97) both; 

250}} 

251 

252.material-icons-flip {{ 

253 animation: material-flip 2s linear infinite; 

254}} 

255 

256@keyframes material-spin {{ 

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

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

259}} 

260 

261@keyframes material-pulse {{ 

262 from {{ opacity: 1; }} 

263 to {{ opacity: 0.25; }} 

264}} 

265 

266@keyframes material-bounce {{ 

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

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

269}} 

270 

271@keyframes material-shake {{ 

272 10%, 90% {{ transform: translate3d(-1px, 0, 0); }} 

273 20%, 80% {{ transform: translate3d(2px, 0, 0); }} 

274 30%, 50%, 70% {{ transform: translate3d(-4px, 0, 0); }} 

275 40%, 60% {{ transform: translate3d(4px, 0, 0); }} 

276}} 

277 

278@keyframes material-flip {{ 

279 0% {{ transform: rotateY(0); }} 

280 50% {{ transform: rotateY(180deg); }} 

281 100% {{ transform: rotateY(360deg); }} 

282}} 

283 

284/* Material Design color utilities */ 

285{self._generate_material_color_classes()} 

286 

287/* Interactive states */ 

288.material-icons-interactive {{ 

289 cursor: pointer; 

290 transition: all 0.2s ease; 

291 border-radius: 50%; 

292 padding: 4px; 

293}} 

294 

295.material-icons-interactive:hover {{ 

296 background-color: rgba(0, 0, 0, 0.04); 

297 transform: scale(1.1); 

298}} 

299 

300.material-icons-interactive:active {{ 

301 background-color: rgba(0, 0, 0, 0.08); 

302 transform: scale(0.95); 

303}} 

304 

305/* States */ 

306.material-icons-disabled {{ 

307 opacity: 0.38; 

308 cursor: not-allowed; 

309}} 

310 

311.material-icons-inactive {{ 

312 opacity: 0.54; 

313}} 

314 

315.material-icons-loading {{ 

316 opacity: 0.6; 

317}} 

318 

319/* Button integration */ 

320.btn .material-icons {{ 

321 margin-right: 8px; 

322 vertical-align: -0.125em; 

323}} 

324 

325.btn .material-icons:last-child {{ 

326 margin-right: 0; 

327 margin-left: 8px; 

328}} 

329 

330.btn .material-icons:only-child {{ 

331 margin: 0; 

332}} 

333 

334.btn-sm .material-icons {{ 

335 font-size: 20px; 

336}} 

337 

338.btn-lg .material-icons {{ 

339 font-size: 28px; 

340}} 

341 

342/* Floating Action Button */ 

343.fab {{ 

344 display: inline-flex; 

345 align-items: center; 

346 justify-content: center; 

347 width: 56px; 

348 height: 56px; 

349 border-radius: 50%; 

350 border: none; 

351 box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px 0 rgba(0,0,0,.14), 0 1px 18px 0 rgba(0,0,0,.12); 

352 cursor: pointer; 

353 transition: all 0.3s ease; 

354}} 

355 

356.fab:hover {{ 

357 box-shadow: 0 5px 5px -3px rgba(0,0,0,.2), 0 8px 10px 1px rgba(0,0,0,.14), 0 3px 14px 2px rgba(0,0,0,.12); 

358 transform: translateY(-2px); 

359}} 

360 

361.fab-mini {{ 

362 width: 40px; 

363 height: 40px; 

364}} 

365 

366.fab-extended {{ 

367 width: auto; 

368 height: 48px; 

369 border-radius: 24px; 

370 padding: 0 16px; 

371}} 

372 

373/* Badge integration */ 

374.badge .material-icons {{ 

375 font-size: 16px; 

376 margin-right: 4px; 

377 vertical-align: baseline; 

378}} 

379 

380/* Navigation integration */ 

381.nav-link .material-icons {{ 

382 margin-right: 8px; 

383 font-size: 20px; 

384}} 

385 

386/* List integration */ 

387.list-group-item .material-icons {{ 

388 margin-right: 16px; 

389 color: rgba(0, 0, 0, 0.54); 

390}} 

391 

392/* Input group integration */ 

393.input-group-text .material-icons {{ 

394 color: rgba(0, 0, 0, 0.54); 

395}} 

396 

397/* Alert integration */ 

398.alert .material-icons {{ 

399 margin-right: 8px; 

400 font-size: 20px; 

401}} 

402 

403/* Card integration */ 

404.card-title .material-icons {{ 

405 margin-right: 8px; 

406}} 

407 

408/* Toolbar integration */ 

409.toolbar .material-icons {{ 

410 color: rgba(255, 255, 255, 0.87); 

411}} 

412 

413/* Dark theme support */ 

414@media (prefers-color-scheme: dark) {{ 

415 .material-icons-interactive:hover {{ 

416 background-color: rgba(255, 255, 255, 0.08); 

417 }} 

418 

419 .material-icons-interactive:active {{ 

420 background-color: rgba(255, 255, 255, 0.12); 

421 }} 

422 

423 .list-group-item .material-icons {{ 

424 color: rgba(255, 255, 255, 0.7); 

425 }} 

426 

427 .input-group-text .material-icons {{ 

428 color: rgba(255, 255, 255, 0.7); 

429 }} 

430}} 

431 

432/* Accessibility */ 

433.material-icons[aria-hidden="false"] {{ 

434 position: relative; 

435}} 

436 

437.material-icons[aria-hidden="false"]:focus {{ 

438 outline: 2px solid #1976d2; 

439 outline-offset: 2px; 

440}} 

441""" 

442 

443 def _generate_material_color_classes(self) -> str: 

444 """Generate Material Design color classes.""" 

445 if not self.settings: 

446 self.settings = MaterialIconsSettings() 

447 

448 css = "/* Material Design Colors */\n" 

449 for name, color in self.settings.material_colors.items(): 

450 css += f".material-icons-{name} {{ color: {color}; }}\n" 

451 

452 # Add semantic colors 

453 css += """ 

454.material-icons-primary { color: var(--primary-color, #1976d2); } 

455.material-icons-secondary { color: var(--secondary-color, #424242); } 

456.material-icons-success { color: var(--success-color, #4caf50); } 

457.material-icons-warning { color: var(--warning-color, #ff9800); } 

458.material-icons-danger { color: var(--danger-color, #f44336); } 

459.material-icons-info { color: var(--info-color, #2196f3); } 

460.material-icons-light { color: var(--light-color, #fafafa); } 

461.material-icons-dark { color: var(--dark-color, #212121); } 

462.material-icons-muted { color: var(--muted-color, #757575); } 

463""" 

464 

465 return css 

466 

467 def get_icon_class(self, icon_name: str, theme: str | None = None) -> str: 

468 """Get Material Icons class with theme support.""" 

469 if not self.settings: 

470 self.settings = MaterialIconsSettings() 

471 

472 # Use default theme if not specified 

473 if not theme: 

474 theme = self.settings.default_theme 

475 

476 # Validate theme 

477 if theme not in self.settings.enabled_themes: 

478 theme = self.settings.default_theme 

479 

480 # Build class name based on theme 

481 if theme == "filled": 

482 return "material-icons" 

483 

484 return f"material-icons-{theme.replace('_', '-')}" 

485 

486 def get_icon_tag( 

487 self, 

488 icon_name: str, 

489 **attributes: Any, 

490 ) -> str: 

491 """Generate Material Icons tag with full customization.""" 

492 # Extract theme and size from attributes 

493 theme = attributes.pop("theme", None) 

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

495 

496 if not self.settings: 

497 self.settings = MaterialIconsSettings() 

498 

499 # Resolve icon aliases 

500 resolved_name = icon_name 

501 if icon_name in self.settings.icon_aliases: 

502 resolved_name = self.settings.icon_aliases[icon_name] 

503 

504 # Get base icon class 

505 icon_class = self.get_icon_class(icon_name, theme) 

506 

507 # Add size class or custom size 

508 if size: 

509 if size in self.settings.size_presets: 

510 icon_class += f" material-icons-{size}" 

511 else: 

512 attributes["style"] = ( 

513 f"font-size: {size}; {attributes.get('style', '')}" 

514 ) 

515 

516 # Add custom classes 

517 if "class" in attributes: 

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

519 

520 # Process attributes using shared utilities 

521 transform_classes, attributes = process_transformations( 

522 attributes, "material-icons" 

523 ) 

524 animation_classes, attributes = process_animations( 

525 attributes, ["spin", "pulse", "bounce", "shake", "flip"], "material-icons" 

526 ) 

527 

528 # Extended semantic colors including material design colors 

529 semantic_colors = [ 

530 "primary", 

531 "secondary", 

532 "success", 

533 "warning", 

534 "danger", 

535 "info", 

536 "light", 

537 "dark", 

538 "muted", 

539 *list(self.settings.material_colors.keys()), 

540 ] 

541 color_class, attributes = process_semantic_colors( 

542 attributes, semantic_colors, "material-icons" 

543 ) 

544 state_classes, attributes = process_state_attributes( 

545 attributes, "material-icons" 

546 ) 

547 

548 # Handle density (Material Design specific feature) 

549 if "density" in attributes: 

550 density = attributes.pop("density") 

551 if density in ("dense", "comfortable", "compact"): 

552 icon_class += f" material-icons-{density}" 

553 

554 # Combine all classes 

555 icon_class += ( 

556 transform_classes + animation_classes + color_class + state_classes 

557 ) 

558 

559 # Build attributes and add accessibility 

560 attrs = {"class": icon_class} | attributes 

561 attrs = add_accessibility_attributes(attrs) 

562 

563 # Generate tag 

564 attr_string = build_attr_string(attrs) 

565 return f"<span {attr_string}>{resolved_name}</span>" 

566 

567 def get_fab_tag( 

568 self, 

569 icon_name: str, 

570 variant: str = "regular", 

571 theme: str | None = None, 

572 **attributes: Any, 

573 ) -> str: 

574 """Generate Material Design Floating Action Button.""" 

575 if not self.settings: 

576 self.settings = MaterialIconsSettings() 

577 

578 # Resolve icon name 

579 resolved_name = icon_name 

580 if icon_name in self.settings.icon_aliases: 

581 resolved_name = self.settings.icon_aliases[icon_name] 

582 

583 # Get icon tag 

584 icon_tag = self.get_icon_tag(resolved_name, theme=theme, size="md") 

585 

586 # Build FAB classes 

587 fab_class = "fab" 

588 if variant == "mini": 

589 fab_class += " fab-mini" 

590 elif variant == "extended": 

591 fab_class += " fab-extended" 

592 

593 # Add custom classes 

594 if "class" in attributes: 

595 fab_class += f" {attributes.pop('class')}" 

596 

597 # Handle extended FAB with text 

598 text = attributes.pop("text", "") 

599 if variant == "extended" and text: 

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

601 else: 

602 content = icon_tag 

603 

604 # Build attributes 

605 attrs = {"class": fab_class} | attributes 

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

607 

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

609 

610 @staticmethod 

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

612 """Get list of available Material Icons by category.""" 

613 return { 

614 "action": [ 

615 "home", 

616 "search", 

617 "settings", 

618 "favorite", 

619 "star", 

620 "bookmark", 

621 "help", 

622 "info", 

623 "check_circle", 

624 "done", 

625 "thumb_up", 

626 "thumb_down", 

627 ], 

628 "communication": [ 

629 "email", 

630 "phone", 

631 "chat", 

632 "message", 

633 "comment", 

634 "forum", 

635 "contact_mail", 

636 "contact_phone", 

637 "textsms", 

638 "call", 

639 ], 

640 "content": [ 

641 "add", 

642 "remove", 

643 "clear", 

644 "create", 

645 "edit", 

646 "delete_forever", 

647 "content_copy", 

648 "content_cut", 

649 "content_paste", 

650 "save", 

651 "undo", 

652 "redo", 

653 ], 

654 "editor": [ 

655 "format_bold", 

656 "format_italic", 

657 "format_underlined", 

658 "format_color_text", 

659 "format_align_left", 

660 "format_align_center", 

661 "format_align_right", 

662 "format_list_bulleted", 

663 ], 

664 "file": [ 

665 "folder", 

666 "folder_open", 

667 "insert_drive_file", 

668 "cloud", 

669 "cloud_download", 

670 "cloud_upload", 

671 "attachment", 

672 "file_download", 

673 "file_upload", 

674 ], 

675 "hardware": [ 

676 "computer", 

677 "phone_android", 

678 "phone_iphone", 

679 "tablet", 

680 "laptop", 

681 "desktop_windows", 

682 "keyboard", 

683 "mouse", 

684 "headset", 

685 "speaker", 

686 ], 

687 "image": [ 

688 "image", 

689 "photo", 

690 "photo_camera", 

691 "video_camera", 

692 "movie", 

693 "music_note", 

694 "palette", 

695 "brush", 

696 "color_lens", 

697 "gradient", 

698 ], 

699 "maps": [ 

700 "location_on", 

701 "location_off", 

702 "my_location", 

703 "navigation", 

704 "map", 

705 "place", 

706 "directions", 

707 "directions_car", 

708 "directions_walk", 

709 "local_taxi", 

710 ], 

711 "navigation": [ 

712 "menu", 

713 "close", 

714 "arrow_back", 

715 "arrow_forward", 

716 "arrow_upward", 

717 "arrow_downward", 

718 "chevron_left", 

719 "chevron_right", 

720 "expand_less", 

721 "expand_more", 

722 "fullscreen", 

723 ], 

724 "notification": [ 

725 "notifications", 

726 "notifications_off", 

727 "notification_important", 

728 "alarm", 

729 "alarm_on", 

730 "alarm_off", 

731 "event", 

732 "event_available", 

733 "schedule", 

734 ], 

735 "social": [ 

736 "person", 

737 "people", 

738 "group", 

739 "public", 

740 "school", 

741 "domain", 

742 "cake", 

743 "mood", 

744 "mood_bad", 

745 "sentiment_satisfied", 

746 "party_mode", 

747 ], 

748 "toggle": [ 

749 "check_box", 

750 "check_box_outline_blank", 

751 "radio_button_checked", 

752 "radio_button_unchecked", 

753 "star", 

754 "star_border", 

755 "favorite", 

756 "favorite_border", 

757 "visibility", 

758 "visibility_off", 

759 ], 

760 "av": [ 

761 "play_arrow", 

762 "pause", 

763 "stop", 

764 "fast_forward", 

765 "fast_rewind", 

766 "skip_next", 

767 "skip_previous", 

768 "volume_up", 

769 "volume_down", 

770 "volume_off", 

771 ], 

772 } 

773 

774 

775# Template filter registration for FastBlocks 

776def _register_material_basic_filters(env: Any) -> None: 

777 """Register basic Material Icons filters.""" 

778 

779 @env.filter("material_icon") # type: ignore[misc] 

780 def material_icon_filter( 

781 icon_name: str, 

782 theme: str | None = None, 

783 size: str | None = None, 

784 **attributes: Any, 

785 ) -> str: 

786 """Template filter for Material Icons.""" 

787 icons = depends.get_sync("icons") 

788 if isinstance(icons, MaterialIcons): 

789 return icons.get_icon_tag(icon_name, theme=theme, size=size, **attributes) 

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

791 

792 @env.filter("material_class") # type: ignore[misc] 

793 def material_class_filter(icon_name: str, theme: str | None = None) -> str: 

794 """Template filter for Material Icons classes.""" 

795 icons = depends.get_sync("icons") 

796 if isinstance(icons, MaterialIcons): 

797 return icons.get_icon_class(icon_name, theme) 

798 return "material-icons" 

799 

800 @env.global_("materialicons_stylesheet_links") # type: ignore[misc] 

801 def materialicons_stylesheet_links() -> str: 

802 """Global function for Material Icons stylesheet links.""" 

803 icons = depends.get_sync("icons") 

804 if isinstance(icons, MaterialIcons): 

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

806 return "" 

807 

808 

809def _register_material_fab_functions(env: Any) -> None: 

810 """Register Material Design FAB functions.""" 

811 

812 @env.global_("material_fab") # type: ignore[misc] 

813 def material_fab( 

814 icon_name: str, 

815 variant: str = "regular", 

816 theme: str | None = None, 

817 **attributes: Any, 

818 ) -> str: 

819 """Generate Material Design Floating Action Button.""" 

820 icons = depends.get_sync("icons") 

821 if isinstance(icons, MaterialIcons): 

822 return icons.get_fab_tag(icon_name, variant, theme, **attributes) 

823 return f"<button class='fab'>{icon_name}</button>" 

824 

825 

826def _register_material_button_functions(env: Any) -> None: 

827 """Register Material Design button functions.""" 

828 

829 @env.global_("material_button") # type: ignore[misc] 

830 def material_button( 

831 text: str, 

832 icon: str | None = None, 

833 theme: str | None = None, 

834 icon_position: str = "left", 

835 **attributes: Any, 

836 ) -> str: 

837 """Generate button with Material Icon.""" 

838 icons = depends.get_sync("icons") 

839 if not isinstance(icons, MaterialIcons): 

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

841 

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

843 

844 # Build button content 

845 content = "" 

846 if icon: 

847 icon_tag = icons.get_icon_tag(icon, theme=theme, size="sm") 

848 

849 if icon_position == "left": 

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

851 elif icon_position == "right": 

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

853 elif icon_position == "only": 

854 content = icon_tag 

855 else: 

856 content = text 

857 else: 

858 content = text 

859 

860 # Build button attributes 

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

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

863 

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

865 

866 

867def _register_material_chip_functions(env: Any) -> None: 

868 """Register Material Design chip functions.""" 

869 

870 @env.global_("material_chip") # type: ignore[misc] 

871 def material_chip( 

872 text: str, 

873 icon: str | None = None, 

874 theme: str | None = None, 

875 deletable: bool = False, 

876 **attributes: Any, 

877 ) -> str: 

878 """Generate Material Design chip with icon.""" 

879 icons = depends.get_sync("icons") 

880 if not isinstance(icons, MaterialIcons): 

881 return f"<div class='chip'>{text}</div>" 

882 

883 chip_class = attributes.pop("class", "chip") 

884 

885 # Build chip content 

886 content = "" 

887 if icon: 

888 icon_tag = icons.get_icon_tag( 

889 icon, theme=theme, size="sm", class_="chip-icon" 

890 ) 

891 content += icon_tag 

892 

893 content += f"<span class='chip-text'>{text}</span>" 

894 

895 if deletable: 

896 delete_icon = icons.get_icon_tag( 

897 "close", theme=theme, size="sm", class_="chip-delete" 

898 ) 

899 content += delete_icon 

900 

901 # Build chip attributes 

902 chip_attrs = {"class": chip_class} | attributes 

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

904 

905 return f"<div {attr_string}>{content}</div>" 

906 

907 

908def register_materialicons_filters(env: Any) -> None: 

909 """Register Material Icons filters for Jinja2 templates.""" 

910 _register_material_basic_filters(env) 

911 _register_material_fab_functions(env) 

912 _register_material_button_functions(env) 

913 _register_material_chip_functions(env) 

914 

915 

916IconsSettings = MaterialIconsSettings 

917Icons = MaterialIcons 

918 

919depends.set(Icons, "materialicons") 

920 

921# ACB 0.19.0+ compatibility 

922__all__ = [ 

923 "MaterialIcons", 

924 "MaterialIconsSettings", 

925 "register_materialicons_filters", 

926 "Icons", 

927 "IconsSettings", 

928]