get_settings(); return ! empty( $s['hot_reload'] ) && current_user_can( 'manage_options' ); } /** * Enqueue the hot-reload client. * * @param string $mode 'front' or 'editor'. * @param array $deps Script deps. * @param bool $full The page has the stylesheet with every utility class. * @return void */ private function enqueue_hot_reload( $mode, $deps = array(), $full = false ) { $css = Css_Generator::get_instance(); wp_enqueue_script( 'tokenforge-hot', TOKENFORGE_URL . 'assets/hot-reload.js', $deps, TOKENFORGE_VERSION, true ); wp_localize_script( 'tokenforge-hot', 'TokenForgeHot', array( 'mode' => $mode, 'ver' => $css->version(), 'url' => $full ? $css->full_css_url() : $css->css_url(), 'shopUrl' => $css->shop_css_url(), 'restUrl' => esc_url_raw( rest_url( Rest::NS ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'interval' => 15000, // fallback poll only: a dashboard save reaches this browser at once by the signals ) ); } /** * Hot reload inside the block editor. * * @return void */ public function editor_hot_reload() { if ( $this->hot_reload_enabled() && file_exists( Css_Generator::get_instance()->css_path() ) ) { $this->enqueue_hot_reload( 'editor', array( 'wp-data', 'wp-block-editor' ) ); } } /** * Apply a stored scheme choice before first paint (avoids a light flash). * * @return void */ public function scheme_head() { if ( ! Scheme::switcher_enabled() || $this->is_builder_ui() ) { return; } wp_print_inline_script_tag( 'try{var s=localStorage.getItem("tf-scheme");if(s==="light"||s==="dark"){document.documentElement.setAttribute("data-scheme",s)}}catch(e){}' ); } /** * Should this request run the motion scripts? Not in builder UIs, not in * the Bricks canvas when effects are disabled in builders (or the * stylesheet isn't loaded there), and only when the stylesheet exists. * * @return bool */ private function motion_enabled() { if ( is_admin() || ! Motion::needs_script() || $this->is_builder_ui() || ! file_exists( Css_Generator::get_instance()->css_path() ) ) { return false; } if ( Platforms\Bricks::is_active() && Platforms\Bricks::is_preview_context() ) { $m = Motion::settings(); $s = Settings::get_instance()->get_settings(); return empty( $m['disable_in_builder'] ) && ! empty( $s['enqueue_in_builder'] ); } return true; } /** * Motion head script (before first paint). * * @return void */ public function motion_head() { if ( $this->motion_enabled() ) { $js = Motion::head_script(); if ( '' !== $js ) { wp_print_inline_script_tag( $js, array( 'id' => 'tokenforge-motion-head' ) ); } } } /** * Motion footer script. * * @return void */ public function motion_footer() { if ( $this->motion_enabled() ) { wp_print_inline_script_tag( Motion::script(), array( 'id' => 'tokenforge-motion' ) ); } } /** * Delegated click handler for any .scheme-toggle / [data-scheme-toggle]. * * @return void */ public function scheme_footer() { if ( ! Scheme::switcher_enabled() || $this->is_builder_ui() ) { return; } $js = '(function(){var r=document.documentElement,mq=window.matchMedia("(prefers-color-scheme: dark)");' . 'function cur(){var a=r.getAttribute("data-scheme");return a||(mq.matches?"dark":"light")}' . 'function sync(){var d=cur()==="dark";document.querySelectorAll(".scheme-toggle,[data-scheme-toggle]").forEach(function(b){b.setAttribute("aria-pressed",d?"true":"false")})}' . 'document.addEventListener("click",function(e){var b=e.target.closest(".scheme-toggle,[data-scheme-toggle]");if(!b){return}e.preventDefault();' . 'var n=cur()==="dark"?"light":"dark";r.setAttribute("data-scheme",n);try{localStorage.setItem("tf-scheme",n)}catch(er){}sync()});' . 'if(mq.addEventListener){mq.addEventListener("change",sync)}sync()})();'; wp_print_inline_script_tag( $js ); } /** * [tokenforge_scheme_toggle class="..."]: a light/dark toggle button. * * @param array $atts Shortcode attributes. * @return string */ public function scheme_shortcode( $atts ) { $atts = shortcode_atts( array( 'class' => '' ), $atts, 'tokenforge_scheme_toggle' ); if ( ! Scheme::switcher_enabled() ) { return ''; } return Scheme::toggle_markup( sanitize_text_field( $atts['class'] ) ); } /** * Add a TokenForge node to the admin bar. * * @param \WP_Admin_Bar $bar Admin bar. * @return void */ public function admin_bar( $bar ) { if ( ! current_user_can( 'manage_options' ) ) { return; } $s = Settings::get_instance()->get_settings(); if ( empty( $s['admin_bar_link'] ) ) { return; } $bar->add_node( array( 'id' => 'tokenforge', 'title' => '' . esc_html__( 'TokenForge', 'tokenforge' ), 'href' => admin_url( 'admin.php?page=tokenforge' ), ) ); } /** * Enqueue the stylesheet on the front end (all sites, all builders). * * @return void */ public function frontend() { $css = Css_Generator::get_instance(); if ( ! file_exists( $css->css_path() ) ) { return; } // NEVER load inside a builder's editor UI (its chrome uses native // inputs/buttons/headings that our base styles would restyle). The // canvas/preview iframe and the real front end still get the stylesheet. if ( $this->is_builder_ui() ) { return; } $bricks = Platforms\Bricks::is_active(); $settings = Settings::get_instance()->get_settings(); // Bricks canvas (preview iframe) is opt-out via Settings. if ( $bricks && Platforms\Bricks::is_preview_context() && empty( $settings['enqueue_in_builder'] ) ) { return; } $deps = ( $bricks && wp_style_is( 'bricks-frontend', 'registered' ) ) ? array( 'bricks-frontend' ) : array(); // Bricks canvas: keep scroll effects still while editing (separate handle // so a hot reload of the main stylesheet doesn't drop it). if ( $bricks && Platforms\Bricks::is_preview_context() ) { $m = Motion::settings(); $still = empty( $m['disable_in_builder'] ) ? '' : Motion::still_css(); if ( '' !== $still ) { wp_register_style( 'tokenforge-motion-still', false, array(), TOKENFORGE_VERSION ); wp_enqueue_style( 'tokenforge-motion-still' ); wp_add_inline_style( 'tokenforge-motion-still', $still ); } } // Unused utility classes left out: the builder canvas and the Customizer // preview get every class (a class you add shows at once), as a file. $full = ( ( $bricks && Platforms\Bricks::is_preview_context() ) || is_customize_preview() ) && Class_Usage::active() && file_exists( $css->full_css_path() ); // Inline mode: no extra request / render-blocking file. $inline = ! $full && ! empty( $settings['inline_css'] ) && $this->inline_style( Css_Generator::HANDLE, $css->css_path(), $deps, $css->version() ); if ( ! $inline ) { wp_enqueue_style( Css_Generator::HANDLE, remove_query_arg( 'ver', $full ? $css->full_css_url() : $css->css_url() ), $deps, $css->version() ); } // WooCommerce shop pages: their own sheet, only where the shop appears. if ( file_exists( $css->shop_css_path() ) && Woo::active() && Woo::shop_styles_needed() ) { $shop_inline = ! empty( $settings['inline_css'] ) && $this->inline_style( self::SHOP_HANDLE, $css->shop_css_path(), array( Css_Generator::HANDLE ), $css->version() ); if ( ! $shop_inline ) { wp_enqueue_style( self::SHOP_HANDLE, remove_query_arg( 'ver', $css->shop_css_url() ), array( Css_Generator::HANDLE ), $css->version() ); } } if ( $this->hot_reload_enabled() ) { $this->enqueue_hot_reload( 'front', array(), $full ); } } /** Handle of the WooCommerce shop pages sheet. */ const SHOP_HANDLE = 'tokenforge-shop'; /** * Print a generated file as an inline style (no extra request). * * @param string $handle Style handle. * @param string $path File. * @param array $deps Dependencies. * @param string $version Version. * @return bool False when the file can't be read. */ private function inline_style( $handle, $path, $deps, $version ) { $contents = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( false === $contents ) { return false; } wp_register_style( $handle, false, $deps, $version ); wp_enqueue_style( $handle ); // Never let a stylesheet close the inline