pride_rs/
dioxus.rs

1#![doc = include_str!("../DIOXUS.md")]
2
3use crate::common::{Direction, FlagLookup, Size, Type};
4use dioxus::prelude::*;
5use dioxus_logger::tracing;
6
7#[derive(Props, PartialEq, Clone)]
8pub struct FlagProps {
9    #[props(default)]
10    pub r#type: Type,
11    #[props(default)]
12    pub size: Size,
13    #[props(default)]
14    pub class: &'static str,
15    #[props(default)]
16    pub aria_label: String,
17    #[props(
18        default = "display: flex; border-radius: 4px; overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: pointer; position: relative;"
19    )]
20    pub style: &'static str,
21    #[props(default = "flex-direction: column;")]
22    pub horizontal_style: &'static str,
23    #[props(default = "flex-direction: row;")]
24    pub vertical_style: &'static str,
25    #[props(default = "flex: 1; min-height: 4px; min-width: 4px;")]
26    pub stripe_style: &'static str,
27    #[props(default = "width: 24px; height: 24px;")]
28    pub small_style: &'static str,
29    #[props(default = "width: 48px; height: 32px;")]
30    pub medium_style: &'static str,
31    #[props(default = "width: 96px; height: 64px;")]
32    pub large_style: &'static str,
33    #[props(default = "position: relative; display: inline-block;")]
34    pub container_style: &'static str,
35    #[props(
36        default = "position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background-color: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; white-space: nowrap; transition: opacity 0.2s ease, visibility 0.2s ease; z-index: 1000; pointer-events: none; opacity: 0; visibility: hidden;"
37    )]
38    pub tooltip_style: &'static str,
39    #[props(default = "flag-container")]
40    pub container_class: &'static str,
41    #[props(default = "flag")]
42    pub flag_class: &'static str,
43    #[props(default = "stripe")]
44    pub stripe_class: &'static str,
45    #[props(default = "tooltip")]
46    pub tooltip_class: &'static str,
47}
48
49#[component]
50pub fn Flag(props: FlagProps) -> Element {
51    let config = match props.r#type.config() {
52        Some(cfg) => cfg,
53        None => {
54            tracing::warn!("Flag configuration not found for {:?}", props.r#type);
55            return rsx! {};
56        }
57    };
58
59    let tooltip_id = format!("tooltip-{}", props.r#type.as_ref());
60    let direction = if config.direction == Direction::Horizontal {
61        props.horizontal_style
62    } else {
63        props.vertical_style
64    };
65    let size = match props.size {
66        Size::Small => props.small_style,
67        Size::Medium => props.medium_style,
68        Size::Large => props.large_style,
69    };
70    let full_style = format!("{} {} {}", props.style, size, direction);
71    let full_class = format!("{} {}", props.flag_class, props.class);
72
73    let mut is_hovered = use_signal(|| false);
74
75    let on_keydown = move |e: Event<KeyboardData>| {
76        if e.key() == Key::Enter {
77            e.prevent_default();
78            tracing::debug!("Selected flag: {}", config.name);
79        }
80    };
81
82    let tooltip_style = if is_hovered() {
83        format!("{} opacity: 1; visibility: visible;", props.tooltip_style)
84    } else {
85        props.tooltip_style.to_string()
86    };
87
88    rsx! {
89        div {
90            class: "{props.container_class}",
91            style: "{props.container_style}",
92            div {
93                class: "{full_class}",
94                style: "{full_style}",
95                role: "img",
96                aria_label: "{props.aria_label}",
97                aria_describedby: "{tooltip_id}",
98                aria_roledescription: "flag",
99                aria_keyshortcuts: "Enter Space",
100                tabindex: "0",
101                onmouseover: move |_| is_hovered.set(true),
102                onmouseout: move |_| is_hovered.set(false),
103                onfocus: move |_| is_hovered.set(true),
104                onblur: move |_| is_hovered.set(false),
105                onkeydown: on_keydown,
106                for (_i, color) in config.colors.iter().enumerate() {
107                    div {
108                        key: {format!("{}-{}", props.r#type.as_ref(), _i)},
109                        class: "{props.stripe_class}",
110                        style: format!("{} background-color: {};", props.stripe_style, color),
111                        aria_hidden: "true",
112                    }
113                }
114            }
115            div {
116                id: "{tooltip_id}",
117                class: "{props.tooltip_class}",
118                role: "tooltip",
119                style: "{tooltip_style}",
120                "{config.name}"
121            }
122        }
123    }
124}
125
126#[derive(Props, PartialEq, Clone)]
127pub struct FlagSectionProps {
128    #[props(default)]
129    pub title: String,
130    #[props(default)]
131    pub flags: Vec<Type>,
132    #[props(default)]
133    pub id: &'static str,
134    #[props(default = "margin-bottom: 32px;")]
135    pub section_style: &'static str,
136    #[props(
137        default = "font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 600; color: #333; margin-bottom: 12px; padding-left: 4px;"
138    )]
139    pub section_title_style: &'static str,
140    #[props(
141        default = "background-color: #ffffff; border: 2px dashed #7b61ff; border-radius: 8px; padding: 12px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-height: 48px; transition: border-color 0.2s ease;"
142    )]
143    pub container_style: &'static str,
144    #[props(
145        default = "color: #666; font-style: italic; font-size: 12px; text-align: center; width: 100%; padding: 16px;"
146    )]
147    pub empty_state_style: &'static str,
148    #[props(default = "section")]
149    pub section_class: &'static str,
150    #[props(default = "section-title")]
151    pub section_title_class: &'static str,
152    #[props(default = "flag-container")]
153    pub container_class: &'static str,
154    #[props(default = "empty-state")]
155    pub empty_state_class: &'static str,
156}
157
158#[component]
159pub fn FlagSection(props: FlagSectionProps) -> Element {
160    let heading_id = format!("{}-heading", props.id);
161    let description_id = format!("{}-description", props.id);
162
163    rsx! {
164        section {
165            class: "{props.section_class}",
166            style: "{props.section_style}",
167            role: "region",
168            aria_labelledby: "{heading_id}",
169            h2 {
170                id: "{heading_id}",
171                class: "{props.section_title_class}",
172                style: "{props.section_title_style}",
173                "{props.title}"
174            }
175            div {
176                class: "{props.container_class}",
177                style: "{props.container_style}",
178                role: "group",
179                aria_labelledby: "{heading_id}",
180                aria_describedby: "{description_id}",
181                aria_roledescription: "flag group",
182                if props.flags.is_empty() {
183                    div {
184                        id: "{description_id}",
185                        class: "{props.empty_state_class}",
186                        style: "{props.empty_state_style}",
187                        aria_live: "polite",
188                        "No flags available in this category"
189                    }
190                } else {
191                    for (_i, flag_type) in props.flags.iter().enumerate() {
192                        Flag {
193                            key: {format!("{}-{}-{}", props.id, flag_type.as_ref(), _i)},
194                            r#type: *flag_type,
195                            size: Size::Medium,
196                            aria_label: flag_type.as_ref().to_string(),
197                            class: "",
198                        }
199                    }
200                }
201            }
202        }
203    }
204}