pepl_ui/components/
feedback.rs1use crate::accessibility;
7use crate::prop_value::PropValue;
8use crate::surface::SurfaceNode;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ToastType {
15 Info,
16 Success,
17 Warning,
18 Error,
19}
20
21impl ToastType {
22 fn as_str(self) -> &'static str {
23 match self {
24 Self::Info => "info",
25 Self::Success => "success",
26 Self::Warning => "warning",
27 Self::Error => "error",
28 }
29 }
30}
31
32pub struct ModalBuilder {
40 visible: bool,
41 on_dismiss: PropValue,
42 title: Option<String>,
43 children: Vec<SurfaceNode>,
44}
45
46impl ModalBuilder {
47 pub fn new(visible: bool, on_dismiss: PropValue) -> Self {
51 Self {
52 visible,
53 on_dismiss,
54 title: None,
55 children: Vec::new(),
56 }
57 }
58
59 pub fn title(mut self, title: impl Into<String>) -> Self {
60 self.title = Some(title.into());
61 self
62 }
63
64 pub fn child(mut self, child: SurfaceNode) -> Self {
66 self.children.push(child);
67 self
68 }
69
70 pub fn build(self) -> SurfaceNode {
71 let mut node = SurfaceNode::new("Modal");
72 node.set_prop("visible", PropValue::Bool(self.visible));
73 node.set_prop("on_dismiss", self.on_dismiss);
74 if let Some(title) = self.title {
75 node.set_prop("title", PropValue::String(title));
76 }
77 for child in self.children {
78 node.add_child(child);
79 }
80 accessibility::ensure_accessible(&mut node);
81 node
82 }
83}
84
85pub struct ToastBuilder {
92 message: String,
93 duration: Option<f64>,
94 toast_type: Option<ToastType>,
95}
96
97impl ToastBuilder {
98 pub fn new(message: impl Into<String>) -> Self {
100 Self {
101 message: message.into(),
102 duration: None,
103 toast_type: None,
104 }
105 }
106
107 pub fn duration(mut self, duration: f64) -> Self {
109 self.duration = Some(duration);
110 self
111 }
112
113 pub fn toast_type(mut self, toast_type: ToastType) -> Self {
115 self.toast_type = Some(toast_type);
116 self
117 }
118
119 pub fn build(self) -> SurfaceNode {
120 let mut node = SurfaceNode::new("Toast");
121 node.set_prop("message", PropValue::String(self.message));
122 if let Some(duration) = self.duration {
123 node.set_prop("duration", PropValue::Number(duration));
124 }
125 if let Some(toast_type) = self.toast_type {
126 node.set_prop("type", PropValue::String(toast_type.as_str().to_string()));
127 }
128 accessibility::ensure_accessible(&mut node);
129 node
130 }
131}
132
133pub fn validate_feedback_node(node: &SurfaceNode) -> Vec<String> {
137 match node.component_type.as_str() {
138 "Modal" => validate_modal(node),
139 "Toast" => validate_toast(node),
140 _ => vec![format!(
141 "Unknown feedback component: {}",
142 node.component_type
143 )],
144 }
145}
146
147fn validate_modal(node: &SurfaceNode) -> Vec<String> {
148 let mut errors = Vec::new();
149
150 match node.props.get("visible") {
152 Some(PropValue::Bool(_)) => {}
153 Some(other) => errors.push(format!(
154 "Modal.visible: expected bool, got {}",
155 other.type_name()
156 )),
157 None => errors.push("Modal.visible: required prop missing".to_string()),
158 }
159
160 match node.props.get("on_dismiss") {
162 Some(PropValue::ActionRef { .. }) => {}
163 Some(other) => errors.push(format!(
164 "Modal.on_dismiss: expected action, got {}",
165 other.type_name()
166 )),
167 None => errors.push("Modal.on_dismiss: required prop missing".to_string()),
168 }
169
170 if let Some(prop) = node.props.get("title") {
172 if !matches!(prop, PropValue::String(_)) {
173 errors.push(format!(
174 "Modal.title: expected string, got {}",
175 prop.type_name()
176 ));
177 }
178 }
179
180 if let Some(prop) = node.props.get("accessible") {
184 errors.extend(accessibility::validate_accessible_prop("Modal", prop));
185 }
186
187 for key in node.props.keys() {
189 if !matches!(
190 key.as_str(),
191 "visible" | "on_dismiss" | "title" | "accessible"
192 ) {
193 errors.push(format!("Modal: unknown prop '{key}'"));
194 }
195 }
196
197 errors
198}
199
200fn validate_toast(node: &SurfaceNode) -> Vec<String> {
201 let mut errors = Vec::new();
202
203 match node.props.get("message") {
205 Some(PropValue::String(_)) => {}
206 Some(other) => errors.push(format!(
207 "Toast.message: expected string, got {}",
208 other.type_name()
209 )),
210 None => errors.push("Toast.message: required prop missing".to_string()),
211 }
212
213 if let Some(prop) = node.props.get("duration") {
215 if !matches!(prop, PropValue::Number(_)) {
216 errors.push(format!(
217 "Toast.duration: expected number, got {}",
218 prop.type_name()
219 ));
220 }
221 }
222
223 if let Some(prop) = node.props.get("type") {
225 match prop {
226 PropValue::String(s)
227 if matches!(s.as_str(), "info" | "success" | "warning" | "error") => {}
228 _ => errors.push(format!(
229 "Toast.type: expected one of [info, success, warning, error], got {:?}",
230 prop
231 )),
232 }
233 }
234
235 if !node.children.is_empty() {
237 errors.push(format!(
238 "Toast: does not accept children, but got {}",
239 node.children.len()
240 ));
241 }
242
243 if let Some(prop) = node.props.get("accessible") {
245 errors.extend(accessibility::validate_accessible_prop("Toast", prop));
246 }
247
248 for key in node.props.keys() {
250 if !matches!(key.as_str(), "message" | "duration" | "type" | "accessible") {
251 errors.push(format!("Toast: unknown prop '{key}'"));
252 }
253 }
254
255 errors
256}