pride_rs/
yew.rs

1#![doc = include_str!("../YEW.md")]
2
3use crate::common::{Direction, FlagLookup, Size, Type};
4use web_sys::KeyboardEvent;
5use yew::prelude::*;
6
7#[derive(Properties, PartialEq, Clone)]
8pub struct FlagProps {
9    #[prop_or_default]
10    pub r#type: Type,
11
12    #[prop_or_default]
13    pub size: Size,
14
15    #[prop_or_default]
16    pub class: &'static str,
17
18    #[prop_or_default]
19    pub aria_label: String,
20
21    #[prop_or(
22        "display: flex; border-radius: 4px; overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: pointer; position: relative;"
23    )]
24    pub style: &'static str,
25
26    #[prop_or("flex-direction: column;")]
27    pub horizontal_style: &'static str,
28
29    #[prop_or("flex-direction: row;")]
30    pub vertical_style: &'static str,
31
32    #[prop_or("flex: 1; min-height: 4px; min-width: 4px;")]
33    pub stripe_style: &'static str,
34
35    #[prop_or("width: 24px; height: 24px;")]
36    pub small_style: &'static str,
37
38    #[prop_or("width: 48px; height: 32px;")]
39    pub medium_style: &'static str,
40
41    #[prop_or("width: 96px; height: 64px;")]
42    pub large_style: &'static str,
43
44    #[prop_or("position: relative; display: inline-block;")]
45    pub container_style: &'static str,
46
47    #[prop_or(
48        "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;"
49    )]
50    pub tooltip_style: &'static str,
51
52    #[prop_or("flag-container")]
53    pub container_class: &'static str,
54
55    #[prop_or("flag")]
56    pub flag_class: &'static str,
57
58    #[prop_or("stripe")]
59    pub stripe_class: &'static str,
60
61    #[prop_or("tooltip")]
62    pub tooltip_class: &'static str,
63}
64
65#[function_component(Flag)]
66pub fn flag(props: &FlagProps) -> Html {
67    let config = props.r#type.config();
68
69    if config.is_none() {
70        log::warn!("Flag configuration not found for type: {:?}", props.r#type);
71        return html! {};
72    }
73
74    let config = config.unwrap();
75    let tooltip_id = format!("tooltip-{}", props.r#type.as_ref());
76
77    let direction_style = match config.direction {
78        Direction::Horizontal => props.horizontal_style,
79        Direction::Vertical => props.vertical_style,
80    };
81
82    let size_style = match props.size {
83        Size::Small => props.small_style,
84        Size::Medium => props.medium_style,
85        Size::Large => props.large_style,
86    };
87
88    let full_style = format!("{} {} {}", props.style, size_style, direction_style);
89    let full_class = format!("{} {}", props.flag_class, props.class);
90
91    let is_hovered = use_state(|| false);
92
93    let on_mouse_over = {
94        let is_hovered = is_hovered.clone();
95        Callback::from(move |_| is_hovered.set(true))
96    };
97
98    let on_mouse_out = {
99        let is_hovered = is_hovered.clone();
100        Callback::from(move |_| is_hovered.set(false))
101    };
102
103    let on_focus = {
104        let is_hovered = is_hovered.clone();
105        Callback::from(move |_| is_hovered.set(true))
106    };
107
108    let on_blur = {
109        let is_hovered = is_hovered.clone();
110        Callback::from(move |_| is_hovered.set(false))
111    };
112
113    let on_key_down = {
114        Callback::from(move |e: KeyboardEvent| {
115            let key = e.key();
116            if key == "Enter" || key == " " {
117                e.prevent_default();
118                log::debug!("Selected flag: {}", config.name);
119            }
120        })
121    };
122
123    let tooltip_style = if *is_hovered {
124        format!("{} opacity: 1; visibility: visible;", props.tooltip_style)
125    } else {
126        props.tooltip_style.to_string()
127    };
128
129    html! {
130        <div class={props.container_class} style={props.container_style}>
131            <div
132                class={full_class}
133                style={full_style}
134                role="img"
135                aria-label={props.aria_label.clone()}
136                aria-describedby={tooltip_id.clone()}
137                aria-roledescription="flag"
138                aria-keyshortcuts="Enter Space"
139                tabindex=0
140                onkeydown={on_key_down}
141                onmouseover={on_mouse_over.clone()}
142                onmouseout={on_mouse_out.clone()}
143                onfocus={on_focus}
144                onblur={on_blur}
145            >
146                { for config.colors.iter().enumerate().map(|(i, color)| {
147                    html! {
148                        <div
149                            key={format!("{}-{}", props.r#type.as_ref(), i)}
150                            class={props.stripe_class}
151                            style={format!("{} background-color: {};", props.stripe_style, color)}
152                            aria-hidden="true"
153                        />
154                    }
155                }) }
156            </div>
157            <div id={tooltip_id} class={props.tooltip_class} role="tooltip" style={tooltip_style}>
158                { &config.name }
159            </div>
160        </div>
161    }
162}
163
164#[derive(Properties, PartialEq, Clone)]
165pub struct FlagSectionProps {
166    #[prop_or_default]
167    pub title: String,
168
169    #[prop_or_default]
170    pub flags: Vec<Type>,
171
172    #[prop_or_default]
173    pub id: &'static str,
174
175    #[prop_or("margin-bottom: 32px;")]
176    pub section_style: &'static str,
177
178    #[prop_or(
179        "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;"
180    )]
181    pub section_title_style: &'static str,
182
183    #[prop_or(
184        "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;"
185    )]
186    pub container_style: &'static str,
187
188    #[prop_or(
189        "color: #666; font-style: italic; font-size: 12px; text-align: center; width: 100%; padding: 16px;"
190    )]
191    pub empty_state_style: &'static str,
192
193    #[prop_or("section")]
194    pub section_class: &'static str,
195
196    #[prop_or("section-title")]
197    pub section_title_class: &'static str,
198
199    #[prop_or("flag-container")]
200    pub container_class: &'static str,
201
202    #[prop_or("empty-state")]
203    pub empty_state_class: &'static str,
204}
205
206#[function_component(FlagSection)]
207pub fn flag_section(props: &FlagSectionProps) -> Html {
208    let heading_id = format!("{}-heading", props.id);
209    let description_id = format!("{}-description", props.id);
210
211    html! {
212        <section
213            class={props.section_class}
214            style={props.section_style}
215            aria-labelledby={heading_id.clone()}
216            role="region"
217        >
218            <h2
219                id={heading_id.clone()}
220                class={props.section_title_class}
221                style={props.section_title_style}
222            >
223                { &props.title }
224            </h2>
225            <div
226                class={props.container_class}
227                style={props.container_style}
228                role="group"
229                aria-labelledby={heading_id}
230                aria-describedby={description_id.clone()}
231                aria-roledescription="flag group"
232            >
233                if props.flags.is_empty() {
234                    <div
235                        id={description_id}
236                        class={props.empty_state_class}
237                        style={props.empty_state_style}
238                        aria-live="polite"
239                    >
240                        { "No flags available in this category" }
241                    </div>
242                } else {
243                    { for props.flags.iter().enumerate().map(|(i, flag_type)| {
244                        html! {
245                            <Flag
246                                key={format!("{}-{}-{}", props.id, flag_type.as_ref(), i)}
247                                r#type={*flag_type}
248                                size={Size::Medium}
249                                aria_label={flag_type.as_ref().to_string()}
250                            />
251                        }
252                    }) }
253                }
254            </div>
255        </section>
256    }
257}