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

184 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-09 00:47 -0700

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.config import Settings 

8from acb.depends import depends 

9 

10from ._base import IconsBase 

11from ._utils import ( 

12 add_accessibility_attributes, 

13 build_attr_string, 

14 process_animations, 

15 process_semantic_colors, 

16 process_state_attributes, 

17 process_transformations, 

18) 

19 

20 

21class MaterialIconsSettings(Settings): # type: ignore[misc] 

22 """Settings for Material Icons adapter.""" 

23 

24 # Required ACB 0.19.0+ metadata 

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

26 MODULE_STATUS: str = "stable" 

27 

28 # Material Icons configuration 

29 version: str = "latest" 

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

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

32 default_size: str = "24px" 

33 

34 # Available themes 

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

36 

37 # Icon mapping for common names 

38 icon_aliases: dict[str, str] = { 

39 "home": "home", 

40 "user": "person", 

41 "settings": "settings", 

42 "search": "search", 

43 "menu": "menu", 

44 "close": "close", 

45 "check": "check", 

46 "error": "error", 

47 "info": "info", 

48 "success": "check_circle", 

49 "warning": "warning", 

50 "edit": "edit", 

51 "delete": "delete", 

52 "save": "save", 

53 "download": "download", 

54 "upload": "upload", 

55 "email": "email", 

56 "phone": "phone", 

57 "location": "location_on", 

58 "calendar": "event", 

59 "clock": "schedule", 

60 "heart": "favorite", 

61 "star": "star", 

62 "share": "share", 

63 "link": "link", 

64 "copy": "content_copy", 

65 "cut": "content_cut", 

66 "paste": "content_paste", 

67 "undo": "undo", 

68 "redo": "redo", 

69 "refresh": "refresh", 

70 "logout": "logout", 

71 "login": "login", 

72 "plus": "add", 

73 "minus": "remove", 

74 "eye": "visibility", 

75 "eye-off": "visibility_off", 

76 "lock": "lock", 

77 "unlock": "lock_open", 

78 "arrow-up": "keyboard_arrow_up", 

79 "arrow-down": "keyboard_arrow_down", 

80 "arrow-left": "keyboard_arrow_left", 

81 "arrow-right": "keyboard_arrow_right", 

82 } 

83 

84 # Size presets 

85 size_presets: dict[str, str] = { 

86 "xs": "16px", 

87 "sm": "20px", 

88 "md": "24px", 

89 "lg": "28px", 

90 "xl": "32px", 

91 "2xl": "40px", 

92 "3xl": "48px", 

93 "4xl": "56px", 

94 "5xl": "64px", 

95 } 

96 

97 # Color palette 

98 material_colors: dict[str, str] = { 

99 "red": "#f44336", 

100 "pink": "#e91e63", 

101 "purple": "#9c27b0", 

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

103 "indigo": "#3f51b5", 

104 "blue": "#2196f3", 

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

106 "cyan": "#00bcd4", 

107 "teal": "#009688", 

108 "green": "#4caf50", 

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

110 "lime": "#cddc39", 

111 "yellow": "#ffeb3b", 

112 "amber": "#ffc107", 

113 "orange": "#ff9800", 

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

115 "brown": "#795548", 

116 "grey": "#9e9e9e", 

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

118 } 

119 

120 

121class MaterialIconsAdapter(IconsBase): 

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

123 

124 # Required ACB 0.19.0+ metadata 

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

126 MODULE_STATUS: str = "stable" 

127 

128 def __init__(self) -> None: 

129 """Initialize Material Icons adapter.""" 

130 super().__init__() 

131 self.settings: MaterialIconsSettings | None = None 

132 

133 # Register with ACB dependency system 

134 with suppress(Exception): 

135 depends.set(self) 

136 

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

138 """Get Material Icons stylesheet links.""" 

139 if not self.settings: 

140 self.settings = MaterialIconsSettings() 

141 

142 links = [] 

143 

144 # Material Icons CSS from Google Fonts 

145 for theme in self.settings.enabled_themes: 

146 if theme == "filled": 

147 # Base Material Icons (filled is default) 

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

149 else: 

150 # Themed variants 

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

152 css_url = ( 

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

154 ) 

155 

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

157 

158 # Custom Material Icons CSS 

159 material_css = self._generate_material_css() 

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

161 

162 return links 

163 

164 def _generate_material_css(self) -> str: 

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

166 if not self.settings: 

167 self.settings = MaterialIconsSettings() 

168 

