diff --git a/addons/members-admin-menus/addon.php b/addons/members-admin-menus/addon.php new file mode 100644 index 0000000..4377bea --- /dev/null +++ b/addons/members-admin-menus/addon.php @@ -0,0 +1,14 @@ +$key = $args[ $key ]; + } + } + } +} diff --git a/addons/members-admin-menus/app/defaults.php b/addons/members-admin-menus/app/defaults.php new file mode 100644 index 0000000..5013e21 --- /dev/null +++ b/addons/members-admin-menus/app/defaults.php @@ -0,0 +1,55 @@ + array( + 'version' => SETTINGS_SCHEMA_VERSION, + 'admin_editable' => false, + 'admin_menu_exempt_user_ids' => array(), + ), + 'roles' => array(), + 'users' => array(), + 'custom_items' => array(), + 'capabilities' => array(), + '_defaults' => array( + 'captured' => false, + ), + ); +} diff --git a/addons/members-admin-menus/app/functions-admin.php b/addons/members-admin-menus/app/functions-admin.php new file mode 100644 index 0000000..06d8295 --- /dev/null +++ b/addons/members-admin-menus/app/functions-admin.php @@ -0,0 +1,1585 @@ + 200000 ) { + return ''; + } + if ( ! preg_match( '/^data:image\/(png|jpeg|jpg|gif|webp);base64,[A-Za-z0-9+\/=]+$/i', $raw ) ) { + return ''; + } + return $raw; + } + $url = esc_url_raw( $raw ); + if ( $url && preg_match( '#^https?://#i', $url ) ) { + return $url; + } + if ( 0 === strpos( $raw, '//' ) ) { + $url = esc_url_raw( 'https:' . $raw ); + if ( $url && preg_match( '#^https://#i', $url ) ) { + return $url; + } + } + return ''; +} + +/** + * Normalize icon_type + icon for persistence (matches Admin Menus JS validation). + * + * @param string $icon_type Stored type (dashicon, fontawesome, image, svg, custom, …). + * @param string $icon Raw icon string. + * @return array{icon_type:string,icon:string} + */ +function members_am_sanitize_stored_icon( $icon_type, $icon ) { + $icon_type = sanitize_key( $icon_type ); + $icon = is_string( $icon ) ? $icon : ''; + + if ( '' === trim( $icon ) ) { + return array( + 'icon_type' => 'dashicon', + 'icon' => '', + ); + } + + if ( preg_match( '/^(https?:)?\/\//i', $icon ) || 0 === strpos( $icon, 'data:image/' ) ) { + $img = members_am_sanitize_icon_image_value( $icon ); + return array( + 'icon_type' => $img ? 'image' : 'dashicon', + 'icon' => $img, + ); + } + + if ( 'fontawesome' === $icon_type || false !== strpos( $icon, 'fa-' ) || preg_match( '/^(fa|fas|far|fab|fal)\s/i', $icon ) ) { + $fa = members_am_sanitize_fa_icon_classes( $icon ); + return array( + 'icon_type' => $fa ? 'fontawesome' : 'dashicon', + 'icon' => $fa, + ); + } + + $d = members_am_sanitize_dashicon_class( $icon ); + return array( + 'icon_type' => 'dashicon', + 'icon' => $d, + ); +} + +/** + * Ensure associative-array keys are stdClass objects so json_encode + * produces {} instead of [] for empty collections. + * + * @param array $settings Settings array. + * @return array + */ +function ensure_objects_for_js( $settings ) { + $object_keys = array( 'roles', 'users', 'capabilities' ); + foreach ( $object_keys as $key ) { + if ( ! isset( $settings[ $key ] ) || ! is_array( $settings[ $key ] ) ) { + $settings[ $key ] = new \stdClass(); + } elseif ( empty( $settings[ $key ] ) ) { + $settings[ $key ] = new \stdClass(); + } else { + foreach ( $settings[ $key ] as $id => $cfg ) { + if ( ! is_array( $cfg ) ) { + continue; + } + foreach ( array( 'submenu_order', 'overrides' ) as $sub ) { + if ( isset( $cfg[ $sub ] ) && is_array( $cfg[ $sub ] ) && empty( $cfg[ $sub ] ) ) { + $settings[ $key ][ $id ][ $sub ] = new \stdClass(); + } elseif ( isset( $cfg[ $sub ] ) && is_array( $cfg[ $sub ] ) ) { + $settings[ $key ][ $id ][ $sub ] = (object) $cfg[ $sub ]; + } + } + if ( isset( $cfg['capabilities'] ) && is_array( $cfg['capabilities'] ) ) { + $settings[ $key ][ $id ]['capabilities'] = empty( $cfg['capabilities'] ) ? new \stdClass() : (object) $cfg['capabilities']; + } + } + $settings[ $key ] = (object) $settings[ $key ]; + } + } + if ( isset( $settings['_meta'] ) && is_array( $settings['_meta'] ) ) { + $settings['_meta'] = (object) $settings['_meta']; + } + return $settings; +} + +/** + * WCAG relative luminance for a 6-digit hex color (after {@see sanitize_hex_color()}). + * + * @param string $hex Hex color, may include leading #. + * @return float Value in 0–1. + */ +function members_am_relative_luminance( $hex ) { + $hex = ltrim( $hex, '#' ); + if ( 3 === strlen( $hex ) ) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + if ( 6 !== strlen( $hex ) || ! ctype_xdigit( $hex ) ) { + return 0.5; + } + + $r = hexdec( substr( $hex, 0, 2 ) ) / 255; + $g = hexdec( substr( $hex, 2, 2 ) ) / 255; + $b = hexdec( substr( $hex, 4, 2 ) ) / 255; + + $to_linear = static function ( $c ) { + return $c <= 0.03928 ? $c / 12.92 : pow( ( $c + 0.055 ) / 1.055, 2.4 ); + }; + + $r = $to_linear( $r ); + $g = $to_linear( $g ); + $b = $to_linear( $b ); + + return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b; +} + +/** + * Primary text color on a solid background (e.g. Light scheme uses dark text on pale base). + * + * @param string $bg_hex Background hex. + * @return string Hex foreground. + */ +function members_am_contrast_fg_for_bg( $bg_hex ) { + return members_am_relative_luminance( $bg_hex ) > 0.45 ? '#1d2327' : '#f0f0f1'; +} + +/** + * Secondary/muted text on a solid background. + * + * @param string $bg_hex Background hex. + * @return string Hex foreground. + */ +function members_am_contrast_muted_for_bg( $bg_hex ) { + return members_am_relative_luminance( $bg_hex ) > 0.45 ? '#646970' : '#a7aaad'; +} + +/** + * Border color that reads on a given background (admin-style neutrals). + * + * @param string $bg_hex Background hex. + * @return string Hex border. + */ +function members_am_border_for_bg( $bg_hex ) { + return members_am_relative_luminance( $bg_hex ) > 0.45 ? '#c3c4c7' : '#50575e'; +} + +/** + * Inline CSS custom properties for the Admin Menus UI, derived from the active admin color scheme. + * + * Maps {@see wp_admin_css_color()} palette entries to semantic variables. Three-color schemes (e.g. Modern) + * use the second and third swatches as accents; four-color schemes use the full base/surface/accent layout. + * + * @return string Safe CSS (no user input; hex values sanitized). + */ +function get_admin_menus_color_scheme_css() { + global $_wp_admin_css_colors; + + $scheme = get_user_option( 'admin_color' ); + if ( empty( $scheme ) || empty( $_wp_admin_css_colors[ $scheme ] ) ) { + $scheme = 'fresh'; + } + + $raw = isset( $_wp_admin_css_colors[ $scheme ]->colors ) ? (array) $_wp_admin_css_colors[ $scheme ]->colors : array(); + $colors = array(); + foreach ( $raw as $hex ) { + if ( ! is_string( $hex ) ) { + continue; + } + $sanitized = sanitize_hex_color( $hex ); + if ( $sanitized ) { + $colors[] = $sanitized; + } + } + + $n = count( $colors ); + + $base = $colors[0] ?? '#1d2327'; + $surface = null; + $accent = '#2271b1'; + $accent_alt = '#72aee6'; + + if ( $n >= 4 ) { + $surface = $colors[1]; + $accent = $colors[2]; + $accent_alt = $colors[3]; + } elseif ( 3 === $n ) { + $accent = $colors[1]; + $accent_alt = $colors[2]; + } elseif ( 2 === $n ) { + $accent = $colors[1]; + $accent_alt = $colors[1]; + } + + $fg_base = members_am_contrast_fg_for_bg( $base ); + $fg_muted = members_am_contrast_muted_for_bg( $base ); + $border_bg = members_am_border_for_bg( $base ); + + $props = array( + '--members-am-base' => $base, + '--members-am-accent' => $accent, + '--members-am-accent-alt' => $accent_alt, + '--members-am-fg-on-base' => $fg_base, + '--members-am-fg-muted-on-base' => $fg_muted, + '--members-am-border-on-base' => $border_bg, + ); + + if ( $surface ) { + $props['--members-am-surface'] = $surface; + $props['--members-am-fg-on-surface'] = members_am_contrast_fg_for_bg( $surface ); + $props['--members-am-border-on-surface'] = members_am_border_for_bg( $surface ); + } + + $decl = ''; + foreach ( $props as $name => $value ) { + $decl .= $name . ':' . $value . ';'; + } + + return '.members-admin-menus-wrap{' . $decl . '}'; +} + +/** + * Collect unique capability strings referenced by the menu tree (for Admin Menus UI checks). + * + * @param array $tree Menu tree from build_menu_tree_for_js(). + * @return string[] + */ +function collect_capability_names_from_menu_tree( $tree ) { + $out = array(); + if ( ! is_array( $tree ) ) { + return $out; + } + foreach ( $tree as $node ) { + if ( ! empty( $node['cap'] ) && is_string( $node['cap'] ) ) { + $c = sanitize_key( $node['cap'] ); + if ( $c ) { + $out[] = $c; + } + } + if ( ! empty( $node['children'] ) && is_array( $node['children'] ) ) { + foreach ( $node['children'] as $child ) { + if ( ! empty( $child['cap'] ) && is_string( $child['cap'] ) ) { + $c = sanitize_key( $child['cap'] ); + if ( $c ) { + $out[] = $c; + } + } + } + } + } + return array_values( array_unique( $out ) ); +} + +/** + * Merge per-item capability overrides from saved settings into the capability list. + * + * @param string[] $caps Base capability names. + * @param array $settings Admin Menus settings. + * @return string[] + */ +function merge_menu_capabilities_from_settings( array $caps, $settings ) { + if ( empty( $settings['capabilities'] ) || ! is_array( $settings['capabilities'] ) ) { + return $caps; + } + foreach ( $settings['capabilities'] as $slug => $cap ) { + if ( is_string( $cap ) && '' !== trim( $cap ) ) { + $c = sanitize_key( preg_replace( '/\s.*/', '', trim( $cap ) ) ); + if ( $c ) { + $caps[] = $c; + } + } + } + return array_values( array_unique( $caps ) ); +} + +/** + * Build an allcaps-style map from a role, then apply the same core {@see 'user_has_cap'} + * grants WordPress registers in {@see wp-includes/default-filters.php}: + * {@see wp_maybe_grant_install_languages_cap()}, {@see wp_maybe_grant_resume_extensions_caps()}, + * {@see wp_maybe_grant_site_health_caps()}. + * + * {@see WP_Role::has_cap()} does not run those callbacks, so the Admin Menus matrix uses this + * merged map (e.g. Site Health, resume plugins/themes, install languages). + * + * @param \WP_Role $role Role object. + * @param \WP_User $pseudo_user User stub for {@see wp_maybe_grant_site_health_caps()} (ID 0: + * single-site grants match install_plugins; multisite skips super-admin-only grant). + * @return array + */ +function role_matrix_allcaps_with_core_runtime_grants( $role, $pseudo_user ) { + if ( ! $role instanceof \WP_Role || ! $pseudo_user instanceof \WP_User ) { + return array(); + } + $allcaps = array(); + foreach ( $role->capabilities as $cap => $grant ) { + if ( $grant ) { + $allcaps[ $cap ] = true; + } + } + $allcaps = \wp_maybe_grant_install_languages_cap( $allcaps ); + $allcaps = \wp_maybe_grant_resume_extensions_caps( $allcaps ); + $allcaps = \wp_maybe_grant_site_health_caps( $allcaps, array(), array(), $pseudo_user ); + return $allcaps; +} + +/** + * For each role, whether the capability applies for UI previews (stored caps + core runtime grants). + * + * Uses the same resolution path as {@see WP_User::has_cap()} ({@see map_meta_cap()} plus the + * `user_has_cap` filter) so menu meta capabilities match real access. {@see WP_Role::has_cap()} + * alone misses core mappings (e.g. GDPR tools: `export_others_personal_data` → `manage_options`), + * which incorrectly showed the “no access” lock for valid roles. + * + * @param string[] $caps Capability names. + * @return array> + */ +function build_role_cap_matrix_for_js( array $caps ) { + $matrix = array(); + + $pseudo_for_grants = new \WP_User(); + $pseudo_for_grants->ID = 0; + + $matrix_user = new \WP_User(); + $matrix_user->ID = 0; + + foreach ( \members_get_roles() as $role_obj ) { + $slug = $role_obj->name; + $wp_role = \get_role( $slug ); + if ( ! $wp_role ) { + $matrix[ $slug ] = array(); + continue; + } + $matrix_user->allcaps = role_matrix_allcaps_with_core_runtime_grants( $wp_role, $pseudo_for_grants ); + $row = array(); + foreach ( $caps as $cap ) { + if ( ! is_string( $cap ) || '' === $cap ) { + continue; + } + $row[ $cap ] = $matrix_user->has_cap( $cap ); + } + $matrix[ $slug ] = $row; + } + return $matrix; +} + +/** + * Deep copy of `$menu` / `$submenu` so nothing later in the request mutates the snapshot. + * + * @param mixed $data Menu branch or leaf. + * @return mixed + */ +function members_am_deep_copy_menu_structure( $data ) { + if ( is_array( $data ) ) { + $out = array(); + foreach ( $data as $key => $value ) { + $out[ $key ] = members_am_deep_copy_menu_structure( $value ); + } + return $out; + } + return $data; +} + +/** + * Enqueue scripts and styles for the Admin Menus page. + * + * @return void + */ +function enqueue_admin_menus_assets() { + wp_enqueue_media(); + wp_enqueue_style( 'members-admin' ); + wp_add_inline_style( 'members-admin', get_admin_menus_color_scheme_css() ); + wp_enqueue_style( 'wp-color-picker' ); + wp_enqueue_style( + 'members-admin-menus-fa', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/' . FONT_AWESOME_CDN_VERSION . '/css/all.min.css', + array(), + FONT_AWESOME_CDN_VERSION + ); + wp_enqueue_script( 'members-admin-menus' ); +} + +/** + * Capture the admin menu and attach data to members-admin-menus (footer script). + * + * Runs on {@see 'admin_head'} at priority 99999 so `$menu` / `$submenu` reflect load-* and + * typical admin_head adjustments, immediately before `menu-header.php` renders the sidebar. + * The script is footer-deferred; {@see wp_localize_script()} is still valid here. + * + * @return void + */ +function localize_admin_menus_script() { + capture_menu_snapshot(); + + $settings = get_settings(); + $tree = build_menu_tree_for_js(); + + // Do not persist on GET: live menuTree is localized below; baseline snapshot is saved on explicit Save only. + + $roles = array(); + foreach ( \members_get_roles() as $role_obj ) { + $roles[] = array( + 'slug' => $role_obj->name, + 'label' => $role_obj->label, + ); + } + + $role_caps = array(); + foreach ( \members_get_roles() as $role_obj ) { + $wp_role = \get_role( $role_obj->name ); + if ( $wp_role && ! empty( $wp_role->capabilities ) ) { + $role_caps[ $role_obj->name ] = array_keys( array_filter( $wp_role->capabilities ) ); + } else { + $role_caps[ $role_obj->name ] = array(); + } + } + + $menu_caps = merge_menu_capabilities_from_settings( collect_capability_names_from_menu_tree( $tree ), $settings ); + $role_cap_matrix = build_role_cap_matrix_for_js( $menu_caps ); + + $exempt_ids = array(); + if ( ! empty( $settings['_meta']['admin_menu_exempt_user_ids'] ) && is_array( $settings['_meta']['admin_menu_exempt_user_ids'] ) ) { + $exempt_ids = array_map( 'absint', $settings['_meta']['admin_menu_exempt_user_ids'] ); + } + $current_wp_user = wp_get_current_user(); + $current_is_administrator = $current_wp_user && $current_wp_user->exists() && in_array( 'administrator', (array) $current_wp_user->roles, true ); + $exempt_user_labels = members_am_exempt_administrator_user_labels( $exempt_ids ); + if ( $current_is_administrator && $current_wp_user->ID ) { + $cid = (string) (int) $current_wp_user->ID; + if ( '' === ( $exempt_user_labels[ $cid ] ?? '' ) ) { + $exempt_user_labels[ $cid ] = sprintf( + /* translators: 1: display name, 2: user_login */ + __( '%1$s (%2$s)', 'members' ), + $current_wp_user->display_name ? $current_wp_user->display_name : $current_wp_user->user_login, + $current_wp_user->user_login + ); + } + } + + wp_localize_script( + 'members-admin-menus', + 'membersAdminMenus', + array( + 'menuTree' => $tree, + 'settings' => ensure_objects_for_js( $settings ), + 'roles' => $roles, + 'roleCaps' => $role_caps, + 'roleCapMatrix' => $role_cap_matrix, + 'adminEditable' => ! empty( $settings['_meta']['admin_editable'] ), + 'currentUserId' => get_current_user_id(), + 'currentUserIsAdministrator' => $current_is_administrator, + 'exemptUserLabels' => $exempt_user_labels, + 'nonce' => wp_create_nonce( 'members_admin_menus' ), + // Use a same-origin path so Local/proxy ports are preserved (avoids CORS). + 'ajaxUrl' => admin_url( 'admin-ajax.php', 'relative' ), + 'exportUrl' => add_query_arg( + array( + 'action' => 'members_admin_menus_export', + 'nonce' => wp_create_nonce( 'members_admin_menus' ), + ), + admin_url( 'admin-ajax.php', 'relative' ) + ), + 'i18n' => array( + 'save' => __( 'Save changes', 'members' ), + 'reset' => __( 'Reset', 'members' ), + 'resetSettingsLabel' => __( 'Reset Settings', 'members' ), + 'resetAdministrator' => __( 'Reset Administrator', 'members' ), + 'resetAdministratorHelp' => __( 'Clear all menu settings for the Administrator role only.', 'members' ), + 'resetAllRolesHelp' => __( 'Clear all menu settings for every role.', 'members' ), + 'confirmResetAdministrator' => __( 'Reset all menu settings for the Administrator role? This cannot be undone.', 'members' ), + 'confirmResetAllRoles' => __( 'Reset ALL menu settings for every role? This cannot be undone.', 'members' ), + 'confirmResetRole' => __( 'Reset all settings for this role? This cannot be undone.', 'members' ), + 'resetAll' => __( 'Reset all roles', 'members' ), + 'resetRole' => __( 'Reset this role', 'members' ), + 'addItem' => __( 'Add custom item', 'members' ), + 'copyRole' => __( 'Copy from role', 'members' ), + 'import' => __( 'Import', 'members' ), + 'export' => __( 'Export', 'members' ), + 'adminEditable' => __( 'Allow editing administrator menus', 'members' ), + 'adminEditableWarn' => __( 'This can lock administrators out of menus. Continue?', 'members' ), + 'saved' => __( 'Settings saved.', 'members' ), + 'invalidJson' => __( 'Invalid JSON.', 'members' ), + 'resetComplete' => __( 'Reset complete.', 'members' ), + 'imported' => __( 'Settings imported.', 'members' ), + 'resetFailed' => __( 'Reset failed.', 'members' ), + 'rolesMustDiffer' => __( 'Source and target roles must be different.', 'members' ), + 'resetNetworkError' => __( 'Could not reset settings. Check your connection and try again.', 'members' ), + 'importNetworkError' => __( 'Could not import settings. Check your connection and try again.', 'members' ), + 'readFileFailed' => __( 'Could not read the file.', 'members' ), + 'networkError' => __( 'Could not save settings. Check your connection and try again.', 'members' ), + 'unsavedChanges' => __( 'You have unsaved changes. If you leave this page, those changes will be lost.', 'members' ), + 'visibility' => __( 'Visibility per role', 'members' ), + 'title' => __( 'Title', 'members' ), + 'url' => __( 'URL', 'members' ), + 'selectRole' => __( 'Select source role', 'members' ), + 'of' => __( 'of', 'members' ), + 'selectParentMenu' => __( 'Select parent menu…', 'members' ), + 'selectParentFirst' => __( 'Please choose a parent menu from the list.', 'members' ), + 'saving' => __( 'Saving…', 'members' ), + 'copying' => __( 'Copying…', 'members' ), + 'resetting' => __( 'Resetting…', 'members' ), + 'importing' => __( 'Importing…', 'members' ), + 'filterItems' => __( 'Filter items…', 'members' ), + 'filterItemsLabel' => __( 'Filter menu items in this column', 'members' ), + 'bulkVisibilityLabel' => __( 'Menu visibility for this column', 'members' ), + 'bulkActionsPlaceholder' => __( 'Choose visibility…', 'members' ), + 'bulkGroupWholeColumn' => __( 'Whole column', 'members' ), + 'bulkGroupCheckedRows' => __( 'Checked rows', 'members' ), + 'bulkShowAllItems' => __( 'Show every menu item', 'members' ), + 'bulkHideAllItems' => __( 'Hide every menu item', 'members' ), + 'bulkKeepOnlyCheckedVisible' => __( 'Hide everything except selected (and parents)', 'members' ), + 'bulkHideCheckedItems' => __( 'Hide checked items', 'members' ), + 'bulkShowCheckedItems' => __( 'Show selected items', 'members' ), + 'showInMenu' => __( 'Show in menu', 'members' ), + 'hideFromMenu' => __( 'Hide from menu', 'members' ), + 'bulkSelectVisible' => __( 'Select visible', 'members' ), + 'bulkClearSelection' => __( 'Clear selection', 'members' ), + 'bulkCheckboxAria' => __( 'Include in bulk actions', 'members' ), + 'bulkSelectCheckedFirst' => __( 'Check one or more menu items first.', 'members' ), + 'bulkSelectItemFirst' => __( 'Select a menu item in the list first.', 'members' ), + 'bulkConfirmHideAll' => __( 'Hide every menu item in this column? You can use “Show every menu item” to undo before saving.', 'members' ), + 'bulkConfirmKeepOnlyChecked' => __( 'Hide all menu items except the selected ones and their parent menus?', 'members' ), + 'bulkConfirmHideChecked' => __( 'Hide the checked items (and their submenus where applicable)?', 'members' ), + 'collapseSubmenus' => __( 'Collapse submenu items', 'members' ), + 'expandSubmenus' => __( 'Expand submenu items', 'members' ), + 'collapseAllMenus' => __( 'Collapse submenus', 'members' ), + 'expandAllMenus' => __( 'Expand submenus', 'members' ), + 'undo' => __( 'Undo last change', 'members' ), + 'undoRestored' => __( 'Last change reverted.', 'members' ), + 'bulkVisibilityHint' => __( 'For bulk visibility (whole column or checked rows), use the tools above each role column.', 'members' ), + 'filterRolesVisibility' => __( 'Filter roles…', 'members' ), + 'filterRolesVisibilityLabel' => __( 'Filter roles in this list', 'members' ), + 'moreToolsShowAria' => __( 'Show additional tools', 'members' ), + 'moreToolsHideAria' => __( 'Hide additional tools', 'members' ), + 'moreToolsPanelHint' => __( 'Administrator editing, copy between roles, exempt administrators, and import/export.', 'members' ), + 'searchUsersToOverride' => __( 'Search users to override…', 'members' ), + 'rowBadgeHidden' => __( 'HIDDEN', 'members' ), + 'rowBadgeHiddenDetail' => __( 'Item manually hidden for this role.', 'members' ), + 'rowBadgeNoAccess' => __( 'NO ACCESS', 'members' ), + 'noAccessTitlePattern' => __( 'This role does not have the stored capability “%s”. Users with multiple roles may still reach the screen if another role grants it. Tags use manage_post_tags when Category & Tag Caps is active (Members → Roles, Taxonomy).', 'members' ), + 'multiRoleMergeHelp' => __( 'Users with multiple roles: a menu item is hidden if any of their roles hides it. When two roles define different labels, icons, or colors for the same item, the first role in the user’s role list wins.', 'members' ), + 'exemptLastAdministrator' => __( 'Keep at least one exempt administrator while this option is enabled.', 'members' ), + 'exemptRemove' => __( 'Remove', 'members' ), + 'exemptSaveRequiresAdministrator' => __( 'When administrator menu editing is enabled, at least one exempt administrator is required. Sign in as an administrator or add one using the search field.', 'members' ), + 'columnsAllHidden' => __( 'All role columns are hidden. Use the role chips above to show at least one role.', 'members' ), + 'showRoleColumn' => __( 'Show role column', 'members' ), + 'hideRoleColumn' => __( 'Hide role column', 'members' ), + 'moveColumnLeft' => __( 'Move column left', 'members' ), + 'moveColumnRight' => __( 'Move column right', 'members' ), + 'closeUserColumn' => __( 'Close user preview column', 'members' ), + 'showAllRoles' => __( 'Show all', 'members' ), + 'hideAllRoles' => __( 'Hide all', 'members' ), + 'copyConfirm' => __( 'Copy menu settings from “%1$s” to “%2$s”? This overwrites the target role’s configuration.', 'members' ), + 'copyConfirmYes' => __( 'Confirm copy', 'members' ), + 'copyConfirmNo' => __( 'Cancel', 'members' ), + 'addItemModalTitle' => __( 'Add custom menu item', 'members' ), + 'addItemModalIntro' => __( 'Add a link to the admin menu. Nothing is saved until you click “Save changes”.', 'members' ), + 'addItemSubmit' => __( 'Add to menu', 'members' ), + 'addItemCancel' => __( 'Cancel', 'members' ), + 'positionTopEnd' => __( 'Top of menu', 'members' ), + 'positionTopStart' => __( 'Bottom of menu', 'members' ), + 'positionSubmenuOf' => __( 'Submenu of…', 'members' ), + 'applyToAllRoles' => __( 'All roles', 'members' ), + 'applyToLabel' => __( 'Apply to:', 'members' ), + 'customTitle' => __( 'Custom title', 'members' ), + 'urlOverride' => __( 'URL override', 'members' ), + 'urlDefaultPlaceholder' => __( 'Default', 'members' ), + 'sectionIcon' => __( 'Icon', 'members' ), + 'iconPickerShow' => __( 'Browse icons…', 'members' ), + 'iconPickerHide' => __( 'Hide icon options', 'members' ), + 'iconSummaryDefault' => __( 'No custom icon; the menu default is used.', 'members' ), + 'iconSummaryDashicon' => __( 'Dashicon: %s', 'members' ), + 'iconSummaryFontAwesome' => __( 'Font Awesome: %s', 'members' ), + 'iconSummaryImage' => __( 'Custom image: %s', 'members' ), + 'sectionBadge' => __( 'Badge', 'members' ), + 'sectionColors' => __( 'Colors', 'members' ), + 'sectionVisibility' => __( 'Visibility per role', 'members' ), + 'visibilitySummaryAllVisible' => __( 'All listed roles show this item.', 'members' ), + 'visibilitySummaryNoneVisible' => __( 'Hidden for all listed roles.', 'members' ), + 'visibilitySummaryPartial' => __( '%1$d of %2$d roles show this item.', 'members' ), + 'selectParentMenuButton' => __( 'Select parent menu', 'members' ), + 'removeMenuItem' => __( 'Remove', 'members' ), + 'badgePreviewLabel' => __( 'Preview', 'members' ), + 'badgeTextFieldLabel' => __( 'Badge text', 'members' ), + 'badgeColorFieldLabel' => __( 'Badge color', 'members' ), + 'colorsApplyFooterNote' => __( 'Colors apply to the “Apply to” target. Each role column shows its own overrides.', 'members' ), + 'popoverCloseAria' => __( 'Close advanced menu', 'members' ), + 'labelUrlSection' => __( 'Label & URL', 'members' ), + 'editPopoverDone' => __( 'Close', 'members' ), + ), + ) + ); +} + +/** + * Render the standalone Admin Menus page. + * + * @return void + */ +function render_admin_menus_page() { + ?> +
+

+
+
+
+
+ + + + +
+ + + + +
+ + + +
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + + + +
+ array(), + 'submenu' => array(), + ); + return; + } + $GLOBALS['members_admin_menus_snapshot'] = array( + 'menu' => members_am_deep_copy_menu_structure( $menu ), + 'submenu' => members_am_deep_copy_menu_structure( $submenu ), + ); +} + +/** + * Convert a menu slug to a full admin URL. + * + * @param string $slug Menu slug. + * @return string + */ +function slug_to_admin_url( $slug ) { + if ( 0 === strpos( $slug, 'http' ) || 0 === strpos( $slug, '//' ) ) { + return $slug; + } + if ( false !== strpos( $slug, '.php' ) ) { + return admin_url( $slug ); + } + return admin_url( 'admin.php?page=' . $slug ); +} + +/** + * Build hierarchical menu tree for JS (run once snapshot captured). + * + * @return array + */ +function build_menu_tree_for_js() { + global $members_admin_menus_snapshot; + if ( empty( $members_admin_menus_snapshot['menu'] ) || ! is_array( $members_admin_menus_snapshot['menu'] ) ) { + return array(); + } + // Sort by numeric key to match WordPress's rendering order (ksort). + $sorted_menu = $members_admin_menus_snapshot['menu']; + ksort( $sorted_menu ); + $tree = array(); + foreach ( $sorted_menu as $key => $item ) { + if ( ! isset( $item[2] ) ) { + continue; + } + $slug = $item[2]; + // Core still registers the legacy Links (blogroll) menu. includes/menu.php may re-parent + // $item[2] to the first submenu slug (e.g. link categories) while $item[5] stays `menu-links`. + // When link_manager_enabled is off, manage_links maps to do_not_allow — the real sidebar + // drops this tree; skip it here so Admin Menus matches. + $is_links_top = ( isset( $item[5] ) && 'menu-links' === $item[5] ) || 'link-manager.php' === $slug; + if ( $is_links_top && ! get_option( 'link_manager_enabled' ) ) { + continue; + } + if ( false !== strpos( $slug, 'separator' ) || false !== strpos( $slug, 'wp-menu-separator' ) ) { + continue; + } + $raw = isset( $item[0] ) ? $item[0] : $slug; + $title = trim( wp_strip_all_tags( preg_replace( '/ ?]*class="[^"]*(?:update-plugins|awaiting-mod|pending-count|count-\d|menu-counter)[^"]*"[^>]*>.*<\/span>/si', '', $raw ) ) ); + // Skip top-level items with empty titles (hidden/internal pages). + if ( '' === $title ) { + continue; + } + $icon = isset( $item[6] ) ? $item[6] : ''; + $icon_type = 'dashicon'; + if ( '' === $icon || 'none' === $icon || 'div' === $icon ) { + $icon_type = 'none'; + $icon = ''; + } elseif ( 0 === strpos( $icon, 'data:image/' ) ) { + $icon_type = 'svg'; + } elseif ( 0 === strpos( $icon, 'http' ) || 0 === strpos( $icon, '//' ) ) { + $icon_type = 'image'; + } + $node = array( + 'id' => $slug, + 'title' => $title, + 'url' => slug_to_admin_url( $slug ), + 'icon' => $icon, + 'icon_type' => $icon_type, + 'type' => 'top', + 'cap' => isset( $item[1] ) ? $item[1] : 'read', + 'children' => array(), + ); + if ( ! empty( $members_admin_menus_snapshot['submenu'][ $slug ] ) ) { + $sorted_subs = $members_admin_menus_snapshot['submenu'][ $slug ]; + ksort( $sorted_subs ); + $seen_subslugs = array(); + foreach ( $sorted_subs as $subitem ) { + if ( empty( $subitem[2] ) ) { + continue; + } + $subslug = $subitem[2]; + // Skip duplicate submenu slugs (some plugins register multiple entries for the same slug). + if ( isset( $seen_subslugs[ $subslug ] ) ) { + continue; + } + $seen_subslugs[ $subslug ] = true; + // Compute the clean title. + $sub_raw = isset( $subitem[0] ) ? $subitem[0] : ''; + $sub_title = trim( wp_strip_all_tags( preg_replace( '/ ?]*class="[^"]*(?:update-plugins|awaiting-mod|pending-count|count-\d|menu-counter)[^"]*"[^>]*>.*<\/span>/si', '', $sub_raw ) ) ); + // Skip items with empty titles — these are hidden/internal pages registered by plugins. + if ( '' === $sub_title ) { + continue; + } + $cid = $slug . '::' . $subslug; + $node['children'][] = array( + 'id' => $cid, + 'title' => $sub_title, + 'url' => slug_to_admin_url( $subslug ), + 'type' => 'sub', + 'cap' => isset( $subitem[1] ) ? $subitem[1] : 'read', + ); + } + } + $tree[] = $node; + } + return $tree; +} + +/** + * Decode JSON settings payload with size and depth limits. + * + * @param mixed $raw Raw POST value. + * @param string $too_large_message Message when payload exceeds byte limit. + * @return array|\WP_Error Decoded array or error. + */ +function members_am_decode_settings_json( $raw, $too_large_message ) { + if ( ! is_string( $raw ) ) { + return new \WP_Error( 'members_am_invalid_json', __( 'Invalid data.', 'members' ) ); + } + if ( strlen( $raw ) > SETTINGS_JSON_MAX_BYTES ) { + return new \WP_Error( 'members_am_payload_too_large', $too_large_message ); + } + $data = json_decode( $raw, true, SETTINGS_JSON_MAX_DEPTH ); + if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) { + return new \WP_Error( 'members_am_json_error', __( 'Invalid JSON.', 'members' ) ); + } + return $data; +} + +/** + * Normalize stored exempt administrator user IDs when "Allow editing administrator menus" is enabled. + * + * @param mixed $raw_ids Client-supplied list (may be non-array). + * @param bool $admin_editable Whether administrator menu editing is enabled. + * @return int[] Unique administrator user IDs (empty when $admin_editable is false). + */ +function members_am_normalize_administrator_exempt_user_ids( $raw_ids, $admin_editable ) { + if ( ! $admin_editable ) { + return array(); + } + $out = array(); + if ( is_array( $raw_ids ) ) { + foreach ( $raw_ids as $id ) { + $id = absint( $id ); + if ( $id < 1 || count( $out ) >= ADMIN_MENU_EXEMPT_USER_IDS_MAX ) { + continue; + } + $user = get_userdata( $id ); + if ( ! $user || ! in_array( 'administrator', (array) $user->roles, true ) ) { + continue; + } + $out[] = $id; + } + $out = array_values( array_unique( $out ) ); + } + if ( empty( $out ) ) { + $uid = get_current_user_id(); + $user = $uid ? get_userdata( $uid ) : false; + if ( $user && in_array( 'administrator', (array) $user->roles, true ) ) { + return array( $uid ); + } + } + return $out; +} + +/** + * Display labels for exempt administrator IDs (localized script data). + * + * @param int[] $ids User IDs. + * @return array Map of string user ID to label. + */ +function members_am_exempt_administrator_user_labels( array $ids ) { + $labels = array(); + foreach ( $ids as $id ) { + $id = absint( $id ); + if ( $id < 1 ) { + continue; + } + $user = get_userdata( $id ); + if ( ! $user || ! in_array( 'administrator', (array) $user->roles, true ) ) { + continue; + } + $labels[ (string) $id ] = sprintf( + /* translators: 1: display name, 2: user_login */ + __( '%1$s (%2$s)', 'members' ), + $user->display_name ? $user->display_name : $user->user_login, + $user->user_login + ); + } + return $labels; +} + +/** + * AJAX: save full settings JSON. + * + * @return void + */ +function ajax_save_settings() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( get_members_settings_capability() ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; + $data = members_am_decode_settings_json( + $raw, + __( 'Settings payload is too large.', 'members' ) + ); + if ( is_wp_error( $data ) ) { + wp_send_json_error( array( 'message' => $data->get_error_message() ), 400 ); + } + $sanitized = sanitize_settings_payload( $data ); + if ( ! empty( $sanitized['_meta']['admin_editable'] ) ) { + $exempt = isset( $sanitized['_meta']['admin_menu_exempt_user_ids'] ) && is_array( $sanitized['_meta']['admin_menu_exempt_user_ids'] ) + ? $sanitized['_meta']['admin_menu_exempt_user_ids'] + : array(); + if ( empty( $exempt ) ) { + wp_send_json_error( + array( + 'message' => __( 'When administrator menu editing is enabled, at least one exempt administrator is required. Sign in as an administrator or add one using the search field.', 'members' ), + ), + 400 + ); + } + } + update_settings_option( $sanitized ); + members_am_invalidate_settings_cache(); + wp_send_json_success( array( 'message' => __( 'Settings saved.', 'members' ) ) ); +} + +/** + * Sanitize settings array from client. + * + * @param array $data Raw data. + * @return array + */ +function sanitize_settings_payload( $data ) { + if ( ! is_array( $data ) ) { + return get_default_settings(); + } + + $defaults = get_default_settings(); + $allowed = array( '_meta', 'roles', 'users', 'custom_items', 'capabilities', '_defaults' ); + $filtered = array(); + foreach ( $allowed as $key ) { + if ( array_key_exists( $key, $data ) ) { + $filtered[ $key ] = $data[ $key ]; + } + } + + $out = wp_parse_args( $filtered, $defaults ); + + $admin_editable = ! empty( $out['_meta']['admin_editable'] ); + $raw_exempt = array(); + if ( isset( $data['_meta'] ) && is_array( $data['_meta'] ) && isset( $data['_meta']['admin_menu_exempt_user_ids'] ) ) { + $raw_exempt = $data['_meta']['admin_menu_exempt_user_ids']; + } elseif ( isset( $out['_meta']['admin_menu_exempt_user_ids'] ) ) { + $raw_exempt = $out['_meta']['admin_menu_exempt_user_ids']; + } + + $out['_meta'] = array( + 'version' => isset( $out['_meta']['version'] ) ? absint( $out['_meta']['version'] ) : 3, + 'admin_editable' => $admin_editable, + 'admin_menu_exempt_user_ids' => members_am_normalize_administrator_exempt_user_ids( $raw_exempt, $admin_editable ), + ); + + if ( isset( $out['_defaults'] ) && is_array( $out['_defaults'] ) ) { + $d = $out['_defaults']; + $out['_defaults'] = array( + 'captured' => ! empty( $d['captured'] ), + ); + // Never persist imported menu trees (size, shape, and trust boundary). + } + + if ( isset( $out['roles'] ) && is_array( $out['roles'] ) ) { + $roles = array(); + foreach ( $out['roles'] as $role_key => $cfg ) { + $sanitized = sanitize_key( $role_key ); + if ( ! $sanitized ) { + continue; + } + $roles[ $sanitized ] = sanitize_role_config( $cfg ); + } + $out['roles'] = $roles; + } + + if ( isset( $out['users'] ) && is_array( $out['users'] ) ) { + $users = array(); + foreach ( $out['users'] as $uid => $cfg ) { + $uid = absint( $uid ); + if ( $uid < 1 ) { + continue; + } + $users[ $uid ] = sanitize_role_config( $cfg ); + } + $out['users'] = $users; + } + + if ( isset( $out['custom_items'] ) && is_array( $out['custom_items'] ) ) { + $out['custom_items'] = array_map( __NAMESPACE__ . '\sanitize_custom_item', $out['custom_items'] ); + } + + if ( isset( $out['capabilities'] ) && is_array( $out['capabilities'] ) ) { + $caps = array(); + foreach ( $out['capabilities'] as $slug => $cap ) { + $s = sanitize_text_field( $slug ); + if ( ! $s || members_am_is_protected_menu_slug( $s ) ) { + continue; + } + $caps[ $s ] = sanitize_key( $cap ); + } + $out['capabilities'] = $caps; + } + + return $out; +} + +/** + * Sanitize role or user config block. + * + * @param mixed $cfg Config. + * @return array + */ +function sanitize_role_config( $cfg ) { + if ( ! is_array( $cfg ) ) { + return array(); + } + $out = array(); + if ( isset( $cfg['hidden'] ) && is_array( $cfg['hidden'] ) ) { + $out['hidden'] = array(); + foreach ( $cfg['hidden'] as $h ) { + $h = sanitize_text_field( $h ); + if ( ! $h || members_am_is_protected_menu_slug( $h ) ) { + continue; + } + if ( ! in_array( $h, $out['hidden'], true ) ) { + $out['hidden'][] = $h; + } + } + } + if ( isset( $cfg['order'] ) && is_array( $cfg['order'] ) ) { + $out['order'] = array_map( 'sanitize_text_field', $cfg['order'] ); + } + if ( isset( $cfg['submenu_order'] ) && is_array( $cfg['submenu_order'] ) ) { + $so = array(); + foreach ( $cfg['submenu_order'] as $parent => $children ) { + $p = sanitize_text_field( $parent ); + if ( ! $p || ! is_array( $children ) ) { + continue; + } + $so[ $p ] = array_map( 'sanitize_text_field', $children ); + } + $out['submenu_order'] = $so; + } + if ( isset( $cfg['capabilities'] ) && is_array( $cfg['capabilities'] ) ) { + $out['capabilities'] = array(); + foreach ( $cfg['capabilities'] as $slug => $cap ) { + $s = sanitize_text_field( $slug ); + if ( ! $s || members_am_is_protected_menu_slug( $s ) ) { + continue; + } + $out['capabilities'][ $s ] = sanitize_key( $cap ); + } + } + if ( isset( $cfg['overrides'] ) && is_array( $cfg['overrides'] ) ) { + $out['overrides'] = array(); + foreach ( $cfg['overrides'] as $slug => $ov ) { + $s = sanitize_text_field( $slug ); + if ( ! $s || ! is_array( $ov ) ) { + continue; + } + $icon_raw = isset( $ov['icon'] ) ? $ov['icon'] : ''; + $icon_type0 = isset( $ov['icon_type'] ) ? sanitize_key( $ov['icon_type'] ) : ''; + $icon_san = members_am_sanitize_stored_icon( $icon_type0, is_string( $icon_raw ) ? $icon_raw : '' ); + + $entry = array( + 'label' => isset( $ov['label'] ) ? sanitize_text_field( $ov['label'] ) : '', + 'icon_type' => $icon_san['icon_type'], + 'icon' => $icon_san['icon'], + 'url' => isset( $ov['url'] ) ? esc_url_raw( $ov['url'] ) : '', + 'color_bg' => isset( $ov['color_bg'] ) ? sanitize_hex_color( $ov['color_bg'] ) : '', + 'color_text' => isset( $ov['color_text'] ) ? sanitize_hex_color( $ov['color_text'] ) : '', + 'color_icon' => isset( $ov['color_icon'] ) ? sanitize_hex_color( $ov['color_icon'] ) : '', + 'badge' => isset( $ov['badge'] ) ? sanitize_text_field( $ov['badge'] ) : '', + 'badge_bg' => isset( $ov['badge_bg'] ) ? sanitize_hex_color( $ov['badge_bg'] ) : '', + ); + // Only include 'parent' when explicitly set — prevents accidental promotion. + if ( isset( $ov['parent'] ) && '' !== $ov['parent'] ) { + $entry['parent'] = sanitize_text_field( $ov['parent'] ); + } + $out['overrides'][ $s ] = $entry; + } + } + return $out; +} + +/** + * Sanitize one custom menu item. + * + * @param mixed $item Item. + * @return array + */ +function sanitize_custom_item( $item ) { + if ( ! is_array( $item ) ) { + return array(); + } + $icon_san = members_am_sanitize_stored_icon( + isset( $item['icon_type'] ) ? sanitize_key( $item['icon_type'] ) : 'dashicon', + isset( $item['icon'] ) && is_string( $item['icon'] ) ? $item['icon'] : '' + ); + $icon_out = $icon_san['icon']; + if ( 'dashicon' === $icon_san['icon_type'] && '' === $icon_out ) { + $icon_out = 'dashicons-admin-generic'; + } + return array( + 'id' => isset( $item['id'] ) ? sanitize_key( $item['id'] ) : wp_unique_id( 'c' ), + 'label' => isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : '', + 'url' => isset( $item['url'] ) ? esc_url_raw( $item['url'] ) : '', + 'icon_type' => $icon_san['icon_type'], + 'icon' => $icon_out, + 'parent' => isset( $item['parent'] ) ? sanitize_text_field( $item['parent'] ) : '', + 'position' => isset( $item['position'] ) ? absint( $item['position'] ) : 99, + 'cap' => isset( $item['cap'] ) ? sanitize_key( $item['cap'] ) : 'read', + ); +} + +/** + * AJAX reset. + * + * @return void + */ +function ajax_reset_settings() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( get_members_settings_capability() ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : 'all'; + $role = isset( $_POST['role'] ) ? sanitize_key( wp_unslash( $_POST['role'] ) ) : ''; + $settings = get_settings(); + if ( 'role' === $scope && $role ) { + unset( $settings['roles'][ $role ] ); + } else { + $defaults = get_default_settings(); + $settings['roles'] = array(); + $settings['users'] = array(); + $settings['custom_items'] = array(); + $settings['capabilities'] = array(); + $settings['_meta'] = $defaults['_meta']; + } + update_settings_option( $settings ); + members_am_invalidate_settings_cache(); + wp_send_json_success( array( 'message' => __( 'Reset complete.', 'members' ) ) ); +} + +/** + * Export JSON download via AJAX redirect or direct. + * + * @return void + */ +function ajax_export_settings() { + if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['nonce'] ), 'members_admin_menus' ) ) { + wp_die( esc_html__( 'Invalid security token.', 'members' ) ); + } + if ( ! current_user_can( get_members_settings_capability() ) ) { + wp_die( esc_html__( 'Permission denied.', 'members' ) ); + } + $data = get_settings(); + if ( ! headers_sent() ) { + nocache_headers(); + header( 'Content-Type: application/json; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="members-admin-menus-export.json"' ); + } + echo wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ); + exit; +} + +/** + * Import settings from uploaded JSON. + * + * @return void + */ +function ajax_import_settings() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( get_members_settings_capability() ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; + $data = members_am_decode_settings_json( + $raw, + __( 'Import payload is too large.', 'members' ) + ); + if ( is_wp_error( $data ) ) { + wp_send_json_error( array( 'message' => $data->get_error_message() ), 400 ); + } + update_settings_option( sanitize_settings_payload( $data ) ); + members_am_invalidate_settings_cache(); + wp_send_json_success( array( 'message' => __( 'Settings imported.', 'members' ) ) ); +} + +/** + * User search for per-user column (Phase 3 UI). + * + * @return void + */ +function ajax_user_search() { + if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['nonce'] ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( get_members_settings_capability() ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $term = isset( $_GET['term'] ) ? sanitize_text_field( wp_unslash( $_GET['term'] ) ) : ''; + if ( strlen( $term ) < 2 ) { + wp_send_json_success( array() ); + } + // Prefix search (e.g. jo*) is cheaper on large user tables than *jo*. Override via members/addons/admin_menus/user_search_pattern. + $search = apply_filters( app()->namespace . '/user_search_pattern', $term . '*', $term ); + $query = new \WP_User_Query( + array( + 'number' => 20, + 'search' => $search, + 'search_columns' => array( 'user_login', 'user_nicename', 'user_email', 'display_name' ), + 'fields' => 'all', + ) + ); + $out = array(); + foreach ( $query->get_results() as $u ) { + $out[] = array( + 'id' => (int) $u->ID, + 'label' => $u->display_name . ' (' . $u->user_login . ')', + 'roles' => isset( $u->roles ) && is_array( $u->roles ) ? $u->roles : array(), + ); + } + wp_send_json_success( $out ); +} diff --git a/addons/members-admin-menus/app/functions.php b/addons/members-admin-menus/app/functions.php new file mode 100644 index 0000000..5f5b04b --- /dev/null +++ b/addons/members-admin-menus/app/functions.php @@ -0,0 +1,2016 @@ +namespace . '/protected_menu_slugs', $list ); + $list = is_array( $list ) ? array_values( array_filter( array_map( 'strval', $list ) ) ) : array(); + return $list; +} + +/** + * Whether a slug is protected from hidden lists and capability-based menu removal. + * + * @param string $slug Stored menu slug. + * @return bool + */ +function members_am_is_protected_menu_slug( $slug ) { + $slug = sanitize_text_field( (string) $slug ); + if ( '' === $slug ) { + return false; + } + return in_array( $slug, members_am_protected_menu_slugs(), true ); +} + +/** + * Enable custom menu order when we have per-role order stored. + * + * @param mixed $enabled Previous value. + * @return bool + */ +function enable_custom_menu_order( $enabled ) { + if ( ! is_admin() || ! get_current_user_id() ) { + return $enabled; + } + $cfg = get_resolved_config_for_user( get_current_user_id() ); + if ( ! empty( $cfg['order'] ) ) { + return true; + } + return $enabled; +} + +/** + * WordPress core menu_order filter — merge with stored order for current user. + * + * @param array $menu_order Menu slugs in order. + * @return array + */ +function filter_menu_order( $menu_order ) { + if ( ! is_admin() || ! get_current_user_id() ) { + return $menu_order; + } + $cfg = get_resolved_config_for_user( get_current_user_id() ); + if ( empty( $cfg['order'] ) || ! is_array( $cfg['order'] ) ) { + return $menu_order; + } + + // Build the ordered slug list, converting sep-* tokens to actual separator slugs. + $result = array(); + $sep_i = 0; + $has_real = false; + foreach ( $cfg['order'] as $token ) { + $token = (string) $token; + if ( 0 === strpos( $token, 'sep-' ) ) { + $result[] = 'separator-members-am-' . $sep_i; + $sep_i++; + } elseif ( false !== strpos( $token, '::' ) ) { + $parts = explode( '::', $token, 2 ); + $result[] = $parts[1]; + $has_real = true; + } else { + $result[] = $token; + $has_real = true; + } + } + if ( ! $has_real ) { + return $menu_order; + } + + // Append any WP menu items not in our order. + $merged = array_merge( $result, array_diff( $menu_order, $result ) ); + return $merged; +} + +/** + * HTML id for a top-level $menu row (matches #adminmenu #… in the DOM). + * + * @param array $item Menu row from global $menu. + * @return string + */ +function members_am_menu_item_dom_id( $item ) { + if ( ! empty( $item[5] ) ) { + return sanitize_html_class( $item[5] ); + } + if ( empty( $item[2] ) || ! function_exists( 'get_plugin_page_hookname' ) ) { + return ''; + } + $hook = get_plugin_page_hookname( $item[2], '' ); + return $hook ? sanitize_html_class( $hook ) : ''; +} + +/** + * Enqueue Font Awesome 6 on admin pages when any override uses FA icons. + * + * @return void + */ +function maybe_enqueue_fontawesome() { + if ( ! is_admin() || ! get_current_user_id() ) { + return; + } + $user_id = get_current_user_id(); + if ( is_user_exempt( $user_id ) ) { + return; + } + $cfg = get_resolved_config_for_user( $user_id ); + if ( empty( $cfg['overrides'] ) || ! is_array( $cfg['overrides'] ) ) { + return; + } + foreach ( $cfg['overrides'] as $ov ) { + if ( is_array( $ov ) && isset( $ov['icon_type'] ) && 'fontawesome' === $ov['icon_type'] ) { + wp_enqueue_style( + 'members-fontawesome', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/' . FONT_AWESOME_CDN_VERSION . '/css/all.min.css', + array(), + FONT_AWESOME_CDN_VERSION + ); + return; + } + } +} + +/** + * Apply reorder, overrides, hiding, custom items (late). + * + * @return void + */ +function apply_menu_modifications() { + if ( ! is_admin() || ! get_current_user_id() ) { + return; + } + $user_id = get_current_user_id(); + $cfg = get_resolved_config_for_user( $user_id ); + if ( empty( $cfg ) ) { + return; + } + + // Exempt users (e.g. administrators when "Allow editing administrator menus" is unchecked) + // skip most customization so they are not locked out of menus — but "Move to submenu" / + // "Make top-level" must still run or the dashboard sidebar never matches saved settings. + if ( is_user_exempt( $user_id ) ) { + if ( ! empty( $cfg['overrides'] ) && is_array( $cfg['overrides'] ) ) { + apply_level_moves( $cfg['overrides'] ); + } + return; + } + + global $menu, $submenu; + + // Phase 2+: reorder top-level menu array (after menu_order filter runs, still need physical reorder). + if ( ! empty( $cfg['order'] ) && is_array( $menu ) ) { + $menu = reorder_menu_by_slug_list( $menu, $cfg['order'] ); + $menu = inject_separators( $menu, $cfg['order'] ); + } + + // Submenu order per parent. + if ( ! empty( $cfg['submenu_order'] ) && is_array( $submenu ) ) { + foreach ( $cfg['submenu_order'] as $parent => $child_order ) { + if ( ! isset( $submenu[ $parent ] ) || ! is_array( $child_order ) ) { + continue; + } + $submenu[ $parent ] = reorder_submenu_items( $submenu[ $parent ], $child_order ); + } + } + + // Phase 2: structural moves, then labels/icons/URLs, then colors (see order note below). + if ( ! empty( $cfg['overrides'] ) && is_array( $cfg['overrides'] ) ) { + // apply_level_moves() must run before apply_menu_overrides(): for Members custom items, + // apply_menu_overrides() replaces $menu / $submenu [2] with the external URL while overrides + // stay keyed by members-am-*. Demote / relocate logic matches rows by the canonical slug in [2]; + // if the URL is applied first, the row is never found and "Move to submenu" has no effect on $menu. + apply_level_moves( $cfg['overrides'] ); + apply_menu_overrides( $cfg['overrides'] ); + // Run after URL/slug mutations so submenu color selectors can use the final href in $submenu[...][2]. + apply_color_overrides( $cfg['overrides'] ); + } + + // Phase 3: capability-based hiding (independent of role hidden lists). + $cap_map = isset( $cfg['capabilities'] ) ? $cfg['capabilities'] : array(); + if ( ! empty( $cap_map ) && is_array( $cap_map ) ) { + foreach ( $cap_map as $slug => $cap ) { + $slug = sanitize_text_field( $slug ); + $cap = sanitize_key( $cap ); + if ( ! $slug || ! $cap || current_user_can( $cap ) ) { + continue; + } + if ( false !== strpos( $slug, '::' ) ) { + $parts = explode( '::', $slug, 2 ); + if ( count( $parts ) === 2 ) { + remove_submenu_page( $parts[0], $parts[1] ); + } + } else { + remove_menu_page( $slug ); + } + } + } + + // Hide items (last). + $hidden = isset( $cfg['hidden'] ) ? $cfg['hidden'] : array(); + if ( empty( $hidden ) || ! is_array( $hidden ) ) { + return; + } + + foreach ( $hidden as $slug ) { + $slug = sanitize_text_field( $slug ); + if ( ! $slug ) { + continue; + } + if ( false !== strpos( $slug, '::' ) ) { + $parts = explode( '::', $slug, 2 ); + if ( count( $parts ) === 2 ) { + remove_submenu_page( $parts[0], $parts[1] ); + } + } else { + remove_menu_page( $slug ); + } + } +} + +/** + * Late registration for custom menu entries (after core menus). + * + * @return void + */ +function inject_custom_menu_items_late() { + if ( ! is_admin() || ! get_current_user_id() || is_user_exempt( get_current_user_id() ) ) { + return; + } + $cfg = get_resolved_config_for_user( get_current_user_id() ); + if ( empty( $cfg['custom_items'] ) ) { + return; + } + inject_custom_menu_items( $cfg['custom_items'] ); +} + +/** + * Map a stored top-level order token to the key used in the global $menu array ($item[2]). + * + * Mirrors {@see filter_menu_order()}: `parent::child` uses the child segment; `sep-*` is unchanged + * (physical reorder skips it; {@see inject_separators()} consumes those tokens from $order). + * + * @param string $token Order entry (slug or composite). + * @return string Slug for $menu lookup, or original token for sep-*. + */ +function members_am_order_token_to_menu_slug( $token ) { + $token = (string) $token; + if ( 0 === strpos( $token, 'sep-' ) ) { + return $token; + } + if ( false !== strpos( $token, '::' ) ) { + $parts = explode( '::', $token, 2 ); + return isset( $parts[1] ) ? (string) $parts[1] : $token; + } + return $token; +} + +/** + * Reorder $menu array by slug list. + * + * @param array $menu Admin menu global. + * @param array $order Ordered slugs. + * @return array + */ +function reorder_menu_by_slug_list( $menu, $order ) { + $by_slug = array(); + foreach ( $menu as $key => $item ) { + if ( isset( $item[2] ) ) { + $by_slug[ $item[2] ] = $item; + } + } + $new = array(); + $used = array(); + $pos = 1; + $order = array_map( 'strval', $order ); + foreach ( $order as $token ) { + if ( 0 === strpos( $token, 'sep-' ) ) { + continue; + } + $slug = members_am_order_token_to_menu_slug( $token ); + if ( ! isset( $by_slug[ $slug ] ) || isset( $used[ $slug ] ) ) { + continue; + } + $new[ $pos ] = $by_slug[ $slug ]; + $used[ $slug ] = true; + $pos++; + } + // Append items not in our order list. + foreach ( $menu as $k => $item ) { + if ( isset( $item[2] ) && ! isset( $used[ $item[2] ] ) ) { + $new[ $pos ] = $item; + $pos++; + } elseif ( ! isset( $item[2] ) ) { + // Separators and other items without a slug. + $new[ $pos ] = $item; + $pos++; + } + } + return $new; +} + +/** + * Reorder submenu items array. + * + * @param array $items Submenu items. + * @param array $child_order Slugs in order. + * @return array + */ +function reorder_submenu_items( $items, $child_order ) { + $by_slug = array(); + foreach ( $items as $idx => $item ) { + if ( isset( $item[2] ) ) { + $by_slug[ $item[2] ] = array( 'idx' => $idx, 'item' => $item ); + } + } + $new = array(); + $seen = array(); + foreach ( $child_order as $slug ) { + if ( isset( $by_slug[ $slug ] ) ) { + $i = $by_slug[ $slug ]['idx']; + if ( ! isset( $seen[ $i ] ) ) { + $new[] = $by_slug[ $slug ]['item']; + $seen[ $i ] = true; + } + } + } + foreach ( $items as $idx => $item ) { + if ( empty( $seen[ $idx ] ) ) { + $new[] = $item; + } + } + return $new; +} + +/** + * Inject real WordPress separator entries into $menu based on sep-* tokens + * in the order array. + * + * Walks through the order array and, whenever a sep-* token is encountered, + * inserts a proper WP separator entry at the corresponding position in $menu. + * + * @param array $menu The admin menu array (already reordered by slug list). + * @param array $order The full order array including sep-* tokens. + * @return array Modified menu array with separators injected. + */ +function inject_separators( $menu, $order ) { + $sep_positions = array(); + $real_idx = 0; + + foreach ( $order as $token ) { + $token = (string) $token; + if ( 0 === strpos( $token, 'sep-' ) ) { + $sep_positions[] = $real_idx; + } else { + $real_idx++; + } + } + + if ( empty( $sep_positions ) ) { + return $menu; + } + + $items = array_values( $menu ); + $result = array(); + $pos = 1; + $item_i = 0; + $sep_i = 0; + $total = count( $items ); + $placed = 0; + + for ( $slot = 0; $placed < $total || $sep_i < count( $sep_positions ); $slot++ ) { + if ( $sep_i < count( $sep_positions ) && $sep_positions[ $sep_i ] === $item_i ) { + $result[ $pos ] = array( + '', + 'read', + 'separator-members-am-' . $sep_i, + '', + 'wp-menu-separator', + ); + $pos++; + $sep_i++; + } elseif ( $item_i < $total ) { + $result[ $pos ] = $items[ $item_i ]; + $pos++; + $item_i++; + $placed++; + } else { + break; + } + } + + while ( $item_i < $total ) { + $result[ $pos ] = $items[ $item_i ]; + $pos++; + $item_i++; + } + + return $result; +} + +/** + * Apply label, icon, URL, colors to menu globals. + * + * @param array $overrides Overrides keyed by canonical slug. + * @return void + */ +function apply_menu_overrides( $overrides ) { + global $menu, $submenu; + $fa_icons = array(); + $img_icon_ids = array(); + + foreach ( $menu as $k => $item ) { + if ( empty( $item[2] ) ) { + continue; + } + $slug = $item[2]; + if ( empty( $overrides[ $slug ] ) || ! is_array( $overrides[ $slug ] ) ) { + continue; + } + $o = $overrides[ $slug ]; + if ( ! empty( $o['label'] ) ) { + $menu[ $k ][0] = wp_strip_all_tags( $o['label'] ); + } + if ( ! empty( $o['badge'] ) ) { + $badge_text = esc_html( $o['badge'] ); + $badge_bg = sanitize_hex_color( $o['badge_bg'] ?? '' ) ?: DEFAULT_MENU_BADGE_BG; + $badge_html = ' ' . $badge_text . ''; + $menu[ $k ][0] .= $badge_html; + } + if ( ! empty( $o['url'] ) && members_am_is_custom_menu_item_slug( $slug ) ) { + $new_slug = esc_url_raw( $o['url'] ); + $menu[ $k ][2] = $new_slug; + // Submenus are keyed by the parent slug; keep them attached when the slug changes. + if ( $new_slug && $new_slug !== $slug && isset( $submenu[ $slug ] ) ) { + $submenu[ $new_slug ] = $submenu[ $slug ]; + unset( $submenu[ $slug ] ); + } + } + if ( ! empty( $o['icon'] ) ) { + $icon = $o['icon']; + $icon_type = ! empty( $o['icon_type'] ) ? $o['icon_type'] : 'dashicon'; + // Auto-detect: if icon is a URL, treat as image regardless of declared type. + if ( 0 === strpos( $icon, 'http://' ) || 0 === strpos( $icon, 'https://' ) || 0 === strpos( $icon, '//' ) ) { + $icon_type = 'image'; + } elseif ( 0 === strpos( $icon, 'data:image/' ) ) { + $icon_type = 'image'; + } + + if ( 'dashicon' === $icon_type ) { + $menu[ $k ][6] = sanitize_text_field( $icon ); + } elseif ( 'fontawesome' === $icon_type ) { + $menu[ $k ][6] = 'none'; + $id = members_am_menu_item_dom_id( $item ); + if ( $id ) { + $fa_icons[ $id ] = esc_attr( $icon ); + } + } elseif ( 'custom' === $icon_type || 'image' === $icon_type ) { + $menu[ $k ][6] = esc_url( $icon ); + $id = members_am_menu_item_dom_id( $item ); + if ( $id ) { + $img_icon_ids[] = $id; + } + } + } + } + + foreach ( $submenu as $parent => $items ) { + foreach ( $items as $idx => $item ) { + if ( empty( $item[2] ) ) { + continue; + } + $canon = $parent . '::' . $item[2]; + if ( empty( $overrides[ $canon ] ) || ! is_array( $overrides[ $canon ] ) ) { + continue; + } + $o = $overrides[ $canon ]; + if ( ! empty( $o['label'] ) ) { + $submenu[ $parent ][ $idx ][0] = wp_strip_all_tags( $o['label'] ); + } + if ( ! empty( $o['badge'] ) ) { + $badge_text = esc_html( $o['badge'] ); + $badge_bg = sanitize_hex_color( $o['badge_bg'] ?? '' ) ?: DEFAULT_MENU_BADGE_BG; + $badge_html = ' ' . $badge_text . ''; + $submenu[ $parent ][ $idx ][0] .= $badge_html; + } + if ( ! empty( $o['url'] ) && members_am_is_custom_menu_item_slug( $item[2] ) ) { + $submenu[ $parent ][ $idx ][2] = esc_url_raw( $o['url'] ); + } + } + } + + if ( ! empty( $fa_icons ) ) { + $GLOBALS['members_am_fa_icons'] = $fa_icons; + add_action( 'admin_head', __NAMESPACE__ . '\output_fa_icon_styles', 998 ); + } + if ( ! empty( $img_icon_ids ) ) { + $GLOBALS['members_am_img_icon_ids'] = $img_icon_ids; + add_action( 'admin_head', __NAMESPACE__ . '\output_img_icon_styles', 998 ); + } +} + +/** + * Output CSS + HTML to render Font Awesome icons in the admin sidebar. + * + * Hides the default Dashicon (and img/svg) and injects a Font Awesome . FA glyphs render on that element, not on .wp-menu-image::before. + * + * @return void + */ +function output_fa_icon_styles() { + if ( empty( $GLOBALS['members_am_fa_icons'] ) ) { + return; + } + $css = ''; + $js = ''; + foreach ( $GLOBALS['members_am_fa_icons'] as $menu_id => $fa_class ) { + $sel = '#adminmenu #' . $menu_id . ' .wp-menu-image'; + // Hide the core Dashicon pseudo-element without setting content: "" (clearer for devtools and avoids edge cases with icon fonts). + $css .= $sel . ':before { display: none !important; }' . "\n"; + $css .= $sel . ' img, ' . $sel . ' svg { display: none !important; }' . "\n"; + $css .= $sel . ' { display: flex !important; align-items: center !important; justify-content: center !important; min-width: 20px !important; }' . "\n"; + $css .= $sel . ' .members-am-fa { font-size: 20px; line-height: 1; display: inline-block; width: 20px; text-align: center; font-style: normal; font-weight: 900; vertical-align: middle; }' . "\n"; + $js .= '(function(mid, faCls){var $w=jQuery("#"+mid+" .wp-menu-image");$w.empty();$w.append(jQuery("").attr("class","members-am-fa "+faCls).attr("aria-hidden","true"));})(' . wp_json_encode( (string) $menu_id ) . ', ' . wp_json_encode( (string) $fa_class ) . ');' . "\n"; + } + echo '\n"; + echo '' . "\n"; +} + +/** + * Output CSS to properly style custom image icons in the admin sidebar. + * + * @return void + */ +function output_img_icon_styles() { + if ( empty( $GLOBALS['members_am_img_icon_ids'] ) ) { + return; + } + $css = ''; + foreach ( $GLOBALS['members_am_img_icon_ids'] as $menu_id ) { + $sel = '#adminmenu #' . $menu_id . ' .wp-menu-image img'; + $css .= $sel . ' { width: 20px !important; height: 20px !important; display: inline-block !important; vertical-align: middle !important; object-fit: contain !important; filter: none !important; padding: 0 !important; }' . "\n"; + $css .= '#adminmenu #' . $menu_id . ' .wp-menu-image { padding: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; }' . "\n"; + } + echo '\n"; +} + +/** + * After demoting a top-level menu under another parent, merge its former submenu (e.g. Updates under Dashboard) + * into the parent's submenu. WordPress only renders one submenu list per flyout ($submenu[ parent ]), so children + * that stayed in $submenu[ index.php ] would not appear until merged into $submenu[ edit.php ]. + * + * @param string $target_parent Parent file slug (e.g. edit.php). + * @param string $demoted_slug Former top-level slug (e.g. index.php). + * @param array $nested_items Copy of $submenu[ $demoted_slug ] before it is cleared. + * @return void + */ +function merge_demoted_submenu_into_parent( $target_parent, $demoted_slug, $nested_items ) { + global $submenu, $_parent_pages; + + $target_parent = plugin_basename( $target_parent ); + $demoted_slug = plugin_basename( $demoted_slug ); + + unset( $submenu[ $demoted_slug ] ); + + if ( empty( $nested_items ) || ! is_array( $nested_items ) ) { + return; + } + + $to_insert = array(); + foreach ( $nested_items as $sub ) { + if ( ! isset( $sub[2] ) ) { + continue; + } + $child_slug = plugin_basename( $sub[2] ); + // Skip the duplicate row that matches the parent file (WP adds "same as parent" for top-level screens). + if ( $child_slug === $demoted_slug ) { + continue; + } + $to_insert[] = $sub; + } + + if ( empty( $to_insert ) || ! isset( $submenu[ $target_parent ] ) || ! is_array( $submenu[ $target_parent ] ) ) { + return; + } + + $flat = array_values( $submenu[ $target_parent ] ); + $pos = false; + foreach ( $flat as $i => $sub ) { + if ( isset( $sub[2] ) && plugin_basename( $sub[2] ) === $demoted_slug ) { + $pos = $i; + break; + } + } + if ( false === $pos ) { + return; + } + + $before = array_slice( $flat, 0, $pos + 1 ); + $after = array_slice( $flat, $pos + 1 ); + $merged = array_merge( $before, $to_insert, $after ); + + $submenu[ $target_parent ] = array(); + foreach ( array_values( $merged ) as $i => $row ) { + $submenu[ $target_parent ][ $i ] = $row; + } + + foreach ( $to_insert as $row ) { + if ( ! empty( $row[2] ) ) { + $_parent_pages[ plugin_basename( $row[2] ) ] = $target_parent; + } + } +} + +/** + * Copy every callback from one admin hook to another (same signature as core add_action). + * + * Used after relocating a submenu so {@see get_plugin_page_hook()} finds handlers on the + * parent-specific hook name WordPress derives from the new flyout. + * + * @param string $old_hook Prior hook suffix (e.g. tools_page_site-health). + * @param string $new_hook New hook suffix (e.g. index_page_site-health). + * @return void + */ +function members_am_clone_admin_hook_to( $old_hook, $new_hook ) { + $old_hook = (string) $old_hook; + $new_hook = (string) $new_hook; + if ( '' === $old_hook || '' === $new_hook || $old_hook === $new_hook ) { + return; + } + if ( ! \has_action( $old_hook ) ) { + return; + } + global $wp_filter; + if ( ! isset( $wp_filter[ $old_hook ] ) || ! ( $wp_filter[ $old_hook ] instanceof \WP_Hook ) ) { + return; + } + $src = $wp_filter[ $old_hook ]; + foreach ( $src->callbacks as $priority => $callbacks ) { + foreach ( $callbacks as $cb ) { + if ( isset( $cb['function'], $cb['accepted_args'] ) ) { + \add_action( $new_hook, $cb['function'], (int) $priority, (int) $cb['accepted_args'] ); + } + } + } +} + +/** + * Register $_registered_pages and clone load/callback hooks after a submenu row moves to a new parent. + * + * Core {@see add_submenu_page()} registers a parent-specific hook name; {@see user_can_access_admin_page()} + * and {@see get_plugin_page_hook()} require the hook for the *new* parent once $submenu / $_parent_pages match it. + * + * @param string $old_parent Old parent basename (plugin_basename). + * @param string $new_parent New parent basename. + * @param string $child_pb Child menu slug basename (item[2]). + * @return void + */ +function members_am_rebind_relocated_submenu_hooks( $old_parent, $new_parent, $child_pb ) { + global $_registered_pages; + + $old_parent = plugin_basename( (string) $old_parent ); + $new_parent = plugin_basename( (string) $new_parent ); + $child_pb = plugin_basename( (string) $child_pb ); + + if ( '' === $child_pb || ! function_exists( 'get_plugin_page_hookname' ) ) { + return; + } + + $old_primary = \get_plugin_page_hookname( $child_pb, $old_parent ); + $new_primary = \get_plugin_page_hookname( $child_pb, $new_parent ); + + $had_old_registration = false; + if ( isset( $_registered_pages ) && is_array( $_registered_pages ) && $old_primary && ! empty( $_registered_pages[ $old_primary ] ) ) { + $had_old_registration = true; + } + if ( 'tools.php' === $old_parent && isset( $_registered_pages ) && is_array( $_registered_pages ) ) { + $compat = \get_plugin_page_hookname( $child_pb, 'edit.php' ); + if ( $compat && ! empty( $_registered_pages[ $compat ] ) ) { + $had_old_registration = true; + } + } + + if ( ! $had_old_registration && $old_primary && \has_action( $old_primary ) ) { + $had_old_registration = true; + } + + if ( ! $had_old_registration ) { + return; + } + + $new_hooknames = array(); + if ( $new_primary ) { + $new_hooknames[] = $new_primary; + } + if ( 'tools.php' === $new_parent ) { + $edit_compat = \get_plugin_page_hookname( $child_pb, 'edit.php' ); + if ( $edit_compat ) { + $new_hooknames[] = $edit_compat; + } + } + $new_hooknames = array_unique( array_filter( $new_hooknames ) ); + + if ( isset( $_registered_pages ) && is_array( $_registered_pages ) ) { + foreach ( $new_hooknames as $nh ) { + $_registered_pages[ $nh ] = true; + } + } + + if ( $old_primary && $new_primary && \has_action( $old_primary ) ) { + members_am_clone_admin_hook_to( $old_primary, $new_primary ); + } +} + +/** + * Move an existing submenu row from one parent file to another (same child hook). + * + * Used when overrides use a composite key parent::child with parent set to a different top-level slug. + * + * @param string $old_parent Parent file slug the row currently belongs to. + * @param string $child_slug Child hook (submenu item[2]); may be a .php file or members-am-*. + * @param string $new_parent Target parent file slug. + * @return bool True if the row was removed from the old parent and inserted under the new one. + */ +function relocate_submenu_row( $old_parent, $child_slug, $new_parent ) { + global $submenu, $_parent_pages; + + $old_parent = plugin_basename( (string) $old_parent ); + $new_parent = plugin_basename( (string) $new_parent ); + $child_pb = plugin_basename( (string) $child_slug ); + + if ( '' === $child_pb || '' === $new_parent || $old_parent === $new_parent ) { + return false; + } + + if ( empty( $submenu[ $old_parent ] ) || ! is_array( $submenu[ $old_parent ] ) ) { + return false; + } + + // Already registered under the target parent — nothing to do (avoid orphaning the row). + if ( ! empty( $submenu[ $new_parent ] ) && is_array( $submenu[ $new_parent ] ) ) { + foreach ( $submenu[ $new_parent ] as $ex ) { + if ( ! empty( $ex[2] ) && plugin_basename( (string) $ex[2] ) === $child_pb ) { + return false; + } + } + } + + $row = null; + $idx_rm = null; + foreach ( $submenu[ $old_parent ] as $idx => $sub ) { + if ( empty( $sub[2] ) ) { + continue; + } + if ( plugin_basename( (string) $sub[2] ) === $child_pb ) { + $row = $sub; + $idx_rm = $idx; + break; + } + } + if ( null === $row || null === $idx_rm ) { + return false; + } + + unset( $submenu[ $old_parent ][ $idx_rm ] ); + $submenu[ $old_parent ] = array_values( $submenu[ $old_parent ] ); + + if ( empty( $submenu[ $new_parent ] ) || ! is_array( $submenu[ $new_parent ] ) ) { + $submenu[ $new_parent ] = array(); + } + $submenu[ $new_parent ][] = $row; + $submenu[ $new_parent ] = array_values( $submenu[ $new_parent ] ); + + if ( isset( $_parent_pages ) && is_array( $_parent_pages ) ) { + $_parent_pages[ $child_pb ] = $new_parent; + } + + members_am_rebind_relocated_submenu_hooks( $old_parent, $new_parent, $child_pb ); + + return true; +} + +/** + * Move items between menu levels based on 'parent' override field. + * + * - If a submenu item has parent = '__promote__', promote it to top-level. + * - If a submenu item has parent = a top-level file slug (not __promote__), move it under that parent's submenu. + * - If a top-level item has a parent slug set, demote it to a submenu of that parent. + * + * @param array $overrides Overrides keyed by canonical slug. + * @return void + */ +function apply_level_moves( $overrides ) { + global $menu, $submenu; + + if ( ! isset( $GLOBALS['members_am_promoted_redirects'] ) || ! is_array( $GLOBALS['members_am_promoted_redirects'] ) ) { + $GLOBALS['members_am_promoted_redirects'] = array(); + } + + foreach ( $overrides as $slug => $o ) { + if ( ! is_array( $o ) || ! array_key_exists( 'parent', $o ) ) { + continue; + } + $target_parent = $o['parent']; + $is_submenu = ( false !== strpos( $slug, '::' ) ); + + if ( $is_submenu && '__promote__' === $target_parent ) { + $parts = explode( '::', $slug, 2 ); + if ( count( $parts ) !== 2 ) { + continue; + } + $old_parent = $parts[0]; + $child_slug = $parts[1]; + + $label = $child_slug; + $cap = 'read'; + $promoted_redirect = ''; + if ( isset( $submenu[ $old_parent ] ) && is_array( $submenu[ $old_parent ] ) ) { + foreach ( $submenu[ $old_parent ] as $idx => $sub ) { + if ( isset( $sub[2] ) && $sub[2] === $child_slug ) { + $label = $sub[0]; + $cap = isset( $sub[1] ) ? $sub[1] : 'read'; + // Resolve the real admin URL while this item is still registered as a submenu. + $promoted_redirect = menu_page_url( $child_slug, false ); + unset( $submenu[ $old_parent ][ $idx ] ); + break; + } + } + } + if ( ! $promoted_redirect && is_string( $child_slug ) && preg_match( '/^[a-zA-Z0-9_.-]+\.php$/', $child_slug ) ) { + $promoted_redirect = admin_url( $child_slug ); + } + if ( ! empty( $o['label'] ) ) { + $label = $o['label']; + } + $icon = 'dashicons-admin-generic'; + if ( ! empty( $o['icon'] ) ) { + $icon = $o['icon']; + } + if ( $promoted_redirect ) { + $GLOBALS['members_am_promoted_redirects'][ $child_slug ] = $promoted_redirect; + } + add_menu_page( + wp_strip_all_tags( $label ), + wp_strip_all_tags( $label ), + $cap, + $child_slug, + $promoted_redirect ? __NAMESPACE__ . '\members_am_promoted_menu_callback' : '', + $icon + ); + + } elseif ( ! $is_submenu && is_string( $target_parent ) && '' !== $target_parent ) { + $found_key = false; + $found_item = null; + foreach ( $menu as $k => $item ) { + if ( isset( $item[2] ) && $item[2] === $slug ) { + $found_key = $k; + $found_item = $item; + break; + } + } + if ( false === $found_key ) { + continue; + } + $label = isset( $found_item[0] ) ? $found_item[0] : $slug; + $cap = isset( $found_item[1] ) ? $found_item[1] : 'read'; + if ( ! empty( $o['label'] ) ) { + $label = $o['label']; + } + + $nested_submenu = isset( $submenu[ $slug ] ) && is_array( $submenu[ $slug ] ) ? $submenu[ $slug ] : array(); + + remove_menu_page( $slug ); + add_submenu_page( $target_parent, wp_strip_all_tags( $label ), wp_strip_all_tags( $label ), $cap, $slug ); + + if ( ! empty( $nested_submenu ) ) { + merge_demoted_submenu_into_parent( $target_parent, $slug, $nested_submenu ); + } + } elseif ( $is_submenu && is_string( $target_parent ) && '' !== $target_parent && '__promote__' !== $target_parent ) { + $parts = explode( '::', $slug, 2 ); + if ( count( $parts ) !== 2 ) { + continue; + } + $old_parent = $parts[0]; + $child_slug = $parts[1]; + relocate_submenu_row( $old_parent, $child_slug, $target_parent ); + } + } +} + +/** + * Render callback for a submenu item promoted to top-level via apply_level_moves(). + * + * Redirects to the canonical URL WordPress would use for that submenu, so the original screen loads. + * + * @return void + */ +function members_am_promoted_menu_callback() { + $map = isset( $GLOBALS['members_am_promoted_redirects'] ) && is_array( $GLOBALS['members_am_promoted_redirects'] ) ? $GLOBALS['members_am_promoted_redirects'] : array(); + global $plugin_page, $pagenow; + $slug = ''; + if ( is_string( $plugin_page ) && '' !== $plugin_page ) { + $slug = $plugin_page; + } elseif ( isset( $_GET['page'] ) ) { + $slug = sanitize_text_field( wp_unslash( $_GET['page'] ) ); + } elseif ( ! empty( $pagenow ) ) { + $slug = $pagenow; + } + if ( $slug && isset( $map[ $slug ] ) && $map[ $slug ] ) { + wp_safe_redirect( $map[ $slug ] ); + exit; + } +} + +/** + * Resolve which overrides key applies to a top-level $menu row (handles members-am-* slug replaced by URL in $item[2]). + * + * @param array $item One $menu row (0–6). + * @param array $overrides Overrides map. + * @return string Matching key or ''. + */ +function members_am_resolve_top_level_override_key( $item, $overrides ) { + if ( empty( $overrides ) || ! is_array( $overrides ) ) { + return ''; + } + $try = isset( $item[2] ) ? (string) $item[2] : ''; + if ( '' !== $try && ! empty( $overrides[ $try ] ) && is_array( $overrides[ $try ] ) ) { + return $try; + } + // $item[5] is the admin page hook (e.g. toplevel_page_members-am-c123); suffix matches the internal menu slug. + if ( ! empty( $item[5] ) && is_string( $item[5] ) && preg_match( '/_page_(.+)$/', $item[5], $m ) ) { + $cand = $m[1]; + if ( '' !== $cand && ! empty( $overrides[ $cand ] ) && is_array( $overrides[ $cand ] ) ) { + return $cand; + } + } + return ''; +} + +/** + * Apply color overrides via admin_head CSS rules. + * + * Instead of wrapping titles in styled spans (which only affects text), + * this injects a \n"; + } +} + +/** + * Register custom top/sub menu items from stored config. + * + * @param array $items Custom items. + * @return void + */ +function inject_custom_menu_items( $items ) { + if ( ! is_array( $items ) ) { + return; + } + global $members_am_custom_redirects; + if ( ! is_array( $members_am_custom_redirects ) ) { + $members_am_custom_redirects = array(); + } + foreach ( $items as $item ) { + if ( empty( $item['id'] ) || empty( $item['label'] ) ) { + continue; + } + $cap = ! empty( $item['cap'] ) ? $item['cap'] : 'read'; + $url = ! empty( $item['url'] ) ? esc_url_raw( $item['url'] ) : admin_url(); + $hook = 'members-am-' . sanitize_key( $item['id'] ); + $members_am_custom_redirects[ $hook ] = $url; + if ( empty( $item['parent'] ) ) { + add_menu_page( + $item['label'], + $item['label'], + $cap, + $hook, + __NAMESPACE__ . '\members_am_custom_menu_callback', + ! empty( $item['icon'] ) ? $item['icon'] : 'dashicons-admin-generic', + isset( $item['position'] ) ? (int) $item['position'] : null + ); + } else { + add_submenu_page( + $item['parent'], + $item['label'], + $item['label'], + $cap, + $hook, + __NAMESPACE__ . '\members_am_custom_menu_callback' + ); + } + } +} + +/** + * Redirect to a stored custom menu item URL. + * + * External URLs are intentionally supported for custom menu items configured by + * users who can edit Members settings. + * + * @param string $url Target URL. + * @return void + */ +function members_am_redirect_to_custom_menu_url( $url ) { + $url = esc_url_raw( $url ); + if ( '' === $url ) { + return; + } + + $target_host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); + $site_host = strtolower( (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ) ); + + if ( '' !== $target_host && '' !== $site_host && $target_host !== $site_host ) { + $allow_external_host = static function( $hosts ) use ( $target_host ) { + if ( ! in_array( $target_host, $hosts, true ) ) { + $hosts[] = $target_host; + } + return $hosts; + }; + add_filter( 'allowed_redirect_hosts', $allow_external_host ); + wp_safe_redirect( $url ); + remove_filter( 'allowed_redirect_hosts', $allow_external_host ); + } else { + wp_safe_redirect( $url ); + } + exit; +} + +/** + * Redirects custom menu items to their target URL. + * + * @return void + */ +function members_am_custom_menu_callback() { + global $members_am_custom_redirects; + if ( empty( $_GET['page'] ) || ! is_array( $members_am_custom_redirects ) ) { + return; + } + $page = sanitize_key( wp_unslash( $_GET['page'] ) ); + if ( isset( $members_am_custom_redirects[ $page ] ) ) { + members_am_redirect_to_custom_menu_url( $members_am_custom_redirects[ $page ] ); + } +} + +/** + * Slug-like identifiers derived from an admin URL for matching hidden / capability maps. + * + * @param string $url Admin URL. + * @return array + */ +function members_am_slugs_for_admin_redirect_url( $url ) { + $path = (string) wp_parse_url( $url, PHP_URL_PATH ); + $query = (string) wp_parse_url( $url, PHP_URL_QUERY ); + $slugs = array(); + $args = array(); + if ( '' !== $query ) { + wp_parse_str( $query, $args ); + if ( ! empty( $args['page'] ) && is_string( $args['page'] ) ) { + $slugs[] = sanitize_text_field( $args['page'] ); + } + } + // Bare edit.php / post-new.php / post.php basename must not match Posts-only hidden ids when this URL is for another post_type (e.g. Pages). + $cpt_slug = ! empty( $args['post_type'] ) && is_string( $args['post_type'] ) ? sanitize_key( $args['post_type'] ) : ''; + $omit_ambiguous_basename = ( '' !== $cpt_slug && 'post' !== $cpt_slug ); + if ( '' !== $path ) { + $base = basename( $path ); + if ( $base && 'admin.php' !== $base ) { + if ( ! ( $omit_ambiguous_basename && in_array( $base, array( 'edit.php', 'post-new.php', 'post.php' ), true ) ) ) { + $slugs[] = $base; + } + } + } + // Composite ids (parity with get_current_screen_slugs) so submenu keys like tools.php::tools.php?page=x match toolbar and deep links. + if ( '' !== $path && ! empty( $args['page'] ) && is_string( $args['page'] ) ) { + $base = basename( $path ); + $page = sanitize_text_field( $args['page'] ); + if ( $base && $page ) { + $slugs[] = $base . '?page=' . $page; + } + } + if ( '' !== $cpt_slug ) { + $base = '' !== $path ? basename( $path ) : ''; + if ( $base && in_array( $base, array( 'edit.php', 'post-new.php', 'post.php' ), true ) ) { + $slugs[] = $base . '?post_type=' . $cpt_slug; + } + } + if ( ! empty( $args['taxonomy'] ) && is_string( $args['taxonomy'] ) ) { + $tax = sanitize_key( $args['taxonomy'] ); + $base = '' !== $path ? basename( $path ) : ''; + if ( $tax && $base && in_array( $base, array( 'edit-tags.php', 'term.php' ), true ) ) { + $slugs[] = $base . '?taxonomy=' . $tax; + } + } + return array_unique( array_filter( $slugs ) ); +} + +/** + * Whether the user's Admin Menus config would block this URL if it were the current screen. + * + * @param int $user_id User ID. + * @param string $url Full admin URL. + * @return bool + */ +function members_am_redirect_target_is_blocked_for_user( $user_id, $url ) { + $slugs = members_am_slugs_for_admin_redirect_url( $url ); + $cfg = get_resolved_config_for_user( $user_id ); + $hidden = isset( $cfg['hidden'] ) ? (array) $cfg['hidden'] : array(); + $cap_map = isset( $cfg['capabilities'] ) ? (array) $cfg['capabilities'] : array(); + + foreach ( $slugs as $cslug ) { + if ( ! $cslug ) { + continue; + } + foreach ( $hidden as $h ) { + if ( $h === $cslug || members_admin_menus_slug_matches( $cslug, $h ) ) { + return true; + } + } + foreach ( $cap_map as $slug => $cap ) { + if ( ! $slug || ! $cap || user_can( $user_id, $cap ) ) { + continue; + } + if ( $slug === $cslug || members_admin_menus_slug_matches( $cslug, $slug ) ) { + return true; + } + } + } + return false; +} + +/** + * Normalize an admin bar node href to an absolute URL under this site's wp-admin, or empty string if not applicable. + * + * @param string $href Raw href from {@see WP_Admin_Bar::get_nodes()}. + * @return string Absolute http(s) URL or ''. + */ +function members_am_normalize_toolbar_href( $href ) { + $href = is_string( $href ) ? trim( $href ) : ''; + if ( '' === $href || '#' === $href || 0 === stripos( $href, 'javascript:' ) ) { + return ''; + } + if ( preg_match( '/\s/', $href ) ) { + return ''; + } + + if ( preg_match( '#^https?://#i', $href ) ) { + $url = $href; + } elseif ( 0 === strpos( $href, '//' ) ) { + $url = ( is_ssl() ? 'https:' : 'http:' ) . $href; + } elseif ( 0 === strpos( $href, '/' ) ) { + $url = home_url( $href ); + } else { + $url = admin_url( $href ); + } + + $url = esc_url_raw( $url ); + if ( '' === $url ) { + return ''; + } + + $parts = wp_parse_url( $url ); + if ( empty( $parts['scheme'] ) || empty( $parts['host'] ) || empty( $parts['path'] ) ) { + return ''; + } + + $site_host = wp_parse_url( site_url( '/' ), PHP_URL_HOST ); + if ( ! is_string( $site_host ) || '' === $site_host || strcasecmp( (string) $parts['host'], $site_host ) !== 0 ) { + return ''; + } + + $admin_path = wp_parse_url( admin_url(), PHP_URL_PATH ); + if ( ! is_string( $admin_path ) || '' === $admin_path ) { + return ''; + } + $admin_base = untrailingslashit( wp_normalize_path( $admin_path ) ); + $req_path = wp_normalize_path( (string) $parts['path'] ); + if ( '' === $admin_base ) { + return ''; + } + if ( 0 !== strpos( $req_path . '/', $admin_base . '/' ) ) { + return ''; + } + + return $url; +} + +/** + * Whether the admin bar still has any node whose parent is the given id. + * + * @param \WP_Admin_Bar $wp_admin_bar Admin bar instance. + * @param string $parent_id Parent node id. + * @return bool + */ +function members_am_admin_bar_parent_has_children( $wp_admin_bar, $parent_id ) { + $parent_id = (string) $parent_id; + foreach ( (array) $wp_admin_bar->get_nodes() as $node ) { + if ( ! is_object( $node ) || empty( $node->id ) ) { + continue; + } + if ( isset( $node->parent ) && (string) $node->parent === $parent_id ) { + return true; + } + } + return false; +} + +/** + * Remove known container nodes that have no remaining children (e.g. empty "New" dropdown). + * + * @param \WP_Admin_Bar $wp_admin_bar Admin bar instance. + * @return void + */ +function members_am_prune_empty_admin_bar_parents( $wp_admin_bar ) { + $prune_ids = apply_filters( + app()->namespace . '/admin_bar_empty_parent_prune_ids', + array( 'new-content' ) + ); + if ( ! is_array( $prune_ids ) || empty( $prune_ids ) ) { + return; + } + $never_prune = apply_filters( + app()->namespace . '/admin_bar_parent_prune_never_remove', + array( 'my-account', 'top-secondary', 'wp-logo' ) + ); + $never = is_array( $never_prune ) ? array_flip( $never_prune ) : array(); + + for ( $i = 0; $i < 10; $i++ ) { + $removed = false; + foreach ( $prune_ids as $pid ) { + $pid = is_string( $pid ) ? sanitize_key( $pid ) : ''; + if ( '' === $pid || isset( $never[ $pid ] ) ) { + continue; + } + $nodes = $wp_admin_bar->get_nodes(); + if ( empty( $nodes[ $pid ] ) ) { + continue; + } + if ( ! members_am_admin_bar_parent_has_children( $wp_admin_bar, $pid ) ) { + $wp_admin_bar->remove_menu( $pid ); + $removed = true; + } + } + if ( ! $removed ) { + break; + } + } +} + +/** + * Remove admin bar links that point at admin screens blocked for this user (same rules as sidebar + URL redirect checks). + * + * Fires on {@see 'wp_before_admin_bar_render'} (late priority) so items added during that same action — not only on + * {@see 'admin_bar_menu'} — are present before we strip them. + * + * Core invokes {@see 'wp_before_admin_bar_render'} with no arguments; this implementation reads the global `$wp_admin_bar` instance. + * The optional parameter exists so a custom caller may pass a bar instance explicitly. + * + * @param \WP_Admin_Bar|null $passed_bar Optional bar instance; core always leaves this null. + * @return void + */ +function members_am_apply_admin_bar_menu_restrictions( $passed_bar = null ) { + $bar = $passed_bar instanceof \WP_Admin_Bar ? $passed_bar : null; + if ( null === $bar ) { + global $wp_admin_bar; + $bar = $wp_admin_bar instanceof \WP_Admin_Bar ? $wp_admin_bar : null; + } + if ( ! $bar instanceof \WP_Admin_Bar ) { + return; + } + $user_id = get_current_user_id(); + if ( $user_id < 1 ) { + return; + } + + /** + * Whether to strip admin bar items using Admin Menus hidden / capability map rules. + * + * @param bool $apply Default true. + * @param int $user_id Current user ID. + */ + if ( ! apply_filters( app()->namespace . '/apply_admin_bar_restrictions', true, $user_id ) ) { + return; + } + + if ( is_user_exempt( $user_id ) ) { + return; + } + + /** + * Node ids to never remove (structural / identity toolbar items). + * + * @param string[] $ids Node ids. + * @param int $user_id Current user ID. + */ + $always_keep = apply_filters( app()->namespace . '/admin_bar_node_ids_always_keep', array( 'menu-toggle' ), $user_id ); + $keep_flip = is_array( $always_keep ) ? array_flip( $always_keep ) : array(); + + /** + * Parent toolbar node ids whose `href` duplicates the first child (core does this for `new-content`). + * Do not remove them in the URL pass — only {@see members_am_prune_empty_admin_bar_parents} may drop them when + * they have no children left, so hiding e.g. Posts does not remove the whole "+ New" menu while Media/Pages remain. + * + * @param string[] $ids Node ids. + * @param int $user_id Current user ID. + */ + $skip_href_parent_ids = apply_filters( + app()->namespace . '/admin_bar_skip_href_removal_parent_ids', + array( 'new-content' ), + $user_id + ); + $skip_href_parents = is_array( $skip_href_parent_ids ) ? array_flip( $skip_href_parent_ids ) : array(); + + foreach ( (array) $bar->get_nodes() as $node ) { + if ( ! is_object( $node ) || empty( $node->id ) ) { + continue; + } + $id = (string) $node->id; + if ( isset( $keep_flip[ $id ] ) ) { + continue; + } + if ( isset( $skip_href_parents[ $id ] ) ) { + continue; + } + if ( empty( $node->href ) || ! is_string( $node->href ) ) { + continue; + } + $abs = members_am_normalize_toolbar_href( $node->href ); + if ( '' === $abs ) { + continue; + } + if ( members_am_redirect_target_is_blocked_for_user( $user_id, $abs ) ) { + $bar->remove_menu( $id ); + } + } + + members_am_prune_empty_admin_bar_parents( $bar ); +} + +/** + * Default redirect when access to a restricted admin screen is denied. + * + * Avoids using the dashboard home URL, which may also be hidden and cause a redirect loop. + * + * @param int $user_id User ID. + * @return string + */ +function members_am_blocked_redirect_fallback_url( $user_id ) { + $candidates = array(); + + $settings_cap = apply_filters( 'members_settings_capability', 'manage_options' ); + if ( user_can( $user_id, $settings_cap ) ) { + $candidates[] = admin_url( 'admin.php?page=members-settings' ); + } + + $candidates[] = admin_url( 'profile.php' ); + + foreach ( $candidates as $candidate ) { + if ( ! members_am_redirect_target_is_blocked_for_user( $user_id, $candidate ) ) { + return $candidate; + } + } + + /** + * Filter last-resort redirect when every admin fallback would still be blocked. + * + * @param string $url Default front-end home URL. + * @param int $user_id User ID. + */ + return apply_filters( app()->namespace . '/blocked_redirect_last_resort_url', home_url( '/' ), $user_id ); +} + +/** + * Block direct access to hidden admin pages. + * + * @return void + */ +function block_restricted_pages() { + if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) { + return; + } + $user_id = get_current_user_id(); + if ( ! $user_id || is_user_exempt( $user_id ) ) { + return; + } + + $cfg = get_resolved_config_for_user( $user_id ); + + $cap_map = isset( $cfg['capabilities'] ) ? $cfg['capabilities'] : array(); + if ( ! empty( $cap_map ) && is_array( $cap_map ) ) { + $current = get_current_screen_slugs(); + foreach ( $current as $cslug ) { + foreach ( $cap_map as $slug => $cap ) { + if ( ! $slug || ! $cap || current_user_can( $cap ) ) { + continue; + } + if ( $slug === $cslug || members_admin_menus_slug_matches( $cslug, $slug ) ) { + $url = apply_filters( app()->namespace . '/redirect_url', members_am_blocked_redirect_fallback_url( $user_id ), $user_id ); + wp_safe_redirect( $url ); + exit; + } + } + } + } + + $hidden = isset( $cfg['hidden'] ) ? $cfg['hidden'] : array(); + if ( empty( $hidden ) || ! is_array( $hidden ) ) { + return; + } + + $current = get_current_screen_slugs(); + foreach ( $current as $cslug ) { + foreach ( $hidden as $h ) { + if ( $h === $cslug || members_admin_menus_slug_matches( $cslug, $h ) ) { + $url = apply_filters( app()->namespace . '/redirect_url', members_am_blocked_redirect_fallback_url( $user_id ), $user_id ); + wp_safe_redirect( $url ); + exit; + } + } + } +} + +/** + * Whether a stored submenu child slug matches a runtime screen slug. + * + * Avoids false positives when the child is a bare admin PHP basename that is only a prefix of + * another screen (e.g. Posts submenu child `edit.php` matching the Pages list `edit.php?post_type=page`). + * + * @param string $current Current screen id from get_current_screen_slugs(). + * @param string $child_slug Submenu file/slug segment after `parent::`. + * @return bool + */ +function members_admin_menus_submenu_child_matches_current( $current, $child_slug ) { + $child_slug = (string) $child_slug; + $current = (string) $current; + if ( '' === $child_slug || '' === $current ) { + return false; + } + if ( $child_slug === $current ) { + return true; + } + // Child includes query args — classic submenu ids (e.g. edit-tags.php?taxonomy=post_tag). + if ( false !== strpos( $child_slug, '?' ) ) { + return false !== strpos( $current, $child_slug ); + } + if ( 0 !== strpos( $current, $child_slug ) ) { + return false; + } + $len = strlen( $child_slug ); + if ( strlen( $current ) === $len ) { + return true; + } + $nxt = $current[ $len ]; + // Require a real path/query boundary (not `edit.php` matching a longer basename). + if ( '?' !== $nxt && '&' !== $nxt ) { + return false; + } + // CPT list / "Add New" screens share filenames; default Posts menu items map to post_type `post`. + $post_type_screens = array( 'edit.php', 'post-new.php' ); + if ( ! in_array( $child_slug, $post_type_screens, true ) ) { + return true; + } + if ( ! preg_match( '/(?:^|[?&])post_type=([^&]+)/', $current, $m ) ) { + return true; + } + return 'post' === $m[1]; +} + +/** + * Loose match for submenu vs top-level. + * + * @param string $current Current screen id. + * @param string $stored Stored hidden id. + * @return bool + */ +function members_admin_menus_slug_matches( $current, $stored ) { + if ( $stored === $current ) { + return true; + } + if ( false !== strpos( $stored, '::' ) ) { + $parts = explode( '::', $stored, 2 ); + if ( isset( $parts[1] ) ) { + return members_admin_menus_submenu_child_matches_current( $current, $parts[1] ); + } + } + return false; +} + +/** + * Build list of slug identifiers for the current admin screen. + * + * @return array + */ +function get_current_screen_slugs() { + global $pagenow; + $slugs = array(); + + if ( ! empty( $_GET['page'] ) && is_string( $_GET['page'] ) ) { + $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); + $slugs[] = $page; + if ( ! empty( $pagenow ) ) { + $slugs[] = $pagenow . '?page=' . $page; + } + } + + if ( ! empty( $_GET['post_type'] ) && in_array( $pagenow, array( 'edit.php', 'post-new.php', 'post.php' ), true ) ) { + $pt = sanitize_key( wp_unslash( $_GET['post_type'] ) ); + $slugs[] = 'edit.php?post_type=' . $pt; + // Match submenu ids like edit.php?post_type=page::post-new.php?post_type=page (not only the list screen). + if ( in_array( $pagenow, array( 'post-new.php', 'post.php' ), true ) ) { + $slugs[] = $pagenow . '?post_type=' . $pt; + } + } elseif ( ! empty( $pagenow ) && empty( $_GET['page'] ) ) { + $slugs[] = $pagenow; + } + + if ( ! empty( $_GET['taxonomy'] ) && in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) { + $tax = sanitize_key( wp_unslash( $_GET['taxonomy'] ) ); + $slugs[] = 'edit-tags.php?taxonomy=' . $tax; + } + + // Also block related pages when a top-level menu is hidden. + // e.g. if edit.php is hidden, also block post-new.php and post.php (for default post type). + if ( in_array( $pagenow, array( 'post-new.php', 'post.php' ), true ) && empty( $_GET['post_type'] ) ) { + $slugs[] = 'edit.php'; + } + + /** + * Filter current screen slug list for URL blocking. + * + * @param array $slugs Slugs. + */ + return array_unique( array_filter( apply_filters( app()->namespace . '/current_screen_slugs', $slugs ) ) ); +} + +/** + * Whether a menu slug is a Members-added custom item (see inject_custom_menu_items). + * + * Only these items support overriding the admin menu link via URL in $menu / $submenu. + * + * @param string $slug Top-level slug or submenu file/slug segment. + * @return bool + */ +function members_am_is_custom_menu_item_slug( $slug ) { + $slug = (string) $slug; + return ( '' !== $slug && 0 === strpos( $slug, 'members-am-' ) ); +} + +/** + * Default option structure. + * + * @return array + */ +function get_default_settings() { + return members_admin_menus_default_settings_data(); +} + +/** + * Get plugin settings. + * + * @return array + */ +function get_settings() { + if ( isset( $GLOBALS['members_am_settings_runtime_cache'] ) && is_array( $GLOBALS['members_am_settings_runtime_cache'] ) ) { + return $GLOBALS['members_am_settings_runtime_cache']; + } + $settings = get_option( OPTION_KEY, array() ); + if ( ! is_array( $settings ) ) { + $settings = array(); + } + $GLOBALS['members_am_settings_runtime_cache'] = wp_parse_args( $settings, get_default_settings() ); + return $GLOBALS['members_am_settings_runtime_cache']; +} + +/** + * Store Admin Menus settings without autoloading the potentially large option. + * + * @param array $settings Settings array. + * @return bool Whether the value was updated. + */ +function update_settings_option( array $settings ) { + return update_option( OPTION_KEY, $settings, false ); +} + +/** + * Clear the in-request settings cache after option updates. + * + * @return void + */ +function members_am_invalidate_settings_cache() { + unset( $GLOBALS['members_am_settings_runtime_cache'] ); +} + +/** + * Remove Admin Menus role entries for slugs that no longer exist (e.g. after Members role reset). + * + * Only prunes `roles[ slug ]` keys; does not alter capability maps or other settings. + * + * @param array $role_slugs Role slugs to remove from stored settings. + * @return void + */ +function members_am_prune_role_settings( array $role_slugs ) { + if ( empty( $role_slugs ) ) { + return; + } + + $slugs = array_unique( array_filter( array_map( 'sanitize_key', $role_slugs ) ) ); + if ( empty( $slugs ) ) { + return; + } + + $settings = get_option( OPTION_KEY, array() ); + if ( ! is_array( $settings ) || empty( $settings['roles'] ) || ! is_array( $settings['roles'] ) ) { + return; + } + + $changed = false; + foreach ( $slugs as $slug ) { + if ( isset( $settings['roles'][ $slug ] ) ) { + unset( $settings['roles'][ $slug ] ); + $changed = true; + } + } + + if ( $changed ) { + update_settings_option( $settings ); + members_am_invalidate_settings_cache(); + } +} + +/** + * Add a user ID to the Admin Menus exempt list (used when administrators are restricted via admin_editable). + * + * Does not enable `admin_editable`. No-op if the user is not an administrator (or super admin on multisite). + * + * @param int $user_id User ID. + * @return void + */ +function members_am_add_exempt_administrator( $user_id ) { + $uid = absint( $user_id ); + if ( $uid < 1 ) { + return; + } + + $user = get_userdata( $uid ); + if ( ! $user ) { + return; + } + + $is_administrator = in_array( 'administrator', (array) $user->roles, true ); + if ( ! $is_administrator && ! ( is_multisite() && is_super_admin( $uid ) ) ) { + return; + } + + $settings = get_option( OPTION_KEY, array() ); + if ( ! is_array( $settings ) ) { + $settings = array(); + } + + if ( empty( $settings['_meta'] ) || ! is_array( $settings['_meta'] ) ) { + $settings['_meta'] = array(); + } + + $exempt_ids = array(); + if ( ! empty( $settings['_meta']['admin_menu_exempt_user_ids'] ) && is_array( $settings['_meta']['admin_menu_exempt_user_ids'] ) ) { + $exempt_ids = array_map( 'absint', $settings['_meta']['admin_menu_exempt_user_ids'] ); + } + + if ( in_array( $uid, $exempt_ids, true ) ) { + return; + } + + $exempt_ids[] = $uid; + $exempt_ids = array_unique( $exempt_ids ); + sort( $exempt_ids, SORT_NUMERIC ); + + $settings['_meta']['admin_menu_exempt_user_ids'] = $exempt_ids; + + update_settings_option( $settings ); + members_am_invalidate_settings_cache(); +} + +/** + * Whether user is exempt from all restrictions. + * + * @param int $user_id User ID. + * @return bool + */ +function is_user_exempt( $user_id ) { + $user = get_userdata( $user_id ); + if ( ! $user ) { + return true; + } + if ( is_multisite() && is_super_admin( $user_id ) ) { + return true; + } + + $meta = get_settings(); + $admin_editable = ! empty( $meta['_meta']['admin_editable'] ); + + if ( in_array( 'administrator', (array) $user->roles, true ) && ! $admin_editable ) { + return true; + } + + if ( $admin_editable && in_array( 'administrator', (array) $user->roles, true ) ) { + $exempt_ids = array(); + if ( ! empty( $meta['_meta']['admin_menu_exempt_user_ids'] ) && is_array( $meta['_meta']['admin_menu_exempt_user_ids'] ) ) { + $exempt_ids = array_map( 'absint', $meta['_meta']['admin_menu_exempt_user_ids'] ); + } + // Legacy or pre-migration: empty list would lock out every administrator — fail open until settings are saved again. + if ( empty( $exempt_ids ) ) { + return (bool) apply_filters( app()->namespace . '/is_user_exempt', true, $user_id ); + } + $is_exempt = in_array( (int) $user_id, $exempt_ids, true ); + return (bool) apply_filters( app()->namespace . '/is_user_exempt', $is_exempt, $user_id ); + } + + return (bool) apply_filters( app()->namespace . '/is_user_exempt', false, $user_id ); +} + +/** + * Resolved config for a user: roles merged + user blocks (menu overrides deep-merged per slug onto role merge). + * + * @param int $user_id User ID. + * @return array + */ +function get_resolved_config_for_user( $user_id ) { + static $cache = array(); + + $uid = absint( $user_id ); + if ( $uid < 1 ) { + return array(); + } + if ( isset( $cache[ $uid ] ) ) { + return $cache[ $uid ]; + } + + $settings = get_settings(); + $user = get_userdata( $uid ); + if ( ! $user ) { + $cache[ $uid ] = array(); + return array(); + } + + // Preserve role order from the user object (deterministic merge; first role wins override conflicts). + $roles = array_values( array_unique( array_filter( (array) $user->roles ) ) ); + + $base = get_resolved_config_for_user_from_roles_only( $settings, $roles ); + + // Phase 3: user-specific blocks. Menu overrides are deep-merged per slug onto the role-merged map so an + // empty or partial users[ id ].overrides entry does not wipe role styling (colors, labels, etc.). + if ( ! empty( $settings['users'][ $uid ] ) && is_array( $settings['users'][ $uid ] ) ) { + $u = $settings['users'][ $uid ]; + foreach ( array( 'hidden', 'order', 'submenu_order', 'custom_items', 'capabilities' ) as $k ) { + if ( isset( $u[ $k ] ) ) { + $base[ $k ] = $u[ $k ]; + } + } + if ( ! empty( $u['overrides'] ) && is_array( $u['overrides'] ) ) { + if ( ! isset( $base['overrides'] ) || ! is_array( $base['overrides'] ) ) { + $base['overrides'] = array(); + } + foreach ( $u['overrides'] as $slug => $user_ov ) { + if ( ! is_string( $slug ) || '' === $slug || ! is_array( $user_ov ) ) { + continue; + } + $slug = sanitize_text_field( $slug ); + $prev = isset( $base['overrides'][ $slug ] ) && is_array( $base['overrides'][ $slug ] ) ? $base['overrides'][ $slug ] : array(); + $merged_row = array_merge( $prev, $user_ov ); + if ( array() !== $merged_row ) { + $base['overrides'][ $slug ] = $merged_row; + } + } + } + } + + // Never hide or cap-gate Members recovery screens (sanitization + legacy/import defense). + if ( ! empty( $base['hidden'] ) && is_array( $base['hidden'] ) ) { + $nh = array(); + foreach ( $base['hidden'] as $h ) { + $h = is_string( $h ) ? sanitize_text_field( $h ) : ''; + if ( ! $h || members_am_is_protected_menu_slug( $h ) ) { + continue; + } + if ( ! in_array( $h, $nh, true ) ) { + $nh[] = $h; + } + } + $base['hidden'] = $nh; + } + if ( ! empty( $base['capabilities'] ) && is_array( $base['capabilities'] ) ) { + foreach ( array_keys( $base['capabilities'] ) as $cap_slug ) { + if ( ! is_string( $cap_slug ) ) { + continue; + } + if ( members_am_is_protected_menu_slug( sanitize_text_field( $cap_slug ) ) ) { + unset( $base['capabilities'][ $cap_slug ] ); + } + } + } + + $cache[ $uid ] = $base; + return $base; +} + +/** + * Resolve hidden from roles only (for merge helper). + * + * @param array $settings Settings. + * @param array $roles Role slugs. + * @return array + */ +function get_resolved_config_for_user_from_roles_only( $settings, $roles ) { + $roles = is_array( $roles ) ? array_values( array_filter( $roles ) ) : array(); + + // Union: if any role hides a menu slug, the combined user view treats it as hidden (matches deny-by-any-role). + $merged_hidden = array(); + foreach ( $roles as $role ) { + $rh = isset( $settings['roles'][ $role ]['hidden'] ) ? (array) $settings['roles'][ $role ]['hidden'] : array(); + foreach ( $rh as $h ) { + $h = sanitize_text_field( $h ); + if ( $h && ! in_array( $h, $merged_hidden, true ) ) { + $merged_hidden[] = $h; + } + } + } + + $order = array(); + $submenu_order = array(); + foreach ( $roles as $role ) { + if ( empty( $settings['roles'][ $role ] ) ) { + continue; + } + $r = $settings['roles'][ $role ]; + if ( ! empty( $r['order'] ) && empty( $order ) ) { + $order = (array) $r['order']; + } + if ( ! empty( $r['submenu_order'] ) && empty( $submenu_order ) ) { + $submenu_order = (array) $r['submenu_order']; + } + } + + // First role in $roles wins per slug: merge from last role toward first so earlier roles overwrite later ones on conflicts. + $overrides = array(); + foreach ( array_reverse( $roles ) as $role ) { + if ( ! empty( $settings['roles'][ $role ]['overrides'] ) ) { + $overrides = array_merge( (array) $settings['roles'][ $role ]['overrides'], $overrides ); + } + } + return array( + 'hidden' => $merged_hidden, + 'order' => $order, + 'submenu_order' => $submenu_order, + 'overrides' => $overrides, + 'custom_items' => isset( $settings['custom_items'] ) ? (array) $settings['custom_items'] : array(), + 'capabilities' => isset( $settings['capabilities'] ) ? (array) $settings['capabilities'] : array(), + ); +} diff --git a/addons/members-admin-menus/bootstrap/app.php b/addons/members-admin-menus/bootstrap/app.php new file mode 100644 index 0000000..3fe8030 --- /dev/null +++ b/addons/members-admin-menus/bootstrap/app.php @@ -0,0 +1,35 @@ +dir . 'app/functions.php'; + +if ( is_admin() ) { + require_once app()->dir . 'app/functions-admin.php'; +} diff --git a/addons/members-admin-menus/config/app.php b/addons/members-admin-menus/config/app.php new file mode 100644 index 0000000..b3b261a --- /dev/null +++ b/addons/members-admin-menus/config/app.php @@ -0,0 +1,16 @@ + trailingslashit( dirname( __DIR__ ) ), + 'namespace' => 'members/addons/admin_menus', +); diff --git a/addons/members-admin-menus/src/Activator.php b/addons/members-admin-menus/src/Activator.php new file mode 100644 index 0000000..a6e2ee3 --- /dev/null +++ b/addons/members-admin-menus/src/Activator.php @@ -0,0 +1,39 @@ + Add-Ons. + * + * @package Members + * @subpackage AddOns + */ + +namespace Members\AddOns\AdminMenus; + +defined( 'ABSPATH' ) || exit; + +/** + * Activator. + */ +class Activator { + + /** + * On activation, seed empty settings if missing. + * + * @return void + */ + public static function activate() { + $existing = get_option( 'members_admin_menus_settings', null ); + if ( null === $existing || ! is_array( $existing ) ) { + add_option( 'members_admin_menus_settings', self::get_default_option(), '', false ); + } + } + + /** + * Default empty structure (same as get_default_settings() in app/functions.php). + * + * @return array + */ + public static function get_default_option() { + require_once dirname( __DIR__ ) . '/app/defaults.php'; + return members_admin_menus_default_settings_data(); + } +} diff --git a/addons/members-category-and-tag-caps/src/Activator.php b/addons/members-category-and-tag-caps/src/Activator.php index c403541..f723bcc 100644 --- a/addons/members-category-and-tag-caps/src/Activator.php +++ b/addons/members-category-and-tag-caps/src/Activator.php @@ -30,6 +30,9 @@ class Activator { */ public static function activate() { + // Activator can run before addon.php loads filters (e.g. first activation). + require_once __DIR__ . '/functions-filters.php'; + // Get the administrator role. $role = get_role( 'administrator' ); @@ -47,5 +50,8 @@ public static function activate() { $role->add_cap( 'edit_post_tags' ); $role->add_cap( 'delete_post_tags' ); } + + // Roles with manage_categories could manage tags before granular caps; keep parity. + sync_post_tag_caps_for_roles_with_manage_categories(); } } diff --git a/addons/members-category-and-tag-caps/src/functions-filters.php b/addons/members-category-and-tag-caps/src/functions-filters.php index bde38bd..b9ccf91 100644 --- a/addons/members-category-and-tag-caps/src/functions-filters.php +++ b/addons/members-category-and-tag-caps/src/functions-filters.php @@ -99,3 +99,61 @@ return $caps; }, 10, 2 ); + +/** + * Grant post-tag granular caps to every role that can manage categories. + * + * Core mapped post_tag `manage_terms` to `manage_categories`. After this add-on remaps + * post_tag to `manage_post_tags`, roles cloned from Administrator (or otherwise missing the + * new primitives) still had `manage_categories` only — Admin Menus then showed "no access" + * for Tags while `current_user_can()` could still pass if another role granted the tag caps. + * + * Sites that need least-privilege (category management without automatic tag caps) can disable + * the sync with {@see 'members_ctc_sync_post_tag_caps_for_manage_categories_roles'}. + * + * @since 1.0.1 + * @return void + */ +function sync_post_tag_caps_for_roles_with_manage_categories() { + /** + * Whether to grant granular post_tag capabilities to every role that has `manage_categories`. + * + * Default true keeps compatibility with earlier add-on releases. Return false to skip the + * automatic grant (use custom role/cap management instead). + * + * @since 1.0.2 + * @param bool $sync Whether to add tag caps to qualifying roles. + */ + if ( ! apply_filters( 'members_ctc_sync_post_tag_caps_for_manage_categories_roles', true ) ) { + return; + } + + $tag_caps = array( 'manage_post_tags', 'assign_post_tags', 'edit_post_tags', 'delete_post_tags' ); + foreach ( wp_roles()->roles as $slug => $_role_info ) { + $role = get_role( $slug ); + if ( ! $role || ! $role->has_cap( 'manage_categories' ) ) { + continue; + } + foreach ( $tag_caps as $cap ) { + $role->add_cap( $cap ); + } + } +} + +/** + * One-time migration for sites that activated the add-on before tag caps were synced. + * + * @since 1.0.1 + * @return void + */ +function maybe_sync_post_tag_caps_roles_migration() { + if ( '1' === get_option( 'members_ctc_sync_post_tag_caps_roles_v1', '' ) ) { + return; + } + if ( apply_filters( 'members_ctc_sync_post_tag_caps_for_manage_categories_roles', true ) ) { + sync_post_tag_caps_for_roles_with_manage_categories(); + } + update_option( 'members_ctc_sync_post_tag_caps_roles_v1', '1', true ); +} + +add_action( 'init', __NAMESPACE__ . '\maybe_sync_post_tag_caps_roles_migration', 20 ); diff --git a/admin/class-settings.php b/admin/class-settings.php index 0521f5e..7090279 100644 --- a/admin/class-settings.php +++ b/admin/class-settings.php @@ -371,7 +371,18 @@ public function settings_page() { ?>
filter_links(); ?>
- get_view( members_get_current_settings_view() )->template(); ?> + get_view( members_get_current_settings_view() ); + if ( $_current_view ) { + $_current_view->template(); + } else { + // Fallback to General view if requested view doesn't exist. + $_fallback_view = $this->get_view( 'general' ); + if ( $_fallback_view ) { + $_fallback_view->template(); + } + } + ?> 'Allows site administrators to control which users have access to the WordPress admin via role.', ), + 'members-admin-menus' => array( + 'url' => '', + 'title' => 'Members - Admin Menus', + 'excerpt' => 'Full admin menu editor: hide, reorder, rename, and customize menu items per role with a visual sidebar editor.', + ), + 'members-core-create-caps' => array( 'url' => '', 'title' => 'Members - Core Create Caps', diff --git a/admin/functions-admin.php b/admin/functions-admin.php index 65067a3..43713a5 100644 --- a/admin/functions-admin.php +++ b/admin/functions-admin.php @@ -52,6 +52,16 @@ function members_admin_register_scripts() { $edit_role_ver = file_exists( $edit_role_file ) ? filemtime( $edit_role_file ) : false; wp_register_script( 'members-edit-role', members_plugin()->uri . "js/edit-role{$min}.js", array( 'postbox', 'wp-util' ), $edit_role_ver, true ); + $admin_menus_file = members_plugin()->dir . "js/admin-menus{$min}.js"; + $admin_menus_ver = file_exists( $admin_menus_file ) ? filemtime( $admin_menus_file ) : false; + wp_register_script( + 'members-admin-menus', + members_plugin()->uri . "js/admin-menus{$min}.js", + array( 'jquery', 'jquery-ui-sortable', 'wp-color-picker', 'wp-util' ), + $admin_menus_ver, + true + ); + // Localize our script with some text we want to pass in. $i18n = array( 'button_role_edit' => esc_html__( 'Edit', 'members' ), @@ -202,7 +212,7 @@ function members_add_pointers() { */ function members_admin_header() { - if ( members_is_memberpress_active() || empty( $_GET['page'] ) || ! in_array( $_GET['page'], array( 'roles', 'members', 'members-settings', 'members-about', 'members-payments', 'members-growth-tools' ) ) ) { + if ( members_is_memberpress_active() || empty( $_GET['page'] ) || ! in_array( $_GET['page'], array( 'roles', 'members', 'members-settings', 'members-about', 'members-payments', 'members-growth-tools', 'members-admin-menus' ), true ) ) { return; } @@ -324,7 +334,7 @@ function members_is_admin_page() { // Fallback: pages registered outside Settings_Page (e.g. the Growth Tools // submenu is registered by the caseproof/growth-tools composer package). - if ( ! empty( $_GET['page'] ) && in_array( $_GET['page'], array( 'roles', 'members', 'members-settings', 'members-about', 'members-payments', 'members-growth-tools' ), true ) ) { + if ( ! empty( $_GET['page'] ) && in_array( $_GET['page'], array( 'roles', 'members', 'members-settings', 'members-about', 'members-payments', 'members-growth-tools', 'members-admin-menus' ), true ) ) { return true; } diff --git a/admin/views/class-view-general.php b/admin/views/class-view-general.php index fd6b7a8..4608371 100644 --- a/admin/views/class-view-general.php +++ b/admin/views/class-view-general.php @@ -42,7 +42,7 @@ public function enqueue() { // Add reset roles data to the settings script (must run after enqueue). wp_localize_script( 'members-settings', 'membersResetRoles', array( 'nonce' => wp_create_nonce( 'members_reset_roles' ), - 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'ajaxurl' => admin_url( 'admin-ajax.php', 'relative' ), 'confirmMessage' => esc_html__( 'This will remove only roles created with Members and reset the five WordPress roles (Administrator, Editor, Author, Contributor, Subscriber) to their default capabilities. Roles from other plugins (e.g. WooCommerce) will not be removed. Continue?', 'members' ), 'successMessage' => esc_html__( 'Members-created roles have been removed and WordPress roles have been reset to their defaults.', 'members' ), 'errorMessage' => esc_html__( 'An error occurred while resetting roles. Please try again.', 'members' ) diff --git a/css/admin.css b/css/admin.css index d829714..2e166e3 100644 --- a/css/admin.css +++ b/css/admin.css @@ -1573,7 +1573,1861 @@ body.admin_page_members-about #wpbody-content { vertical-align: middle; } +/* Members — Admin Menus add-on (inherits active admin color scheme via --members-am-*; aligns with wp-admin theme variables when present) */ +.members-admin-menus-wrap { + max-width: none; + /* Prefer palette from wp_admin_css_color (inline) over :root defaults from other admin bundles. */ + --members-am-accent-use: var(--members-am-accent, var(--wp-admin-theme-color, #2271b1)); + --members-am-surface-use: var(--members-am-surface, color-mix(in srgb, var(--members-am-base, #1d2327) 88%, #fff 12%)); + /* Inline rules set --members-am-fg-on-base / --members-am-border-on-base from scheme luminance (e.g. Light = dark text). */ + --members-am-border-on-base-fallback: color-mix(in srgb, var(--members-am-base, #1d2327) 62%, #fff 38%); + --members-am-chrome-bg: var(--wp-components-color-gray-100, #f6f7f7); + --members-am-chrome-border: var(--wp-components-color-gray-400, #c3c4c7); + --members-am-muted: var(--wp-components-color-gray-600, #646970); +} + +.members-am-notices { + margin: 0 0 12px; +} + +.members-am-notices .notice { + margin: 0 0 8px; +} + +.members-admin-menus-toolbar { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0; + margin-bottom: 16px; + border: 1px solid var(--members-am-chrome-border, #c3c4c7); + border-radius: 2px; + background: var(--wp-components-color-background, #fff); + /* Must not clip the Reset dropdown (position absolute, extends below this box). */ + overflow: visible; +} + +.members-am-toolbar-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.members-am-toolbar-row--primary { + padding: 10px 12px; + justify-content: space-between; + row-gap: 10px; +} + +.members-am-toolbar-group { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.members-am-toolbar-group--document { + position: relative; + z-index: 5; + padding-right: 12px; + border-right: 1px solid var(--members-am-chrome-border, #c3c4c7); +} + +.members-am-toolbar-row--primary .members-am-toolbar-primary-user { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 0 1 auto; + min-width: 0; +} + +.members-am-toolbar-row--primary .members-am-toolbar-primary-user #members-am-user-search.members-am-user-search-input { + width: 11rem; + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.members-am-toolbar-extra-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px 16px; +} + +.members-am-toolbar-extra .members-am-toolbar-extra-io { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + flex: 0 0 auto; + margin-left: auto; + padding-left: 12px; + border-left: 1px solid var(--members-am-chrome-border, #c3c4c7); +} + +.members-am-toolbar-group--view { + margin-left: auto; + flex-wrap: nowrap; +} + +.members-am-toolbar-extra { + background: var(--members-am-chrome-bg, #f6f7f7); + border-top: 1px solid var(--members-am-chrome-border, #c3c4c7); + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.members-am-toolbar-extra[hidden] { + display: none !important; +} + +.members-am-toolbar-extra-hint { + margin: 0 0 8px; + max-width: 52rem; +} + +.members-am-toolbar-admin-editable-inline { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 0 1 auto; + white-space: normal; + max-width: min(100%, 22rem); +} + +.members-am-toolbar-extra .members-am-toolbar-admin-editable-inline { + max-width: min(100%, 26rem); +} + +.members-am-toolbar-row--extra-tools { + justify-content: flex-start; + align-items: center; + flex-wrap: wrap; + row-gap: 10px; +} + +.members-am-toolbar-row--extra-tools .members-am-copy-wrap { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-left: 4px; + padding-left: 12px; + border-left: 1px solid var(--members-am-chrome-border, #c3c4c7); +} + +/* Copy role controls inline with “Allow editing administrator menus” in More tools. */ +.members-am-toolbar-extra-row .members-am-copy-wrap--extra-inline { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 8px; + flex: 0 1 auto; + min-width: 0; + margin-left: 8px; + padding-left: 12px; + border-left: 1px solid var(--members-am-chrome-border, #c3c4c7); +} + +.members-am-toolbar-extra-row .members-am-copy-wrap--extra-inline .members-am-copy-confirm-area { + flex-basis: 100%; + width: 100%; +} + +.members-am-toolbar-extra .members-am-copy-select { + width: 11rem; + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.members-am-toolbar-row--exempt { + flex-basis: 100%; + width: 100%; + margin-top: 0; + padding-top: 10px; + border-top: 1px solid var(--members-am-chrome-border, #c3c4c7); +} + +.members-am-toolbar-extra .members-am-toolbar-row--exempt { + margin-top: 4px; + padding-top: 12px; +} + +.members-am-toolbar-extra .members-am-exempt-wrap { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.members-am-toolbar-extra .members-am-exempt-search { + width: min(22rem, 100%); + max-width: 100%; + box-sizing: border-box; +} + +.members-am-exempt-wrap .members-am-exempt-chips { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 8px 0; +} + +.members-am-exempt-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border: 1px solid var(--members-am-chrome-border, #c3c4c7); + border-radius: 4px; + background: var(--wp-components-color-background, #fff); + font-size: 13px; +} + +.members-am-exempt-chip .members-am-exempt-remove { + padding: 0 2px; + min-height: 0; + line-height: 1.3; +} + +/* Override core .button-link underline (it can fragment and appear only under the SVG chevron). */ +.members-am-more-tools, +.members-am-more-tools:hover, +.members-am-more-tools:focus, +.members-am-more-tools:focus-visible, +.members-am-more-tools:active { + text-decoration: none !important; +} + +.members-am-more-tools { + display: inline-flex; + align-items: center; + gap: 4px; + white-space: nowrap; + padding: 2px 2px 2px 4px; + vertical-align: middle; + line-height: 1.4; +} + +.members-am-more-tools .members-am-more-tools-text, +.members-am-more-tools:hover .members-am-more-tools-text, +.members-am-more-tools:focus .members-am-more-tools-text, +.members-am-more-tools:focus-visible .members-am-more-tools-text, +.members-am-more-tools:active .members-am-more-tools-text, +.members-am-more-tools .members-am-more-tools-chevron, +.members-am-more-tools:hover .members-am-more-tools-chevron, +.members-am-more-tools:focus .members-am-more-tools-chevron, +.members-am-more-tools:focus-visible .members-am-more-tools-chevron, +.members-am-more-tools:active .members-am-more-tools-chevron, +.members-am-more-tools .members-am-more-tools-chevron-svg, +.members-am-more-tools:hover .members-am-more-tools-chevron-svg, +.members-am-more-tools:focus .members-am-more-tools-chevron-svg, +.members-am-more-tools:focus-visible .members-am-more-tools-chevron-svg, +.members-am-more-tools:active .members-am-more-tools-chevron-svg { + text-decoration: none !important; +} + +.members-am-more-tools .members-am-more-tools-text { + display: inline-flex; + align-items: center; + line-height: 1.4; +} + +/* Inline SVG chevron (replaces Dashicon) so collapsed/expanded states share the same vertical metrics as the label. */ +.members-am-more-tools .members-am-more-tools-chevron { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + line-height: 0; + opacity: 0.88; +} + +.members-am-more-tools .members-am-more-tools-chevron-svg { + display: block; + overflow: visible; + transition: transform 0.15s ease; + transform-origin: 50% 50%; + shape-rendering: geometricPrecision; +} + +.members-am-more-tools .members-am-more-tools-chevron-svg polygon { + stroke: none; +} + +.members-am-more-tools.is-open .members-am-more-tools-chevron-svg { + transform: rotate(180deg); +} + +@media (prefers-reduced-motion: reduce) { + .members-am-more-tools .members-am-more-tools-chevron-svg { + transition: none; + } +} + +@media screen and (max-width: 782px) { + .members-am-toolbar-group--document { + border-right: none; + padding-right: 0; + border-bottom: 1px solid var(--members-am-chrome-border, #c3c4c7); + padding-bottom: 10px; + width: 100%; + } + + .members-am-toolbar-group--view { + margin-left: 0; + width: 100%; + justify-content: space-between; + } + + .members-am-toolbar-row--primary .members-am-toolbar-primary-user { + flex: 1 1 100%; + width: 100%; + padding-top: 8px; + border-top: 1px solid var(--members-am-chrome-border, #c3c4c7); + } + + .members-am-toolbar-row--primary .members-am-toolbar-primary-user #members-am-user-search.members-am-user-search-input { + width: 100%; + max-width: 100%; + } + + .members-am-toolbar-extra .members-am-copy-select { + width: 100%; + max-width: 100%; + } + + .members-am-toolbar-extra .members-am-toolbar-admin-editable-inline { + width: 100%; + max-width: none; + } + + .members-am-toolbar-extra .members-am-toolbar-extra-io { + margin-left: 0; + padding-left: 0; + border-left: none; + padding-top: 8px; + border-top: 1px solid var(--members-am-chrome-border, #dcdcde); + width: 100%; + } + + .members-am-toolbar-extra-row .members-am-copy-wrap--extra-inline { + flex: 1 1 100%; + width: 100%; + margin-left: 0; + padding-left: 0; + border-left: none; + padding-top: 8px; + border-top: 1px solid var(--members-am-chrome-border, #dcdcde); + } + +} + +.members-am-toolbar-loading { + display: inline-flex; + align-items: center; + gap: 8px; + margin-left: 4px; + color: var(--members-am-muted, #646970); + font-size: 13px; + line-height: 1; +} + +.members-am-toolbar-loading .spinner { + float: none; + margin: 0; +} + +.members-am-toolbar-loading[hidden] { + display: none !important; +} + +.members-am-copy-wrap label { + margin-right: 8px; +} + +.members-am-copy-confirm-area { + margin-top: 8px; + width: 100%; + flex-basis: 100%; +} + +.members-am-inline-copy-notice { + margin: 0; +} + +.members-am-info-bar { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; + margin: 0 0 12px; + padding: 10px 12px; + background: #fcf9e8; + border: 1px solid #dba617; + border-radius: 2px; + font-size: 12px; + color: var(--wp-components-color-foreground, #1d2327); + box-sizing: border-box; +} + +/* HIDDEN + NO ACCESS: one row, grouped from the start (no space-between stretch). */ +.members-am-info-bar-legends { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: flex-start; + gap: 12px 20px; + min-width: 0; +} + +.members-am-info-bar-legends .members-am-info-item { + flex: 0 1 auto; + min-width: 0; + max-width: none; + overflow-wrap: break-word; +} + +.members-am-info-item--note { + width: 100%; + box-sizing: border-box; + font-style: italic; + color: var(--wp-components-color-gray-700, #50575e); +} + +.members-am-info-icon { + margin-right: 4px; + vertical-align: text-bottom; +} + +.members-am-legend-hidden-mark { + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 6px; + padding: 2px 5px; + border-radius: 2px; + vertical-align: text-bottom; + background: #d63638; + color: #fff; +} + +.members-am-legend-hidden-mark .dashicons { + font-size: 14px; + width: 14px; + height: 14px; + line-height: 1; +} + +.members-am-chips-wrap { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.members-am-chips-actions { + display: inline-flex; + flex-wrap: wrap; + gap: 6px; + flex-shrink: 0; +} + +.members-am-chips, +.members-am-chips-inner { + display: flex; + flex-wrap: wrap; + gap: 8px; + flex: 1 1 auto; + min-width: 0; +} + +.members-am-columns-empty { + margin: 0; + padding: 24px 12px; + text-align: center; + font-size: 13px; + color: var(--members-am-muted, #646970); + flex: 1 1 100%; + align-self: center; +} + +/* Advanced item editor — floating centered panel (viewport overlay) */ +.members-am-edit-popover-root:not([hidden]) { + position: fixed; + inset: 0; + z-index: 100001; + display: flex; + align-items: center; + justify-content: center; + padding: min(24px, 4vw); + box-sizing: border-box; +} + +.members-am-edit-popover-root[hidden] { + display: none !important; +} + +.members-am-edit-popover-overlay { + position: absolute; + inset: 0; + z-index: 0; + background: rgba(20, 24, 28, 0.42); +} + +.members-am-edit-popover-dialog { + position: relative; + z-index: 1; + flex: 0 1 auto; + width: min(680px, calc(100vw - 32px)); + max-width: 100%; + max-height: min(92vh, calc(100vh - 32px)); + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; + background: var(--wp-components-color-background, #fff); + border: 1px solid var(--members-am-chrome-border, #c3c4c7); + border-radius: 8px; + box-shadow: + 0 4px 6px rgba(0, 0, 0, 0.06), + 0 16px 48px rgba(0, 0, 0, 0.18), + 0 0 0 1px rgba(0, 0, 0, 0.04); +} + +.members-am-edit-popover-arrow { + display: none; +} + +.members-am-edit-popover-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + background: var(--members-am-base, #1d2327); + color: var(--members-am-fg-on-base, #f0f0f1); + flex-shrink: 0; +} + +.members-am-edit-popover-heading { + min-width: 0; + flex: 1; +} + +.members-am-edit-popover-title { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1.3; + color: inherit; +} + +.members-am-edit-popover-subtitle { + margin: 3px 0 0; + font-size: 11px; + font-weight: 400; + opacity: 0.65; + color: inherit; +} + +.members-am-edit-popover-close { + color: color-mix(in srgb, currentColor 62%, transparent) !important; + font-size: 18px; + line-height: 1; + padding: 0 4px !important; + text-decoration: none !important; +} + +.members-am-edit-popover-close:hover, +.members-am-edit-popover-close:focus { + color: inherit !important; +} + +.members-am-edit-popover-placeholder { + margin: 0 12px 12px; +} + +.members-am-edit-popover-chrome.members-am-edit-toolbar { + display: block; + margin: 0; +} + +.members-am-edit-popover-scope { + padding: 10px 16px; + border-bottom: 1px solid var(--members-am-chrome-border, #dcdcde); + background: var(--members-am-chrome-bg, #f6f7f7); + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + font-size: 12px; + color: var(--members-am-muted, #646970); + flex-shrink: 0; +} + +.members-am-edit-popover-body { + padding: 16px 18px; + overflow-y: auto; + flex: 1 1 auto; + min-height: 0; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; +} + +.members-am-edit-popover-footer { + padding: 12px 16px; + border-top: 1px solid var(--members-am-chrome-border, #dcdcde); + background: var(--members-am-chrome-bg, #f6f7f7); + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + justify-content: flex-end; + flex-shrink: 0; +} + +.members-am-edit-target-wrap { + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.members-am-edit-target-label { + font-weight: 600; +} + +.members-am-edit-target-select { + flex: 1 1 200px; + min-width: min(200px, 100%); + max-width: 100%; + font-size: 13px; + padding: 4px 8px; + min-height: 30px; +} + +.members-am-edit-section-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--members-am-muted, #646970); + margin: 0 0 6px; + padding-bottom: 4px; + border-bottom: 1px solid var(--members-am-chrome-border, #dcdcde); +} + +.members-am-edit-popover-actions { + padding: 10px 16px; + border-bottom: 1px solid var(--members-am-chrome-border, #dcdcde); + background: var(--members-am-chrome-bg, #f6f7f7); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.members-am-btn-danger { + color: #b32d2e !important; + border-color: #d63638 !important; + box-shadow: none !important; +} + +/* Core .button display can override the UA [hidden] rule; keep destructive control off-layout for core items. */ +#members-am-remove-custom[hidden] { + display: none !important; +} + +.members-am-btn-danger:hover { + color: #fff !important; + background: #d63638 !important; + border-color: #d63638 !important; +} + +.members-am-demote-parent-label { + font-size: 13px; + margin-right: 4px; +} + +.members-am-edit-row-title-url { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 12px; +} + +.members-am-edit-field label { + display: block; + font-weight: 600; + margin-bottom: 4px; +} + +.members-am-edit-section { + margin: 0; + padding: 0; + border: none; +} + +.members-am-edit-section-title { + margin: 0 0 10px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--members-am-muted, #646970); +} + +.members-am-edit-badge-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 12px; +} + +.members-am-badge-preview-row { + display: flex; + align-items: center; + gap: 10px; +} + +.members-am-badge-preview-label { + font-size: 12px; + color: var(--members-am-muted, #646970); +} + +.members-am-badge-preview { + display: inline-block; + min-width: 48px; + padding: 3px 10px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.members-am-edit-colors-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px 32px; +} + +.members-am-edit-color-field { + margin: 0; +} + +.members-am-edit-color-field label { + display: block; + margin: 0 0 6px; +} + +.members-am-edit-color-field .wp-picker-container { + display: inline-block; + max-width: 100%; +} + +.members-am-edit-cap-field { + margin-top: 12px; +} + +.members-am-edit-cap-field label { + display: block; + font-weight: 600; + margin-bottom: 4px; +} + +@media screen and (max-width: 600px) { + .members-am-edit-row-title-url, + .members-am-edit-badge-row, + .members-am-edit-colors-grid { + grid-template-columns: 1fr; + } +} + +.members-am-modal[hidden] { + display: none !important; +} + +.members-am-modal:not([hidden]) { + position: fixed; + inset: 0; + z-index: 100000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px 12px; + box-sizing: border-box; +} + +.members-am-modal-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.45); +} + +.members-am-modal-dialog { + position: relative; + z-index: 1; + width: 100%; + max-width: 480px; + max-height: 90vh; + overflow: auto; + background: var(--wp-components-color-background, #fff); + border-radius: 4px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); +} + +.members-am-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--members-am-chrome-border, #c3c4c7); +} + +.members-am-modal-header h2 { + margin: 0; + font-size: 16px; +} + +.members-am-modal-body { + padding: 12px 16px; +} + +.members-am-modal-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--members-am-chrome-border, #c3c4c7); +} + +.members-am-chip { + display: inline-flex; + align-items: center; + gap: 8px; + border-radius: 999px; + padding: 4px 12px 4px 8px; + border: 1px solid var(--members-am-chrome-border, #c3c4c7); + background: var(--wp-components-color-background, #fff); + cursor: pointer; +} + +.members-am-chip-mark { + width: 14px; + height: 14px; + border-radius: 3px; + border: 2px solid var(--members-am-role-accent, #8c8f94); + background: var(--wp-components-color-background, #fff); + flex-shrink: 0; + box-sizing: border-box; +} + +.members-am-chip.is-active { + background: color-mix(in srgb, var(--members-am-role-accent, var(--members-am-accent-use)) 12%, var(--wp-components-color-background, #fff)); + border-color: var(--members-am-role-accent, var(--members-am-accent-use)); + color: var(--members-am-base, #1d2327); +} + +.members-am-chip.is-active .members-am-chip-mark { + background: var(--members-am-role-accent, var(--members-am-accent-use)); + border-color: var(--members-am-role-accent, var(--members-am-accent-use)); + box-shadow: inset 0 0 0 2px var(--wp-components-color-background, #fff); +} + +.members-am-chip-label { + font-size: 13px; + line-height: 1.2; +} + +/* Role columns: one horizontal row with scroll (matches admin-menus-v5 mockup; no carousel). */ +.members-am-cols-host { + margin-bottom: 12px; +} + +.members-am-cols-wrap { + display: flex; + justify-content: safe center; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + padding-bottom: 6px; + scroll-behavior: smooth; +} + +.members-am-cols-inner { + display: block; + flex: 0 1 auto; + min-width: 0; + max-width: 100%; +} + +.members-am-columns { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + gap: 14px; + width: max-content; + min-height: 320px; + margin: 0 auto; +} + +.members-am-column { + /* Fixed track width for role / user columns. */ + flex: 0 0 350px; + width: 350px; + min-width: 350px; + max-width: 350px; + flex-shrink: 0; + background: var(--members-am-base, #1d2327); + border: 1px solid var(--members-am-border-on-base, var(--members-am-border-on-base-fallback)); + border-radius: 4px; + color: var(--members-am-fg-on-base, #f0f0f1); + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.members-am-role-chip-wrap { + display: inline-flex; + align-items: center; + vertical-align: middle; +} + +.members-am-role-chip-cb { + margin: 0; + flex-shrink: 0; + align-self: center; + accent-color: var(--members-am-role-accent, var(--members-am-accent-use)); +} + +.members-am-chip-pill { + padding: 4px 10px 4px 8px; + border-radius: 999px; + border: 1px solid transparent; + gap: 6px; +} + +.members-am-chip-pill.is-active { + background: var(--members-am-role-accent, var(--members-am-accent-use)); + border-color: var(--members-am-role-accent, var(--members-am-accent-use)); + color: #fff; +} + +.members-am-chip-pill.is-active .members-am-role-chip-cb { + accent-color: #fff; +} + +.members-am-chip-pill.members-am-chip--inactive { + opacity: 0.45; + background: var(--wp-components-color-background, #fff); + border-color: var(--members-am-chrome-border, #c3c4c7); + color: var(--members-am-base, #1d2327); +} + +.members-am-chip-pill.members-am-chip--inactive .members-am-chip-pill-action { + text-decoration: line-through; +} + +.members-am-chip-pill-action { + border: 0; + background: transparent; + padding: 0; + margin: 0; + font: inherit; + font-size: 13px; + line-height: 1.2; + color: inherit; + cursor: pointer; + text-align: left; + min-width: 0; + box-shadow: none; +} + +.members-am-chip-pill-action:hover, +.members-am-chip-pill-action:focus { + color: inherit; +} + +.members-am-chip-pill-action:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.members-am-user-column { + border: 2px dashed var(--wp-components-color-gray-700, #8c8f94); +} + +.members-am-sidebar-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + padding: 9px 10px; + background: linear-gradient(180deg, var(--members-am-surface-use) 0%, var(--members-am-base, #1d2327) 100%); + border-radius: 4px 4px 0 0; + border-bottom: 1px solid var(--members-am-border-on-base, var(--members-am-border-on-base-fallback)); + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--members-am-fg-on-base, #f0f0f1); +} + +.members-am-sidebar-head-titles { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1 1 auto; +} + +.members-am-sidebar-role-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + background: var(--members-am-accent-use); +} + +.members-am-sidebar-role-dot--user { + background: #a7aaad; +} + +.members-am-sidebar-head .members-am-sidebar-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.members-am-col-filter { + padding: 6px 8px 8px; + border-bottom: 1px solid var(--members-am-border-on-base, var(--members-am-border-on-base-fallback)); + flex-shrink: 0; +} + +.members-am-col-filter-input { + width: 100%; + max-width: 100%; + box-sizing: border-box; + font-size: 12px; + line-height: 1.4; + padding: 4px 8px; + border-radius: 2px; + border: 1px solid var(--members-am-border-on-base, var(--members-am-border-on-base-fallback)); + background: var(--members-am-base, #1d2327); + color: var(--members-am-fg-on-base, #f0f0f1); +} + +.members-am-col-filter-input::placeholder { + color: var(--members-am-fg-muted-on-base, var(--wp-components-color-gray-700, #8c8f94)); + opacity: 1; +} + +.members-am-col-filter-input:focus { + border-color: var(--members-am-accent-use); + outline: 1px solid transparent; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1px) var(--members-am-accent-use); +} + +.members-am-col-bulk { + padding: 8px 8px 10px; + border-bottom: 1px solid var(--members-am-border-on-base, var(--members-am-border-on-base-fallback)); + flex-shrink: 0; + background: rgba(0, 0, 0, 0.12); +} + +.members-am-col-bulk-toolbar { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 0; + align-items: stretch; +} + +/* Single compact grid: selection + expand/collapse (replaces two stacked toolbars). */ +.members-am-col-bulk-toolbar--grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 5px; + margin-bottom: 8px; +} + +.members-am-col-bulk-toolbar--grid .button.button-small { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + text-align: center; + white-space: normal; + line-height: 1.25; + padding: 5px 6px; + min-height: 0; +} + +.members-am-col-bulk-toolbar .button.button-small { + font-size: 11px; + min-height: 26px; + line-height: 24px; + padding: 0 8px; + background: var(--members-am-surface-use); + border-color: var(--members-am-border-on-surface, var(--members-am-border-on-base, var(--members-am-border-on-base-fallback))); + color: var(--members-am-fg-on-surface, var(--members-am-fg-on-base, #f0f0f1)); +} + +.members-am-col-bulk-toolbar .button.button-small:hover { + background: color-mix(in srgb, var(--members-am-surface-use) 85%, #fff 15%); + border-color: var(--wp-components-color-gray-700, #8c8f94); + color: var(--members-am-fg-on-surface, var(--members-am-fg-on-base, #f0f0f1)); +} + +.members-am-item-lead { + flex-shrink: 0; + display: flex; + align-items: flex-start; + justify-content: center; + width: 22px; + padding: 2px 0 0; +} + +.members-am-collapse-spacer { + display: block; + width: 20px; + height: 20px; + flex-shrink: 0; +} + +.members-am-collapse-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + margin: 0; + padding: 0; + border: none; + background: transparent; + color: var(--members-am-fg-muted-on-base, var(--members-am-chrome-border, #c3c4c7)); + border-radius: 2px; + cursor: pointer; + flex-shrink: 0; +} + +.members-am-collapse-toggle:hover, +.members-am-collapse-toggle:focus { + color: var(--members-am-fg-on-base, #f0f0f1); + background: color-mix(in srgb, var(--members-am-fg-on-base, #f0f0f1) 12%, transparent); + outline: none; +} + +.members-am-collapse-toggle .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +.members-am-item-cb-wrap { + flex-shrink: 0; + display: flex; + align-items: flex-start; + padding: 2px 4px 0 0; +} + +.members-am-item-cb { + margin: 0; + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.members-am-bulk-select { + width: 100%; + max-width: 100%; + box-sizing: border-box; + font-size: 12px; + line-height: 1.4; + padding: 4px 8px; + border-radius: 2px; + border: 1px solid var(--members-am-border-on-base, var(--members-am-border-on-base-fallback)); + background: var(--members-am-base, #1d2327); + color: var(--members-am-fg-on-base, #f0f0f1); +} + +.members-am-bulk-select:focus { + border-color: var(--members-am-accent-use); + outline: 1px solid transparent; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1px) var(--members-am-accent-use); +} + +.members-am-bulk-select optgroup { + font-weight: 600; + color: #a7aaad; + background: var(--members-am-base, #1d2327); +} + +.members-am-bulk-select option { + color: var(--members-am-fg-on-base, #f0f0f1); + background: var(--members-am-base, #1d2327); +} + +.members-am-item.members-am-filter-hidden, +.members-am-sep.members-am-filter-hidden, +.members-am-item.members-am-collapse-hidden { + display: none !important; +} + +.members-am-col-move { + display: inline-flex; + align-items: center; + gap: 3px; + flex-shrink: 0; +} + +.members-am-sidebar-head .members-am-col-move-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + min-height: 28px; + padding: 0 !important; + margin: 0; + line-height: 1; + background: color-mix(in srgb, currentColor 8%, transparent); + border: 1px solid color-mix(in srgb, currentColor 24%, transparent); + color: inherit; + border-radius: 3px; +} + +.members-am-sidebar-head .members-am-col-move-btn .dashicons { + width: 18px; + height: 18px; + font-size: 18px; + line-height: 1; + opacity: 0.92; +} + +.members-am-sidebar-head .members-am-col-move-btn:hover, +.members-am-sidebar-head .members-am-col-move-btn:focus { + background: color-mix(in srgb, currentColor 16%, transparent); + border-color: color-mix(in srgb, currentColor 42%, transparent); + color: inherit; +} + +.members-am-sidebar-head .members-am-user-col-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + min-height: 28px; + padding: 0 !important; + margin: 0; + background: color-mix(in srgb, currentColor 8%, transparent); + border: 1px solid color-mix(in srgb, currentColor 24%, transparent); + color: inherit; + border-radius: 3px; +} + +.members-am-sidebar-head .members-am-user-col-close .dashicons { + width: 18px; + height: 18px; + font-size: 18px; + line-height: 1; +} + +.members-am-sidebar-head .members-am-user-col-close:hover, +.members-am-sidebar-head .members-am-user-col-close:focus { + background: rgba(214, 54, 56, 0.35); + border-color: rgba(214, 54, 56, 0.65); + color: #fff; +} + +.members-am-sidebar-list { + padding: 6px 0 12px; + overflow-y: auto; + max-height: 560px; +} + +.members-am-item { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + padding: 6px 10px 6px 6px; + cursor: pointer; + border-left: 3px solid transparent; + font-size: 13px; +} + +.members-am-item.is-sub { + padding-left: 22px; + font-size: 12px; + opacity: 0.95; +} + +.members-am-item.is-sub-deep { + padding-left: 36px; +} + +.members-am-sidebar-list .members-am-item { + cursor: grab; +} + +.members-am-sidebar-list .members-am-item:active { + cursor: grabbing; +} + +.members-am-sort-placeholder { + box-sizing: border-box; + width: 100%; + min-height: 36px; + margin: 2px 0; + background: color-mix(in srgb, var(--members-am-accent-use) 8%, transparent); + border: 1px dashed var(--members-am-accent-use); + border-radius: 2px; +} + +/* Keep drag helper the same width as the column list so flex row (checkbox / label) does not collapse. */ +.members-am-sidebar-list > .ui-sortable-helper { + box-sizing: border-box; + width: 100% !important; + max-width: 100%; +} + +.members-am-item.is-hidden { + opacity: 0.72; +} + +.members-am-item.is-no-cap { + opacity: 0.55; +} + +.members-am-item.is-no-cap.is-hidden { + opacity: 0.5; +} + +.members-am-item.is-selected { + border-left-color: var(--members-am-accent-use); + background: color-mix(in srgb, var(--members-am-accent-use) 15%, transparent); +} + +.members-am-item-main { + flex: 1; + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.members-am-item-main .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +.members-am-item-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.members-am-item-actions { + display: none; + gap: 2px; +} + +.members-am-item:hover .members-am-item-actions { + display: flex; +} + +.members-am-item-actions button { + min-height: 22px; + padding: 0 4px; + line-height: 20px; + font-size: 11px; +} + +.members-am-item-actions button.members-am-eye, +.members-am-item-actions button.members-am-user-eye { + padding: 0 2px; +} + +.members-am-item-actions button.members-am-eye .dashicons, +.members-am-item-actions button.members-am-user-eye .dashicons { + font-size: 16px; + width: 16px; + height: 16px; + line-height: 1.2; +} + +.members-am-sep { + text-align: center; + opacity: 0.35; + font-size: 11px; + padding: 4px 0; +} + +.members-am-badge { + font-size: 9px; + text-transform: uppercase; + padding: 1px 4px; + border-radius: 2px; + margin-right: 4px; + vertical-align: middle; +} + +.members-am-badge-new { + background: var(--members-am-accent-use); + color: var(--wp-components-color-accent-inverted, #fff); +} + +.members-am-badge-edit { + background: #dba617; + color: var(--members-am-base, #1d2327); +} + +.members-am-badge-nocap { + display: inline-flex; + align-items: center; + justify-content: center; + background: #dba617; + color: var(--members-am-base, #1d2327); + padding: 2px 5px; + border-radius: 2px; +} + +.members-am-badge-nocap-icon { + font-size: 14px; + width: 14px; + height: 14px; + line-height: 1; +} + +.members-am-badge-hidden { + display: inline-flex; + align-items: center; + justify-content: center; + background: #d63638; + color: #fff; + padding: 2px 5px; + border-radius: 2px; +} + +.members-am-badge-hidden-icon { + font-size: 14px; + width: 14px; + height: 14px; + line-height: 1; +} + +/* Row color overrides must not recolor badge UI icons (inline color on .dashicons). */ +.members-am-badge-hidden .members-am-badge-hidden-icon, +.members-am-badge-nocap .members-am-badge-nocap-icon { + color: inherit !important; +} + +.members-am-legend { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin: 0 0 8px; + padding: 0; + font-size: 12px; + color: var(--members-am-muted, #646970); +} + +.members-am-legend-item { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.members-am-edit-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.members-am-level-actions .button { + margin-right: 6px; +} + +.members-am-demote-wrap { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-right: 6px; +} + +.members-am-demote-wrap .members-am-demote-select { + min-width: 200px; + max-width: min(420px, 100%); + font-size: 13px; +} + +.members-am-edit-grid { + display: flex; + flex-direction: column; + gap: 24px; +} + +.members-am-edit-col label { + display: block; + font-weight: 600; + margin-bottom: 4px; +} + +.members-am-colors-hint { + margin: 0 0 10px; +} + +/* Expandable edit sections (native
) — Admin Menus popover */ +.members-am-expand-details { + border: 1px solid var(--wp-components-color-gray-200, #dcdcde); + border-radius: 4px; + background: var(--wp-components-color-background, #fff); + margin: 0; +} + +.members-am-expand-details-summary { + cursor: pointer; + list-style: none; + display: flex; + align-items: flex-start; + gap: 8px; + padding: 10px 12px; + margin: 0; + font-weight: normal; +} + +.members-am-expand-details-summary::-webkit-details-marker, +.members-am-expand-details-summary::marker { + display: none; +} + +.members-am-expand-details-summary::before { + content: ''; + flex-shrink: 0; + width: 0; + height: 0; + margin-top: 5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid var(--members-am-muted, #646970); + transform: rotate(0deg); + transition: transform 0.12s ease; +} + +.members-am-expand-details[open] > .members-am-expand-details-summary::before { + transform: rotate(90deg); +} + +.members-am-expand-summary-text { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + flex: 1; +} + +.members-am-expand-details-summary .members-am-edit-section-title { + margin: 0; + font-size: 14px; +} + +.members-am-expand-current-summary { + margin: 0; + font-size: 13px; + line-height: 1.4; + color: var(--members-am-fg-muted-on-base, #646970); +} + +.members-am-expand-details[open] > .members-am-expand-panel { + border-top: 1px solid var(--wp-components-color-gray-200, #dcdcde); +} + +.members-am-expand-panel { + padding: 12px; + margin: 0; +} + +.members-am-icon-heading-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 4px; +} + +.members-am-icon-heading-row .members-am-edit-section-title { + margin: 0; +} + +.members-am-icon-current-summary { + margin: 0 0 12px; + font-size: 13px; +} + +.members-am-icon-panel > .members-am-icon-tabs { + margin-top: 0; +} + +.members-am-icon-tabs { + margin-bottom: 8px; +} + +.members-am-icon-tabs .button { + margin-right: 4px; + margin-bottom: 4px; +} + +.members-am-icon-tabs .button.is-active { + background: var(--members-am-accent-use); + border-color: var(--members-am-accent-use); + color: var(--wp-components-color-accent-inverted, #fff); +} + +.members-am-icon-grid { + display: flex; + flex-wrap: wrap; + gap: 5px; + max-height: min(240px, 38vh); + overflow-y: auto; + border: 1px solid var(--wp-components-color-gray-200, #dcdcde); + border-radius: 4px; + padding: 10px; + background: var(--members-am-chrome-bg, #f6f7f7); + margin-bottom: 10px; +} + +.members-am-icon-pick { + width: 36px; + height: 36px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--members-am-chrome-border, #c3c4c7); + background: var(--wp-components-color-background, #fff); + cursor: pointer; +} + +.members-am-bulk-visibility-hint { + margin: 0 0 8px; + font-size: 12px; + color: var(--members-am-fg-muted-on-base, #646970); +} + +.members-am-vis-role-filter-wrap { + margin-bottom: 8px; +} + +.members-am-vis-row.members-am-vis-filter-hidden { + display: none; +} + +.members-am-vis-row { + display: block; + margin-bottom: 6px; + font-weight: normal; +} + +.members-am-reset-dropdown { + position: absolute; + z-index: 100000; + background: var(--wp-components-color-background, #fff); + border: 1px solid var(--members-am-chrome-border, #c3c4c7); + border-radius: 6px; + padding: 0; + display: flex; + flex-direction: column; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + min-width: 280px; + top: 100%; + left: 0; + margin-top: 4px; + overflow: hidden; +} + +.members-am-reset-title { + padding: 10px 14px; + font-weight: 600; + font-size: 13px; + border-bottom: 1px solid var(--wp-components-color-gray-200, #f0f0f0); + color: var(--wp-components-color-foreground, #1d2327); +} + +.members-am-reset-dropdown > .members-am-reset-option:last-of-type { + border-bottom: none; +} + +.members-am-reset-option { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border: none; + background: none; + cursor: pointer; + text-align: left; + width: 100%; + border-bottom: 1px solid var(--wp-components-color-gray-200, #f0f0f0); + transition: background 0.15s; +} + +.members-am-reset-option:hover { + background: var(--members-am-chrome-bg, #f6f7f7); +} + +.members-am-reset-option .dashicons { + margin-top: 2px; + color: var(--members-am-muted, #646970); +} + +.members-am-reset-option-text { + display: flex; + flex-direction: column; + gap: 2px; +} + +.members-am-reset-option-text strong { + font-size: 13px; + color: var(--wp-components-color-foreground, #1d2327); +} + +.members-am-reset-option-text small { + font-size: 12px; + color: var(--members-am-muted, #646970); +} + +.members-am-reset-danger:hover { + background: #fcf0f1; +} + +.members-am-reset-danger .dashicons { + color: #d63638; +} + +.members-am-reset-danger:hover strong { + color: #d63638; +} + +@media screen and (max-width: 782px) { + /* Keep columns in one scrollable row on narrow admin (same as v5). */ + .members-am-columns { + flex-direction: row; + } +} + +.members-am-user-suggestions, +.members-am-exempt-suggestions { + position: absolute; + z-index: 100; + background: var(--wp-components-color-background, #fff); + border: 1px solid var(--members-am-chrome-border, #c3c4c7); + border-radius: 4px; + max-height: 200px; + overflow-y: auto; + width: 100%; + box-shadow: 0 2px 6px rgba(0,0,0,0.15); + top: 100%; + left: 0; +} + +.members-am-user-suggestion, +.members-am-exempt-suggestion { + padding: 8px 12px; + cursor: pointer; + font-size: 13px; + border-bottom: 1px solid var(--wp-components-color-gray-200, #f0f0f0); +} + +.members-am-user-suggestion:hover, +.members-am-exempt-suggestion:hover { + background: color-mix(in srgb, var(--members-am-accent-use) 8%, var(--wp-components-color-background, #fff)); +} + +.members-am-user-suggestion:last-child, +.members-am-exempt-suggestion:last-child { + border-bottom: none; +} + +/* Admin Menus screen — toolbar & edit panel (moved from inline styles in functions-admin.php) */ +#members-am-import-file.members-am-import-file-hidden { + display: none; +} + +.members-am-user-search-wrap { + display: inline-flex; + align-items: center; + gap: 6px; +} + +#members-am-user-search.members-am-user-search-input { + width: 168px; + max-width: 100%; +} + +.members-am-legend-visibility-icon { + font-size: 14px; + width: 14px; + height: 14px; + vertical-align: middle; +} + +.members-am-legend-nocap-badge { + display: inline-block; + background: #8c8f94; + color: #fff; + font-size: 9px; + padding: 1px 4px; + border-radius: 2px; + vertical-align: middle; +} + +#members-am-icon-preview.members-am-icon-preview { + display: none; + width: 32px; + height: 32px; + object-fit: contain; + margin-bottom: 6px; + border: 1px solid #ddd; + border-radius: 3px; + padding: 2px; + background: #f0f0f1; +} + +.members-am-icons .members-am-icon-upload-desc { + margin-top: 4px; +} + +/* Admin sidebar menu badges (dynamic background-color still set inline in PHP) */ +#adminmenu .members-am-menu-badge { + display: inline-block; + color: #fff; + font-size: 9px; + padding: 1px 5px; + border-radius: 2px; + line-height: 1.4; + vertical-align: middle; +} /* Import / Export */ .members-import-status { font-weight: 600; } .members-import-status--exists { color: #d63638; } -.members-import-status--new { color: #00a32a; } \ No newline at end of file +.members-import-status--new { color: #00a32a; } diff --git a/css/admin.min.css b/css/admin.min.css index 5788c9f..8591622 100644 --- a/css/admin.min.css +++ b/css/admin.min.css @@ -1 +1 @@ -#members-cp h2.hndle,#members-cp-side h2.hndle{border-bottom:none !important;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}#members-cp h2.hndle:before,#members-cp-side h2.hndle:before{content:url("data:image/svg+xml,%3Csvg width='15px' aria-hidden='true' focusable='false' data-prefix='fas' data-icon='users-cog' class='svg-inline--fa fa-users-cog fa-w-20' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath fill='currentColor' d='M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z'%3E%3C/path%3E%3C/svg%3E");margin-right:5px}#members-cp-side .postbox-header{border-bottom:none !important}#members-cp-side .handle-actions .handle-order-higher,#members-cp-side .handle-actions .handle-order-lower{display:none}#members-cp-side .handle-actions .handlediv{position:absolute;right:5px}.members-upgrade-header{text-align:center;padding:5px}.members-upgrade-header a{color:#f59431}.memberpress-paid-memberships{background-color:#f5f5f5;padding:50px 10px;text-align:center}.memberpress-paid-memberships a{background-color:#0e4598;display:inline-block;padding:10px 20px;border-radius:300px;color:#fff;text-decoration:none;margin-top:20px}#members-cp-side .members-tabs{position:relative}#members-cp-side .members-tab-nav{background-color:transparent;margin-bottom:20px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title{width:33.333%;height:50px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title a{border:0}#members-cp-side .members-tab-content h3{font-weight:bold;font-size:15px;margin-top:0;color:#000;margin-bottom:10px}#members-cp-side .members-tab-content .memberpress-paid-memberships{text-align:left;padding:20px}#members-cp-side .members-tab-content .memberpress-paid-memberships a{display:block;text-align:center}#members-cp-side .members-tabs .members-tab-nav li a,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a i{color:#555}#members-cp-side .members-tabs .members-tab-nav li a svg,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a svg{fill:#555}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a{background-color:transparent;border-bottom:2px solid #3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a i{color:#3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a svg{fill:#3582ae}.members-tab-title svg,.members-tab-title[aria-selected="false"] svg{fill:#0073aa}.members-tab-title[aria-selected="true"] svg{fill:#555}#wpcontent #members-admin-header{margin-left:-20px;padding:0 28px 0 22px;background-color:#0b1220;background-image:radial-gradient(circle at 12% 35%,rgba(14,69,152,0.45),transparent 55%),radial-gradient(circle at 90% 85%,rgba(245,148,49,0.14),transparent 60%);background-repeat:no-repeat;min-height:56px;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}#wpcontent #members-admin-header img.members-logo{height:42px;display:block}#wpcontent #members-admin-header .members-by-mp{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:12px;margin-left:auto;text-decoration:none;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:opacity .15s ease;transition:opacity .15s ease}#wpcontent #members-admin-header .members-by-mp:hover,#wpcontent #members-admin-header .members-by-mp:focus{opacity:.75;text-decoration:none;-webkit-box-shadow:none;box-shadow:none}#wpcontent #members-admin-header .members-by-label{color:#94a3b8;font-size:12px;font-weight:500}#wpcontent #members-admin-header img.members-mp-logo{height:18px;display:block}#wpcontent #members-admin-header #membersAdminHeaderNotifications{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:36px;height:36px;padding:0;margin:0 0 0 18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.12);border-radius:8px;color:#fff;cursor:pointer;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:background .15s ease,border-color .15s ease;transition:background .15s ease,border-color .15s ease;line-height:1;vertical-align:middle;min-height:0;text-decoration:none}#wpcontent #members-admin-header #membersAdminHeaderNotifications:hover,#wpcontent #members-admin-header #membersAdminHeaderNotifications:focus{background:rgba(255,255,255,0.12);border-color:rgba(255,255,255,0.24);color:#fff;-webkit-box-shadow:none;box-shadow:none;outline:0}#wpcontent #members-admin-header #membersAdminHeaderNotifications svg{width:16px;height:auto;display:block}#wpcontent #members-admin-header #membersAdminHeaderNotifications svg path{fill:#e2e8f0}#wpcontent #members-admin-header #membersAdminHeaderNotifications .members-notifications-count{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#f59431;color:#0b1220;font-size:10px;font-weight:700;line-height:16px;text-align:center;border:1.5px solid #0b1220;-webkit-box-sizing:content-box;box-sizing:content-box}.members-upgrade-header #close-members-upgrade-header{position:absolute;right:20px;top:3px}.members-upgrade-header #close-members-upgrade-header:hover{cursor:pointer}.memberpress-welcome-panel.welcome-panel{margin-top:20px;padding:20px 28px;background:#fff;border-radius:10px;min-height:0}.memberpress-welcome-panel.welcome-panel .welcome-panel-content{min-height:0;height:auto}.members-about{--ink:#0f172a;--ink-soft:#334155;--ink-muted:#64748b;--mp-blue:#0e4598;--mp-teal:#3582ae;--mp-orange:#f59431;--rule:#e5e7eb;--paper:#fbfaf7;--paper-soft:#f3efe6;--paper-line:#e8e3d5;--display-font:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--sans-font:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}body.members_page_members-about #wpbody-content,body.toplevel_page_members-about #wpbody-content,body.admin_page_members-about #wpbody-content{background:var(--paper)}.members-about.wrap{max-width:1120px;margin:0 auto;padding:48px 24px 80px;color:var(--ink);font-family:var(--sans-font)}.members-about__hero{display:grid;grid-template-columns:minmax(0,1fr) 200px;grid-template-areas:"head mark" "body mark";gap:32px 56px;-webkit-box-align:start;-ms-flex-align:start;align-items:start;padding-bottom:64px;border-bottom:1px solid var(--paper-line)}.members-about__hero-head{grid-area:head}.members-about__body{grid-area:body}.members-about__mark{grid-area:mark;align-self:start}.members-about__eyebrow{display:inline-block;font-family:var(--sans-font);font-size:12px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-muted);padding-bottom:18px}.members-about__title{font-family:var(--display-font);font-weight:900;font-size:clamp(40px,5.5vw,64px);line-height:1;letter-spacing:-0.035em;color:var(--ink);margin:0}.members-about__title em{font-style:normal;font-weight:900;color:var(--mp-blue)}.members-about__title-dot{color:var(--mp-blue);font-style:normal;font-weight:900}.members-about__body{max-width:60ch;font-size:15.5px;line-height:1.62;color:var(--ink-soft)}.members-about__body p{margin:0 0 14px}.members-about__body p:last-child{margin-bottom:0}.members-about__lead{font-family:var(--sans-font);font-weight:500;font-size:17px;line-height:1.5;color:var(--ink);margin-bottom:24px !important;padding-left:18px;border-left:2px solid var(--mp-blue)}.members-about__body a,.members-about__lead a{color:var(--mp-blue);text-decoration:none;background-image:-webkit-gradient(linear,left top,left bottom,from(currentColor),to(currentColor));background-image:linear-gradient(currentColor,currentColor);background-size:100% 1px;background-position:0 100%;background-repeat:no-repeat;padding-bottom:1px;-webkit-transition:color .2s ease,background-size .2s ease;transition:color .2s ease,background-size .2s ease}.members-about__body a:hover,.members-about__lead a:hover{color:var(--mp-orange)}.members-about__body em,.members-about__lead em{font-style:normal;font-weight:700;color:var(--mp-blue)}.members-about__mark{padding:24px;background:#fff;border:1px solid var(--paper-line);border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(15,23,42,0.03);box-shadow:0 1px 2px rgba(15,23,42,0.03);-webkit-transition:-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:transform .25s ease,box-shadow .25s ease;transition:transform .25s ease,box-shadow .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease}.members-about__mark:hover{-webkit-transform:translateY(-2px);transform:translateY(-2px);-webkit-box-shadow:0 8px 20px rgba(15,23,42,0.08);box-shadow:0 8px 20px rgba(15,23,42,0.08)}.members-about__mark a{display:block;-webkit-box-shadow:none !important;box-shadow:none !important;text-decoration:none}.members-about__mark img{display:block;width:100%;height:auto;max-width:160px;margin:0 auto}.members-about__products{padding-top:56px}.members-about__products-head{margin-bottom:28px;text-align:center}.members-about__products-head h3{font-family:var(--display-font);font-weight:700;font-size:15px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-muted);margin:0}.members-about__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}.members-about__card{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding:24px;background:#fff;border:1px solid var(--paper-line);border-radius:6px;-webkit-transition:border-color .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:border-color .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease}.members-about__card:hover{-webkit-transform:translateY(-3px);transform:translateY(-3px);-webkit-box-shadow:0 12px 28px rgba(15,23,42,0.08);box-shadow:0 12px 28px rgba(15,23,42,0.08);border-color:rgba(14,69,152,0.2)}.members-about__card-head{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:14px;margin-bottom:14px}.members-about__card-icon{-webkit-box-flex:0;-ms-flex:0 0 44px;flex:0 0 44px;width:44px;height:44px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;background:var(--paper-soft);border-radius:10px;overflow:hidden}.members-about__card-icon img{display:block;max-width:32px;max-height:32px;width:auto;height:auto}.members-about__card-title{margin:0;font-family:var(--display-font);font-weight:700;font-size:17px;line-height:1.2;letter-spacing:-0.01em;color:var(--ink)}.members-about__card-title a{color:inherit;text-decoration:none;-webkit-box-shadow:none;box-shadow:none}.members-about__card-title a:hover{color:var(--mp-blue)}.members-about__card-desc{-webkit-box-flex:1;-ms-flex:1;flex:1;margin:0 0 24px;font-size:14px;line-height:1.55;color:var(--ink-soft)}.members-about__card-foot{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;gap:16px;padding-top:16px;border-top:1px solid var(--paper-line)}.members-about__status{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted)}.members-about__status::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}.members-about__status.is-active{color:#10b981}.members-about__status.is-inactive{color:var(--mp-orange)}.members-about__status.is-missing{color:var(--ink-muted)}.members-about__cta{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;padding:8px 14px;font-size:13px;font-weight:600;letter-spacing:.01em;border-radius:4px;text-decoration:none;border:1px solid transparent;-webkit-transition:background .2s ease,border-color .2s ease,color .2s ease,-webkit-transform .15s ease;transition:background .2s ease,border-color .2s ease,color .2s ease,-webkit-transform .15s ease;transition:background .2s ease,border-color .2s ease,color .2s ease,transform .15s ease;transition:background .2s ease,border-color .2s ease,color .2s ease,transform .15s ease,-webkit-transform .15s ease;-webkit-box-shadow:none;box-shadow:none}.members-about__cta:focus,.members-about__cta:focus-visible{outline:2px solid var(--mp-blue);outline-offset:2px;-webkit-box-shadow:none;box-shadow:none}.members-about__cta.is-primary{background:var(--mp-blue);color:#fff}.members-about__cta.is-primary:hover{background:#093374;color:#fff;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.members-about__cta.is-secondary{background:transparent;color:var(--ink);border-color:var(--rule)}.members-about__cta.is-secondary:hover{border-color:var(--ink);background:#fff;color:var(--ink)}.members-about__cta svg{-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease}.members-about__cta:hover svg{-webkit-transform:translate(1px,-1px);transform:translate(1px,-1px)}@media(max-width:900px){.members-about__hero{grid-template-columns:1fr;grid-template-areas:"head" "body" "mark";gap:24px}.members-about__mark{max-width:160px}.members-about__grid{grid-template-columns:1fr}}.memberpress-welcome-panel.welcome-panel:before{background:0}.memberpress-welcome-panel.welcome-panel .about-description{margin:20px 0}.memberpress-welcome-panel.welcome-panel-content{margin:0}.memberpress-welcome-panel.welcome-panel-content .mp-logo-wrap>a{display:block}@media screen and (min-width:870px){.members_page_members-settings .members-short-p{max-width:612px;margin-left:auto;margin-right:auto}.members_page_members-settings .welcome-panel-content{text-align:center}}.members_page_members-settings .welcome-panel .button.button-hero{margin-left:auto;margin-right:auto}.members_page_members-settings .members-svg-wrap{display:inline-block;margin:0 auto 20px}@media screen and (max-width:870px){.members_page_members-settings .welcome-panel{padding:20px}.members_page_members-settings .members-svg-wrap{float:left;margin-right:28px}.admin_page_members-settings.rtl .members-svg-wrap{float:right;margin-right:0;margin-left:28px}}.members_page_members-settings .members-svg-link{display:table-cell;text-align:center;width:128px;height:128px;background:#363b3f;color:#fff;padding:24px 16px 16px;border-radius:50%;-webkit-box-sizing:border-box;box-sizing:border-box;border:4px solid #fff;-webkit-box-shadow:0 0 0 4px #363b3f;box-shadow:0 0 0 4px #363b3f}.members_page_members-settings .members-svg-link svg{max-width:84px;max-height:64px;width:auto;height:auto}.members-addons{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:20px;grid-column-gap:20px}.members-addons .plugin-card{margin:0}.members-addons .plugin-card-top{width:100%}.members-addon{width:100%;position:relative;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-addon .desc{margin-bottom:80px}.addon-activate{text-align:center;border:1px solid #ddd;border-radius:3px;padding:5px 10px 10px;display:inline-block;width:100px;position:absolute;right:20px;bottom:20px}.activate-toggle:hover{cursor:pointer}.activate-toggle svg{max-width:20px;position:relative;top:5px}.activate-toggle svg{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.activate-toggle svg.active{-webkit-transform:rotate(0);transform:rotate(0)}.activate-toggle svg path{fill:#ccc}.activate-toggle svg.active path{fill:#8cbd5a}.activate-toggle.processing{cursor:not-allowed;pointer-events:none;opacity:.5}.activate-toggle .action-label{display:inline-block;width:50px;text-align:left}.mepr_dummy_txns{-webkit-filter:blur(3px);filter:blur(3px);position:absolute;top:0;left:0;width:100%;z-index:5;pointer-events:none}.mepr_dummy_txns::after{background-color:rgba(0,0,0,.2);position:absolute;top:0;left:0;width:100%;height:100%;z-index:6}.mepr-upgrade-table{position:relative}.mepr-upgrade-table .mepr-upgrade{position:relative;top:100px;z-index:10;background:rgba(0,0,0,.2);width:100%;height:100%;max-width:600px;margin:0 auto;-webkit-box-shadow:0 0 30px #ccc;box-shadow:0 0 30px #ccc}.mepr-upgrade-content{background-color:#fff;padding:20px;text-align:center}.mepr-upgrade-content h2{font-size:24px}.mepr-upgrade-content h4{font-size:18px}.mepr-upgrade-content .features{display:grid;grid-template-columns:1fr 1fr;grid-column-gap:30px;grid-row-gap:10px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:left;padding:10px}.features li:before{content:"\f147";font-family:dashicons;color:green;font-size:20px;position:relative;top:4px;margin-right:10px}.mepr-upgrade-cta{text-align:center;background-color:#f2f2f2;padding:20px}.mepr-upgrade-cta .mepr-cta-button{display:block;max-width:300px;margin:0 auto 20px;background-color:#fd9c27;color:#fff;padding:15px;font-size:18px;font-weight:bold;text-decoration:none;border-radius:300px}.members-plugin-card.plugin-card:nth-child(even){margin-right:inherit}.members-plugin-card.plugin-card:nth-child(odd){clear:none;margin-left:inherit}.members-plugin-card.plugin-card .plugin-card-top{min-height:0;padding:30px 20px}.members-plugin-card.plugin-card .plugin-icon{top:30px}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{font-weight:bold}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .active{color:#8cbd5a}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .inactive{color:red}.members-plugin-card.plugin-card .plugin-icon{width:64px;height:auto}.plugin-icon-small{width:32px;height:auto}.members-plugin-card.plugin-card .name,.members-plugin-card.plugin-card .desc{margin-right:0;margin-left:84px}.members-addons .plugin-card .name,.members-addons .plugin-card .desc p{margin-right:0}@media screen and (max-width:1100px){.members-addons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media screen and (max-width:600px){.members_page_members-settings .members-svg-link{width:64px;height:64px}.members_page_members-settings .members-svg-link svg{position:absolute;top:14px;left:15px;max-width:30px;max-height:30px}.members_page_members-settings .plugin-card-members-edd-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-acf-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-givewp-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-meta-box-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-woocommerce-integration .members-svg-link svg{top:10px;left:8px;max-width:40px;max-height:40px}.members_page_members-settings .plugin-card-members-block-permissions .members-svg-link svg,.members_page_members-settings .plugin-card-members-role-hierarchy .members-svg-link svg{left:14px}.members-addons .plugin-card .desc>p,.members-addons .plugin-card .name{margin-left:84px}}.memberpress-about .welcome-panel-column-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:40px;padding:0;margin:0}.memberpress-about .mp-desc p{font-size:13px;line-height:1.6;color:#334155;margin:0 0 12px}.memberpress-about .mp-desc p:first-child{font-size:15px;font-weight:600;color:#0f172a;margin-top:0;margin-bottom:14px}.memberpress-about .mp-desc p:last-child{margin-bottom:0}.memberpress-about .mp-logo-wrap{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.memberpress-about .mp-logo{display:block;max-width:220px;height:auto;margin:0}.members-about-addons{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-column-gap:20px}.members-about-addons .members-plugin-card{margin:0;width:100%}.members-about-addons .members-plugin-card .plugin-icon{position:static;width:auto;height:28px;max-width:40px;-o-object-fit:contain;object-fit:contain;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;display:block}.members-about-addons .members-plugin-card .name,.members-about-addons .members-plugin-card .desc{margin-left:0}.members-about-addons .members-plugin-card .plugin-card-top{padding:20px}.members-about-addons .members-plugin-card .name h3{margin:0 0 10px;min-height:0;height:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;line-height:1.2}.members-about-addons .members-plugin-card .name h3 a{text-decoration:none;line-height:1.2}.members-about-addons .members-plugin-card .plugin-card-bottom{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:1099px){.members-about-addons{grid-template-columns:1fr;grid-row-gap:20px}}@media screen and (min-width:1100px) and (max-width:1600px){.members-about-addons .members-plugin-card{float:none;width:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.members-plugin-card.plugin-card .desc{margin-left:0;margin-top:50px}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{float:none}.members-plugin-card.plugin-card .plugin-card-bottom .column-updated{float:none;width:auto}}@media screen and (max-width:1200px){.welcome-panel .welcome-panel-column-container{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse;gap:0;padding:0}.memberpress-about .mp-logo{margin:0 0 20px}}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}@media only screen and (min-width:783px){.members_page_roles .column-users,.members_page_roles .column-granted_caps,.members_page_roles .column-denied_caps{width:100px;text-align:center}}.members-title-div #titlewrap input{padding:0 8px;font-size:1.7em;line-height:normal;height:1.7em;width:100%;outline:0;margin:0 0 3px;background-color:#fff}.members-title-div input[name="role"]{font-size:13px;height:22px;margin:0;width:16em}#tabcapsdiv{margin-top:1em}#tabcapsdiv>.hndle{padding:10px;border-bottom:1px solid #eee}#tabcapsdiv .inside,#members-cp .inside{margin:0;padding:0}.members-cap-tabs,.members-tabs{overflow:hidden;background:#fff;background:-webkit-gradient(linear,left top,right top,from(#fafafa),color-stop(20%,#fafafa),color-stop(20%,#fff),to(#fff));background:linear-gradient(90deg,#fafafa 0,#fafafa 20%,#fff 20%,#fff 100%)}@media only screen and (max-width:782px){.members-cap-tabs,.members-tabs{background:linear-gradient(90deg,#fafafa 0,#fafafa 48px,#fff 48px,#fff 100%)}}.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{position:relative;float:left;list-style:none;width:20%;line-height:1em;margin:0 0 -1px 0;padding:0;background-color:#fafafa;border-right:1px solid #eee;-webkit-box-sizing:border-box;box-sizing:border-box}.members-cap-tabs .members-tab-nav li,.members-tabs .members-tab-nav li{display:block;position:relative;margin:0;padding:0;line-height:20px}.members-cap-tabs .members-tab-nav li a,.members-tabs .members-tab-nav li a{display:block;margin:0;padding:10px;line-height:20px !important;text-decoration:none;border-bottom:1px solid #eee;-webkit-box-shadow:none;box-shadow:none}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{line-height:20px;margin-right:3px}.members-cap-tabs .members-tab-nav li[aria-selected="true"] a,.members-tabs .members-tab-nav li[aria-selected="true"] a{position:relative;font-weight:bold;color:#555;background-color:#e0e0e0}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{width:48px}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}.members-tab-nav li .dashicons::before,.members-tab-nav li .dashicons::before{width:24px;height:24px}.members-tab-nav li .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{float:left;width:80%;margin-left:-1px}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{width:calc(100% - 48px)}}#members-cp .members-tab-content{padding:10px;border-left:1px solid #e5e5e5}.members-roles-select th,.members-roles-select td{width:190px;overflow:hidden}.members-roles-select .check-all-th{text-align:center}.members-roles-select .check-all-th label{padding-left:5px}@media only screen and (min-width:850px){#side-sortables .members-tabs{background:#fff}#side-sortables .members-tabs .members-tab-wrap{width:100%}#side-sortables .members-tabs .members-tab-nav{display:table;width:100%}#side-sortables .members-tabs .members-tab-nav li{display:table-cell;text-align:center;border-right:1px solid #eee}#side-sortables .members-tabs .members-tab-nav li:last-of-type{border-right:0}#side-sortables .members-tabs .members-tab-nav li a{padding:10px 0}#side-sortables .members-tabs .members-tab-nav .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}#side-sortables .members-tabs .members-tab-nav .dashicons::before{width:24px;height:24px}#side-sortables .members-tabs .members-tab-nav .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-tabs .members-tabs-label{display:block !important;font-weight:bold;display:inline-block;margin-bottom:4px}.members-tabs .butterbean-control-checkbox .members-tabs-label{display:inline !important}.members-tabs .members-tabs-description{display:block;font-style:italic;margin-top:4px}.members-tabs .members-tabs-label+.members-tabs-description{margin-top:0;margin-bottom:4px}#tabcapsdiv table{border-right:0;border-top:0;border-bottom:0}#tabcapsdiv table td,#tabcapsdiv table th{padding:10px;padding-bottom:10px;border-bottom:1px solid #eee;font-size:13px;line-height:20px}#tabcapsdiv table td{padding:9px}#tabcapsdiv tbody tr:last-of-type td{border-bottom:0}#tabcapsdiv tfoot th{border-color:#eee}@media only screen and (max-width:782px){#tabcapsdiv table td,#tabcapsdiv table th{line-height:24px}}.members-roles-select .column-grant,.members-roles-select .column-deny{width:70px !important;text-align:left;display:table-cell !important;clear:none !important}.members-roles-select thead th,.members-roles-select tfoot th{padding:9px !important}.members-roles-select .column-grant input,.members-roles-select .column-deny input{margin:0 5px 0 0;vertical-align:middle}.members-roles-select tbody tr:nth-child(even){background:#f2f2f2}.members-cap-tabs button{display:inline;margin:-4px;line-height:inherit;padding:4px 8px;border:1px solid transparent;background:transparent;border-radius:0;outline:0;-webkit-transition:all .25s ease-out;transition:all .25s ease-out}.members-cap-tabs button:hover,.members-cap-tabs button:focus{border-color:#eee;background:#fafafa;cursor:pointer}.members-cap-tabs button:active{color:#0073aa;border-color:#0073aa}.members-cap-tabs button+.dashicons{display:none;margin-top:1px;margin-bottom:-1px;line-height:inherit}.members-cap-tabs button:hover+.dashicons,.members-cap-tabs button:focus+.dashicons{display:inline-block}.members-tab-content .members-highlight{background-color:rgba(0,115,170,0.05)}.members-tab-content tbody{-webkit-transition:all 2s ease-in-out;transition:all 2s ease-in-out}.members-cp-role-list-wrap{overflow:auto;min-height:42px;max-height:200px;padding:0 .9em;border:solid 1px #dfdfdf;background-color:#fdfdfd}#wpbody-content{padding-bottom:200px}.members-footer-promotion{text-align:center;font-weight:400;font-size:13px;line-height:16px;color:#787c82;padding:20px 0 30px 0;margin-bottom:20px}.members-footer-promotion p{font-weight:600}.members-footer-promotion-links,.members-footer-promotion-social{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-footer-promotion-links{margin:9px 0 0}.members-footer-promotion-links span{color:#c3c4c7;padding:0 7px}.members-footer-promotion-social{margin:10px 0 0 0;gap:10px}.members-footer-promotion-social li{margin-bottom:0}.members-footer-promotion-social a{display:block;height:16px}.members_page_members-settings.rtl{.addon-activate{left:20px;right:unset}.members-addons .plugin-card{.name,.desc p{margin-left:0;margin-right:148px}}}.button.button-warning{background:#dc3232;border-color:#dc3232;color:#fff}.button.button-warning:hover,.button.button-warning:focus{background:#c92424;border-color:#c92424;color:#fff}.members-reset-spinner{float:none;margin-top:0;vertical-align:middle} \ No newline at end of file +#members-cp h2.hndle,#members-cp-side h2.hndle{border-bottom:none !important;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}#members-cp h2.hndle:before,#members-cp-side h2.hndle:before{content:url("data:image/svg+xml,%3Csvg width='15px' aria-hidden='true' focusable='false' data-prefix='fas' data-icon='users-cog' class='svg-inline--fa fa-users-cog fa-w-20' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath fill='currentColor' d='M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z'%3E%3C/path%3E%3C/svg%3E");margin-right:5px}#members-cp-side .postbox-header{border-bottom:none !important}#members-cp-side .handle-actions .handle-order-higher,#members-cp-side .handle-actions .handle-order-lower{display:none}#members-cp-side .handle-actions .handlediv{position:absolute;right:5px}.members-upgrade-header{text-align:center;padding:5px}.members-upgrade-header a{color:#f59431}.memberpress-paid-memberships{background-color:#f5f5f5;padding:50px 10px;text-align:center}.memberpress-paid-memberships a{background-color:#0e4598;display:inline-block;padding:10px 20px;border-radius:300px;color:#fff;text-decoration:none;margin-top:20px}#members-cp-side .members-tabs{position:relative}#members-cp-side .members-tab-nav{background-color:transparent;margin-bottom:20px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title{width:33.333%;height:50px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title a{border:0}#members-cp-side .members-tab-content h3{font-weight:bold;font-size:15px;margin-top:0;color:#000;margin-bottom:10px}#members-cp-side .members-tab-content .memberpress-paid-memberships{text-align:left;padding:20px}#members-cp-side .members-tab-content .memberpress-paid-memberships a{display:block;text-align:center}#members-cp-side .members-tabs .members-tab-nav li a,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a i{color:#555}#members-cp-side .members-tabs .members-tab-nav li a svg,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a svg{fill:#555}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a{background-color:transparent;border-bottom:2px solid #3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a i{color:#3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a svg{fill:#3582ae}.members-tab-title svg,.members-tab-title[aria-selected="false"] svg{fill:#0073aa}.members-tab-title[aria-selected="true"] svg{fill:#555}#wpcontent #members-admin-header{margin-left:-20px;padding:0 28px 0 22px;background-color:#0b1220;background-image:radial-gradient(circle at 12% 35%,rgba(14,69,152,0.45),transparent 55%),radial-gradient(circle at 90% 85%,rgba(245,148,49,0.14),transparent 60%);background-repeat:no-repeat;min-height:56px;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}#wpcontent #members-admin-header img.members-logo{height:42px;display:block}#wpcontent #members-admin-header .members-by-mp{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:12px;margin-left:auto;text-decoration:none;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:opacity .15s ease;transition:opacity .15s ease}#wpcontent #members-admin-header .members-by-mp:hover,#wpcontent #members-admin-header .members-by-mp:focus{opacity:.75;text-decoration:none;-webkit-box-shadow:none;box-shadow:none}#wpcontent #members-admin-header .members-by-label{color:#94a3b8;font-size:12px;font-weight:500}#wpcontent #members-admin-header img.members-mp-logo{height:18px;display:block}#wpcontent #members-admin-header #membersAdminHeaderNotifications{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:36px;height:36px;padding:0;margin:0 0 0 18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.12);border-radius:8px;color:#fff;cursor:pointer;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:background .15s ease,border-color .15s ease;transition:background .15s ease,border-color .15s ease;line-height:1;vertical-align:middle;min-height:0;text-decoration:none}#wpcontent #members-admin-header #membersAdminHeaderNotifications:hover,#wpcontent #members-admin-header #membersAdminHeaderNotifications:focus{background:rgba(255,255,255,0.12);border-color:rgba(255,255,255,0.24);color:#fff;-webkit-box-shadow:none;box-shadow:none;outline:0}#wpcontent #members-admin-header #membersAdminHeaderNotifications svg{width:16px;height:auto;display:block}#wpcontent #members-admin-header #membersAdminHeaderNotifications svg path{fill:#e2e8f0}#wpcontent #members-admin-header #membersAdminHeaderNotifications .members-notifications-count{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#f59431;color:#0b1220;font-size:10px;font-weight:700;line-height:16px;text-align:center;border:1.5px solid #0b1220;-webkit-box-sizing:content-box;box-sizing:content-box}.members-upgrade-header #close-members-upgrade-header{position:absolute;right:20px;top:3px}.members-upgrade-header #close-members-upgrade-header:hover{cursor:pointer}.memberpress-welcome-panel.welcome-panel{margin-top:20px;padding:20px 28px;background:#fff;border-radius:10px;min-height:0}.memberpress-welcome-panel.welcome-panel .welcome-panel-content{min-height:0;height:auto}.members-about{--ink:#0f172a;--ink-soft:#334155;--ink-muted:#64748b;--mp-blue:#0e4598;--mp-teal:#3582ae;--mp-orange:#f59431;--rule:#e5e7eb;--paper:#fbfaf7;--paper-soft:#f3efe6;--paper-line:#e8e3d5;--display-font:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--sans-font:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}body.members_page_members-about #wpbody-content,body.toplevel_page_members-about #wpbody-content,body.admin_page_members-about #wpbody-content{background:var(--paper)}.members-about.wrap{max-width:1120px;margin:0 auto;padding:48px 24px 80px;color:var(--ink);font-family:var(--sans-font)}.members-about__hero{display:grid;grid-template-columns:minmax(0,1fr) 200px;grid-template-areas:"head mark" "body mark";gap:32px 56px;-webkit-box-align:start;-ms-flex-align:start;align-items:start;padding-bottom:64px;border-bottom:1px solid var(--paper-line)}.members-about__hero-head{grid-area:head}.members-about__body{grid-area:body}.members-about__mark{grid-area:mark;align-self:start}.members-about__eyebrow{display:inline-block;font-family:var(--sans-font);font-size:12px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-muted);padding-bottom:18px}.members-about__title{font-family:var(--display-font);font-weight:900;font-size:clamp(40px,5.5vw,64px);line-height:1;letter-spacing:-0.035em;color:var(--ink);margin:0}.members-about__title em{font-style:normal;font-weight:900;color:var(--mp-blue)}.members-about__title-dot{color:var(--mp-blue);font-style:normal;font-weight:900}.members-about__body{max-width:60ch;font-size:15.5px;line-height:1.62;color:var(--ink-soft)}.members-about__body p{margin:0 0 14px}.members-about__body p:last-child{margin-bottom:0}.members-about__lead{font-family:var(--sans-font);font-weight:500;font-size:17px;line-height:1.5;color:var(--ink);margin-bottom:24px !important;padding-left:18px;border-left:2px solid var(--mp-blue)}.members-about__body a,.members-about__lead a{color:var(--mp-blue);text-decoration:none;background-image:-webkit-gradient(linear,left top,left bottom,from(currentColor),to(currentColor));background-image:linear-gradient(currentColor,currentColor);background-size:100% 1px;background-position:0 100%;background-repeat:no-repeat;padding-bottom:1px;-webkit-transition:color .2s ease,background-size .2s ease;transition:color .2s ease,background-size .2s ease}.members-about__body a:hover,.members-about__lead a:hover{color:var(--mp-orange)}.members-about__body em,.members-about__lead em{font-style:normal;font-weight:700;color:var(--mp-blue)}.members-about__mark{padding:24px;background:#fff;border:1px solid var(--paper-line);border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(15,23,42,0.03);box-shadow:0 1px 2px rgba(15,23,42,0.03);-webkit-transition:-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:transform .25s ease,box-shadow .25s ease;transition:transform .25s ease,box-shadow .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease}.members-about__mark:hover{-webkit-transform:translateY(-2px);transform:translateY(-2px);-webkit-box-shadow:0 8px 20px rgba(15,23,42,0.08);box-shadow:0 8px 20px rgba(15,23,42,0.08)}.members-about__mark a{display:block;-webkit-box-shadow:none !important;box-shadow:none !important;text-decoration:none}.members-about__mark img{display:block;width:100%;height:auto;max-width:160px;margin:0 auto}.members-about__products{padding-top:56px}.members-about__products-head{margin-bottom:28px;text-align:center}.members-about__products-head h3{font-family:var(--display-font);font-weight:700;font-size:15px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-muted);margin:0}.members-about__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}.members-about__card{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding:24px;background:#fff;border:1px solid var(--paper-line);border-radius:6px;-webkit-transition:border-color .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:border-color .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease;transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease,-webkit-transform .25s ease,-webkit-box-shadow .25s ease}.members-about__card:hover{-webkit-transform:translateY(-3px);transform:translateY(-3px);-webkit-box-shadow:0 12px 28px rgba(15,23,42,0.08);box-shadow:0 12px 28px rgba(15,23,42,0.08);border-color:rgba(14,69,152,0.2)}.members-about__card-head{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:14px;margin-bottom:14px}.members-about__card-icon{-webkit-box-flex:0;-ms-flex:0 0 44px;flex:0 0 44px;width:44px;height:44px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;background:var(--paper-soft);border-radius:10px;overflow:hidden}.members-about__card-icon img{display:block;max-width:32px;max-height:32px;width:auto;height:auto}.members-about__card-title{margin:0;font-family:var(--display-font);font-weight:700;font-size:17px;line-height:1.2;letter-spacing:-0.01em;color:var(--ink)}.members-about__card-title a{color:inherit;text-decoration:none;-webkit-box-shadow:none;box-shadow:none}.members-about__card-title a:hover{color:var(--mp-blue)}.members-about__card-desc{-webkit-box-flex:1;-ms-flex:1;flex:1;margin:0 0 24px;font-size:14px;line-height:1.55;color:var(--ink-soft)}.members-about__card-foot{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;gap:16px;padding-top:16px;border-top:1px solid var(--paper-line)}.members-about__status{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-muted)}.members-about__status::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}.members-about__status.is-active{color:#10b981}.members-about__status.is-inactive{color:var(--mp-orange)}.members-about__status.is-missing{color:var(--ink-muted)}.members-about__cta{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;padding:8px 14px;font-size:13px;font-weight:600;letter-spacing:.01em;border-radius:4px;text-decoration:none;border:1px solid transparent;-webkit-transition:background .2s ease,border-color .2s ease,color .2s ease,-webkit-transform .15s ease;transition:background .2s ease,border-color .2s ease,color .2s ease,-webkit-transform .15s ease;transition:background .2s ease,border-color .2s ease,color .2s ease,transform .15s ease;transition:background .2s ease,border-color .2s ease,color .2s ease,transform .15s ease,-webkit-transform .15s ease;-webkit-box-shadow:none;box-shadow:none}.members-about__cta:focus,.members-about__cta:focus-visible{outline:2px solid var(--mp-blue);outline-offset:2px;-webkit-box-shadow:none;box-shadow:none}.members-about__cta.is-primary{background:var(--mp-blue);color:#fff}.members-about__cta.is-primary:hover{background:#093374;color:#fff;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.members-about__cta.is-secondary{background:transparent;color:var(--ink);border-color:var(--rule)}.members-about__cta.is-secondary:hover{border-color:var(--ink);background:#fff;color:var(--ink)}.members-about__cta svg{-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease}.members-about__cta:hover svg{-webkit-transform:translate(1px,-1px);transform:translate(1px,-1px)}@media(max-width:900px){.members-about__hero{grid-template-columns:1fr;grid-template-areas:"head" "body" "mark";gap:24px}.members-about__mark{max-width:160px}.members-about__grid{grid-template-columns:1fr}}.memberpress-welcome-panel.welcome-panel:before{background:0}.memberpress-welcome-panel.welcome-panel .about-description{margin:20px 0}.memberpress-welcome-panel.welcome-panel-content{margin:0}.memberpress-welcome-panel.welcome-panel-content .mp-logo-wrap>a{display:block}@media screen and (min-width:870px){.members_page_members-settings .members-short-p{max-width:612px;margin-left:auto;margin-right:auto}.members_page_members-settings .welcome-panel-content{text-align:center}}.members_page_members-settings .welcome-panel .button.button-hero{margin-left:auto;margin-right:auto}.members_page_members-settings .members-svg-wrap{display:inline-block;margin:0 auto 20px}@media screen and (max-width:870px){.members_page_members-settings .welcome-panel{padding:20px}.members_page_members-settings .members-svg-wrap{float:left;margin-right:28px}.admin_page_members-settings.rtl .members-svg-wrap{float:right;margin-right:0;margin-left:28px}}.members_page_members-settings .members-svg-link{display:table-cell;text-align:center;width:128px;height:128px;background:#363b3f;color:#fff;padding:24px 16px 16px;border-radius:50%;-webkit-box-sizing:border-box;box-sizing:border-box;border:4px solid #fff;-webkit-box-shadow:0 0 0 4px #363b3f;box-shadow:0 0 0 4px #363b3f}.members_page_members-settings .members-svg-link svg{max-width:84px;max-height:64px;width:auto;height:auto}.members-addons{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:20px;grid-column-gap:20px}.members-addons .plugin-card{margin:0}.members-addons .plugin-card-top{width:100%}.members-addon{width:100%;position:relative;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-addon .desc{margin-bottom:80px}.addon-activate{text-align:center;border:1px solid #ddd;border-radius:3px;padding:5px 10px 10px;display:inline-block;width:100px;position:absolute;right:20px;bottom:20px}.activate-toggle:hover{cursor:pointer}.activate-toggle svg{max-width:20px;position:relative;top:5px}.activate-toggle svg{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.activate-toggle svg.active{-webkit-transform:rotate(0);transform:rotate(0)}.activate-toggle svg path{fill:#ccc}.activate-toggle svg.active path{fill:#8cbd5a}.activate-toggle.processing{cursor:not-allowed;pointer-events:none;opacity:.5}.activate-toggle .action-label{display:inline-block;width:50px;text-align:left}.mepr_dummy_txns{-webkit-filter:blur(3px);filter:blur(3px);position:absolute;top:0;left:0;width:100%;z-index:5;pointer-events:none}.mepr_dummy_txns::after{background-color:rgba(0,0,0,.2);position:absolute;top:0;left:0;width:100%;height:100%;z-index:6}.mepr-upgrade-table{position:relative}.mepr-upgrade-table .mepr-upgrade{position:relative;top:100px;z-index:10;background:rgba(0,0,0,.2);width:100%;height:100%;max-width:600px;margin:0 auto;-webkit-box-shadow:0 0 30px #ccc;box-shadow:0 0 30px #ccc}.mepr-upgrade-content{background-color:#fff;padding:20px;text-align:center}.mepr-upgrade-content h2{font-size:24px}.mepr-upgrade-content h4{font-size:18px}.mepr-upgrade-content .features{display:grid;grid-template-columns:1fr 1fr;grid-column-gap:30px;grid-row-gap:10px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:left;padding:10px}.features li:before{content:"\f147";font-family:dashicons;color:green;font-size:20px;position:relative;top:4px;margin-right:10px}.mepr-upgrade-cta{text-align:center;background-color:#f2f2f2;padding:20px}.mepr-upgrade-cta .mepr-cta-button{display:block;max-width:300px;margin:0 auto 20px;background-color:#fd9c27;color:#fff;padding:15px;font-size:18px;font-weight:bold;text-decoration:none;border-radius:300px}.members-plugin-card.plugin-card:nth-child(even){margin-right:inherit}.members-plugin-card.plugin-card:nth-child(odd){clear:none;margin-left:inherit}.members-plugin-card.plugin-card .plugin-card-top{min-height:0;padding:30px 20px}.members-plugin-card.plugin-card .plugin-icon{top:30px}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{font-weight:bold}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .active{color:#8cbd5a}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .inactive{color:red}.members-plugin-card.plugin-card .plugin-icon{width:64px;height:auto}.plugin-icon-small{width:32px;height:auto}.members-plugin-card.plugin-card .name,.members-plugin-card.plugin-card .desc{margin-right:0;margin-left:84px}.members-addons .plugin-card .name,.members-addons .plugin-card .desc p{margin-right:0}@media screen and (max-width:1100px){.members-addons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media screen and (max-width:600px){.members_page_members-settings .members-svg-link{width:64px;height:64px}.members_page_members-settings .members-svg-link svg{position:absolute;top:14px;left:15px;max-width:30px;max-height:30px}.members_page_members-settings .plugin-card-members-edd-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-acf-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-givewp-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-meta-box-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-woocommerce-integration .members-svg-link svg{top:10px;left:8px;max-width:40px;max-height:40px}.members_page_members-settings .plugin-card-members-block-permissions .members-svg-link svg,.members_page_members-settings .plugin-card-members-role-hierarchy .members-svg-link svg{left:14px}.members-addons .plugin-card .desc>p,.members-addons .plugin-card .name{margin-left:84px}}.memberpress-about .welcome-panel-column-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:40px;padding:0;margin:0}.memberpress-about .mp-desc p{font-size:13px;line-height:1.6;color:#334155;margin:0 0 12px}.memberpress-about .mp-desc p:first-child{font-size:15px;font-weight:600;color:#0f172a;margin-top:0;margin-bottom:14px}.memberpress-about .mp-desc p:last-child{margin-bottom:0}.memberpress-about .mp-logo-wrap{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.memberpress-about .mp-logo{display:block;max-width:220px;height:auto;margin:0}.members-about-addons{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-column-gap:20px}.members-about-addons .members-plugin-card{margin:0;width:100%}.members-about-addons .members-plugin-card .plugin-icon{position:static;width:auto;height:28px;max-width:40px;-o-object-fit:contain;object-fit:contain;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;display:block}.members-about-addons .members-plugin-card .name,.members-about-addons .members-plugin-card .desc{margin-left:0}.members-about-addons .members-plugin-card .plugin-card-top{padding:20px}.members-about-addons .members-plugin-card .name h3{margin:0 0 10px;min-height:0;height:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;line-height:1.2}.members-about-addons .members-plugin-card .name h3 a{text-decoration:none;line-height:1.2}.members-about-addons .members-plugin-card .plugin-card-bottom{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:1099px){.members-about-addons{grid-template-columns:1fr;grid-row-gap:20px}}@media screen and (min-width:1100px) and (max-width:1600px){.members-about-addons .members-plugin-card{float:none;width:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.members-plugin-card.plugin-card .desc{margin-left:0;margin-top:50px}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{float:none}.members-plugin-card.plugin-card .plugin-card-bottom .column-updated{float:none;width:auto}}@media screen and (max-width:1200px){.welcome-panel .welcome-panel-column-container{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse;gap:0;padding:0}.memberpress-about .mp-logo{margin:0 0 20px}}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}@media only screen and (min-width:783px){.members_page_roles .column-users,.members_page_roles .column-granted_caps,.members_page_roles .column-denied_caps{width:100px;text-align:center}}.members-title-div #titlewrap input{padding:0 8px;font-size:1.7em;line-height:normal;height:1.7em;width:100%;outline:0;margin:0 0 3px;background-color:#fff}.members-title-div input[name="role"]{font-size:13px;height:22px;margin:0;width:16em}#tabcapsdiv{margin-top:1em}#tabcapsdiv>.hndle{padding:10px;border-bottom:1px solid #eee}#tabcapsdiv .inside,#members-cp .inside{margin:0;padding:0}.members-cap-tabs,.members-tabs{overflow:hidden;background:#fff;background:-webkit-gradient(linear,left top,right top,from(#fafafa),color-stop(20%,#fafafa),color-stop(20%,#fff),to(#fff));background:linear-gradient(90deg,#fafafa 0,#fafafa 20%,#fff 20%,#fff 100%)}@media only screen and (max-width:782px){.members-cap-tabs,.members-tabs{background:linear-gradient(90deg,#fafafa 0,#fafafa 48px,#fff 48px,#fff 100%)}}.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{position:relative;float:left;list-style:none;width:20%;line-height:1em;margin:0 0 -1px 0;padding:0;background-color:#fafafa;border-right:1px solid #eee;-webkit-box-sizing:border-box;box-sizing:border-box}.members-cap-tabs .members-tab-nav li,.members-tabs .members-tab-nav li{display:block;position:relative;margin:0;padding:0;line-height:20px}.members-cap-tabs .members-tab-nav li a,.members-tabs .members-tab-nav li a{display:block;margin:0;padding:10px;line-height:20px !important;text-decoration:none;border-bottom:1px solid #eee;-webkit-box-shadow:none;box-shadow:none}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{line-height:20px;margin-right:3px}.members-cap-tabs .members-tab-nav li[aria-selected="true"] a,.members-tabs .members-tab-nav li[aria-selected="true"] a{position:relative;font-weight:bold;color:#555;background-color:#e0e0e0}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{width:48px}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}.members-tab-nav li .dashicons::before,.members-tab-nav li .dashicons::before{width:24px;height:24px}.members-tab-nav li .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{float:left;width:80%;margin-left:-1px}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{width:calc(100% - 48px)}}#members-cp .members-tab-content{padding:10px;border-left:1px solid #e5e5e5}.members-roles-select th,.members-roles-select td{width:190px;overflow:hidden}.members-roles-select .check-all-th{text-align:center}.members-roles-select .check-all-th label{padding-left:5px}@media only screen and (min-width:850px){#side-sortables .members-tabs{background:#fff}#side-sortables .members-tabs .members-tab-wrap{width:100%}#side-sortables .members-tabs .members-tab-nav{display:table;width:100%}#side-sortables .members-tabs .members-tab-nav li{display:table-cell;text-align:center;border-right:1px solid #eee}#side-sortables .members-tabs .members-tab-nav li:last-of-type{border-right:0}#side-sortables .members-tabs .members-tab-nav li a{padding:10px 0}#side-sortables .members-tabs .members-tab-nav .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}#side-sortables .members-tabs .members-tab-nav .dashicons::before{width:24px;height:24px}#side-sortables .members-tabs .members-tab-nav .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-tabs .members-tabs-label{display:block !important;font-weight:bold;display:inline-block;margin-bottom:4px}.members-tabs .butterbean-control-checkbox .members-tabs-label{display:inline !important}.members-tabs .members-tabs-description{display:block;font-style:italic;margin-top:4px}.members-tabs .members-tabs-label+.members-tabs-description{margin-top:0;margin-bottom:4px}#tabcapsdiv table{border-right:0;border-top:0;border-bottom:0}#tabcapsdiv table td,#tabcapsdiv table th{padding:10px;padding-bottom:10px;border-bottom:1px solid #eee;font-size:13px;line-height:20px}#tabcapsdiv table td{padding:9px}#tabcapsdiv tbody tr:last-of-type td{border-bottom:0}#tabcapsdiv tfoot th{border-color:#eee}@media only screen and (max-width:782px){#tabcapsdiv table td,#tabcapsdiv table th{line-height:24px}}.members-roles-select .column-grant,.members-roles-select .column-deny{width:70px !important;text-align:left;display:table-cell !important;clear:none !important}.members-roles-select thead th,.members-roles-select tfoot th{padding:9px !important}.members-roles-select .column-grant input,.members-roles-select .column-deny input{margin:0 5px 0 0;vertical-align:middle}.members-roles-select tbody tr:nth-child(even){background:#f2f2f2}.members-cap-tabs button{display:inline;margin:-4px;line-height:inherit;padding:4px 8px;border:1px solid transparent;background:transparent;border-radius:0;outline:0;-webkit-transition:all .25s ease-out;transition:all .25s ease-out}.members-cap-tabs button:hover,.members-cap-tabs button:focus{border-color:#eee;background:#fafafa;cursor:pointer}.members-cap-tabs button:active{color:#0073aa;border-color:#0073aa}.members-cap-tabs button+.dashicons{display:none;margin-top:1px;margin-bottom:-1px;line-height:inherit}.members-cap-tabs button:hover+.dashicons,.members-cap-tabs button:focus+.dashicons{display:inline-block}.members-tab-content .members-highlight{background-color:rgba(0,115,170,0.05)}.members-tab-content tbody{-webkit-transition:all 2s ease-in-out;transition:all 2s ease-in-out}.members-cp-role-list-wrap{overflow:auto;min-height:42px;max-height:200px;padding:0 .9em;border:solid 1px #dfdfdf;background-color:#fdfdfd}#wpbody-content{padding-bottom:200px}.members-footer-promotion{text-align:center;font-weight:400;font-size:13px;line-height:16px;color:#787c82;padding:20px 0 30px 0;margin-bottom:20px}.members-footer-promotion p{font-weight:600}.members-footer-promotion-links,.members-footer-promotion-social{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-footer-promotion-links{margin:9px 0 0}.members-footer-promotion-links span{color:#c3c4c7;padding:0 7px}.members-footer-promotion-social{margin:10px 0 0 0;gap:10px}.members-footer-promotion-social li{margin-bottom:0}.members-footer-promotion-social a{display:block;height:16px}.members_page_members-settings.rtl{.addon-activate{left:20px;right:unset}.members-addons .plugin-card{.name,.desc p{margin-left:0;margin-right:148px}}}.button.button-warning{background:#dc3232;border-color:#dc3232;color:#fff}.button.button-warning:hover,.button.button-warning:focus{background:#c92424;border-color:#c92424;color:#fff}.members-reset-spinner{float:none;margin-top:0;vertical-align:middle}.members-admin-menus-wrap{max-width:none;--members-am-accent-use:var(--members-am-accent,var(--wp-admin-theme-color,#2271b1));--members-am-surface-use:var(--members-am-surface,color-mix(in srgb,var(--members-am-base,#1d2327) 88%,#fff 12%));--members-am-border-on-base-fallback:color-mix(in srgb,var(--members-am-base,#1d2327) 62%,#fff 38%);--members-am-chrome-bg:var(--wp-components-color-gray-100,#f6f7f7);--members-am-chrome-border:var(--wp-components-color-gray-400,#c3c4c7);--members-am-muted:var(--wp-components-color-gray-600,#646970)}.members-am-notices{margin:0 0 12px}.members-am-notices .notice{margin:0 0 8px}.members-admin-menus-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;gap:0;margin-bottom:16px;border:1px solid var(--members-am-chrome-border,#c3c4c7);border-radius:2px;background:var(--wp-components-color-background,#fff);overflow:visible}.members-am-toolbar-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px}.members-am-toolbar-row--primary{padding:10px 12px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;row-gap:10px}.members-am-toolbar-group{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px}.members-am-toolbar-group--document{position:relative;z-index:5;padding-right:12px;border-right:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-toolbar-row--primary .members-am-toolbar-primary-user{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;min-width:0}.members-am-toolbar-row--primary .members-am-toolbar-primary-user #members-am-user-search.members-am-user-search-input{width:11rem;max-width:100%;min-width:0;-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-toolbar-extra-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px 16px}.members-am-toolbar-extra .members-am-toolbar-extra-io{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;margin-left:auto;padding-left:12px;border-left:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-toolbar-group--view{margin-left:auto;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.members-am-toolbar-extra{background:var(--members-am-chrome-bg,#f6f7f7);border-top:1px solid var(--members-am-chrome-border,#c3c4c7);padding:10px 12px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:10px}.members-am-toolbar-extra[hidden]{display:none !important}.members-am-toolbar-extra-hint{margin:0 0 8px;max-width:52rem}.members-am-toolbar-admin-editable-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;white-space:normal;max-width:min(100%,22rem)}.members-am-toolbar-extra .members-am-toolbar-admin-editable-inline{max-width:min(100%,26rem)}.members-am-toolbar-row--extra-tools{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap;row-gap:10px}.members-am-toolbar-row--extra-tools .members-am-copy-wrap{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;margin-left:4px;padding-left:12px;border-left:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-toolbar-extra-row .members-am-copy-wrap--extra-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px 8px;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;min-width:0;margin-left:8px;padding-left:12px;border-left:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-toolbar-extra-row .members-am-copy-wrap--extra-inline .members-am-copy-confirm-area{-ms-flex-preferred-size:100%;flex-basis:100%;width:100%}.members-am-toolbar-extra .members-am-copy-select{width:11rem;max-width:100%;min-width:0;-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-toolbar-row--exempt{-ms-flex-preferred-size:100%;flex-basis:100%;width:100%;margin-top:0;padding-top:10px;border-top:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-toolbar-extra .members-am-toolbar-row--exempt{margin-top:4px;padding-top:12px}.members-am-toolbar-extra .members-am-exempt-wrap{width:100%;max-width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-toolbar-extra .members-am-exempt-search{width:min(22rem,100%);max-width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-exempt-wrap .members-am-exempt-chips{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;margin:8px 0}.members-am-exempt-chip{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px;padding:2px 8px;border:1px solid var(--members-am-chrome-border,#c3c4c7);border-radius:4px;background:var(--wp-components-color-background,#fff);font-size:13px}.members-am-exempt-chip .members-am-exempt-remove{padding:0 2px;min-height:0;line-height:1.3}.members-am-more-tools,.members-am-more-tools:hover,.members-am-more-tools:focus,.members-am-more-tools:focus-visible,.members-am-more-tools:active{text-decoration:none !important}.members-am-more-tools{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px;white-space:nowrap;padding:2px 2px 2px 4px;vertical-align:middle;line-height:1.4}.members-am-more-tools .members-am-more-tools-text,.members-am-more-tools:hover .members-am-more-tools-text,.members-am-more-tools:focus .members-am-more-tools-text,.members-am-more-tools:focus-visible .members-am-more-tools-text,.members-am-more-tools:active .members-am-more-tools-text,.members-am-more-tools .members-am-more-tools-chevron,.members-am-more-tools:hover .members-am-more-tools-chevron,.members-am-more-tools:focus .members-am-more-tools-chevron,.members-am-more-tools:focus-visible .members-am-more-tools-chevron,.members-am-more-tools:active .members-am-more-tools-chevron,.members-am-more-tools .members-am-more-tools-chevron-svg,.members-am-more-tools:hover .members-am-more-tools-chevron-svg,.members-am-more-tools:focus .members-am-more-tools-chevron-svg,.members-am-more-tools:focus-visible .members-am-more-tools-chevron-svg,.members-am-more-tools:active .members-am-more-tools-chevron-svg{text-decoration:none !important}.members-am-more-tools .members-am-more-tools-text{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1.4}.members-am-more-tools .members-am-more-tools-chevron{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-negative:0;flex-shrink:0;line-height:0;opacity:.88}.members-am-more-tools .members-am-more-tools-chevron-svg{display:block;overflow:visible;-webkit-transition:-webkit-transform .15s ease;transition:-webkit-transform .15s ease;transition:transform .15s ease;transition:transform .15s ease,-webkit-transform .15s ease;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;shape-rendering:geometricPrecision}.members-am-more-tools .members-am-more-tools-chevron-svg polygon{stroke:none}.members-am-more-tools.is-open .members-am-more-tools-chevron-svg{-webkit-transform:rotate(180deg);transform:rotate(180deg)}@media(prefers-reduced-motion:reduce){.members-am-more-tools .members-am-more-tools-chevron-svg{-webkit-transition:none;transition:none}}@media screen and (max-width:782px){.members-am-toolbar-group--document{border-right:0;padding-right:0;border-bottom:1px solid var(--members-am-chrome-border,#c3c4c7);padding-bottom:10px;width:100%}.members-am-toolbar-group--view{margin-left:0;width:100%;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.members-am-toolbar-row--primary .members-am-toolbar-primary-user{-webkit-box-flex:1;-ms-flex:1 1 100%;flex:1 1 100%;width:100%;padding-top:8px;border-top:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-toolbar-row--primary .members-am-toolbar-primary-user #members-am-user-search.members-am-user-search-input{width:100%;max-width:100%}.members-am-toolbar-extra .members-am-copy-select{width:100%;max-width:100%}.members-am-toolbar-extra .members-am-toolbar-admin-editable-inline{width:100%;max-width:none}.members-am-toolbar-extra .members-am-toolbar-extra-io{margin-left:0;padding-left:0;border-left:0;padding-top:8px;border-top:1px solid var(--members-am-chrome-border,#dcdcde);width:100%}.members-am-toolbar-extra-row .members-am-copy-wrap--extra-inline{-webkit-box-flex:1;-ms-flex:1 1 100%;flex:1 1 100%;width:100%;margin-left:0;padding-left:0;border-left:0;padding-top:8px;border-top:1px solid var(--members-am-chrome-border,#dcdcde)}}.members-am-toolbar-loading{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;margin-left:4px;color:var(--members-am-muted,#646970);font-size:13px;line-height:1}.members-am-toolbar-loading .spinner{float:none;margin:0}.members-am-toolbar-loading[hidden]{display:none !important}.members-am-copy-wrap label{margin-right:8px}.members-am-copy-confirm-area{margin-top:8px;width:100%;-ms-flex-preferred-size:100%;flex-basis:100%}.members-am-inline-copy-notice{margin:0}.members-am-info-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;gap:8px;margin:0 0 12px;padding:10px 12px;background:#fcf9e8;border:1px solid #dba617;border-radius:2px;font-size:12px;color:var(--wp-components-color-foreground,#1d2327);-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-info-bar-legends{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;gap:12px 20px;min-width:0}.members-am-info-bar-legends .members-am-info-item{-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;min-width:0;max-width:none;overflow-wrap:break-word}.members-am-info-item--note{width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-style:italic;color:var(--wp-components-color-gray-700,#50575e)}.members-am-info-icon{margin-right:4px;vertical-align:text-bottom}.members-am-legend-hidden-mark{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-right:6px;padding:2px 5px;border-radius:2px;vertical-align:text-bottom;background:#d63638;color:#fff}.members-am-legend-hidden-mark .dashicons{font-size:14px;width:14px;height:14px;line-height:1}.members-am-chips-wrap{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;margin-bottom:12px}.members-am-chips-actions{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:6px;-ms-flex-negative:0;flex-shrink:0}.members-am-chips,.members-am-chips-inner{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:8px;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-width:0}.members-am-columns-empty{margin:0;padding:24px 12px;text-align:center;font-size:13px;color:var(--members-am-muted,#646970);-webkit-box-flex:1;-ms-flex:1 1 100%;flex:1 1 100%;-ms-flex-item-align:center;align-self:center}.members-am-edit-popover-root:not([hidden]){position:fixed;inset:0;z-index:100001;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding:min(24px,4vw);-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-edit-popover-root[hidden]{display:none !important}.members-am-edit-popover-overlay{position:absolute;inset:0;z-index:0;background:rgba(20,24,28,0.42)}.members-am-edit-popover-dialog{position:relative;z-index:1;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:min(680px,calc(100vw - 32px));max-width:100%;max-height:min(92vh,calc(100vh - 32px));overflow:hidden;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-height:0;background:var(--wp-components-color-background,#fff);border:1px solid var(--members-am-chrome-border,#c3c4c7);border-radius:8px;-webkit-box-shadow:0 4px 6px rgba(0,0,0,0.06),0 16px 48px rgba(0,0,0,0.18),0 0 0 1px rgba(0,0,0,0.04);box-shadow:0 4px 6px rgba(0,0,0,0.06),0 16px 48px rgba(0,0,0,0.18),0 0 0 1px rgba(0,0,0,0.04)}.members-am-edit-popover-arrow{display:none}.members-am-edit-popover-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;gap:12px;padding:12px 16px;background:var(--members-am-base,#1d2327);color:var(--members-am-fg-on-base,#f0f0f1);-ms-flex-negative:0;flex-shrink:0}.members-am-edit-popover-heading{min-width:0;-webkit-box-flex:1;-ms-flex:1;flex:1}.members-am-edit-popover-title{margin:0;font-size:16px;font-weight:600;line-height:1.3;color:inherit}.members-am-edit-popover-subtitle{margin:3px 0 0;font-size:11px;font-weight:400;opacity:.65;color:inherit}.members-am-edit-popover-close{color:color-mix(in srgb,currentColor 62%,transparent) !important;font-size:18px;line-height:1;padding:0 4px !important;text-decoration:none !important}.members-am-edit-popover-close:hover,.members-am-edit-popover-close:focus{color:inherit !important}.members-am-edit-popover-placeholder{margin:0 12px 12px}.members-am-edit-popover-chrome.members-am-edit-toolbar{display:block;margin:0}.members-am-edit-popover-scope{padding:10px 16px;border-bottom:1px solid var(--members-am-chrome-border,#dcdcde);background:var(--members-am-chrome-bg,#f6f7f7);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;-ms-flex-wrap:wrap;flex-wrap:wrap;font-size:12px;color:var(--members-am-muted,#646970);-ms-flex-negative:0;flex-shrink:0}.members-am-edit-popover-body{padding:16px 18px;overflow-y:auto;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-height:0;-ms-scroll-chaining:none;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}.members-am-edit-popover-footer{padding:12px 16px;border-top:1px solid var(--members-am-chrome-border,#dcdcde);background:var(--members-am-chrome-bg,#f6f7f7);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:8px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-ms-flex-negative:0;flex-shrink:0}.members-am-edit-target-wrap{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px;white-space:nowrap}.members-am-edit-target-label{font-weight:600}.members-am-edit-target-select{-webkit-box-flex:1;-ms-flex:1 1 200px;flex:1 1 200px;min-width:min(200px,100%);max-width:100%;font-size:13px;padding:4px 8px;min-height:30px}.members-am-edit-section-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--members-am-muted,#646970);margin:0 0 6px;padding-bottom:4px;border-bottom:1px solid var(--members-am-chrome-border,#dcdcde)}.members-am-edit-popover-actions{padding:10px 16px;border-bottom:1px solid var(--members-am-chrome-border,#dcdcde);background:var(--members-am-chrome-bg,#f6f7f7);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;-ms-flex-negative:0;flex-shrink:0}.members-am-btn-danger{color:#b32d2e !important;border-color:#d63638 !important;-webkit-box-shadow:none !important;box-shadow:none !important}#members-am-remove-custom[hidden]{display:none !important}.members-am-btn-danger:hover{color:#fff !important;background:#d63638 !important;border-color:#d63638 !important}.members-am-demote-parent-label{font-size:13px;margin-right:4px}.members-am-edit-row-title-url{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:12px}.members-am-edit-field label{display:block;font-weight:600;margin-bottom:4px}.members-am-edit-section{margin:0;padding:0;border:0}.members-am-edit-section-title{margin:0 0 10px;font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--members-am-muted,#646970)}.members-am-edit-badge-row{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:12px}.members-am-badge-preview-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px}.members-am-badge-preview-label{font-size:12px;color:var(--members-am-muted,#646970)}.members-am-badge-preview{display:inline-block;min-width:48px;padding:3px 10px;border-radius:3px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.02em}.members-am-edit-colors-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 32px}.members-am-edit-color-field{margin:0}.members-am-edit-color-field label{display:block;margin:0 0 6px}.members-am-edit-color-field .wp-picker-container{display:inline-block;max-width:100%}.members-am-edit-cap-field{margin-top:12px}.members-am-edit-cap-field label{display:block;font-weight:600;margin-bottom:4px}@media screen and (max-width:600px){.members-am-edit-row-title-url,.members-am-edit-badge-row,.members-am-edit-colors-grid{grid-template-columns:1fr}}.members-am-modal[hidden]{display:none !important}.members-am-modal:not([hidden]){position:fixed;inset:0;z-index:100000;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding:24px 12px;-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,0.45)}.members-am-modal-dialog{position:relative;z-index:1;width:100%;max-width:480px;max-height:90vh;overflow:auto;background:var(--wp-components-color-background,#fff);border-radius:4px;-webkit-box-shadow:0 4px 24px rgba(0,0,0,0.2);box-shadow:0 4px 24px rgba(0,0,0,0.2)}.members-am-modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-modal-header h2{margin:0;font-size:16px}.members-am-modal-body{padding:12px 16px}.members-am-modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--members-am-chrome-border,#c3c4c7)}.members-am-chip{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;border-radius:999px;padding:4px 12px 4px 8px;border:1px solid var(--members-am-chrome-border,#c3c4c7);background:var(--wp-components-color-background,#fff);cursor:pointer}.members-am-chip-mark{width:14px;height:14px;border-radius:3px;border:2px solid var(--members-am-role-accent,#8c8f94);background:var(--wp-components-color-background,#fff);-ms-flex-negative:0;flex-shrink:0;-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-chip.is-active{background:color-mix(in srgb,var(--members-am-role-accent,var(--members-am-accent-use)) 12%,var(--wp-components-color-background,#fff));border-color:var(--members-am-role-accent,var(--members-am-accent-use));color:var(--members-am-base,#1d2327)}.members-am-chip.is-active .members-am-chip-mark{background:var(--members-am-role-accent,var(--members-am-accent-use));border-color:var(--members-am-role-accent,var(--members-am-accent-use));-webkit-box-shadow:inset 0 0 0 2px var(--wp-components-color-background,#fff);box-shadow:inset 0 0 0 2px var(--wp-components-color-background,#fff)}.members-am-chip-label{font-size:13px;line-height:1.2}.members-am-cols-host{margin-bottom:12px}.members-am-cols-wrap{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:safe center;-ms-flex-pack:safe center;justify-content:safe center;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;padding-bottom:6px;scroll-behavior:smooth}.members-am-cols-inner{display:block;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;min-width:0;max-width:100%}.members-am-columns{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;gap:14px;width:-webkit-max-content;width:-moz-max-content;width:max-content;min-height:320px;margin:0 auto}.members-am-column{-webkit-box-flex:0;-ms-flex:0 0 350px;flex:0 0 350px;width:350px;min-width:350px;max-width:350px;-ms-flex-negative:0;flex-shrink:0;background:var(--members-am-base,#1d2327);border:1px solid var(--members-am-border-on-base,var(--members-am-border-on-base-fallback));border-radius:4px;color:var(--members-am-fg-on-base,#f0f0f1);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-sizing:border-box;box-sizing:border-box}.members-am-role-chip-wrap{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;vertical-align:middle}.members-am-role-chip-cb{margin:0;-ms-flex-negative:0;flex-shrink:0;-ms-flex-item-align:center;align-self:center;accent-color:var(--members-am-role-accent,var(--members-am-accent-use))}.members-am-chip-pill{padding:4px 10px 4px 8px;border-radius:999px;border:1px solid transparent;gap:6px}.members-am-chip-pill.is-active{background:var(--members-am-role-accent,var(--members-am-accent-use));border-color:var(--members-am-role-accent,var(--members-am-accent-use));color:#fff}.members-am-chip-pill.is-active .members-am-role-chip-cb{accent-color:#fff}.members-am-chip-pill.members-am-chip--inactive{opacity:.45;background:var(--wp-components-color-background,#fff);border-color:var(--members-am-chrome-border,#c3c4c7);color:var(--members-am-base,#1d2327)}.members-am-chip-pill.members-am-chip--inactive .members-am-chip-pill-action{text-decoration:line-through}.members-am-chip-pill-action{border:0;background:transparent;padding:0;margin:0;font:inherit;font-size:13px;line-height:1.2;color:inherit;cursor:pointer;text-align:left;min-width:0;-webkit-box-shadow:none;box-shadow:none}.members-am-chip-pill-action:hover,.members-am-chip-pill-action:focus{color:inherit}.members-am-chip-pill-action:focus-visible{outline:2px solid currentColor;outline-offset:2px}.members-am-user-column{border:2px dashed var(--wp-components-color-gray-700,#8c8f94)}.members-am-sidebar-head{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;padding:9px 10px;background:-webkit-gradient(linear,left top,left bottom,from(var(--members-am-surface-use)),to(var(--members-am-base,#1d2327)));background:linear-gradient(180deg,var(--members-am-surface-use) 0,var(--members-am-base,#1d2327) 100%);border-radius:4px 4px 0 0;border-bottom:1px solid var(--members-am-border-on-base,var(--members-am-border-on-base-fallback));font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--members-am-fg-on-base,#f0f0f1)}.members-am-sidebar-head-titles{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;min-width:0;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.members-am-sidebar-role-dot{width:8px;height:8px;border-radius:50%;-ms-flex-negative:0;flex-shrink:0;background:var(--members-am-accent-use)}.members-am-sidebar-role-dot--user{background:#a7aaad}.members-am-sidebar-head .members-am-sidebar-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.members-am-col-filter{padding:6px 8px 8px;border-bottom:1px solid var(--members-am-border-on-base,var(--members-am-border-on-base-fallback));-ms-flex-negative:0;flex-shrink:0}.members-am-col-filter-input{width:100%;max-width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:12px;line-height:1.4;padding:4px 8px;border-radius:2px;border:1px solid var(--members-am-border-on-base,var(--members-am-border-on-base-fallback));background:var(--members-am-base,#1d2327);color:var(--members-am-fg-on-base,#f0f0f1)}.members-am-col-filter-input::-webkit-input-placeholder{color:var(--members-am-fg-muted-on-base,var(--wp-components-color-gray-700,#8c8f94));opacity:1}.members-am-col-filter-input::-moz-placeholder{color:var(--members-am-fg-muted-on-base,var(--wp-components-color-gray-700,#8c8f94));opacity:1}.members-am-col-filter-input:-ms-input-placeholder{color:var(--members-am-fg-muted-on-base,var(--wp-components-color-gray-700,#8c8f94));opacity:1}.members-am-col-filter-input::-ms-input-placeholder{color:var(--members-am-fg-muted-on-base,var(--wp-components-color-gray-700,#8c8f94));opacity:1}.members-am-col-filter-input::placeholder{color:var(--members-am-fg-muted-on-base,var(--wp-components-color-gray-700,#8c8f94));opacity:1}.members-am-col-filter-input:focus{border-color:var(--members-am-accent-use);outline:1px solid transparent;-webkit-box-shadow:0 0 0 var(--wp-admin-border-width-focus,1px) var(--members-am-accent-use);box-shadow:0 0 0 var(--wp-admin-border-width-focus,1px) var(--members-am-accent-use)}.members-am-col-bulk{padding:8px 8px 10px;border-bottom:1px solid var(--members-am-border-on-base,var(--members-am-border-on-base-fallback));-ms-flex-negative:0;flex-shrink:0;background:rgba(0,0,0,0.12)}.members-am-col-bulk-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:6px;margin-bottom:0;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.members-am-col-bulk-toolbar--grid{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px}.members-am-col-bulk-toolbar--grid .button.button-small{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;text-align:center;white-space:normal;line-height:1.25;padding:5px 6px;min-height:0}.members-am-col-bulk-toolbar .button.button-small{font-size:11px;min-height:26px;line-height:24px;padding:0 8px;background:var(--members-am-surface-use);border-color:var(--members-am-border-on-surface,var(--members-am-border-on-base,var(--members-am-border-on-base-fallback)));color:var(--members-am-fg-on-surface,var(--members-am-fg-on-base,#f0f0f1))}.members-am-col-bulk-toolbar .button.button-small:hover{background:color-mix(in srgb,var(--members-am-surface-use) 85%,#fff 15%);border-color:var(--wp-components-color-gray-700,#8c8f94);color:var(--members-am-fg-on-surface,var(--members-am-fg-on-base,#f0f0f1))}.members-am-item-lead{-ms-flex-negative:0;flex-shrink:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:22px;padding:2px 0 0}.members-am-collapse-spacer{display:block;width:20px;height:20px;-ms-flex-negative:0;flex-shrink:0}.members-am-collapse-toggle{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:22px;height:22px;margin:0;padding:0;border:0;background:transparent;color:var(--members-am-fg-muted-on-base,var(--members-am-chrome-border,#c3c4c7));border-radius:2px;cursor:pointer;-ms-flex-negative:0;flex-shrink:0}.members-am-collapse-toggle:hover,.members-am-collapse-toggle:focus{color:var(--members-am-fg-on-base,#f0f0f1);background:color-mix(in srgb,var(--members-am-fg-on-base,#f0f0f1) 12%,transparent);outline:0}.members-am-collapse-toggle .dashicons{font-size:18px;width:18px;height:18px}.members-am-item-cb-wrap{-ms-flex-negative:0;flex-shrink:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:2px 4px 0 0}.members-am-item-cb{margin:0;width:16px;height:16px;-ms-flex-negative:0;flex-shrink:0}.members-am-bulk-select{width:100%;max-width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:12px;line-height:1.4;padding:4px 8px;border-radius:2px;border:1px solid var(--members-am-border-on-base,var(--members-am-border-on-base-fallback));background:var(--members-am-base,#1d2327);color:var(--members-am-fg-on-base,#f0f0f1)}.members-am-bulk-select:focus{border-color:var(--members-am-accent-use);outline:1px solid transparent;-webkit-box-shadow:0 0 0 var(--wp-admin-border-width-focus,1px) var(--members-am-accent-use);box-shadow:0 0 0 var(--wp-admin-border-width-focus,1px) var(--members-am-accent-use)}.members-am-bulk-select optgroup{font-weight:600;color:#a7aaad;background:var(--members-am-base,#1d2327)}.members-am-bulk-select option{color:var(--members-am-fg-on-base,#f0f0f1);background:var(--members-am-base,#1d2327)}.members-am-item.members-am-filter-hidden,.members-am-sep.members-am-filter-hidden,.members-am-item.members-am-collapse-hidden{display:none !important}.members-am-col-move{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:3px;-ms-flex-negative:0;flex-shrink:0}.members-am-sidebar-head .members-am-col-move-btn{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:28px;height:28px;min-height:28px;padding:0 !important;margin:0;line-height:1;background:color-mix(in srgb,currentColor 8%,transparent);border:1px solid color-mix(in srgb,currentColor 24%,transparent);color:inherit;border-radius:3px}.members-am-sidebar-head .members-am-col-move-btn .dashicons{width:18px;height:18px;font-size:18px;line-height:1;opacity:.92}.members-am-sidebar-head .members-am-col-move-btn:hover,.members-am-sidebar-head .members-am-col-move-btn:focus{background:color-mix(in srgb,currentColor 16%,transparent);border-color:color-mix(in srgb,currentColor 42%,transparent);color:inherit}.members-am-sidebar-head .members-am-user-col-close{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:28px;height:28px;min-height:28px;padding:0 !important;margin:0;background:color-mix(in srgb,currentColor 8%,transparent);border:1px solid color-mix(in srgb,currentColor 24%,transparent);color:inherit;border-radius:3px}.members-am-sidebar-head .members-am-user-col-close .dashicons{width:18px;height:18px;font-size:18px;line-height:1}.members-am-sidebar-head .members-am-user-col-close:hover,.members-am-sidebar-head .members-am-user-col-close:focus{background:rgba(214,54,56,0.35);border-color:rgba(214,54,56,0.65);color:#fff}.members-am-sidebar-list{padding:6px 0 12px;overflow-y:auto;max-height:560px}.members-am-item{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;gap:4px;padding:6px 10px 6px 6px;cursor:pointer;border-left:3px solid transparent;font-size:13px}.members-am-item.is-sub{padding-left:22px;font-size:12px;opacity:.95}.members-am-item.is-sub-deep{padding-left:36px}.members-am-sidebar-list .members-am-item{cursor:-webkit-grab;cursor:grab}.members-am-sidebar-list .members-am-item:active{cursor:-webkit-grabbing;cursor:grabbing}.members-am-sort-placeholder{-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;min-height:36px;margin:2px 0;background:color-mix(in srgb,var(--members-am-accent-use) 8%,transparent);border:1px dashed var(--members-am-accent-use);border-radius:2px}.members-am-sidebar-list>.ui-sortable-helper{-webkit-box-sizing:border-box;box-sizing:border-box;width:100% !important;max-width:100%}.members-am-item.is-hidden{opacity:.72}.members-am-item.is-no-cap{opacity:.55}.members-am-item.is-no-cap.is-hidden{opacity:.5}.members-am-item.is-selected{border-left-color:var(--members-am-accent-use);background:color-mix(in srgb,var(--members-am-accent-use) 15%,transparent)}.members-am-item-main{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;min-width:0}.members-am-item-main .dashicons{font-size:18px;width:18px;height:18px}.members-am-item-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.members-am-item-actions{display:none;gap:2px}.members-am-item:hover .members-am-item-actions{display:-webkit-box;display:-ms-flexbox;display:flex}.members-am-item-actions button{min-height:22px;padding:0 4px;line-height:20px;font-size:11px}.members-am-item-actions button.members-am-eye,.members-am-item-actions button.members-am-user-eye{padding:0 2px}.members-am-item-actions button.members-am-eye .dashicons,.members-am-item-actions button.members-am-user-eye .dashicons{font-size:16px;width:16px;height:16px;line-height:1.2}.members-am-sep{text-align:center;opacity:.35;font-size:11px;padding:4px 0}.members-am-badge{font-size:9px;text-transform:uppercase;padding:1px 4px;border-radius:2px;margin-right:4px;vertical-align:middle}.members-am-badge-new{background:var(--members-am-accent-use);color:var(--wp-components-color-accent-inverted,#fff)}.members-am-badge-edit{background:#dba617;color:var(--members-am-base,#1d2327)}.members-am-badge-nocap{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;background:#dba617;color:var(--members-am-base,#1d2327);padding:2px 5px;border-radius:2px}.members-am-badge-nocap-icon{font-size:14px;width:14px;height:14px;line-height:1}.members-am-badge-hidden{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;background:#d63638;color:#fff;padding:2px 5px;border-radius:2px}.members-am-badge-hidden-icon{font-size:14px;width:14px;height:14px;line-height:1}.members-am-badge-hidden .members-am-badge-hidden-icon,.members-am-badge-nocap .members-am-badge-nocap-icon{color:inherit !important}.members-am-legend{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:16px;margin:0 0 8px;padding:0;font-size:12px;color:var(--members-am-muted,#646970)}.members-am-legend-item{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px}.members-am-edit-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:12px;margin-bottom:12px}.members-am-level-actions .button{margin-right:6px}.members-am-demote-wrap{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:6px}.members-am-demote-wrap .members-am-demote-select{min-width:200px;max-width:min(420px,100%);font-size:13px}.members-am-edit-grid{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:24px}.members-am-edit-col label{display:block;font-weight:600;margin-bottom:4px}.members-am-colors-hint{margin:0 0 10px}.members-am-expand-details{border:1px solid var(--wp-components-color-gray-200,#dcdcde);border-radius:4px;background:var(--wp-components-color-background,#fff);margin:0}.members-am-expand-details-summary{cursor:pointer;list-style:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;gap:8px;padding:10px 12px;margin:0;font-weight:normal}.members-am-expand-details-summary::-webkit-details-marker,.members-am-expand-details-summary::marker{display:none}.members-am-expand-details-summary::before{content:'';-ms-flex-negative:0;flex-shrink:0;width:0;height:0;margin-top:5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:6px solid var(--members-am-muted,#646970);-webkit-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .12s ease;transition:-webkit-transform .12s ease;transition:transform .12s ease;transition:transform .12s ease,-webkit-transform .12s ease}.members-am-expand-details[open]>.members-am-expand-details-summary::before{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.members-am-expand-summary-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:4px;min-width:0;-webkit-box-flex:1;-ms-flex:1;flex:1}.members-am-expand-details-summary .members-am-edit-section-title{margin:0;font-size:14px}.members-am-expand-current-summary{margin:0;font-size:13px;line-height:1.4;color:var(--members-am-fg-muted-on-base,#646970)}.members-am-expand-details[open]>.members-am-expand-panel{border-top:1px solid var(--wp-components-color-gray-200,#dcdcde)}.members-am-expand-panel{padding:12px;margin:0}.members-am-icon-heading-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;gap:8px;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-bottom:4px}.members-am-icon-heading-row .members-am-edit-section-title{margin:0}.members-am-icon-current-summary{margin:0 0 12px;font-size:13px}.members-am-icon-panel>.members-am-icon-tabs{margin-top:0}.members-am-icon-tabs{margin-bottom:8px}.members-am-icon-tabs .button{margin-right:4px;margin-bottom:4px}.members-am-icon-tabs .button.is-active{background:var(--members-am-accent-use);border-color:var(--members-am-accent-use);color:var(--wp-components-color-accent-inverted,#fff)}.members-am-icon-grid{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:5px;max-height:min(240px,38vh);overflow-y:auto;border:1px solid var(--wp-components-color-gray-200,#dcdcde);border-radius:4px;padding:10px;background:var(--members-am-chrome-bg,#f6f7f7);margin-bottom:10px}.members-am-icon-pick{width:36px;height:36px;padding:0;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border:1px solid var(--members-am-chrome-border,#c3c4c7);background:var(--wp-components-color-background,#fff);cursor:pointer}.members-am-bulk-visibility-hint{margin:0 0 8px;font-size:12px;color:var(--members-am-fg-muted-on-base,#646970)}.members-am-vis-role-filter-wrap{margin-bottom:8px}.members-am-vis-row.members-am-vis-filter-hidden{display:none}.members-am-vis-row{display:block;margin-bottom:6px;font-weight:normal}.members-am-reset-dropdown{position:absolute;z-index:100000;background:var(--wp-components-color-background,#fff);border:1px solid var(--members-am-chrome-border,#c3c4c7);border-radius:6px;padding:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-shadow:0 4px 12px rgba(0,0,0,0.15);box-shadow:0 4px 12px rgba(0,0,0,0.15);min-width:280px;top:100%;left:0;margin-top:4px;overflow:hidden}.members-am-reset-title{padding:10px 14px;font-weight:600;font-size:13px;border-bottom:1px solid var(--wp-components-color-gray-200,#f0f0f0);color:var(--wp-components-color-foreground,#1d2327)}.members-am-reset-dropdown>.members-am-reset-option:last-of-type{border-bottom:0}.members-am-reset-option{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;gap:10px;padding:12px 14px;border:0;background:0;cursor:pointer;text-align:left;width:100%;border-bottom:1px solid var(--wp-components-color-gray-200,#f0f0f0);-webkit-transition:background .15s;transition:background .15s}.members-am-reset-option:hover{background:var(--members-am-chrome-bg,#f6f7f7)}.members-am-reset-option .dashicons{margin-top:2px;color:var(--members-am-muted,#646970)}.members-am-reset-option-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:2px}.members-am-reset-option-text strong{font-size:13px;color:var(--wp-components-color-foreground,#1d2327)}.members-am-reset-option-text small{font-size:12px;color:var(--members-am-muted,#646970)}.members-am-reset-danger:hover{background:#fcf0f1}.members-am-reset-danger .dashicons{color:#d63638}.members-am-reset-danger:hover strong{color:#d63638}@media screen and (max-width:782px){.members-am-columns{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}}.members-am-user-suggestions,.members-am-exempt-suggestions{position:absolute;z-index:100;background:var(--wp-components-color-background,#fff);border:1px solid var(--members-am-chrome-border,#c3c4c7);border-radius:4px;max-height:200px;overflow-y:auto;width:100%;-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.15);box-shadow:0 2px 6px rgba(0,0,0,0.15);top:100%;left:0}.members-am-user-suggestion,.members-am-exempt-suggestion{padding:8px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--wp-components-color-gray-200,#f0f0f0)}.members-am-user-suggestion:hover,.members-am-exempt-suggestion:hover{background:color-mix(in srgb,var(--members-am-accent-use) 8%,var(--wp-components-color-background,#fff))}.members-am-user-suggestion:last-child,.members-am-exempt-suggestion:last-child{border-bottom:0}#members-am-import-file.members-am-import-file-hidden{display:none}.members-am-user-search-wrap{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px}#members-am-user-search.members-am-user-search-input{width:168px;max-width:100%}.members-am-legend-visibility-icon{font-size:14px;width:14px;height:14px;vertical-align:middle}.members-am-legend-nocap-badge{display:inline-block;background:#8c8f94;color:#fff;font-size:9px;padding:1px 4px;border-radius:2px;vertical-align:middle}#members-am-icon-preview.members-am-icon-preview{display:none;width:32px;height:32px;-o-object-fit:contain;object-fit:contain;margin-bottom:6px;border:1px solid #ddd;border-radius:3px;padding:2px;background:#f0f0f1}.members-am-icons .members-am-icon-upload-desc{margin-top:4px}#adminmenu .members-am-menu-badge{display:inline-block;color:#fff;font-size:9px;padding:1px 5px;border-radius:2px;line-height:1.4;vertical-align:middle} \ No newline at end of file diff --git a/img/members-admin-menus.svg b/img/members-admin-menus.svg new file mode 100644 index 0000000..d166ef5 --- /dev/null +++ b/img/members-admin-menus.svg @@ -0,0 +1,11 @@ + +Members – Admin Menus + + + + + + + + + \ No newline at end of file diff --git a/js/admin-menus.js b/js/admin-menus.js new file mode 100644 index 0000000..699cfd5 --- /dev/null +++ b/js/admin-menus.js @@ -0,0 +1,4565 @@ +/** + * Members — Admin Menus add-on settings UI. + */ +(function ($) { + 'use strict'; + + var state = { + settings: $.extend(true, {}, membersAdminMenus.settings), + tree: [], + activeRoleSlugs: [], + selectedId: null, + iconTab: 'dashicons', + previewUserId: null, + previewUserRoles: [], + userSuggestions: [], + mediaFrame: null, + allowUnload: false, + syncScroll: (function () { + try { return localStorage.getItem('members_am_sync_scroll') !== '0'; } catch (e) { return true; } + })(), + /** Per-column list filter query (role slug, or `u:` + user id for preview column). */ + columnFilters: {}, + /** Per-column bulk checkbox selection: key -> { ids: { [itemId]: true } }. */ + columnBulkSelection: {}, + /** Per-column collapsed parent ids: key -> { [parentItemId]: true } when children are folded away. */ + collapsedParents: {}, + /** Display labels for exempt administrator IDs (string keys). */ + exemptUserLabels: {}, + /** + * When opening the item editor, optionally pre-select Apply to: from the column that was acted on. + * { type: 'role', slug: string } | { type: 'user', id: number } — consumed once in openEditPanel(). + */ + pendingEditApplyTarget: null, + /** Last menu item id the visibility expand panel was opened for (collapse when selection changes). */ + visibilityDetailsAnchor: null, + }; + + /** + * Distinct hues for role chips (medium–dark for white label text). + * Larger set than before so similar-looking roles collide less often. + */ + var ROLE_ACCENT_PALETTE = [ + '#2271b1', '#1d4ed8', '#0369a1', '#0e7490', '#0f766e', '#15803d', '#4d7c0f', '#a16207', + '#c2410c', '#ea580c', '#b91c1c', '#be185d', '#db2777', '#c026d3', '#9333ea', '#7c3aed', + '#6d28d9', '#4338ca', '#312e81', '#92400e', '#854d0e', '#57534e', '#475569', '#7c2d12', + ]; + + /** + * Stable accent per role slug (colored chip + column header dot). + * FNV-1a 32-bit: the previous djb2+xor mix mapped common WP role slugs to only 3 palette slots (mod 24). + */ + function roleAccentColor(slug) { + slug = String(slug || ''); + var h = 2166136261 >>> 0; + for (var i = 0; i < slug.length; i++) { + h ^= slug.charCodeAt(i); + h = Math.imul(h, 16777619) >>> 0; + } + return ROLE_ACCENT_PALETTE[h % ROLE_ACCENT_PALETTE.length]; + } + + /** Snapshot of persisted settings for unsaved-change detection (object key order–independent). */ + var initialSettingsSerialized = ''; + + /** Skip wpColorPicker `change` / `clear` while programmatically syncing Iris (avoids double save/render). */ + var membersAmColorPickerSuppress = false; + + /** + * Stable JSON string for comparing settings payloads (sorts object keys recursively). + * + * @param {*} value Value from state.settings tree. + * @return {string} + */ + function stableStringify(value) { + if (value === null) { + return 'null'; + } + var t = typeof value; + if (t === 'string' || t === 'number' || t === 'boolean') { + return JSON.stringify(value); + } + if (t === 'undefined') { + return 'null'; + } + if (Array.isArray(value)) { + return '[' + value.map(function (v) { return stableStringify(v); }).join(',') + ']'; + } + if (t === 'object') { + var keys = Object.keys(value).sort(); + return '{' + keys.map(function (k) { + return JSON.stringify(k) + ':' + stableStringify(value[k]); + }).join(',') + '}'; + } + return JSON.stringify(value); + } + + function getSettingsSnapshot() { + return stableStringify(state.settings); + } + + function isSettingsDirty() { + if (state.allowUnload) { + return false; + } + return getSettingsSnapshot() !== initialSettingsSerialized; + } + + function getBeforeUnloadPrompt() { + if (!isSettingsDirty()) { + return; + } + return (membersAdminMenus.i18n && membersAdminMenus.i18n.unsavedChanges) || ''; + } + + var NOTICE_STORAGE_KEY = 'members_am_notice'; + + /** Auto-dismiss success notices after this many ms (WordPress dismiss animation). 0 = off. */ + var MEMBERS_AM_NOTICE_AUTO_DISMISS_MS = 5000; + + /** + * Dismiss a Members Admin Menus notice the same way core does (fade/slide). + * + * @param {JQuery} $notice Notice element. + * @return {void} + */ + function scheduleMembersAmNoticeAutoDismiss($notice) { + if (!MEMBERS_AM_NOTICE_AUTO_DISMISS_MS || MEMBERS_AM_NOTICE_AUTO_DISMISS_MS < 1) { + return; + } + window.setTimeout(function () { + if (!$notice || !$notice.length || !$notice.closest('body').length) { + return; + } + var $btn = $notice.find('.notice-dismiss'); + if ($btn.length) { + $btn.trigger('click'); + return; + } + $notice.fadeTo(200, 0, function () { + $notice.slideUp(200, function () { + $notice.remove(); + }); + }); + }, MEMBERS_AM_NOTICE_AUTO_DISMISS_MS); + } + + /** + * WordPress admin–style dismissible notice. + * + * Do not add `.notice-dismiss` yourself: `makeNoticesDismissible()` in wp-admin/common.js + * skips binding when a dismiss button already exists (see #4.4.0). Trigger `wp-notice-added` + * so core appends the button and wires fade/slide dismissal. + * + * @param {string} type success|error|warning|info + * @param {string} message Plain text. + */ + function showMembersAmNotice(type, message) { + if (!message) { + return; + } + var $container = $('#members-am-notices'); + if (!$container.length) { + $('.members-admin-menus-wrap h1').first().after( + '
' + ); + $container = $('#members-am-notices'); + } + var $notice = $('
', { + class: 'notice is-dismissible', + }).addClass('notice-' + (type || 'info')); + $notice.append($('

').text(message)); + $container.prepend($notice); + $(document).trigger('wp-notice-added'); + if (type === 'success') { + scheduleMembersAmNoticeAutoDismiss($notice); + } + } + + function flashNoticeAfterReload(type, message) { + try { + sessionStorage.setItem( + NOTICE_STORAGE_KEY, + JSON.stringify({ type: type || 'success', message: message }) + ); + } catch (e) {} + } + + function consumeFlashNotice() { + try { + var raw = sessionStorage.getItem(NOTICE_STORAGE_KEY); + if (!raw) { + return; + } + sessionStorage.removeItem(NOTICE_STORAGE_KEY); + var data = JSON.parse(raw); + if (data && data.message) { + showMembersAmNotice(data.type, data.message); + } + } catch (e) {} + } + + var DASHICONS = [ + 'dashicons-menu', 'dashicons-admin-dashboard', 'dashicons-admin-post', 'dashicons-admin-page', + 'dashicons-admin-media', 'dashicons-admin-comments', 'dashicons-admin-appearance', 'dashicons-admin-plugins', + 'dashicons-admin-users', 'dashicons-admin-tools', 'dashicons-admin-settings', 'dashicons-admin-generic', + 'dashicons-edit', 'dashicons-plus', 'dashicons-chart-bar', 'dashicons-cart', 'dashicons-products', + 'dashicons-email', 'dashicons-groups', 'dashicons-heart', 'dashicons-star-filled', 'dashicons-smiley', + 'dashicons-info', 'dashicons-lock', 'dashicons-unlock', 'dashicons-visibility', 'dashicons-hidden', + 'dashicons-arrow-up', 'dashicons-arrow-down', 'dashicons-admin-network', 'dashicons-performance', + ]; + + var FA_ICONS = [ + 'fa-solid fa-house', 'fa-solid fa-user', 'fa-solid fa-gear', 'fa-solid fa-file', 'fa-solid fa-image', + 'fa-solid fa-cart-shopping', 'fa-solid fa-chart-line', 'fa-solid fa-envelope', 'fa-solid fa-book', + 'fa-solid fa-link', 'fa-solid fa-bell', 'fa-solid fa-star', 'fa-solid fa-heart', 'fa-solid fa-lock', + 'fa-solid fa-unlock', 'fa-solid fa-pen', 'fa-solid fa-trash', 'fa-solid fa-plus', 'fa-solid fa-minus', + ]; + + var VIEW_STORAGE_KEY = 'members_am_view_state'; + + function saveViewState() { + try { + localStorage.setItem(VIEW_STORAGE_KEY, JSON.stringify({ + activeRoleSlugs: state.activeRoleSlugs, + })); + } catch (e) {} + } + + function loadViewState() { + try { + var raw = localStorage.getItem(VIEW_STORAGE_KEY); + if (raw) { + return JSON.parse(raw); + } + } catch (e) {} + return null; + } + + function deepClone(o) { + return JSON.parse(JSON.stringify(o)); + } + + /** One-level undo: deep clone of `state.settings` before the last undoable operation. */ + var undoSettingsSnapshot = null; + + function pushUndoSnapshot() { + undoSettingsSnapshot = deepClone(state.settings); + updateUndoButton(); + } + + function updateUndoButton() { + var $btn = $('#members-am-undo'); + if (!$btn.length) { + return; + } + var has = !!undoSettingsSnapshot; + $btn.prop('disabled', !has).attr('aria-disabled', has ? 'false' : 'true'); + } + + function performUndo() { + if (!undoSettingsSnapshot) { + return; + } + state.settings = deepClone(undoSettingsSnapshot); + undoSettingsSnapshot = null; + ensureSettings(); + syncExemptUserLabels(); + state.tree = buildTreeWithCustoms(); + updateUndoButton(); + renderAll(); + var msg = + (membersAdminMenus.i18n && membersAdminMenus.i18n.undoRestored) || + 'Last change reverted.'; + showMembersAmNotice('success', msg); + } + + function getRolesList() { + return membersAdminMenus.roles || []; + } + + function ensureSettings() { + if (!state.settings._meta || Array.isArray(state.settings._meta)) { + state.settings._meta = { + version: 3, + admin_editable: false, + admin_menu_exempt_user_ids: [], + }; + } + if (!Array.isArray(state.settings._meta.admin_menu_exempt_user_ids)) { + state.settings._meta.admin_menu_exempt_user_ids = []; + } + if (!state.settings.roles || Array.isArray(state.settings.roles)) { + state.settings.roles = {}; + } + if (!state.settings.users || Array.isArray(state.settings.users)) { + state.settings.users = {}; + } + if (!Array.isArray(state.settings.custom_items)) { + state.settings.custom_items = []; + } + if (!state.settings.capabilities || Array.isArray(state.settings.capabilities)) { + state.settings.capabilities = {}; + } + } + + function getExemptUserIds() { + ensureSettings(); + var m = state.settings._meta.admin_menu_exempt_user_ids; + if (!Array.isArray(m)) { + return []; + } + return m + .map(function (x) { + return parseInt(x, 10); + }) + .filter(function (n) { + return n > 0; + }); + } + + function syncExemptUserLabels() { + var base = membersAdminMenus.exemptUserLabels || {}; + getExemptUserIds().forEach(function (id) { + var sk = String(id); + if (!state.exemptUserLabels[sk]) { + state.exemptUserLabels[sk] = base[sk] || 'User #' + id; + } + }); + Object.keys(state.exemptUserLabels).forEach(function (sk) { + if (getExemptUserIds().indexOf(parseInt(sk, 10)) === -1) { + delete state.exemptUserLabels[sk]; + } + }); + } + + function renderExemptUi() { + var on = !!state.settings._meta.admin_editable; + var $row = $('#members-am-exempt-row'); + if (!$row.length) { + return; + } + $row.prop('hidden', !on); + if (!on) { + return; + } + var i18n = membersAdminMenus.i18n || {}; + var $chips = $('#members-am-exempt-chips').empty(); + var ids = getExemptUserIds(); + var lastOne = ids.length <= 1; + ids.forEach(function (id) { + var sk = String(id); + var label = state.exemptUserLabels[sk] || 'User #' + id; + var $chip = $(''); + $chip.append($('').text(label)); + var $rm = $('