@jpgilldev / steid

2.6 KBRaw
1//! Steid's own component. Not from the `topcoat` registry — it has no alert.
2//!
3//! Follows the same house style as the borrowed primitives: a variant enum returning
4//! Tailwind classes, tokens only, and `Attributes` forwarded to the root element.
5
6use topcoat::{
7 Result,
8 view::{Attributes, View, class, component, view},
9};
10
11/// What kind of message this is.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13#[allow(dead_code)]
14pub enum FlashKind {
15 /// Something worked.
16 Success,
17 /// Something failed and the reader can fix it.
18 #[default]
19 Error,
20 /// Neutral context, neither good nor bad.
21 Info,
22}
23
24impl FlashKind {
25 /// The Tailwind classes for this kind.
26 ///
27 /// A tinted fill at low opacity rather than a solid one, so the message reads as
28 /// part of the page rather than an interruption, and stays legible in both colour
29 /// schemes without `dark:` overrides.
30 fn classes(self) -> &'static str {
31 match self {
32 Self::Success => "border-success/30 bg-success/10 text-foreground",
33 Self::Error => "border-destructive/30 bg-destructive/10 text-foreground",
34 Self::Info => "border-border bg-foreground/5 text-foreground",
35 }
36 }
37
38 /// The accent applied to the leading marker.
39 fn marker(self) -> &'static str {
40 match self {
41 Self::Success => "bg-success",
42 Self::Error => "bg-destructive",
43 Self::Info => "bg-muted-foreground",
44 }
45 }
46
47 /// What a screen reader announces this as.
48 ///
49 /// Errors interrupt; confirmations wait for a pause. Getting this wrong makes a
50 /// form unusable without sight, which is invisible in a screenshot.
51 fn aria_role(self) -> &'static str {
52 match self {
53 Self::Error => "alert",
54 _ => "status",
55 }
56 }
57}
58
59const BASE: &str = "flex items-start gap-3 rounded-lg border px-4 py-3 text-sm";
60
61/// A message about what just happened.
62///
63/// ```ignore
64/// view! {
65/// flash(kind: FlashKind::Success, "Profile updated.")
66/// }
67/// ```
68#[component]
69pub async fn flash(
70 #[default] kind: FlashKind,
71 #[default] mut attrs: Attributes,
72 #[default] child: View,
73) -> Result {
74 view! {
75 <div
76 role=(kind.aria_role())
77 class=(class!(BASE, kind.classes(), attrs.remove("class")))
78 (attrs)
79 >
80 <span
81 aria-hidden="true"
82 class=(class!("mt-1.5 size-2 shrink-0 rounded-full", kind.marker()))
83 ></span>
84 <div>(child)</div>
85 </div>
86 }
87}