*/ /** * No dependencies on purpose: WordPress silently drops a stylesheet whose * dependency is missing, and some plugins deregister core handles on their * screens (SEOPress removes 'forms'), which would take the whole theme * down. Enqueued at admin_enqueue_scripts after core's admin styles, so it * still prints after them. * * @var array */ private const STYLE_DEPS = []; /** User meta holding a personal theme ('dark' = Default | 'modern' | 'light'); empty = site default. */ public const SIDEBAR_META = 'featherdash_sidebar'; private Settings $settings; public function register( Settings $settings ) : void { $this->settings = $settings; /** * Filters whether the admin theme is on. Runs on `plugins_loaded`, so * hook it from a plugin or mu-plugin, not a theme. * * @param bool $enabled */ if ( !apply_filters( 'featherdash/theme/enabled', (bool) $settings->get( 'admin.theme.enabled', true ) ) ) { return; } add_filter( 'admin_body_class', [$this, 'bodyClass'] ); add_action( 'admin_enqueue_scripts', [$this, 'enqueue'], 20 ); add_filter( 'script_loader_tag', [$this, 'moduleTag'], 10, 3 ); add_action( 'admin_init', [$this, 'removeColorSchemePicker'] ); // List tables: the post's status and states as pills after the title // (so the date column drops its "Published" / "Last Modified" line). add_filter( 'post_states_html', [$this, 'statePills'], 10, 3 ); add_filter( 'post_date_column_status', '__return_empty_string' ); // Classic (TinyMCE) editors, ACF's too: content in Inter with the accent. if ( is_admin() ) { add_filter( 'mce_css', [$this, 'editorContentCss'] ); add_filter( 'tiny_mce_before_init', [$this, 'editorContentAccent'] ); } } /** * While the theme paints the whole admin, core's "Administration Color * Scheme" picker is meaningless: drop its row on profile.php and * user-edit.php (theme off → the hook is never removed). */ /** * Adds dist/tinymce.css to the stylesheets loaded inside TinyMCE's iframe. * * @param mixed $css Comma-separated stylesheet URLs. */ public function editorContentCss( $css ) : string { $css = ( is_string( $css ) ? $css : '' ); if ( !file_exists( FEATHERDASH_DIR . 'dist/tinymce.css' ) ) { return $css; } $url = add_query_arg( 'ver', $this->version( 'dist/tinymce.css' ), FEATHERDASH_URL . 'dist/tinymce.css' ); return ( $css === '' ? $url : $css . ',' . $url ); } /** * The accent for the editor content (links, quotes, selection). * * @param mixed $init * @return array */ public function editorContentAccent( $init ) : array { $init = ( is_array( $init ) ? $init : [] ); $style = 'body#tinymce.wp-editor{--fd-accent:' . $this->accent() . '}'; $init['content_style'] = trim( (string) ($init['content_style'] ?? '') . ' ' . $style ); return $init; } /** Post states the status pill already shows. */ private const STATUS_STATES = [ 'draft', 'pending', 'scheduled', 'private' ]; /** * Replaces core's " — Draft, Sticky" after a list-table title with pills: * the post's status first (Published too, coloured by status), then its * other states (Front Page, Sticky, Password protected…). Attachments get * no status pill. * * @param mixed $html * @param mixed $states * @param mixed $post */ public function statePills( $html, $states, $post ) : string { if ( !$post instanceof \WP_Post ) { return ( is_string( $html ) ? $html : '' ); } $pills = ''; if ( $post->post_type !== 'attachment' ) { $key = $post->post_status; $object = get_post_status_object( $key ); $label = ( $object !== null ? (string) $object->label : $key ); if ( $key === 'future' && time() > (int) get_post_timestamp( $post ) ) { $key = 'missed'; $label = __( 'Missed schedule', 'featherdash' ); } $pills .= sprintf( '%s', esc_attr( sanitize_html_class( $key ) ), esc_html( $label ) ); } $states = ( is_array( $states ) ? $states : [] ); $others = ( $post->post_type === 'attachment' ? $states : array_diff_key( $states, array_flip( self::STATUS_STATES ) ) ); foreach ( $others as $state ) { // Labels come from core/plugins already escaped, sometimes with markup. $pills .= '' . wp_kses_post( (string) $state ) . ''; } return ( $pills === '' ? '' : '' . $pills . '' ); } public function removeColorSchemePicker() : void { remove_action( 'admin_color_scheme_picker', 'admin_color_scheme_picker' ); } public function bodyClass( string $classes ) : string { if ( !$this->isBlockEditor() ) { $classes .= ' fd-theme'; } if ( $this->isNarrowScreen() ) { $classes .= ' fd-narrow'; } if ( $this->settings->get( 'admin.theme.accordion', true ) ) { $classes .= ' fd-accordion'; } return $classes; } public function enqueue() : void { $accent = $this->accent(); if ( $this->isBlockEditor() ) { if ( $this->settings->get( 'admin.theme.block_editor', true ) && file_exists( FEATHERDASH_DIR . 'dist/editor.css' ) ) { wp_enqueue_style( 'featherdash-editor', FEATHERDASH_URL . 'dist/editor.css', [], $this->version( 'dist/editor.css' ) ); wp_add_inline_style( 'featherdash-editor', $this->editorInlineStyle( $accent ) ); } return; } // Flags the page load in , before the body paints, so the accent // progress bar (admin-bar.css) starts at once. if ( $this->settings->get( 'admin.theme.loading_bar', true ) ) { wp_register_script( 'featherdash-loading', false, [], FEATHERDASH_VERSION, false ); wp_enqueue_script( 'featherdash-loading' ); wp_add_inline_script( 'featherdash-loading', 'document.documentElement.classList.add("fd-loading");' ); } if ( file_exists( FEATHERDASH_DIR . 'dist/admin.css' ) ) { wp_enqueue_style( 'featherdash-admin', FEATHERDASH_URL . 'dist/admin.css', self::STYLE_DEPS, $this->version( 'dist/admin.css' ) ); $rgb = str_replace( ' ', '', $this->hexToRgb( $accent ) ?? '15,192,192' ); wp_add_inline_style( 'featherdash-admin', 'body.fd-theme{--fd-accent:' . $accent . ';--fd-accent-rgb:' . $rgb . '}' ); // Plugins screen: the Active / Inactive pills are CSS ::after // content, so their (translated) labels come in as variables. $screen = get_current_screen(); if ( $screen !== null && in_array( $screen->id, ['plugins', 'plugins-network'], true ) ) { $label = static fn( string $text ): string => (string) wp_json_encode( $text, JSON_HEX_TAG | JSON_UNESCAPED_UNICODE ); wp_add_inline_style( 'featherdash-admin', 'body.fd-theme{--fd-l-active:' . $label( __( 'Active', 'featherdash' ) ) . ';--fd-l-inactive:' . $label( __( 'Inactive', 'featherdash' ) ) . '}' ); } } if ( file_exists( FEATHERDASH_DIR . 'dist/admin.js' ) ) { wp_enqueue_script( 'featherdash-admin', FEATHERDASH_URL . 'dist/admin.js', [], $this->version( 'dist/admin.js' ), [ 'in_footer' => true, 'strategy' => 'defer', ] ); wp_add_inline_script( 'featherdash-admin', 'window.featherdash = ' . wp_json_encode( [ 'accordion' => (bool) $this->settings->get( 'admin.theme.accordion', true ), 'loadingBar' => (bool) $this->settings->get( 'admin.theme.loading_bar', true ), 'toasts' => (bool) $this->settings->get( 'cleanup.toasts', true ), 'i18n' => [ 'moreActions' => __( 'Actions', 'featherdash' ), 'close' => __( 'Close', 'featherdash' ), 'dismiss' => __( 'Dismiss this notice.', 'featherdash' ), ], ] ) . ';', 'before' ); } } /** * @param string $tag * @param string $handle * @param string $src */ public function moduleTag( $tag, $handle, $src ) : string { $modules = ['featherdash-admin']; if ( !in_array( $handle, $modules, true ) ) { return $tag; } $tag = preg_replace( '/\\stype=([\'"])[^\'"]*\\1/', '', $tag ) ?? $tag; return str_replace( '