Coverage for src / lexigram / ui / molecules / toggle.py: 63%

30 statements  

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

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.ui.atoms.icons import get_icon 

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

7 

8 

9class Toggle(Component): 

10 """Switch-like toggle component for forms. 

11 

12 Renders a labeled switch with the same structure used by `Switch` atom but 

13 centralizes classes and props for consistency. 

14 """ 

15 

16 def __init__( 

17 self, 

18 name: str, 

19 checked: bool = False, 

20 label: str | None = None, 

21 size: str = "md", 

22 **props, 

23 ) -> None: 

24 super().__init__(name=name, checked=checked, label=label, size=size, **props) 

25 self.name = name 

26 self.checked = checked 

27 self.label = label 

28 self.size = size 

29 self.props = props 

30 

31 def render(self) -> Any: 

32 knob_cls = "inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" 

33 wrapper_cls = "relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" 

34 

35 return el( 

36 "div", 

37 el( 

38 "div", 

39 el( 

40 "div", 

41 el( 

42 "span", 

43 self.label or "", 

44 class_="text-sm font-medium text-foreground", 

45 id=f"{self.name}-label", 

46 ), 

47 el( 

48 "button", 

49 el( 

50 "span", 

51 aria_hidden="true", 

52 class_=knob_cls, 

53 x_bind__class="enabled ? 'translate-x-5' : 'translate-x-0'", 

54 ), 

55 type="button", 

56 role="switch", 

57 aria_checked=str(self.checked).lower(), 

58 aria_labelledby=f"{self.name}-label", 

59 x_data=f"{{ enabled: {'true' if self.checked else 'false'} }}", 

60 x_on_click="enabled = !enabled; $refs.hiddenInput.checked = enabled", 

61 x_bind__class="enabled ? 'bg-primary' : 'bg-input'", 

62 class_=wrapper_cls, 

63 **self.props, 

64 ), 

65 class_="flex-grow flex flex-col", 

66 ), 

67 # Hidden input for form submission 

68 el( 

69 "input", 

70 type="checkbox", 

71 name=self.name, 

72 x_ref="hiddenInput", 

73 checked=self.checked, 

74 class_="hidden", 

75 ), 

76 class_="flex items-center justify-between", 

77 ), 

78 class_="mb-4", 

79 ) 

80 

81 

82class ToggleIcon(Component): 

83 """Icon-based toggle button useful for theme toggles. 

84 

85 Args: 

86 icon_on: icon name to show when state var is true 

87 icon_off: icon name to show when state var is false 

88 state_var: the JS state variable in scope to toggle (e.g., 'darkMode') 

89 aria_label: accessible label 

90 """ 

91 

92 def __init__( 

93 self, 

94 icon_on: str = "sun", 

95 icon_off: str = "moon", 

96 state_var: str = "darkMode", 

97 aria_label: str = "Toggle", 

98 size: str = "sm", 

99 **props, 

100 ) -> None: 

101 super().__init__( 

102 icon_on=icon_on, 

103 icon_off=icon_off, 

104 state_var=state_var, 

105 aria_label=aria_label, 

106 size=size, 

107 **props, 

108 ) 

109 self.icon_on = icon_on 

110 self.icon_off = icon_off 

111 self.state_var = state_var 

112 self.aria_label = aria_label 

113 self.size = size 

114 self.props = props 

115 

116 def render(self) -> Any: 

117 icon_size = "w-5 h-5" if self.size == "sm" else "w-6 h-6" 

118 on_icon = get_icon(self.icon_on, size=icon_size) 

119 off_icon = get_icon(self.icon_off, size=icon_size) 

120 

121 return el( 

122 "button", 

123 el("span", on_icon, x_show=f"{self.state_var}", class_="text-warning", aria_hidden="true"), 

124 el( 

125 "span", 

126 off_icon, 

127 x_show=f"!{self.state_var}", 

128 class_="text-muted-foreground", 

129 aria_hidden="true", 

130 ), 

131 class_="p-2 rounded-lg hover:bg-accent transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring", 

132 **{"x-on:click": f"{self.state_var} = !{self.state_var}"}, 

133 aria_label=self.aria_label, 

134 **self.props, 

135 )