Coverage for src / lexigram / ui / atoms / inputs / base.py: 48%
48 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"""
2Base input component with shared functionality.
3All input types should inherit from this class.
4"""
6from __future__ import annotations
8from abc import ABC, abstractmethod
9from typing import Any
11from lexigram.ui.core.base import Component, el
14class AbstractInput(ABC, Component):
15 """
16 Abstract base for all input components.
18 Provides:
19 - Common props handling (name, value, label, error, disabled, required)
20 - Shared CSS class generation
21 - Common wrapper rendering (label + error display)
23 Subclasses must implement:
24 - _render_input(): Returns the actual input element
25 """
27 # Shared CSS classes - defined once, used everywhere
28 BASE_CLASSES = (
29 "block w-full rounded-lg border py-1.5 px-3 h-8 shadow-sm "
30 "border-input bg-card text-foreground "
31 "placeholder:text-muted-foreground "
32 "focus:border-ring focus:ring-2 focus:ring-ring "
33 "focus:ring-offset-1 focus:ring-offset-[var(--card)] "
34 "sm:text-sm leading-5 transition-all duration-200 "
35 "disabled:opacity-50 disabled:cursor-not-allowed"
36 )
38 ERROR_CLASSES = "border-destructive focus:border-destructive focus:ring-destructive"
39 NORMAL_CLASSES = ""
40 READONLY_CLASSES = "bg-muted opacity-80"
42 LABEL_CLASSES = "block text-sm font-medium leading-6 text-foreground"
43 ERROR_MSG_CLASSES = "mt-1 text-xs text-destructive min-h-[1.25rem]"
44 WRAPPER_CLASSES = "flex flex-col gap-1.5 w-full"
46 def __init__(
47 self,
48 name: str,
49 value: Any = None,
50 label: str | None = None,
51 error: str | None = None,
52 disabled: bool = False,
53 required: bool = False,
54 readonly: bool = False,
55 **props,
56 ):
57 super().__init__(**props)
58 self.name = name
59 self.value = value
60 self.label = label
61 self.error = error
62 self.disabled = disabled
63 self.required = required
64 self.readonly = readonly
66 @property
67 def input_id(self) -> str:
68 """Get the input ID (from props or fallback to name)."""
69 return self.props.get("id") or self.name
71 def _get_input_classes(self, extra_classes: str = "") -> str:
72 """
73 Generate complete CSS class string for input element.
75 Handles:
76 - Base styling
77 - Error state
78 - Readonly state
79 - Custom width classes
80 - Additional classes from props
81 """
82 custom = self.props.get("class_", "")
84 # Don't add w-full if custom width is specified
85 has_custom_width = any(w in custom for w in ["w-", "max-w-", "min-w-"])
87 parts = [
88 self.BASE_CLASSES,
89 "" if has_custom_width else "w-full",
90 self.ERROR_CLASSES if self.error else self.NORMAL_CLASSES,
91 self.READONLY_CLASSES if self.readonly else "",
92 extra_classes,
93 custom,
94 ]
96 return " ".join(filter(None, parts))
98 def _get_extra_props(self, exclude: list[str] | None = None) -> dict:
99 """
100 Extract non-standard props for passthrough to element.
102 Filters out standard input props and returns the rest
103 (useful for hx_* attributes, data-* attributes, etc.)
104 """
105 standard_props = {
106 "name",
107 "value",
108 "label",
109 "error",
110 "disabled",
111 "required",
112 "readonly",
113 "class_",
114 "class",
115 "id",
116 *(exclude or []),
117 }
118 return {k: v for k, v in self.props.items() if k not in standard_props}
120 def _render_label(self) -> Any:
121 """Render label element if label text is provided."""
122 if not self.label:
123 return None
125 return el(
126 "label",
127 self.label,
128 for_=self.input_id,
129 class_=self.LABEL_CLASSES,
130 )
132 def _render_error(self) -> Any:
133 """Render error message if present."""
134 if not self.error:
135 return None
137 return el(
138 "p",
139 self.error,
140 class_=self.ERROR_MSG_CLASSES,
141 )
143 def _render_with_wrapper(self, input_el: Any) -> Any:
144 """
145 Wrap input with label and error message.
147 If no label is provided, returns just the input element.
148 """
149 if not self.label:
150 return input_el
152 return el(
153 "div",
154 self._render_label(),
155 input_el,
156 self._render_error(),
157 class_=self.WRAPPER_CLASSES,
158 )
160 @abstractmethod
161 def _render_input(self) -> Any:
162 """
163 Render the actual input element.
165 Subclasses MUST implement this method.
166 Should return the raw input element without wrapper.
167 """
168 ...
170 def render(self) -> Any:
171 """
172 Render complete input component.
174 Calls _render_input() and wraps with label/error if needed.
175 """
176 return self._render_with_wrapper(self._render_input())