browser_rs/
yew.rs

1#![doc = include_str!("../YEW.md")]
2
3use crate::common::{ButtonType, Size, Variant};
4use gloo_timers::callback::Timeout;
5use web_sys::{
6    Element, HtmlInputElement, KeyboardEvent,
7    wasm_bindgen::{JsCast, prelude::*},
8};
9use yew::prelude::*;
10
11#[derive(Properties, PartialEq, Clone)]
12pub struct AddressBarProps {
13    #[prop_or_default]
14    pub url: String,
15    #[prop_or("Enter URL or search...")]
16    pub placeholder: &'static str,
17    #[prop_or_default]
18    pub on_url_change: Callback<InputEvent>,
19    #[prop_or(false)]
20    pub read_only: bool,
21
22    #[prop_or_default]
23    pub class: &'static str,
24    #[prop_or_default]
25    pub style: &'static str,
26
27    #[prop_or("Website address or search query")]
28    pub label: &'static str,
29    #[prop_or("Enter a website URL or search term. Press Enter to navigate.")]
30    pub describedby: &'static str,
31    #[prop_or("browser-url-input")]
32    pub input_id: &'static str,
33
34    #[prop_or("w-full text-gray-900 dark:text-white bg-white dark:bg-gray-700 pr-8")]
35    pub input_class: &'static str,
36
37    #[prop_or(
38        "flex-1 mx-4 border border-gray-300 dark:border-gray-500 rounded-md px-3 py-1 bg-transparent text-sm relative bg-white dark:bg-gray-700"
39    )]
40    pub container_class: &'static str,
41
42    #[prop_or(
43        "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; border: none; cursor: pointer; color: #d1d5db;"
44    )]
45    pub refresh_button_style: &'static str,
46    #[prop_or("Refresh")]
47    pub refresh_button_aria_label: &'static str,
48}
49
50#[function_component(AddressBar)]
51pub fn address_bar(props: &AddressBarProps) -> Html {
52    let input_value = use_state(|| props.url.to_string());
53    let is_focused = use_state(|| false);
54    let input_ref = use_node_ref();
55
56    {
57        let input_value = input_value.clone();
58        use_effect_with(props.url.clone(), move |url| {
59            input_value.set(url.clone());
60        });
61    }
62
63    let on_input_change = {
64        let input_value = input_value.clone();
65        let on_url_change = props.on_url_change.clone();
66        Callback::from(move |e: InputEvent| {
67            if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
68                let value = input.value();
69                input_value.set(value.clone());
70                on_url_change.emit(e);
71            }
72        })
73    };
74
75    let on_key_down = {
76        let input_ref = input_ref.clone();
77        let value = (*input_value).clone();
78        Callback::from(move |e: KeyboardEvent| {
79            if e.key() == "Enter" {
80                e.prevent_default();
81                if let Some(input) = input_ref.cast::<HtmlInputElement>() {
82                    input.blur().ok();
83                }
84
85                let document = web_sys::window().unwrap().document().unwrap();
86                let live_region = document.create_element("div").unwrap();
87                live_region.set_attribute("aria-live", "polite").unwrap();
88                live_region.set_attribute("aria-atomic", "true").unwrap();
89                live_region.set_class_name("sr-only");
90                live_region.set_text_content(Some(&format!("Navigating to {}", value)));
91                document.body().unwrap().append_child(&live_region).unwrap();
92
93                let live_region_clone = live_region.clone();
94                Timeout::new(1000, move || {
95                    let _ = document.body().unwrap().remove_child(&live_region_clone);
96                })
97                .forget();
98            }
99        })
100    };
101
102    let on_focus = {
103        let is_focused = is_focused.clone();
104        Callback::from(move |_| {
105            is_focused.set(true);
106        })
107    };
108
109    let on_blur = {
110        let is_focused = is_focused.clone();
111        Callback::from(move |_| {
112            is_focused.set(false);
113        })
114    };
115
116    html! {
117        <div class={format!("{} {}", props.container_class, props.class)} style={props.style}>
118            <label for={props.input_id} class="sr-only">{ props.label }</label>
119            <input
120                ref={input_ref.clone()}
121                id={props.input_id}
122                type="text"
123                value={(*input_value).clone()}
124                oninput={on_input_change}
125                onkeydown={on_key_down}
126                onfocus={on_focus}
127                onblur={on_blur}
128                placeholder={props.placeholder}
129                readonly={props.read_only}
130                class={props.input_class}
131                aria-describedby={props.describedby}
132                autocomplete="url"
133                spellcheck={Some("false")}
134            />
135            <button
136                style={props.refresh_button_style}
137                aria-label={props.refresh_button_aria_label}
138                onclick={Callback::from(|_| {
139                    let _ = web_sys::window().unwrap().location().reload();
140                })}
141            >
142                <svg
143                    width="11"
144                    height="13"
145                    viewBox="0 0 11 13"
146                    fill="none"
147                    xmlns="http://www.w3.org/2000/svg"
148                >
149                    <path
150                        d="M4.99385 1.00002L7.33006 3.33623L4.99385 5.67244M10 7.61925C10 10.1998 7.9081 12.2917 5.3276 12.2917C2.74709 12.2917 0.655182 10.1998 0.655182 7.61925C0.655182 5.03875 2.74709 2.94684 5.3276 2.94684C5.8737 2.94684 6.4957 2.94684 7.27443 3.33621"
151                        stroke="#767676"
152                        stroke-linecap="round"
153                        stroke-linejoin="round"
154                    />
155                </svg>
156            </button>
157        </div>
158    }
159}
160
161#[derive(Properties, PartialEq, Clone)]
162pub struct BrowserContentProps {
163    #[prop_or_default]
164    pub children: Children,
165    #[prop_or_default]
166    pub class: &'static str,
167    #[prop_or_default]
168    pub style: &'static str,
169    #[prop_or("Browser content area")]
170    pub aria_label: &'static str,
171    #[prop_or_default]
172    pub aria_describedby: &'static str,
173}
174
175#[function_component(BrowserContent)]
176pub fn browser_content(props: &BrowserContentProps) -> Html {
177    html! {
178        <main
179            class={props.class}
180            style={props.style}
181            role="main"
182            aria-label={props.aria_label}
183            aria-describedby={props.aria_describedby}
184            tabindex={Some("-1")}
185        >
186            { for props.children.iter() }
187        </main>
188    }
189}
190
191#[derive(Properties, PartialEq, Clone)]
192pub struct ControlButtonProps {
193    pub r#type: ButtonType,
194
195    #[prop_or_default]
196    pub on_click: Callback<()>,
197    #[prop_or_default]
198    pub on_mouse_over: Callback<()>,
199    #[prop_or_default]
200    pub on_mouse_out: Callback<()>,
201    #[prop_or_default]
202    pub on_focus: Callback<FocusEvent>,
203    #[prop_or_default]
204    pub on_blur: Callback<FocusEvent>,
205
206    #[prop_or(
207        "w-5 h-5 flex items-center justify-center rounded-full transition-all duration-200 cursor-pointer"
208    )]
209    pub base_class: &'static str,
210    #[prop_or_default]
211    pub class: &'static str,
212    #[prop_or_default]
213    pub svg_class: &'static str,
214    #[prop_or_default]
215    pub path_class: &'static str,
216
217    #[prop_or("button")]
218    pub button_type: &'static str,
219    #[prop_or_default]
220    pub aria_label: &'static str,
221    #[prop_or_default]
222    pub title: &'static str,
223    #[prop_or("0")]
224    pub tabindex: &'static str,
225}
226
227#[function_component(ControlButton)]
228pub fn control_button(props: &ControlButtonProps) -> Html {
229    let ControlButtonProps {
230        r#type,
231        on_click,
232        on_mouse_over,
233        on_mouse_out,
234        on_focus,
235        on_blur,
236        base_class,
237        class,
238        svg_class,
239        path_class,
240        button_type,
241        aria_label,
242        title,
243        tabindex,
244    } = props.clone();
245
246    let full_class = format!("{} {}", base_class, class);
247
248    let aria_label = if aria_label.is_empty() {
249        r#type.default_aria_label()
250    } else {
251        aria_label
252    };
253
254    let title = if title.is_empty() {
255        r#type.default_title()
256    } else {
257        title
258    };
259
260    let (fill, stroke) = match r#type {
261        ButtonType::Close => ("#FF5F57", "#E14640"),
262        ButtonType::Minimize => ("#FFBD2E", "#DFA123"),
263        ButtonType::Maximize => ("#28CA42", "#1DAD2C"),
264    };
265    let onclick = Callback::from(move |_| on_click.emit(()));
266    let onmouseover = Callback::from(move |_| on_mouse_over.emit(()));
267    let onmouseout = Callback::from(move |_| on_mouse_out.emit(()));
268
269    html! {
270        <button
271            type={button_type}
272            class={full_class}
273            onclick={onclick}
274            onmouseover={onmouseover}
275            onmouseout={onmouseout}
276            onfocus={on_focus}
277            onblur={on_blur}
278            aria-label={aria_label}
279            title={title}
280            tabindex={tabindex}
281        >
282            <svg
283                class={svg_class}
284                width="12"
285                height="12"
286                viewBox="0 0 12 12"
287                fill="none"
288                xmlns="http://www.w3.org/2000/svg"
289            >
290                <path
291                    class={path_class}
292                    d="M6 0.5C9.03757 0.5 11.5 2.96243 11.5 6C11.5 9.03757 9.03757 11.5 6 11.5C2.96243 11.5 0.5 9.03757 0.5 6C0.5 2.96243 2.96243 0.5 6 0.5Z"
293                    fill={fill}
294                    stroke={stroke}
295                />
296            </svg>
297        </button>
298    }
299}
300
301#[derive(Properties, PartialEq, Clone)]
302pub struct BrowserControlsProps {
303    #[prop_or_default]
304    pub show_controls: bool,
305    #[prop_or("flex items-center px-3 py-2 bg-gray-200 rounded-t-lg")]
306    pub class: &'static str,
307
308    #[prop_or_default]
309    pub on_close: Callback<()>,
310    #[prop_or_default]
311    pub on_close_mouse_over: Callback<()>,
312    #[prop_or_default]
313    pub on_close_mouse_out: Callback<()>,
314    #[prop_or_default]
315    pub on_close_focus: Callback<FocusEvent>,
316    #[prop_or_default]
317    pub on_close_blur: Callback<FocusEvent>,
318    #[prop_or_default]
319    pub close_class: &'static str,
320    #[prop_or_default]
321    pub close_svg_class: &'static str,
322    #[prop_or_default]
323    pub close_path_class: &'static str,
324    #[prop_or("button")]
325    pub close_button_type: &'static str,
326    #[prop_or_default]
327    pub close_aria_label: &'static str,
328    #[prop_or_default]
329    pub close_title: &'static str,
330    #[prop_or("0")]
331    pub close_tabindex: &'static str,
332
333    #[prop_or_default]
334    pub on_minimize: Callback<()>,
335    #[prop_or_default]
336    pub on_minimize_mouse_over: Callback<()>,
337    #[prop_or_default]
338    pub on_minimize_mouse_out: Callback<()>,
339    #[prop_or_default]
340    pub on_minimize_focus: Callback<FocusEvent>,
341    #[prop_or_default]
342    pub on_minimize_blur: Callback<FocusEvent>,
343    #[prop_or("ml-2")]
344    pub minimize_class: &'static str,
345    #[prop_or_default]
346    pub minimize_svg_class: &'static str,
347    #[prop_or_default]
348    pub minimize_path_class: &'static str,
349    #[prop_or("button")]
350    pub minimize_button_type: &'static str,
351    #[prop_or_default]
352    pub minimize_aria_label: &'static str,
353    #[prop_or_default]
354    pub minimize_title: &'static str,
355    #[prop_or("0")]
356    pub minimize_tabindex: &'static str,
357
358    #[prop_or_default]
359    pub on_maximize: Callback<()>,
360    #[prop_or_default]
361    pub on_maximize_mouse_over: Callback<()>,
362    #[prop_or_default]
363    pub on_maximize_mouse_out: Callback<()>,
364    #[prop_or_default]
365    pub on_maximize_focus: Callback<FocusEvent>,
366    #[prop_or_default]
367    pub on_maximize_blur: Callback<FocusEvent>,
368    #[prop_or("ml-2")]
369    pub maximize_class: &'static str,
370    #[prop_or_default]
371    pub maximize_svg_class: &'static str,
372    #[prop_or_default]
373    pub maximize_path_class: &'static str,
374    #[prop_or("button")]
375    pub maximize_button_type: &'static str,
376    #[prop_or_default]
377    pub maximize_aria_label: &'static str,
378    #[prop_or_default]
379    pub maximize_title: &'static str,
380    #[prop_or("0")]
381    pub maximize_tabindex: &'static str,
382}
383
384#[function_component(BrowserControls)]
385pub fn browser_controls(props: &BrowserControlsProps) -> Html {
386    if !props.show_controls {
387        return html! {};
388    }
389
390    html! {
391        <nav class={props.class} role="toolbar" aria-label="Browser window controls">
392            <ControlButton
393                r#type={ButtonType::Close}
394                on_click={props.on_close.clone()}
395                on_mouse_over={props.on_close_mouse_over.clone()}
396                on_mouse_out={props.on_close_mouse_out.clone()}
397                on_focus={props.on_close_focus.clone()}
398                on_blur={props.on_close_blur.clone()}
399                class={props.close_class}
400                svg_class={props.close_svg_class}
401                path_class={props.close_path_class}
402                button_type={props.close_button_type}
403                aria_label={props.close_aria_label}
404                title={props.close_title}
405                tabindex={props.close_tabindex}
406            />
407            <ControlButton
408                r#type={ButtonType::Minimize}
409                on_click={props.on_minimize.clone()}
410                on_mouse_over={props.on_minimize_mouse_over.clone()}
411                on_mouse_out={props.on_minimize_mouse_out.clone()}
412                on_focus={props.on_minimize_focus.clone()}
413                on_blur={props.on_minimize_blur.clone()}
414                class={props.minimize_class}
415                svg_class={props.minimize_svg_class}
416                path_class={props.minimize_path_class}
417                button_type={props.minimize_button_type}
418                aria_label={props.minimize_aria_label}
419                title={props.minimize_title}
420                tabindex={props.minimize_tabindex}
421            />
422            <ControlButton
423                r#type={ButtonType::Maximize}
424                on_click={props.on_maximize.clone()}
425                on_mouse_over={props.on_maximize_mouse_over.clone()}
426                on_mouse_out={props.on_maximize_mouse_out.clone()}
427                on_focus={props.on_maximize_focus.clone()}
428                on_blur={props.on_maximize_blur.clone()}
429                class={props.maximize_class}
430                svg_class={props.maximize_svg_class}
431                path_class={props.maximize_path_class}
432                button_type={props.maximize_button_type}
433                aria_label={props.maximize_aria_label}
434                title={props.maximize_title}
435                tabindex={props.maximize_tabindex}
436            />
437        </nav>
438    }
439}
440
441#[derive(Properties, PartialEq, Clone)]
442pub struct BrowserHeaderProps {
443    #[prop_or_default]
444    pub url: String,
445    #[prop_or_default]
446    pub placeholder: &'static str,
447    #[prop_or_default]
448    pub on_url_change: Option<Callback<InputEvent>>,
449    #[prop_or(true)]
450    pub show_controls: bool,
451    #[prop_or(true)]
452    pub show_address_bar: bool,
453    #[prop_or(false)]
454    pub read_only: bool,
455    #[prop_or_default]
456    pub variant: Variant,
457    #[prop_or_default]
458    pub size: Size,
459    #[prop_or_default]
460    pub custom_buttons: Vec<Html>,
461    #[prop_or_default]
462    pub class: &'static str,
463
464    #[prop_or(
465        "flex-1 mx-4 border border-gray-300 dark:border-gray-500 rounded-md px-3 py-1 bg-transparent text-sm relative bg-white dark:bg-gray-700"
466    )]
467    pub container_class: &'static str,
468    #[prop_or("w-full text-gray-900 dark:text-white bg-white dark:bg-gray-700 pr-8")]
469    pub input_class: &'static str,
470    #[prop_or(
471        "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; border: none; cursor: pointer; color: #d1d5db;"
472    )]
473    pub refresh_button_style: &'static str,
474    #[prop_or("Refresh")]
475    pub refresh_button_aria_label: &'static str,
476
477    #[prop_or("padding: 4px; border: none; cursor: pointer; color: #d1d5db;")]
478    pub icon_button_style: &'static str,
479
480    #[prop_or("flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
481    pub address_wrapper_base_style: &'static str,
482
483    #[prop_or("display: flex; align-items: center; position: relative;")]
484    pub header_base_style: &'static str,
485
486    #[prop_or_default]
487    pub on_close: Callback<()>,
488    #[prop_or_default]
489    pub on_close_mouse_over: Callback<()>,
490    #[prop_or_default]
491    pub on_close_mouse_out: Callback<()>,
492    #[prop_or_default]
493    pub on_close_focus: Callback<FocusEvent>,
494    #[prop_or_default]
495    pub on_close_blur: Callback<FocusEvent>,
496    #[prop_or_default]
497    pub close_class: &'static str,
498    #[prop_or_default]
499    pub close_svg_class: &'static str,
500    #[prop_or_default]
501    pub close_path_class: &'static str,
502    #[prop_or("button")]
503    pub close_button_type: &'static str,
504    #[prop_or_default]
505    pub close_aria_label: &'static str,
506    #[prop_or_default]
507    pub close_title: &'static str,
508    #[prop_or("0")]
509    pub close_tabindex: &'static str,
510
511    #[prop_or_default]
512    pub on_minimize: Callback<()>,
513    #[prop_or_default]
514    pub on_minimize_mouse_over: Callback<()>,
515    #[prop_or_default]
516    pub on_minimize_mouse_out: Callback<()>,
517    #[prop_or_default]
518    pub on_minimize_focus: Callback<FocusEvent>,
519    #[prop_or_default]
520    pub on_minimize_blur: Callback<FocusEvent>,
521    #[prop_or("ml-2")]
522    pub minimize_class: &'static str,
523    #[prop_or_default]
524    pub minimize_svg_class: &'static str,
525    #[prop_or_default]
526    pub minimize_path_class: &'static str,
527    #[prop_or("button")]
528    pub minimize_button_type: &'static str,
529    #[prop_or_default]
530    pub minimize_aria_label: &'static str,
531    #[prop_or_default]
532    pub minimize_title: &'static str,
533    #[prop_or("0")]
534    pub minimize_tabindex: &'static str,
535
536    #[prop_or_default]
537    pub on_maximize: Callback<()>,
538    #[prop_or_default]
539    pub on_maximize_mouse_over: Callback<()>,
540    #[prop_or_default]
541    pub on_maximize_mouse_out: Callback<()>,
542    #[prop_or_default]
543    pub on_maximize_focus: Callback<FocusEvent>,
544    #[prop_or_default]
545    pub on_maximize_blur: Callback<FocusEvent>,
546    #[prop_or("ml-2")]
547    pub maximize_class: &'static str,
548    #[prop_or_default]
549    pub maximize_svg_class: &'static str,
550    #[prop_or_default]
551    pub maximize_path_class: &'static str,
552    #[prop_or("button")]
553    pub maximize_button_type: &'static str,
554    #[prop_or_default]
555    pub maximize_aria_label: &'static str,
556    #[prop_or_default]
557    pub maximize_title: &'static str,
558    #[prop_or("0")]
559    pub maximize_tabindex: &'static str,
560
561    #[prop_or_default]
562    pub share_button_style: &'static str,
563    #[prop_or_default]
564    pub share_onclick: Callback<()>,
565    #[prop_or_default]
566    pub share_onmouseover: Callback<()>,
567    #[prop_or_default]
568    pub share_onmouseout: Callback<()>,
569    #[prop_or_default]
570    pub share_onfocus: Callback<FocusEvent>,
571    #[prop_or_default]
572    pub share_onblur: Callback<FocusEvent>,
573    #[prop_or_default]
574    pub share_tabindex: &'static str,
575
576    #[prop_or_default]
577    pub tabs_button_style: &'static str,
578    #[prop_or_default]
579    pub tabs_onclick: Callback<()>,
580    #[prop_or_default]
581    pub tabs_onmouseover: Callback<()>,
582    #[prop_or_default]
583    pub tabs_onmouseout: Callback<()>,
584    #[prop_or_default]
585    pub tabs_onfocus: Callback<FocusEvent>,
586    #[prop_or_default]
587    pub tabs_onblur: Callback<FocusEvent>,
588    #[prop_or_default]
589    pub tabs_tabindex: &'static str,
590
591    #[prop_or_default]
592    pub more_button_style: &'static str,
593    #[prop_or_default]
594    pub more_onclick: Callback<()>,
595    #[prop_or_default]
596    pub more_onmouseover: Callback<()>,
597    #[prop_or_default]
598    pub more_onmouseout: Callback<()>,
599    #[prop_or_default]
600    pub more_onfocus: Callback<FocusEvent>,
601    #[prop_or_default]
602    pub more_onblur: Callback<FocusEvent>,
603    #[prop_or_default]
604    pub more_tabindex: &'static str,
605}
606
607#[function_component(BrowserHeader)]
608pub fn browser_header(props: &BrowserHeaderProps) -> Html {
609    let is_ios = props.variant == Variant::Ios;
610    let is_tabs = props.variant == Variant::Tabs;
611
612    let base_style = {
613        let padding = match props.size {
614            Size::Small => "4px 6px",
615            Size::Large => "10px 16px",
616            _ => "6px 12px",
617        };
618        let height = match (props.variant.clone(), props.size.clone()) {
619            (Variant::Tabs, _) => "40px",
620            (Variant::Ios, _) => "56px",
621            (_, Size::Large) => "60px",
622            (_, Size::Small) => "38px",
623            _ => "48px",
624        };
625        let border_radius = if is_tabs {
626            "6px"
627        } else if props.variant == Variant::Default {
628            "8px 8px 0 0"
629        } else {
630            "0"
631        };
632        let border = if is_tabs { "1px solid #d1d5db" } else { "none" };
633        let box_shadow = if props.variant == Variant::Default {
634            "0 2px 6px rgba(0,0,0,0.1)"
635        } else {
636            "none"
637        };
638
639        format!(
640            "{} justify-content: {}; padding: {}; height: {}; border-radius: {}; border: {}; box-shadow: {};",
641            props.header_base_style,
642            if is_ios {
643                "space-between"
644            } else {
645                "flex-start"
646            },
647            padding,
648            height,
649            border_radius,
650            border,
651            box_shadow
652        )
653    };
654
655    let address_wrapper_style = format!(
656        "{} padding-left: {};",
657        props.address_wrapper_base_style,
658        if props.show_controls { "8px" } else { "0" }
659    );
660    let share_onclick = props.share_onclick.clone();
661    let share_onmouseover = props.share_onmouseover.clone();
662    let share_onmouseout = props.share_onmouseout.clone();
663
664    let tabs_onclick = props.tabs_onclick.clone();
665    let tabs_onmouseover = props.tabs_onmouseover.clone();
666    let tabs_onmouseout = props.tabs_onmouseout.clone();
667
668    let more_onclick = props.more_onclick.clone();
669    let more_onmouseover = props.more_onmouseover.clone();
670    let more_onmouseout = props.more_onmouseout.clone();
671
672    let share_onclick = Callback::from(move |_| share_onclick.emit(()));
673    let share_onmouseover = Callback::from(move |_| share_onmouseover.emit(()));
674    let share_onmouseout = Callback::from(move |_| share_onmouseout.emit(()));
675
676    let tabs_onclick = Callback::from(move |_| tabs_onclick.emit(()));
677    let tabs_onmouseover = Callback::from(move |_| tabs_onmouseover.emit(()));
678    let tabs_onmouseout = Callback::from(move |_| tabs_onmouseout.emit(()));
679
680    let more_onclick = Callback::from(move |_| more_onclick.emit(()));
681    let more_onmouseover = Callback::from(move |_| more_onmouseover.emit(()));
682    let more_onmouseout = Callback::from(move |_| more_onmouseout.emit(()));
683
684    html! {
685        <header style={base_style} class={props.class} aria-label="Browser window header">
686            <div style="display: flex; align-items: center; gap: 6px;">
687                if props.show_controls {
688                    <BrowserControls
689                        on_close={props.on_close.clone()}
690                        on_minimize={props.on_minimize.clone()}
691                        on_maximize={props.on_maximize.clone()}
692                        show_controls={props.show_controls}
693                        on_close={props.on_close.clone()}
694                        on_close_mouse_over={props.on_close_mouse_over.clone()}
695                        on_close_mouse_out={props.on_close_mouse_out.clone()}
696                        on_close_focus={props.on_close_focus.clone()}
697                        on_close_blur={props.on_close_blur.clone()}
698                        close_class={props.close_class}
699                        close_svg_class={props.close_svg_class}
700                        close_path_class={props.close_path_class}
701                        close_button_type={props.close_button_type}
702                        close_aria_label={props.close_aria_label}
703                        close_title={props.close_title}
704                        close_tabindex={props.close_tabindex}
705                        on_minimize={props.on_minimize.clone()}
706                        on_minimize_mouse_over={props.on_minimize_mouse_over.clone()}
707                        on_minimize_mouse_out={props.on_minimize_mouse_out.clone()}
708                        on_minimize_focus={props.on_minimize_focus.clone()}
709                        on_minimize_blur={props.on_minimize_blur.clone()}
710                        minimize_class={props.minimize_class}
711                        minimize_svg_class={props.minimize_svg_class}
712                        minimize_path_class={props.minimize_path_class}
713                        minimize_button_type={props.minimize_button_type}
714                        minimize_aria_label={props.minimize_aria_label}
715                        minimize_title={props.minimize_title}
716                        minimize_tabindex={props.minimize_tabindex}
717                        on_maximize={props.on_maximize.clone()}
718                        on_maximize_mouse_over={props.on_maximize_mouse_over.clone()}
719                        on_maximize_mouse_out={props.on_maximize_mouse_out.clone()}
720                        on_maximize_focus={props.on_maximize_focus.clone()}
721                        on_maximize_blur={props.on_maximize_blur.clone()}
722                        maximize_class={props.maximize_class}
723                        maximize_svg_class={props.maximize_svg_class}
724                        maximize_path_class={props.maximize_path_class}
725                        maximize_button_type={props.maximize_button_type}
726                        maximize_aria_label={props.maximize_aria_label}
727                        maximize_title={props.maximize_title}
728                        maximize_tabindex={props.maximize_tabindex}
729                    />
730                }
731                if props.show_controls {
732                    if !is_ios {
733                        <button style={props.icon_button_style} aria-label="Sidebar">
734                            <svg
735                                width="20"
736                                height="15"
737                                viewBox="0 0 20 15"
738                                fill="none"
739                                xmlns="http://www.w3.org/2000/svg"
740                            >
741                                <path
742                                    d="M2.62346 15H16.4609C18.2202 15 19.0844 14.1358 19.0844 12.4074V2.59259C19.0844 0.864204 18.2202 0 16.4609 0H2.62346C0.874483 0 0 0.864204 0 2.59259V12.4074C0 14.1358 0.874483 15 2.62346 15ZM2.64404 13.5082C1.90329 13.5082 1.48149 13.1173 1.48149 12.3354V2.66461C1.48149 1.89301 1.90329 1.49177 2.64404 1.49177H6.22427V13.5082H2.64404ZM16.4403 1.49177C17.1811 1.49177 17.6029 1.89301 17.6029 2.66461V12.3354C17.6029 13.1173 17.1811 13.5082 16.4403 13.5082H7.67489V1.49177H16.4403ZM4.67078 4.47532C4.94857 4.47532 5.18518 4.2284 5.18518 3.9609C5.18518 3.69341 4.94857 3.46708 4.67078 3.46708H3.05556C2.78806 3.46708 2.55144 3.69341 2.55144 3.9609C2.55144 4.2284 2.78806 4.47532 3.05556 4.47532H4.67078ZM4.67078 6.53293C4.94857 6.53293 5.18518 6.29629 5.18518 6.01853C5.18518 5.75102 4.94857 5.52469 4.67078 5.52469H3.05556C2.78806 5.52469 2.55144 5.75102 2.55144 6.01853C2.55144 6.29629 2.78806 6.53293 3.05556 6.53293H4.67078ZM4.67078 8.59054C4.94857 8.59054 5.18518 8.35392 5.18518 8.08642C5.18518 7.81893 4.94857 7.5926 4.67078 7.5926H3.05556C2.78806 7.5926 2.55144 7.81893 2.55144 8.08642C2.55144 8.35392 2.78806 8.59054 3.05556 8.59054H4.67078Z"
743                                    fill="#767676"
744                                />
745                            </svg>
746                        </button>
747                        <button style={props.icon_button_style} aria-label="Back">
748                            <svg
749                                width="9"
750                                height="16"
751                                viewBox="0 0 9 16"
752                                fill="none"
753                                xmlns="http://www.w3.org/2000/svg"
754                            >
755                                <path
756                                    d="M7.5 1.5L1 8L7.5 14.5"
757                                    stroke="#737373"
758                                    stroke-width="1.5"
759                                    stroke-linecap="round"
760                                    stroke-linejoin="round"
761                                />
762                            </svg>
763                        </button>
764                        <button style={props.icon_button_style} aria-label="Forward">
765                            <svg
766                                width="9"
767                                height="16"
768                                viewBox="0 0 9 16"
769                                fill="none"
770                                xmlns="http://www.w3.org/2000/svg"
771                            >
772                                <path
773                                    d="M1 14.5L7.5 8L1 1.5"
774                                    stroke="#BFBFBF"
775                                    stroke-width="1.5"
776                                    stroke-linecap="round"
777                                    stroke-linejoin="round"
778                                />
779                            </svg>
780                        </button>
781                    }
782                }
783            </div>
784            if props.show_address_bar {
785                <div style={address_wrapper_style}>
786                    <AddressBar
787                        url={props.url.clone()}
788                        placeholder={props.placeholder}
789                        on_url_change={props.on_url_change.clone().unwrap_or_default()}
790                        read_only={props.read_only}
791                        input_class={props.input_class}
792                        container_class={props.container_class}
793                        refresh_button_style={props.refresh_button_style}
794                        refresh_button_aria_label={props.refresh_button_aria_label}
795                    />
796                </div>
797            }
798            <div style="display: flex; align-items: center; gap: 6px; margin-left: auto;">
799                if props.show_controls {
800                    { for props.custom_buttons.iter().cloned() }
801                    <button
802                        style={props.icon_button_style}
803                        onclick={share_onclick.clone()}
804                        onmouseover={share_onmouseover.clone()}
805                        onmouseout={share_onmouseout.clone()}
806                        onfocus={props.share_onfocus.clone()}
807                        onblur={props.share_onblur.clone()}
808                        aria-label="Share"
809                        title="Share"
810                        tabindex={props.share_tabindex}
811                    >
812                        <svg
813                            width="15"
814                            height="19"
815                            viewBox="0 0 15 19"
816                            fill="none"
817                            xmlns="http://www.w3.org/2000/svg"
818                        >
819                            <path
820                                d="M7.49467 12.3969C7.91045 12.3969 8.26225 12.056 8.26225 11.6513V3.34416L8.1983 2.06613L8.64605 2.55604L9.81876 3.82343C9.95736 3.97254 10.1493 4.04709 10.3305 4.04709C10.7356 4.04709 11.0341 3.77017 11.0341 3.38676C11.0341 3.17377 10.9488 3.02467 10.7996 2.88621L8.04905 0.255589C7.85715 0.0638861 7.69722 0 7.49467 0C7.30277 0 7.14286 0.0638861 6.94029 0.255589L4.18977 2.88621C4.05117 3.02467 3.96589 3.17377 3.96589 3.38676C3.96589 3.77017 4.25372 4.04709 4.65885 4.04709C4.84009 4.04709 5.04264 3.97254 5.18124 3.82343L6.35395 2.55604L6.80171 2.06613L6.73774 3.34416V11.6513C6.73774 12.056 7.08955 12.3969 7.49467 12.3969ZM2.71855 19H12.2814C14.1045 19 15 18.1054 15 16.3161V8.12611C15 6.33688 14.1045 5.44225 12.2814 5.44225H9.98934V6.98654H12.2601C13.0171 6.98654 13.4648 7.4019 13.4648 8.20066V16.2416C13.4648 17.051 13.0171 17.4557 12.2601 17.4557H2.73988C1.97228 17.4557 1.53519 17.051 1.53519 16.2416V8.20066C1.53519 7.4019 1.97228 6.98654 2.73988 6.98654H5.01065V5.44225H2.71855C0.906181 5.44225 0 6.33688 0 8.12611V16.3161C0 18.1054 0.906181 19 2.71855 19Z"
821                                fill="#767676"
822                            />
823                        </svg>
824                    </button>
825                    <button
826                        style={props.icon_button_style}
827                        onclick={tabs_onclick.clone()}
828                        onmouseover={tabs_onmouseover.clone()}
829                        onmouseout={tabs_onmouseout.clone()}
830                        onfocus={props.tabs_onfocus.clone()}
831                        onblur={props.tabs_onblur.clone()}
832                        aria-label="Tabs"
833                        title="Tabs"
834                        tabindex={props.tabs_tabindex}
835                    >
836                        <svg
837                            width="15"
838                            height="15"
839                            viewBox="0 0 15 15"
840                            fill="none"
841                            xmlns="http://www.w3.org/2000/svg"
842                        >
843                            <path
844                                d="M7.01662 14.6401C7.4887 14.6401 7.87493 14.2646 7.87493 13.7925V8.3745H13.1642C13.6255 8.3745 14.0225 7.97755 14.0225 7.50547C14.0225 7.03341 13.6255 6.63643 13.1642 6.63643H7.87493V1.20768C7.87493 0.735619 7.4887 0.360107 7.01662 0.360107C6.54456 0.360107 6.14758 0.735619 6.14758 1.20768V6.63643H0.869031C0.396973 6.63643 0 7.03341 0 7.50547C0 7.97755 0.396973 8.3745 0.869031 8.3745H6.14758V13.7925C6.14758 14.2646 6.54456 14.6401 7.01662 14.6401Z"
845                                fill="#767676"
846                            />
847                        </svg>
848                    </button>
849                    <button
850                        style={props.icon_button_style}
851                        onclick={more_onclick.clone()}
852                        onmouseover={more_onmouseover.clone()}
853                        onmouseout={more_onmouseout.clone()}
854                        onfocus={props.more_onfocus.clone()}
855                        onblur={props.more_onblur.clone()}
856                        aria-label="More options"
857                        title="More options"
858                        tabindex={props.more_tabindex}
859                    >
860                        <svg
861                            width="18"
862                            height="19"
863                            viewBox="0 0 18 19"
864                            fill="none"
865                            xmlns="http://www.w3.org/2000/svg"
866                        >
867                            <path
868                                d="M2.67776 14.2898H3.97934V15.5914C3.97934 17.3407 4.85401 18.205 6.63458 18.205H14.8189C16.5891 18.205 17.4742 17.3407 17.4742 15.5914V7.32373C17.4742 5.5744 16.5891 4.71016 14.8189 4.71016H13.5174V3.40857C13.5174 1.65923 12.6323 0.794983 10.8621 0.794983H2.67776C0.897191 0.794983 0.022522 1.65923 0.022522 3.40857V11.6762C0.022522 13.4256 0.897191 14.2898 2.67776 14.2898ZM2.69859 12.7904C1.94886 12.7904 1.52195 12.3843 1.52195 11.5929V3.49187C1.52195 2.70051 1.94886 2.29442 2.69859 2.29442H10.8413C11.591 2.29442 12.0179 2.70051 12.0179 3.49187V4.71016H6.63458C4.85401 4.71016 3.97934 5.5744 3.97934 7.32373V12.7904H2.69859ZM6.65539 16.7056C5.90568 16.7056 5.47878 16.2995 5.47878 15.5081V7.40704C5.47878 6.61567 5.90568 6.20957 6.65539 6.20957H14.7981C15.5478 6.20957 15.9747 6.61567 15.9747 7.40704V15.5081C15.9747 16.2995 15.5478 16.7056 14.7981 16.7056H6.65539Z"
869                                fill="#767676"
870                            />
871                        </svg>
872                    </button>
873                }
874            </div>
875        </header>
876    }
877}
878
879#[derive(Clone, PartialEq)]
880pub struct KeyboardNavigationOptions {
881    pub on_escape: Option<Callback<()>>,
882    pub on_enter: Option<Callback<()>>,
883    pub trap_focus: bool,
884}
885
886#[hook]
887pub fn use_keyboard(options: KeyboardNavigationOptions) -> NodeRef {
888    let container_ref = use_node_ref();
889
890    {
891        let options = options.clone();
892        let container_ref = container_ref.clone();
893
894        use_effect(move || {
895            let closure = Closure::<dyn Fn(KeyboardEvent)>::wrap(Box::new(
896                move |event: KeyboardEvent| {
897                    let key = event.key();
898                    let target = event.target();
899
900                    match key.as_str() {
901                        "Escape" => {
902                            if let Some(callback) = &options.on_escape {
903                                event.prevent_default();
904                                callback.emit(());
905                            }
906                        }
907                        "Enter" => {
908                            if let Some(callback) = &options.on_enter {
909                                if let Some(target_elem) =
910                                    target.and_then(|t| t.dyn_into::<Element>().ok())
911                                {
912                                    if Some(target_elem) == container_ref.cast::<Element>() {
913                                        event.prevent_default();
914                                        callback.emit(());
915                                    }
916                                }
917                            }
918                        }
919                        "Tab" if options.trap_focus => {
920                            if let Some(container) = container_ref.cast::<Element>() {
921                                let selector = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
922                                let focusables = container.query_selector_all(selector).unwrap();
923
924                                let length = focusables.length();
925                                if length == 0 {
926                                    return;
927                                }
928
929                                let first = focusables
930                                    .item(0)
931                                    .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
932                                let last = focusables
933                                    .item(length - 1)
934                                    .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
935
936                                let document = web_sys::window().unwrap().document().unwrap();
937                                let active = document.active_element();
938
939                                if event.shift_key() {
940                                    if active == first.as_ref().map(|e| e.clone().into()) {
941                                        event.prevent_default();
942                                        if let Some(elem) = last {
943                                            elem.focus().ok();
944                                        }
945                                    }
946                                } else if active == last.as_ref().map(|e| e.clone().into()) {
947                                    event.prevent_default();
948                                    if let Some(elem) = first {
949                                        elem.focus().ok();
950                                    }
951                                }
952                            }
953                        }
954                        _ => {}
955                    }
956                },
957            )
958                as Box<dyn Fn(KeyboardEvent)>);
959
960            web_sys::window()
961                .unwrap()
962                .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
963                .unwrap();
964
965            move || {
966                web_sys::window()
967                    .unwrap()
968                    .remove_event_listener_with_callback(
969                        "keydown",
970                        closure.as_ref().unchecked_ref(),
971                    )
972                    .unwrap();
973                drop(closure);
974            }
975        });
976    }
977
978    container_ref
979}
980
981/// Properties for the `BrowserFrame` component.
982///
983/// This struct provides a wide range of customization options and handlers
984/// for the browser frame's appearance and behavior.
985#[derive(Properties, PartialEq, Clone)]
986pub struct BrowserFrameProps {
987    /// Child components to render inside the browser frame.
988    #[prop_or_default]
989    pub children: Children,
990
991    /// The current URL displayed in the address bar.
992    #[prop_or_default]
993    pub url: String,
994
995    /// Placeholder text for the address bar input.
996    #[prop_or_default]
997    pub placeholder: &'static str,
998
999    /// Callback for when the URL is changed by the user.
1000    #[prop_or_default]
1001    pub on_url_change: Option<Callback<InputEvent>>,
1002
1003    /// Callback when the close button is clicked.
1004    #[prop_or_default]
1005    pub on_close: Callback<()>,
1006
1007    /// Callback when the minimize button is clicked.
1008    #[prop_or_default]
1009    pub on_minimize: Callback<()>,
1010
1011    /// Callback when the maximize button is clicked.
1012    #[prop_or_default]
1013    pub on_maximize: Callback<()>,
1014
1015    /// Whether to show the window controls (close, minimize, maximize).
1016    ///
1017    /// Defaults to `true`.
1018    #[prop_or(true)]
1019    pub show_controls: bool,
1020
1021    /// Whether to show the address bar.
1022    ///
1023    /// Defaults to `true`.
1024    #[prop_or(true)]
1025    pub show_address_bar: bool,
1026
1027    /// Whether the address bar is read-only.
1028    ///
1029    /// Defaults to `false`.
1030    #[prop_or(false)]
1031    pub read_only: bool,
1032
1033    /// The size of the browser frame (e.g., small, medium, large).
1034    #[prop_or_default]
1035    pub size: Size,
1036
1037    /// The visual variant of the browser frame.
1038    #[prop_or_default]
1039    pub variant: Variant,
1040
1041    /// Custom buttons to render in the browser header.
1042    #[prop_or_default]
1043    pub custom_buttons: Vec<Html>,
1044
1045    /// CSS classes for styling the outer container of the browser frame.
1046    ///
1047    /// Defaults to: `"rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"`.
1048    #[prop_or(
1049        "rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
1050    )]
1051    pub class: &'static str,
1052
1053    /// Inline styles for the outer container.
1054    #[prop_or_default]
1055    pub style: &'static str,
1056
1057    /// Optional ID for the outer container.
1058    #[prop_or_default]
1059    pub id: &'static str,
1060
1061    /// ARIA label for the browser frame container.
1062    ///
1063    /// Defaults to `"Browser window"`.
1064    #[prop_or("Browser window")]
1065    pub aria_label: &'static str,
1066
1067    /// ARIA description for the browser frame container.
1068    #[prop_or_default]
1069    pub aria_describedby: &'static str,
1070
1071    /// CSS classes for the address bar container.
1072    ///
1073    /// Defaults to: `"flex-1 mx-4 border border-gray-300 dark:border-gray-500 rounded-md px-3 py-1 bg-transparent text-sm relative bg-white dark:bg-gray-700"`.
1074    #[prop_or(
1075        "flex-1 mx-4 border border-gray-300 dark:border-gray-500 rounded-md px-3 py-1 bg-transparent text-sm relative bg-white dark:bg-gray-700"
1076    )]
1077    pub container_class: &'static str,
1078
1079    /// CSS classes for the address bar input element.
1080    ///
1081    /// Defaults to: `"w-full text-gray-900 dark:text-white bg-white dark:bg-gray-700 pr-8"`.
1082    #[prop_or("w-full text-gray-900 dark:text-white bg-white dark:bg-gray-700 pr-8")]
1083    pub input_class: &'static str,
1084
1085    /// Inline styles for the refresh button.
1086    ///
1087    /// Defaults to: `"position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; border: none; cursor: pointer; color: #d1d5db;"`.
1088    #[prop_or(
1089        "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; border: none; cursor: pointer; color: #d1d5db;"
1090    )]
1091    pub refresh_button_style: &'static str,
1092
1093    /// ARIA label for the refresh button.
1094    ///
1095    /// Defaults to `"Refresh"`.
1096    #[prop_or("Refresh")]
1097    pub refresh_button_aria_label: &'static str,
1098
1099    /// Inline styles for icon buttons (close, minimize, maximize).
1100    ///
1101    /// Defaults to: `"padding: 4px; border: none; cursor: pointer; color: #d1d5db;"`.
1102    #[prop_or("padding: 4px; border: none; cursor: pointer; color: #d1d5db;")]
1103    pub icon_button_style: &'static str,
1104
1105    /// Inline styles for the address bar wrapper.
1106    ///
1107    /// Defaults to: `"flex: 1; display: flex; justify-content: center; padding-right: 8px;"`.
1108    #[prop_or("flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
1109    pub address_wrapper_base_style: &'static str,
1110
1111    /// Inline styles for the header container.
1112    ///
1113    /// Defaults to: `"display: flex; align-items: center; position: relative;"`.
1114    #[prop_or("display: flex; align-items: center; position: relative;")]
1115    pub header_base_style: &'static str,
1116
1117    /// Callbacks and styles for the close button and related elements.
1118    #[prop_or_default]
1119    pub on_close_mouse_over: Callback<()>,
1120    #[prop_or_default]
1121    pub on_close_mouse_out: Callback<()>,
1122    #[prop_or_default]
1123    pub on_close_focus: Callback<FocusEvent>,
1124    #[prop_or_default]
1125    pub on_close_blur: Callback<FocusEvent>,
1126    #[prop_or_default]
1127    pub close_class: &'static str,
1128    #[prop_or_default]
1129    pub close_svg_class: &'static str,
1130    #[prop_or_default]
1131    pub close_path_class: &'static str,
1132    #[prop_or("button")]
1133    pub close_button_type: &'static str,
1134    #[prop_or_default]
1135    pub close_aria_label: &'static str,
1136    #[prop_or_default]
1137    pub close_title: &'static str,
1138    #[prop_or("0")]
1139    pub close_tabindex: &'static str,
1140
1141    /// Callbacks and styles for the minimize button and related elements.
1142    #[prop_or_default]
1143    pub on_minimize_mouse_over: Callback<()>,
1144    #[prop_or_default]
1145    pub on_minimize_mouse_out: Callback<()>,
1146    #[prop_or_default]
1147    pub on_minimize_focus: Callback<FocusEvent>,
1148    #[prop_or_default]
1149    pub on_minimize_blur: Callback<FocusEvent>,
1150    #[prop_or("ml-2")]
1151    pub minimize_class: &'static str,
1152    #[prop_or_default]
1153    pub minimize_svg_class: &'static str,
1154    #[prop_or_default]
1155    pub minimize_path_class: &'static str,
1156    #[prop_or("button")]
1157    pub minimize_button_type: &'static str,
1158    #[prop_or_default]
1159    pub minimize_aria_label: &'static str,
1160    #[prop_or_default]
1161    pub minimize_title: &'static str,
1162    #[prop_or("0")]
1163    pub minimize_tabindex: &'static str,
1164
1165    /// Callbacks and styles for the maximize button and related elements.
1166    #[prop_or_default]
1167    pub on_maximize_mouse_over: Callback<()>,
1168    #[prop_or_default]
1169    pub on_maximize_mouse_out: Callback<()>,
1170    #[prop_or_default]
1171    pub on_maximize_focus: Callback<FocusEvent>,
1172    #[prop_or_default]
1173    pub on_maximize_blur: Callback<FocusEvent>,
1174    #[prop_or("ml-2")]
1175    pub maximize_class: &'static str,
1176    #[prop_or_default]
1177    pub maximize_svg_class: &'static str,
1178    #[prop_or_default]
1179    pub maximize_path_class: &'static str,
1180    #[prop_or("button")]
1181    pub maximize_button_type: &'static str,
1182    #[prop_or_default]
1183    pub maximize_aria_label: &'static str,
1184    #[prop_or_default]
1185    pub maximize_title: &'static str,
1186    #[prop_or("0")]
1187    pub maximize_tabindex: &'static str,
1188
1189    /// Style and callbacks for the share button.
1190    #[prop_or_default]
1191    pub share_button_style: &'static str,
1192    #[prop_or_default]
1193    pub share_onclick: Callback<()>,
1194    #[prop_or_default]
1195    pub share_onmouseover: Callback<()>,
1196    #[prop_or_default]
1197    pub share_onmouseout: Callback<()>,
1198    #[prop_or_default]
1199    pub share_onfocus: Callback<FocusEvent>,
1200    #[prop_or_default]
1201    pub share_onblur: Callback<FocusEvent>,
1202    #[prop_or_default]
1203    pub share_tabindex: &'static str,
1204
1205    /// Style and callbacks for the tabs button.
1206    #[prop_or_default]
1207    pub tabs_button_style: &'static str,
1208    #[prop_or_default]
1209    pub tabs_onclick: Callback<()>,
1210    #[prop_or_default]
1211    pub tabs_onmouseover: Callback<()>,
1212    #[prop_or_default]
1213    pub tabs_onmouseout: Callback<()>,
1214    #[prop_or_default]
1215    pub tabs_onfocus: Callback<FocusEvent>,
1216    #[prop_or_default]
1217    pub tabs_onblur: Callback<FocusEvent>,
1218    #[prop_or_default]
1219    pub tabs_tabindex: &'static str,
1220
1221    /// Style and callbacks for the more button.
1222    #[prop_or_default]
1223    pub more_button_style: &'static str,
1224    #[prop_or_default]
1225    pub more_onclick: Callback<()>,
1226    #[prop_or_default]
1227    pub more_onmouseover: Callback<()>,
1228    #[prop_or_default]
1229    pub more_onmouseout: Callback<()>,
1230    #[prop_or_default]
1231    pub more_onfocus: Callback<FocusEvent>,
1232    #[prop_or_default]
1233    pub more_onblur: Callback<FocusEvent>,
1234    #[prop_or_default]
1235    pub more_tabindex: &'static str,
1236}
1237/// BrowserFrame Component
1238///
1239/// A Yew component that emulates a browser window, complete with customizable controls (close, minimize, maximize),
1240/// an address bar, and optional custom buttons. It wraps its child components in a browser-like interface and provides
1241/// various hooks for interaction events such as focus, hover, and clicks.
1242///
1243/// # Features
1244/// - Configurable address bar and controls (show/hide, read-only).
1245/// - Fully customizable styling and classes for different parts of the frame.
1246/// - Emits callbacks for URL changes and control interactions (close, minimize, maximize).
1247/// - Supports additional custom buttons and slots for user-defined functionality.
1248/// - Keyboard navigation support (Escape to close).
1249///
1250/// # Examples
1251///
1252/// ## Basic Usage
1253/// ```rust
1254/// use yew::prelude::*;
1255/// use browser_rs::yew::BrowserFrame;
1256///
1257/// #[function_component(App)]
1258/// pub fn app() -> Html {
1259///     let on_close = Callback::from(|_| log::info!("Browser closed"));
1260///
1261///     html! {
1262///         <BrowserFrame
1263///             url={"https://opensass.org".to_string()}
1264///             on_close={on_close}
1265///         >
1266///             <p>{ "Your embedded content here." }</p>
1267///         </BrowserFrame>
1268///     }
1269/// }
1270/// ```
1271///
1272/// ## With Custom Buttons
1273/// ```rust
1274/// use yew::prelude::*;
1275/// use browser_rs::yew::BrowserFrame;
1276///
1277/// #[function_component(App)]
1278/// pub fn app() -> Html {
1279///     let custom_button = html! {
1280///         <button>{ "Custom Button" }</button>
1281///     };
1282///
1283///     html! {
1284///         <BrowserFrame
1285///             url={"https://opensass.org".to_string()}
1286///             custom_buttons={vec![custom_button]}
1287///         >
1288///             <p>{ "Custom button in the header!" }</p>
1289///         </BrowserFrame>
1290///     }
1291/// }
1292/// ```
1293///
1294/// ## Styling and Class Customization
1295/// ```rust
1296/// use yew::prelude::*;
1297/// use browser_rs::yew::BrowserFrame;
1298///
1299/// #[function_component(App)]
1300/// pub fn app() -> Html {
1301///     html! {
1302///         <BrowserFrame
1303///             url={"https://opensass.org".to_string()}
1304///             class={"rounded-xl shadow-xl"}
1305///             input_class={"bg-gray-200 text-gray-900"}
1306///             container_class={"flex-1 mx-4"}
1307///         >
1308///             <p>{ "Styled browser frame!" }</p>
1309///         </BrowserFrame>
1310///     }
1311/// }
1312/// ```
1313///
1314/// # Behavior
1315/// - The `BrowserFrame` uses a `BrowserHeader` subcomponent for controls and an address bar,
1316///   and a `BrowserContent` subcomponent for rendering child content.
1317/// - The `on_url_change` callback is called when the address bar's URL changes.
1318/// - Control buttons (close, minimize, maximize) emit their respective callbacks when interacted with.
1319/// - Keyboard navigation is enabled: Escape key triggers the `on_close` callback.
1320///
1321/// # Notes
1322/// - Supports both light and dark themes through provided classes and styles.
1323/// - Default styling can be customized via `class`, `style`, and other related props.
1324/// - Accessibility attributes (`aria-*`) are provided.
1325#[function_component(BrowserFrame)]
1326pub fn browser_frame(props: &BrowserFrameProps) -> Html {
1327    let on_close = props.on_close.clone();
1328    let container_ref = use_keyboard(KeyboardNavigationOptions {
1329        on_escape: Some(Callback::from(move |_| on_close.emit(()))),
1330        on_enter: None,
1331        trap_focus: false,
1332    });
1333
1334    let size_style = props.size.to_style();
1335    let combined_style = format!("{} {}", size_style, props.style);
1336
1337    html! {
1338        <article
1339            ref={container_ref}
1340            id={props.id}
1341            class={props.class}
1342            style={combined_style}
1343            role="application"
1344            aria-label={props.aria_label}
1345            aria-describedby={props.aria_describedby}
1346            tabindex={Some("-1")}
1347        >
1348            <BrowserHeader
1349                url={props.url.clone()}
1350                placeholder={props.placeholder}
1351                on_url_change={props.on_url_change.clone()}
1352                on_close={props.on_close.clone()}
1353                on_minimize={props.on_minimize.clone()}
1354                on_maximize={props.on_maximize.clone()}
1355                show_controls={props.show_controls}
1356                show_address_bar={props.show_address_bar}
1357                read_only={props.read_only}
1358                variant={props.variant.clone()}
1359                size={props.size.clone()}
1360                custom_buttons={props.custom_buttons.clone()}
1361                class=""
1362                container_class={props.container_class}
1363                input_class={props.input_class}
1364                refresh_button_style={props.refresh_button_style}
1365                refresh_button_aria_label={props.refresh_button_aria_label}
1366                icon_button_style={props.icon_button_style}
1367                address_wrapper_base_style={props.address_wrapper_base_style}
1368                header_base_style={props.header_base_style}
1369                on_close_mouse_over={props.on_close_mouse_over.clone()}
1370                on_close_mouse_out={props.on_close_mouse_out.clone()}
1371                on_close_focus={props.on_close_focus.clone()}
1372                on_close_blur={props.on_close_blur.clone()}
1373                close_class={props.close_class}
1374                close_svg_class={props.close_svg_class}
1375                close_path_class={props.close_path_class}
1376                close_button_type={props.close_button_type}
1377                close_aria_label={props.close_aria_label}
1378                close_title={props.close_title}
1379                close_tabindex={props.close_tabindex}
1380                on_minimize_mouse_over={props.on_minimize_mouse_over.clone()}
1381                on_minimize_mouse_out={props.on_minimize_mouse_out.clone()}
1382                on_minimize_focus={props.on_minimize_focus.clone()}
1383                on_minimize_blur={props.on_minimize_blur.clone()}
1384                minimize_class={props.minimize_class}
1385                minimize_svg_class={props.minimize_svg_class}
1386                minimize_path_class={props.minimize_path_class}
1387                minimize_button_type={props.minimize_button_type}
1388                minimize_aria_label={props.minimize_aria_label}
1389                minimize_title={props.minimize_title}
1390                minimize_tabindex={props.minimize_tabindex}
1391                on_maximize_mouse_over={props.on_maximize_mouse_over.clone()}
1392                on_maximize_mouse_out={props.on_maximize_mouse_out.clone()}
1393                on_maximize_focus={props.on_maximize_focus.clone()}
1394                on_maximize_blur={props.on_maximize_blur.clone()}
1395                maximize_class={props.maximize_class}
1396                maximize_svg_class={props.maximize_svg_class}
1397                maximize_path_class={props.maximize_path_class}
1398                maximize_button_type={props.maximize_button_type}
1399                maximize_aria_label={props.maximize_aria_label}
1400                maximize_title={props.maximize_title}
1401                maximize_tabindex={props.maximize_tabindex}
1402                share_button_style={props.share_button_style}
1403                share_onclick={props.share_onclick.clone()}
1404                share_onmouseover={props.share_onmouseover.clone()}
1405                share_onmouseout={props.share_onmouseout.clone()}
1406                share_onfocus={props.share_onfocus.clone()}
1407                share_onblur={props.share_onblur.clone()}
1408                share_tabindex={props.share_tabindex}
1409                tabs_button_style={props.tabs_button_style}
1410                tabs_onclick={props.tabs_onclick.clone()}
1411                tabs_onmouseover={props.tabs_onmouseover.clone()}
1412                tabs_onmouseout={props.tabs_onmouseout.clone()}
1413                tabs_onfocus={props.tabs_onfocus.clone()}
1414                tabs_onblur={props.tabs_onblur.clone()}
1415                tabs_tabindex={props.tabs_tabindex}
1416                more_button_style={props.more_button_style}
1417                more_onclick={props.more_onclick.clone()}
1418                more_onmouseover={props.more_onmouseover.clone()}
1419                more_onmouseout={props.more_onmouseout.clone()}
1420                more_onfocus={props.more_onfocus.clone()}
1421                more_onblur={props.more_onblur.clone()}
1422                more_tabindex={props.more_tabindex}
1423            />
1424            <BrowserContent aria_describedby={props.aria_describedby}>
1425                { for props.children.iter() }
1426            </BrowserContent>
1427        </article>
1428    }
1429}