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}