169 return f""" 

170/* Material Icons Base Styles */ 

171.material-icons {{ 

172 font-family: 'Material Icons'; 

173 font-weight: normal; 

174 font-style: normal; 

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

176 line-height: 1; 

177 letter-spacing: normal; 

178 text-transform: none; 

179 display: inline-block; 

180 white-space: nowrap; 

181 word-wrap: normal; 

182 direction: ltr; 

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

184 -webkit-font-smoothing: antialiased; 

185 vertical-align: -0.125em; 

186}} 

187 

188/* Theme-specific font families */ 

189.material-icons-outlined {{ 

190 font-family: 'Material Icons Outlined'; 

191}} 

192 

193.material-icons-round {{ 

194 font-family: 'Material Icons Round'; 

195}} 

196 

197.material-icons-sharp {{ 

198 font-family: 'Material Icons Sharp'; 

199}} 

200 

201.material-icons-two-tone {{ 

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

203}} 

204 

205/* Size variants */ 

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

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

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

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

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

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

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

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

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

215 

216/* Density variants */ 

217.material-icons-dense {{ 

218 font-size: 20px; 

219}} 

220 

221.material-icons-comfortable {{ 

222 font-size: 24px; 

223}} 

224 

225.material-icons-compact {{ 

226 font-size: 18px; 

227}} 

228 

229/* Rotation and transformation */ 

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

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

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

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

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

235 

236/* Animation support */ 

237.material-icons-spin {{ 

238 animation: material-spin 2s linear infinite; 

239}} 

240 

241.material-icons-pulse {{ 

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

243}} 

244 

245.material-icons-bounce {{ 

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

247}} 

248 

249.material-icons-shake {{ 

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

251}} 

252 

253.material-icons-flip {{ 

254 animation: material-flip 2s linear infinite; 

255}} 

256 

257@keyframes material-spin {{ 

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

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

260}} 

261 

262@keyframes material-pulse {{ 

263 from {{ opacity: 1; }} 

264 to {{ opacity: 0.25; }} 

265}} 

266 

267@keyframes material-bounce {{ 

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

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

270}} 

271 

272@keyframes material-shake {{ 

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

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

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

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

277}} 

278 

279@keyframes material-flip {{ 

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

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

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

283}} 

284 

285/* Material Design color utilities */ 

286{self._generate_material_color_classes()} 

287 

288/* Interactive states */ 

289.material-icons-interactive {{ 

290 cursor: pointer; 

291 transition: all 0.2s ease; 

292 border-radius: 50%; 

293 padding: 4px; 

294}} 

295 

296.material-icons-interactive:hover {{ 

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

298 transform: scale(1.1); 

299}} 

300 

301.material-icons-interactive:active {{ 

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

303 transform: scale(0.95); 

304}} 

305 

306/* States */ 

307.material-icons-disabled {{ 

308 opacity: 0.38; 

309 cursor: not-allowed; 

310}} 

311 

312.material-icons-inactive {{ 

313 opacity: 0.54; 

314}} 

315 

316.material-icons-loading {{ 

317 opacity: 0.6; 

318}} 

319 

320/* Button integration */ 

321.btn .material-icons {{ 

322 margin-right: 8px; 

323 vertical-align: -0.125em; 

324}} 

325 

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

327 margin-right: 0; 

328 margin-left: 8px; 

329}} 

330 

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

332 margin: 0; 

333}} 

334 

335.btn-sm .material-icons {{ 

336 font-size: 20px; 

337}} 

338 

339.btn-lg .material-icons {{ 

340 font-size: 28px; 

341}} 

342 

343/* Floating Action Button */ 

344.fab {{ 

345 display: inline-flex; 

346 align-items: center; 

347 justify-content: center; 

348 width: 56px; 

349 height: 56px; 

350 border-radius: 50%; 

351 border: none; 

352 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); 

353 cursor: pointer; 

354 transition: all 0.3s ease; 

355}} 

356 

357.fab:hover {{ 

358 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); 

359 transform: translateY(-2px); 

360}} 

361 

362.fab-mini {{ 

363 width: 40px; 

364 height: 40px; 

365}} 

366 

367.fab-extended {{ 

368 width: auto; 

369 height: 48px; 

370 border-radius: 24px; 

371 padding: 0 16px; 

372}} 

373 

374/* Badge integration */ 

375.badge .material-icons {{ 

376 font-size: 16px; 

377 margin-right: 4px; 

378 vertical-align: baseline; 

379}} 

380 

381/* Navigation integration */ 

382.nav-link .material-icons {{ 

383 margin-right: 8px; 

384 font-size: 20px; 

385}} 

386 

387/* List integration */ 

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

389 margin-right: 16px; 

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

391}} 

392 

393/* Input group integration */ 

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

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

396}} 

397 

398/* Alert integration */ 

399.alert .material-icons {{ 

400 margin-right: 8px; 

401 font-size: 20px; 

402}} 

403 

404/* Card integration */ 

405.card-title .material-icons {{ 

406 margin-right: 8px; 

407}} 

408 

409/* Toolbar integration */ 

410.toolbar .material-icons {{ 

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

412}} 

413 

414/* Dark theme support */ 

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

416 .material-icons-interactive:hover {{ 

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

418 }} 

419 

420 .material-icons-interactive:active {{ 

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

422 }} 

423 

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

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

426 }} 

427 

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

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

430 }} 

431}} 

432 

433/* Accessibility */ 

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

435 position: relative; 

436}} 

437 

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

439 outline: 2px solid #1976d2; 

440 outline-offset: 2px; 

441}} 

442""" 

