Coverage for src / lexigram / ui / molecules / form_actions.py: 100%

23 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-10 04:11 +0800

1""" 

2FormActions component for form buttons. 

3 

4Provides primary/secondary action buttons with loading states. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import Any 

10 

11from lexigram.ui.core.base import Component, el 

12 

13 

14class FormActions(Component): 

15 """ 

16 Form actions component for submit/cancel buttons. 

17 

18 Example: 

19 FormActions( 

20 primary_text="Save Changes", 

21 secondary_text="Cancel", 

22 primary_loading=False, 

23 cancel_url="/admin/users" 

24 ) 

25 """ 

26 

27 def __init__( 

28 self, 

29 primary_text: str = "Save", 

30 secondary_text: str | None = "Cancel", 

31 primary_loading: bool = False, 

32 primary_disabled: bool = False, 

33 cancel_url: str | None = None, 

34 align: str = "right", # left, center, right 

35 **props, 

36 ): 

37 """ 

38 Initialize form actions. 

39 

40 Args: 

41 primary_text: Primary button text 

42 secondary_text: Secondary button text (None to hide) 

43 primary_loading: Show loading state on primary button 

44 primary_disabled: Disable primary button 

45 cancel_url: URL for cancel button (if not provided, uses history.back()) 

46 align: Button alignment (left, center, right) 

47 **props: Additional properties 

48 """ 

49 super().__init__( 

50 primary_text=primary_text, 

51 secondary_text=secondary_text, 

52 primary_loading=primary_loading, 

53 primary_disabled=primary_disabled, 

54 cancel_url=cancel_url, 

55 align=align, 

56 **props, 

57 ) 

58 self.primary_text = primary_text 

59 self.secondary_text = secondary_text 

60 self.primary_loading = primary_loading 

61 self.primary_disabled = primary_disabled 

62 self.cancel_url = cancel_url 

63 self.align = align 

64 

65 def render(self) -> Any: 

66 """Render the form actions.""" 

67 from lexigram.ui.atoms.button import SubmitButton 

68 

69 # Primary button (submit) 

70 primary_btn = SubmitButton( 

71 self.primary_text, 

72 disabled=self.primary_disabled or self.primary_loading, 

73 class_="px-6 py-2", 

74 ) 

75 

76 # Secondary button (cancel) 

77 secondary_btn = "" 

78 if self.secondary_text: 

79 if self.cancel_url: 

80 # Link to cancel URL 

81 secondary_btn = el( 

82 "a", 

83 self.secondary_text, 

84 href=self.cancel_url, 

85 class_="inline-flex items-center px-6 py-2 border border-border " 

86 "rounded-md shadow-sm text-sm font-medium text-foreground " 

87 "bg-card hover:bg-accent " 

88 "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring", 

89 ) 

90 else: 

91 # JavaScript back button -> use standardized ActionButton for consistent styling 

92 from lexigram.ui.molecules.action_button import ( 

93 ActionButton, 

94 ) 

95 

96 secondary_btn = ActionButton( 

97 label=self.secondary_text, 

98 color="secondary", 

99 size="md", 

100 hx_on_click="history.back()", 

101 class_="inline-flex items-center px-6 py-2 border border-border rounded-md shadow-sm text-sm font-medium text-foreground bg-card hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring", 

102 ).render() 

103 

104 # Alignment classes 

105 align_classes = { 

106 "left": "justify-start", 

107 "center": "justify-center", 

108 "right": "justify-end", 

109 } 

110 

111 return el( 

112 "div", 

113 el( 

114 "div", 

115 secondary_btn, 

116 primary_btn.render(), 

117 class_=f"flex gap-3 {align_classes.get(self.align, 'justify-end')}", 

118 ), 

119 class_="mt-6 pt-6 border-t border-border", 

120 )