form_rs/common.rs
1// Copyright 2026 Open SASS Core Maintainers.
2//
3// Licensed under the MIT license
4// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
5// option. This file may not be copied, modified, or distributed
6// except according to those terms.
7
8/// Controls whether validation errors are enforced natively by the browser or
9/// communicated via ARIA attributes for realtime display.
10///
11/// # Default
12///
13/// [`ValidationBehavior::Native`] is the default, which blocks form submission
14/// when fields are invalid using the browser's built-in constraint validation.
15///
16/// # Examples
17///
18/// ```rust
19/// use form_rs::ValidationBehavior;
20///
21/// let b = ValidationBehavior::Aria;
22/// assert_eq!(b.as_str(), "aria");
23/// ```
24#[derive(Debug, Clone, PartialEq, Default, Copy)]
25pub enum ValidationBehavior {
26 /// Uses native HTML5 constraint validation. Blocks submission on errors.
27 #[default]
28 Native,
29
30 /// Uses ARIA attributes for validation display. Errors shown in realtime
31 /// as the user types; does not block form submission.
32 Aria,
33}
34
35impl ValidationBehavior {
36 /// Returns the string representation used in internal logic.
37 pub fn as_str(self) -> &'static str {
38 match self {
39 Self::Native => "native",
40 Self::Aria => "aria",
41 }
42 }
43
44 /// Returns `true` when native browser validation is active.
45 pub fn is_native(self) -> bool {
46 self == Self::Native
47 }
48}
49
50/// The MIME encoding type for form data on submission.
51///
52/// # Default
53///
54/// [`EncType::UrlEncoded`] is the default, matching the HTML `<form>` default.
55#[derive(Debug, Clone, PartialEq, Default, Copy)]
56pub enum EncType {
57 /// `application/x-www-form-urlencoded`, the default.
58 #[default]
59 UrlEncoded,
60
61 /// `multipart/form-data`, required when the form contains file inputs.
62 MultipartFormData,
63
64 /// `text/plain`, useful for debugging; not recommended for production.
65 TextPlain,
66}
67
68impl EncType {
69 /// Returns the MIME type string for the `enctype` HTML attribute.
70 pub fn as_str(self) -> &'static str {
71 match self {
72 Self::UrlEncoded => "application/x-www-form-urlencoded",
73 Self::MultipartFormData => "multipart/form-data",
74 Self::TextPlain => "text/plain",
75 }
76 }
77}
78
79/// The HTTP method used when submitting a form.
80///
81/// # Default
82///
83/// [`Method::Get`] is the default, matching the HTML `<form>` default.
84#[derive(Debug, Clone, PartialEq, Default, Copy)]
85pub enum Method {
86 /// Appends form data to the URL as query parameters. No side effects.
87 #[default]
88 Get,
89
90 /// Sends form data in the request body. Use for side-effecting operations.
91 Post,
92
93 /// Closes the enclosing `<dialog>` on submission without sending data.
94 Dialog,
95}
96
97impl Method {
98 /// Returns the lowercase string for the HTML `method` attribute.
99 pub fn as_str(self) -> &'static str {
100 match self {
101 Self::Get => "get",
102 Self::Post => "post",
103 Self::Dialog => "dialog",
104 }
105 }
106}
107
108/// Where the browser displays the response after form submission.
109///
110/// # Default
111///
112/// [`Target::Self_`] is the default, loading the response in the same frame.
113#[derive(Debug, Clone, PartialEq, Default, Copy)]
114pub enum Target {
115 /// Load into the same browsing context. Default.
116 #[default]
117 Self_,
118
119 /// Load into a new unnamed browsing context (new tab/window).
120 Blank,
121
122 /// Load into the parent browsing context.
123 Parent,
124
125 /// Load into the top-level browsing context.
126 Top,
127
128 /// Arbitrary `target` name for named frames.
129 Custom(&'static str),
130}
131
132impl Target {
133 /// Returns the string for the HTML `target` attribute.
134 pub fn as_str(self) -> &'static str {
135 match self {
136 Self::Self_ => "_self",
137 Self::Blank => "_blank",
138 Self::Parent => "_parent",
139 Self::Top => "_top",
140 Self::Custom(s) => s,
141 }
142 }
143}
144
145/// Lifecycle state of a [`Form`] submission.
146///
147/// Tracks the current submission cycle from idle through completion or error.
148///
149/// # Default
150///
151/// [`FormStatus::Idle`] is the default variant.
152#[derive(Debug, Clone, PartialEq, Default, Copy)]
153pub enum FormStatus {
154 /// No submission is in progress.
155 #[default]
156 Idle,
157
158 /// A submission is currently in progress.
159 Submitting,
160
161 /// The last submission completed successfully.
162 Submitted,
163
164 /// The last submission ended with an error.
165 Error,
166}
167
168impl FormStatus {
169 /// Returns `true` when a submission is actively running.
170 pub fn is_submitting(self) -> bool {
171 self == Self::Submitting
172 }
173}
174
175/// Visual style variant for [`Control`]-wrapped input fields.
176///
177/// # Default
178///
179/// [`Variant::Outlined`] is the default, rendering a bordered box style.
180#[derive(Debug, Clone, PartialEq, Default, Copy)]
181pub enum Variant {
182 /// Outlined field with a visible border. Default.
183 #[default]
184 Outlined,
185
186 /// Filled field with a background tint, no bottom border by default.
187 Filled,
188
189 /// Minimal underline-only style.
190 Standard,
191}
192
193impl Variant {
194 /// Returns the BEM modifier class for this variant.
195 pub fn to_class(self) -> &'static str {
196 match self {
197 Self::Outlined => "form-control--outlined",
198 Self::Filled => "form-control--filled",
199 Self::Standard => "form-control--standard",
200 }
201 }
202
203 /// Returns the inline border CSS for a field in this variant.
204 pub fn to_field_style(self) -> &'static str {
205 match self {
206 Self::Outlined => {
207 "border: 1.5px solid #3f3f46; border-radius: 8px; background: transparent;"
208 }
209 Self::Filled => {
210 "border: none; border-bottom: 1.5px solid #3f3f46; border-radius: 8px 8px 0 0; background: rgba(255,255,255,0.05);"
211 }
212 Self::Standard => {
213 "border: none; border-bottom: 1.5px solid #3f3f46; border-radius: 0; background: transparent;"
214 }
215 }
216 }
217}
218
219/// Color theme applied to a [`Control`] and its label/helper text.
220///
221/// # Default
222///
223/// [`Color::Primary`] is the default.
224#[derive(Debug, Clone, PartialEq, Default, Copy)]
225pub enum Color {
226 /// Primary purple accent: `#7c3aed`.
227 #[default]
228 Primary,
229
230 /// Secondary blue accent: `#3b82f6`.
231 Secondary,
232
233 /// Error red: `#dc2626`.
234 Error,
235
236 /// Info cyan: `#06b6d4`.
237 Info,
238
239 /// Success green: `#16a34a`.
240 Success,
241
242 /// Warning amber: `#d97706`.
243 Warning,
244
245 /// Arbitrary inline CSS color value, e.g. `"#ff6b6b"`.
246 Custom(&'static str),
247}
248
249impl Color {
250 /// Returns the hex color string for this variant.
251 pub fn to_hex(self) -> &'static str {
252 match self {
253 Self::Primary => "#7c3aed",
254 Self::Secondary => "#3b82f6",
255 Self::Error => "#dc2626",
256 Self::Info => "#06b6d4",
257 Self::Success => "#16a34a",
258 Self::Warning => "#d97706",
259 Self::Custom(s) => s,
260 }
261 }
262
263 /// Returns the focus ring box-shadow CSS for this color.
264 pub fn to_focus_ring(self) -> String {
265 format!(
266 "border-color: {}; box-shadow: 0 0 0 3px {}40;",
267 self.to_hex(),
268 self.to_hex()
269 )
270 }
271
272 /// Returns the label color CSS when the field is focused.
273 pub fn to_label_color(self) -> String {
274 format!("color: {};", self.to_hex())
275 }
276}
277
278/// Size of a [`Control`] and its contained input field.
279///
280/// # Default
281///
282/// [`Size::Medium`] is the default.
283#[derive(Debug, Clone, PartialEq, Default, Copy)]
284pub enum Size {
285 /// Compact size: smaller padding and font.
286 Small,
287
288 /// Standard size. Default.
289 #[default]
290 Medium,
291}
292
293impl Size {
294 /// Returns the inline padding CSS for an input field at this size.
295 pub fn to_input_style(self) -> &'static str {
296 match self {
297 Self::Small => "padding: 6px 10px; font-size: 13px;",
298 Self::Medium => "padding: 10px 14px; font-size: 15px;",
299 }
300 }
301
302 /// Returns the BEM modifier class for this size.
303 pub fn to_class(self) -> &'static str {
304 match self {
305 Self::Small => "form-control--small",
306 Self::Medium => "form-control--medium",
307 }
308 }
309}
310
311/// Vertical spacing adjustment for a [`Control`].
312///
313/// # Default
314///
315/// [`Margin::None`] is the default.
316#[derive(Debug, Clone, PartialEq, Default, Copy)]
317pub enum Margin {
318 /// No additional vertical margin.
319 #[default]
320 None,
321
322 /// Reduced vertical margin for denser layouts.
323 Dense,
324
325 /// Standard vertical margin matching the form baseline rhythm.
326 Normal,
327}
328
329impl Margin {
330 /// Returns the inline margin CSS for this spacing level.
331 pub fn to_style(self) -> &'static str {
332 match self {
333 Self::None => "",
334 Self::Dense => "margin-top: 4px; margin-bottom: 4px;",
335 Self::Normal => "margin-top: 8px; margin-bottom: 4px;",
336 }
337 }
338
339 /// Returns the BEM modifier class for this margin.
340 pub fn to_class(self) -> &'static str {
341 match self {
342 Self::None => "",
343 Self::Dense => "form-control--margin-dense",
344 Self::Normal => "form-control--margin-normal",
345 }
346 }
347}
348
349/// Position of a label relative to its control in [`ControlLabel`].
350///
351/// # Default
352///
353/// [`LabelPlacement::End`] is the default (label to the right of the control).
354#[derive(Debug, Clone, PartialEq, Default, Copy)]
355pub enum LabelPlacement {
356 /// Label placed at the end (right) of the control. Default.
357 #[default]
358 End,
359
360 /// Label placed at the start (left) of the control.
361 Start,
362
363 /// Label placed above the control.
364 Top,
365
366 /// Label placed below the control.
367 Bottom,
368}
369
370impl LabelPlacement {
371 /// Returns the flex-direction CSS for the label placement.
372 pub fn to_flex_direction(self) -> &'static str {
373 match self {
374 Self::End => "flex-direction: row; align-items: center;",
375 Self::Start => "flex-direction: row-reverse; align-items: center;",
376 Self::Top => "flex-direction: column-reverse; align-items: flex-start;",
377 Self::Bottom => "flex-direction: column; align-items: flex-start;",
378 }
379 }
380
381 /// Returns the BEM modifier class for this label placement.
382 pub fn to_class(self) -> &'static str {
383 match self {
384 Self::End => "form-control-label--end",
385 Self::Start => "form-control-label--start",
386 Self::Top => "form-control-label--top",
387 Self::Bottom => "form-control-label--bottom",
388 }
389 }
390}
391
392/// Validation state for a field, carrying an optional error message.
393///
394/// # Default
395///
396/// [`ValidationState::None`] is the default, no active validation display.
397#[derive(Debug, Clone, PartialEq, Default)]
398pub enum ValidationState {
399 /// No validation state applied.
400 #[default]
401 None,
402
403 /// The field value is valid.
404 Valid,
405
406 /// The field value is invalid; the message is shown as helper text.
407 Invalid(String),
408}
409
410impl ValidationState {
411 /// Returns `true` when the field is in an error state.
412 pub fn is_invalid(&self) -> bool {
413 matches!(self, Self::Invalid(_))
414 }
415
416 /// Returns the error message if the state is [`ValidationState::Invalid`].
417 pub fn error_message(&self) -> Option<&str> {
418 match self {
419 Self::Invalid(msg) => Some(msg.as_str()),
420 _ => None,
421 }
422 }
423}
424
425/// Returns the base inline CSS for the `<form>` element.
426pub fn base_form_style() -> &'static str {
427 "display: flex; flex-direction: column; gap: 16px;"
428}
429
430/// Returns the base inline CSS for a [`Control`] container `<div>`.
431pub fn base_form_control_style() -> &'static str {
432 "display: flex; flex-direction: column; gap: 4px; position: relative; width: 100%;"
433}
434
435/// Returns the base inline CSS for a [`FormLabel`] element.
436pub fn base_label_style() -> &'static str {
437 "font-size: 13px; font-weight: 500; color: #a1a1aa; line-height: 1.4; \
438transition: color 0.15s ease; letter-spacing: 0.01em;"
439}
440
441/// Returns the base inline CSS for a [`Helper`] element.
442pub fn base_helper_text_style() -> &'static str {
443 "font-size: 11.5px; color: #71717a; margin: 0; line-height: 1.5; \
444transition: color 0.15s ease;"
445}
446
447/// Returns the base inline CSS for a [`Group`] container.
448pub fn base_form_group_style() -> &'static str {
449 "display: flex; flex-direction: column; gap: 8px;"
450}
451
452/// Returns the base inline CSS for a row-layout [`Group`].
453pub fn base_form_group_row_style() -> &'static str {
454 "display: flex; flex-direction: row; flex-wrap: wrap; gap: 16px; align-items: center;"
455}
456
457/// Returns the base inline CSS for an input field within a [`Control`].
458pub fn base_input_field_style() -> &'static str {
459 "width: 100%; background: transparent; outline: none; \
460font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; \
461transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; \
462box-sizing: border-box;"
463}
464
465/// Returns the inline CSS applied to an input field in its error state.
466pub fn field_error_style() -> &'static str {
467 "border-color: #dc2626 !important; box-shadow: 0 0 0 3px rgba(220,38,38,0.18);"
468}
469
470/// Returns the inline CSS applied to an input field in its valid state.
471pub fn field_valid_style() -> &'static str {
472 "border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,0.15);"
473}
474
475/// Returns the inline CSS applied to a disabled input field.
476pub fn field_disabled_style() -> &'static str {
477 "opacity: 0.5; cursor: not-allowed; pointer-events: none;"
478}
479
480/// Returns the error label color CSS string.
481pub fn label_error_style() -> &'static str {
482 "color: #dc2626;"
483}
484
485/// Returns the valid label color CSS string.
486pub fn label_valid_style() -> &'static str {
487 "color: #16a34a;"
488}
489
490/// Returns the helper text error CSS string.
491pub fn helper_error_style() -> &'static str {
492 "color: #dc2626; font-weight: 500;"
493}
494
495/// Returns the helper text valid CSS string.
496pub fn helper_valid_style() -> &'static str {
497 "color: #16a34a;"
498}
499
500/// Returns the inline CSS for the required asterisk `*` suffix on labels.
501pub fn required_asterisk_style() -> &'static str {
502 "color: #dc2626; margin-inline-start: 2px;"
503}
504
505// Copyright 2026 Open SASS Core Maintainers.
506//
507// Licensed under the MIT license
508// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
509// option. This file may not be copied, modified, or distributed
510// except according to those terms.