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