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

1"""Footer component for admin layout. 

2 

3Renders the page footer with copyright, links, and version info. 

4""" 

5 

6from __future__ import annotations 

7 

8from dataclasses import dataclass 

9from datetime import datetime 

10 

11from markupsafe import escape 

12 

13from lexigram.ui.config import FooterConfig 

14 

15 

16@dataclass 

17class FooterLink: 

18 """A footer link.""" 

19 

20 label: str 

21 url: str 

22 target: str = "_self" 

23 icon: str | None = None 

24 

25 

26class FooterRenderer: 

27 """Renders the admin footer.""" 

28 

29 def __init__(self, config: FooterConfig | None = None): 

30 """Initialize the renderer. 

31 

32 Args: 

33 config: Footer configuration 

34 """ 

35 self.config = config or FooterConfig() 

36 

37 def render(self) -> str: 

38 """Render the footer. 

39 

40 Returns: 

41 HTML string for footer 

42 """ 

43 parts: list[str] = [] 

44 

45 sticky_class = "footer-sticky" if self.config.sticky else "" 

46 divider_class = "footer-divider" if self.config.show_divider else "" 

47 

48 parts.append(f'<footer class="admin-footer {sticky_class} {divider_class}">') 

49 parts.append('<div class="footer-content">') 

50 

51 # Left section 

52 parts.append('<div class="footer-left">') 

53 

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

58 

59 parts.append("</div>") 

60 

61 # Center section (links) 

62 if self.config.links: 

63 parts.append(self._render_links()) 

64 

65 # Right section 

66 parts.append('<div class="footer-right">') 

67 

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 ) 

74 

75 parts.append("</div>") 

76 

77 parts.append("</div>") 

78 parts.append("</footer>") 

79 

80 return "\n".join(parts) 

81 

82 def _render_copyright(self) -> str: 

83 """Render copyright text.""" 

84 current_year = datetime.now().year 

85 

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) 

93 

94 holder = self.config.copyright_holder or "All rights reserved" 

95 

96 return ( 

97 f'<span class="footer-copyright">&copy; {year_str} {escape(holder)}</span>' 

98 ) 

99 

100 def _render_links(self) -> str: 

101 """Render footer links.""" 

102 parts: list[str] = [] 

103 

104 parts.append('<nav class="footer-links">') 

105 

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> ' 

110 

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

118 

119 parts.append("</nav>") 

120 

121 return "\n".join(parts) 

122 

123 

124__all__ = ["FooterConfig", "FooterLink", "FooterRenderer"]