443 

444 def _generate_material_color_classes(self) -> str: 

445 """Generate Material Design color classes.""" 

446 if not self.settings: 

447 self.settings = MaterialIconsSettings() 

448 

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

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

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

452 

453 # Add semantic colors 

454 css += """ 

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

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

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

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

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

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

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

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

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

464""" 

465 

466 return css 

467 

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

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

470 if not self.settings: 

471 self.settings = MaterialIconsSettings() 

472 

473 # Use default theme if not specified 

474 if not theme: 

475 theme = self.settings.default_theme 

476 

477 # Validate theme 

478 if theme not in self.settings.enabled_themes: 

479 theme = self.settings.default_theme 

480 

481 # Build class name based on theme 

482 if theme == "filled": 

483 return "material-icons" 

484 

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

486 

487 def get_icon_tag( 

488 self, 

489 icon_name: str, 

490 **attributes: Any, 

491 ) -> str: 

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

493 # Extract theme and size from attributes 

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

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

496 

497 if not self.settings: 

498 self.settings = MaterialIconsSettings() 

499 

500 # Resolve icon aliases 

501 resolved_name = icon_name 

502 if icon_name in self.settings.icon_aliases: 

503 resolved_name = self.settings.icon_aliases[icon_name] 

504 

505 # Get base icon class 

506 icon_class = self.get_icon_class(icon_name, theme) 

507 

508 # Add size class or custom size 

509 if size: 

510 if size in self.settings.size_presets: 

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

512 else: 

513 attributes["style"] = ( 

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

515 ) 

516 

517 # Add custom classes 

518 if "class" in attributes: 

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

520 

521 # Process attributes using shared utilities 

522 transform_classes, attributes = process_transformations( 

523 attributes, "material-icons" 

524 ) 

525 animation_classes, attributes = process_animations( 

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

527 ) 

528 

529 # Extended semantic colors including material design colors 

530 semantic_colors = [ 

531 "primary", 

532 "secondary", 

533 "success", 

534 "warning", 

535 "danger", 

536 "info", 

537 "light", 

538 "dark", 

539 "muted", 

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

541 ] 

542 color_class, attributes = process_semantic_colors( 

543 attributes, semantic_colors, "material-icons" 

544 ) 

545 state_classes, attributes = process_state_attributes( 

546 attributes, "material-icons" 

547 ) 

548 

549 # Handle density (Material Design specific feature) 

550 if "density" in attributes: 

551 density = attributes.pop("density") 

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

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

554 

555 # Combine all classes 

556 icon_class += ( 

557 transform_classes + animation_classes + color_class + state_classes 

558 ) 

559 

560 # Build attributes and add accessibility 

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

562 attrs = add_accessibility_attributes(attrs) 

563 

564 # Generate tag 

565 attr_string = build_attr_string(attrs) 

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

567 

568 def get_fab_tag( 

569 self, 

570 icon_name: str, 

571 variant: str = "regular", 

572 theme: str | None = None, 

573 **attributes: Any, 

574 ) -> str: 

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

576 if not self.settings: 

577 self.settings = MaterialIconsSettings() 

578 

579 # Resolve icon name 

580 resolved_name = icon_name 

581 if icon_name in self.settings.icon_aliases: 

582 resolved_name = self.settings.icon_aliases[icon_name] 

583 

584 # Get icon tag 

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

586 

587 # Build FAB classes 

588 fab_class = "fab" 

589 if variant == "mini": 

590 fab_class += " fab-mini" 

591 elif variant == "extended": 

592 fab_class += " fab-extended" 

593 

594 # Add custom classes 

595 if "class" in attributes: 

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

597 

598 # Handle extended FAB with text 

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

600 if variant == "extended" and text: 

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

602 else: 

603 content = icon_tag 

604 

605 # Build attributes 

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

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

608 

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

610 

611 def get_available_icons(self) -> 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("icons") 

788 if isinstance(icons, MaterialIconsAdapter): 

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("icons") 

796 if isinstance(icons, MaterialIconsAdapter): 

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("icons") 

804 if isinstance(icons, MaterialIconsAdapter): 

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("icons") 

821 if isinstance(icons, MaterialIconsAdapter): 

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("icons") 

839 if not isinstance(icons, MaterialIconsAdapter): 

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("icons") 

880 if not isinstance(icons, MaterialIconsAdapter): 

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 

916# ACB 0.19.0+ compatibility 

917__all__ = [ 

918 "MaterialIconsAdapter", 

919 "MaterialIconsSettings", 

920 "register_materialicons_filters", 

921]