Coverage for src / lexigram / ui / layouts / footer.py: 100%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""Footer component for admin layout.
3Renders the page footer with copyright, links, and version info.
4"""
6from __future__ import annotations
8from dataclasses import dataclass
9from datetime import datetime
11from markupsafe import escape
13from lexigram.ui.config import FooterConfig
16@dataclass
17class FooterLink:
18 """A footer link."""
20 label: str
21 url: str
22 target: str = "_self"
23 icon: str | None = None
26class FooterRenderer:
27 """Renders the admin footer."""
29 def __init__(self, config: FooterConfig | None = None):
30 """Initialize the renderer.
32 Args:
33 config: Footer configuration
34 """
35 self.config = config or FooterConfig()
37 def render(self) -> str:
38 """Render the footer.
40 Returns:
41 HTML string for footer
42 """
43 parts: list[str] = []
45 sticky_class = "footer-sticky" if self.config.sticky else ""
46 divider_class = "footer-divider" if self.config.show_divider else ""
48 parts.append(f'<footer class="admin-footer {sticky_class} {divider_class}">')
49 parts.append('<div class="footer-content">')
51 # Left section
52 parts.append('<div class="footer-left">')
54 if self.config.custom_left:
55 parts.append(self.config.custom_left)
56 elif self.config.show_copyright:
57 parts.append(self._render_copyright())
59 parts.append("</div>")
61 # Center section (links)
62 if self.config.links:
63 parts.append(self._render_links())
65 # Right section
66 parts.append('<div class="footer-right">')
68 if self.config.custom_right:
69 parts.append(self.config.custom_right)
70 elif self.config.show_version and self.config.version:
71 parts.append(
72 f'<span class="footer-version">v{escape(self.config.version)}</span>',
73 )
75 parts.append("</div>")
77 parts.append("</div>")
78 parts.append("</footer>")
80 return "\n".join(parts)
82 def _render_copyright(self) -> str:
83 """Render copyright text."""
84 current_year = datetime.now().year
86 if (
87 self.config.copyright_start_year
88 and self.config.copyright_start_year < current_year
89 ):
90 year_str = f"{self.config.copyright_start_year}-{current_year}"
91 else:
92 year_str = str(current_year)
94 holder = self.config.copyright_holder or "All rights reserved"
96 return (
97 f'<span class="footer-copyright">© {year_str} {escape(holder)}</span>'
98 )
100 def _render_links(self) -> str:
101 """Render footer links."""
102 parts: list[str] = []
104 parts.append('<nav class="footer-links">')
106 for link in self.config.links:
107 icon = ""
108 if link.icon:
109 icon = f'<i data-lucide="{escape(link.icon)}" class="w-4 h-4"></i> '
111 parts.append(f"""
112 <a href="{escape(link.url)}"
113 target="{escape(link.target)}"
114 class="footer-link">
115 {icon}{escape(link.label)}
116 </a>
117 """)
119 parts.append("</nav>")
121 return "\n".join(parts)
124__all__ = ["FooterConfig", "FooterLink", "FooterRenderer"]