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
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""
2FormActions component for form buttons.
4Provides primary/secondary action buttons with loading states.
5"""
7from __future__ import annotations
9from typing import Any
11from lexigram.ui.core.base import Component, el
14class FormActions(Component):
15 """
16 Form actions component for submit/cancel buttons.
18 Example:
19 FormActions(
20 primary_text="Save Changes",
21 secondary_text="Cancel",
22 primary_loading=False,
23 cancel_url="/admin/users"
24 )
25 """
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.
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
65 def render(self) -> Any:
66 """Render the form actions."""
67 from lexigram.ui.atoms.button import SubmitButton
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 )
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 )
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()
104 # Alignment classes
105 align_classes = {
106 "left": "justify-start",
107 "center": "justify-center",
108 "right": "justify-end",
109 }
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 )