Skip to main content

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.