rustyle_css/components/
input.rs1use super::{ComponentStyle, Size, State};
6use crate::css::{Color, Radius, Spacing};
7use crate::tokens::{BorderTokens, ColorTokens, SpacingTokens};
8
9#[derive(Clone, Debug)]
11pub struct InputStyle {
12 pub size: Size,
13 pub state: State,
14 pub validation_state: Option<ValidationState>,
15 pub tokens: Option<InputTokens>,
16}
17
18#[derive(Clone, Debug, PartialEq)]
20pub enum ValidationState {
21 Valid,
22 Invalid,
23}
24
25#[derive(Clone, Debug)]
27pub struct InputTokens {
28 pub colors: ColorTokens,
29 pub spacing: SpacingTokens,
30 pub borders: BorderTokens,
31}
32
33impl InputStyle {
34 pub fn new(size: Size) -> Self {
36 Self {
37 size,
38 state: State::Default,
39 validation_state: None,
40 tokens: None,
41 }
42 }
43
44 pub fn state(mut self, state: State) -> Self {
46 self.state = state;
47 self
48 }
49
50 pub fn validation(mut self, validation: ValidationState) -> Self {
52 self.validation_state = Some(validation);
53 self
54 }
55
56 pub fn tokens(mut self, tokens: InputTokens) -> Self {
58 self.tokens = Some(tokens);
59 self
60 }
61
62 fn border_color(&self) -> Color {
64 let default_colors = ColorTokens::default();
65 let colors = self
66 .tokens
67 .as_ref()
68 .map(|t| &t.colors)
69 .unwrap_or(&default_colors);
70
71 if let Some(ref validation) = self.validation_state {
72 match validation {
73 ValidationState::Valid => colors.semantic.success.clone(),
74 ValidationState::Invalid => colors.semantic.error.clone(),
75 }
76 } else if self.state == State::Focus || self.state == State::FocusVisible {
77 colors.primary.c500.clone()
78 } else {
79 colors.secondary.c300.clone()
80 }
81 }
82
83 fn padding(&self) -> Spacing {
85 let default_spacing = SpacingTokens::default();
86 let spacing = self
87 .tokens
88 .as_ref()
89 .map(|t| &t.spacing)
90 .unwrap_or(&default_spacing);
91
92 match &self.size {
93 Size::Small => Spacing::vh(spacing.xs.clone(), spacing.sm.clone()),
94 Size::Medium => Spacing::vh(spacing.sm.clone(), spacing.md.clone()),
95 Size::Large => Spacing::vh(spacing.md.clone(), spacing.lg.clone()),
96 Size::ExtraLarge => Spacing::vh(spacing.lg.clone(), spacing.xl.clone()),
97 }
98 }
99
100 fn border_radius(&self) -> Radius {
102 let default_borders = BorderTokens::default();
103 let borders = self
104 .tokens
105 .as_ref()
106 .map(|t| &t.borders)
107 .unwrap_or(&default_borders);
108
109 Radius::all(borders.radius.sm.clone())
110 }
111}
112
113impl ComponentStyle for InputStyle {
114 fn to_css(&self) -> String {
115 let mut css = String::new();
116
117 css.push_str("background-color: white; ");
118 css.push_str(&format!(
119 "border: 1px solid {}; ",
120 self.border_color().to_css()
121 ));
122 css.push_str(&format!("padding: {}; ", self.padding().to_css()));
123 css.push_str(&format!(
124 "border-radius: {}; ",
125 self.border_radius().to_css()
126 ));
127 css.push_str("outline: none; ");
128 css.push_str("transition: border-color 0.2s ease; ");
129
130 if self.state == State::Focus || self.state == State::FocusVisible {
131 let default_colors = ColorTokens::default();
132 let colors = self
133 .tokens
134 .as_ref()
135 .map(|t| &t.colors)
136 .unwrap_or(&default_colors);
137 css.push_str(&format!(
138 "box-shadow: 0 0 0 3px {}; ",
139 colors.primary.c500.with_opacity(0.1).to_css()
140 ));
141 }
142
143 if self.state == State::Disabled {
144 css.push_str("opacity: 0.6; ");
145 css.push_str("cursor: not-allowed; ");
146 }
147
148 css
149 }
150
151 fn class_name(&self) -> &str {
152 "rustyle-input"
153 }
154}
155
156impl Default for InputStyle {
157 fn default() -> Self {
158 Self::new(Size::Medium)
159 }
160}