Skip to main content

euv_core/vdom/css/
impl.rs

1use crate::*;
2
3/// Implementation of CssClass construction and style injection.
4impl CssClass {
5    /// Creates a new CSS class with the given name and style declarations.
6    ///
7    /// Automatically injects the styles into the DOM upon creation.
8    ///
9    /// # Arguments
10    ///
11    /// - `String` - The class name.
12    /// - `String` - The CSS style declarations.
13    ///
14    /// # Returns
15    ///
16    /// - `Self` - A new CSS class with injected styles.
17    pub fn new(name: String, style: String) -> Self {
18        let mut css_class: CssClass = CssClass::default();
19        css_class.set_name(name);
20        css_class.set_style(style);
21        css_class.inject_style();
22        css_class
23    }
24
25    /// Injects this class's styles into the DOM if not already present.
26    ///
27    /// Creates a `<style>` element with id `euv-css-injected` on first call,
28    /// then appends the class rule. Subsequent calls for the same class name
29    /// are no-ops. On first creation, also injects global CSS keyframes
30    /// required by built-in animations.
31    ///
32    /// # Panics
33    ///
34    /// Panics if `window()` or `document()` is unavailable on the current platform.
35    pub fn inject_style(&self) {
36        #[cfg(target_arch = "wasm32")]
37        {
38            let style_id: &str = "euv-css-injected";
39            let document: web_sys::Document = web_sys::window()
40                .expect("no global window exists")
41                .document()
42                .expect("no document exists");
43            let style_element: web_sys::HtmlStyleElement = match document
44                .get_element_by_id(style_id)
45            {
46                Some(el) => el.dyn_into::<web_sys::HtmlStyleElement>().unwrap(),
47                None => {
48                    let el: web_sys::HtmlStyleElement = document
49                        .create_element("style")
50                        .unwrap()
51                        .dyn_into::<web_sys::HtmlStyleElement>()
52                        .unwrap();
53                    el.set_id(style_id);
54                    let keyframes: &str = "@keyframes euv-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes euv-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes euv-scale-in { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes euv-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } } @keyframes euv-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } } @keyframes euv-slide-left { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes euv-fade-in-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }";
55                    let global: &str = "html, body, #app { height: 100%; margin: 0; padding: 0; overflow: hidden; } * { -webkit-tap-highlight-color: transparent; }";
56                    let media_queries: &str = "@media (max-width: 767px) { .c_app_nav { display: none; } .c_app_main { padding: 20px 16px; max-width: 100%; } .c_page_title { font-size: 22px; } .c_page_subtitle { font-size: 14px; } .c_card { padding: 16px; margin: 12px 0; border-radius: 10px; } .c_card_title { font-size: 16px; } .c_form_grid { grid-template-columns: 1fr; } .c_browser_api_row { grid-template-columns: 1fr; } .c_modal_content { max-width: 100%; width: calc(100% - 32px); border-radius: 16px 16px 0 0; position: fixed; bottom: 0; left: 16px; height: 80vh; animation: euv-slide-up 0.25s ease; } .c_modal_overlay { align-items: flex-end; } .c_event_stats { gap: 12px; flex-wrap: wrap; } .c_event_section_row { gap: 12px; flex-wrap: wrap; } .c_event_section_col { min-width: 100%; } .c_counter_value { font-size: 20px; } .c_timer_value { font-size: 36px; } .c_not_found_code { font-size: 56px; } .c_not_found_container { padding: 40px 20px; } .c_list_input_row { flex-direction: column; } .c_vconsole_button { bottom: 16px; right: 16px; width: 44px; height: 44px; border-radius: 12px; } .c_tab_bar { flex-wrap: wrap; } .c_primary_button { padding: 10px 18px; font-size: 14px; } .c_badge { padding: 4px 10px; font-size: 11px; } .c_badge_outline { padding: 4px 10px; font-size: 11px; } .c_browser_info_grid { grid-template-columns: 1fr; } .c_anim_spin { font-size: 36px; } .c_anim_spin_stopped { font-size: 36px; } .c_anim_pulse { font-size: 36px; } .c_anim_pulse_stopped { font-size: 36px; } }";
57                    el.set_inner_text(&format!("{} {} {}", global, keyframes, media_queries));
58                    document.head().unwrap().append_child(&el).unwrap();
59                    el
60                }
61            };
62            let existing_css: String = style_element.inner_text();
63            let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
64            if !existing_css.contains(&class_rule) {
65                let new_css: String = if existing_css.is_empty() {
66                    class_rule
67                } else {
68                    format!("{}\n{}", existing_css, class_rule)
69                };
70                style_element.set_inner_text(&new_css);
71            }
72        }
73    }
74}
75
76/// Displays the CSS class name.
77///
78/// This enables `format!("{}", css_class)` to produce the class name string,
79/// which is required for reactive `if` conditions in `class:` attributes.
80impl std::fmt::Display for CssClass {
81    /// Formats the CSS class as its name string.
82    ///
83    /// # Arguments
84    ///
85    /// - `&mut Formatter` - The formatter.
86    ///
87    /// # Returns
88    ///
89    /// - `std::fmt::Result` - The formatting result.
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(f, "{}", self.get_name())
92    }
93}