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}