diff --git a/assets/dev/js/components/confirm-dialog/index.js b/assets/dev/js/components/confirm-dialog/index.js index fd294ad0..d0cadc9d 100644 --- a/assets/dev/js/components/confirm-dialog/index.js +++ b/assets/dev/js/components/confirm-dialog/index.js @@ -1,4 +1,4 @@ -import { AlertTriangleFilledIcon } from '@elementor/icons'; +import AlertTriangleFilledIcon from '@elementor/icons/AlertTriangleFilledIcon'; import Button from '@elementor/ui/Button'; import Dialog from '@elementor/ui/Dialog'; import DialogActions from '@elementor/ui/DialogActions'; diff --git a/assets/dev/js/components/notifications/index.js b/assets/dev/js/components/notifications/index.js index 66cc545b..2237dfb4 100644 --- a/assets/dev/js/components/notifications/index.js +++ b/assets/dev/js/components/notifications/index.js @@ -1,6 +1,6 @@ import Alert from '@elementor/ui/Alert'; import Snackbar from '@elementor/ui/Snackbar'; -import { useNotificationSettings } from '@ea11y/hooks'; +import { useNotificationSettings } from '@ea11y-apps/global/hooks/use-notifications'; const Notifications = ({ type, message }) => { const { diff --git a/modules/settings/assets/js/icons/crown-filled.js b/assets/dev/js/icons/crown-filled.js similarity index 100% rename from modules/settings/assets/js/icons/crown-filled.js rename to assets/dev/js/icons/crown-filled.js diff --git a/assets/dev/js/services/mixpanel/mixpanel-events.js b/assets/dev/js/services/mixpanel/mixpanel-events.js index de113e44..9e5b25fa 100644 --- a/assets/dev/js/services/mixpanel/mixpanel-events.js +++ b/assets/dev/js/services/mixpanel/mixpanel-events.js @@ -38,7 +38,7 @@ export const mixpanelEvents = { fixWithAiButtonClicked: 'fix_with_ai_button_clicked', markAsDecorativeSelected: 'mark_as_decorative_selected', resolveButtonClicked: 'resolve_button_clicked', - navigationImageClicked: 'navigation_image_clicked', + navigationChanged: 'navigation_changed', aiSuggestionAccepted: 'ai_suggestion_accepted', markAsResolveClicked: 'mark_as_resolve_clicked', issueSkipped: 'issue_skipped', @@ -50,6 +50,9 @@ export const mixpanelEvents = { contrastResetClicked: 'contrast_reset_clicked', backgroundAdaptorTriggered: 'background_adaptor_triggered', backgroundAdaptorChanged: 'background_adaptor_changed', + tabSelected: 'tab_selected', + markAsGlobalToggled: 'mark_as_global_toggled', + applyGlobalFixConfirmationClicked: 'apply_global_fix_confirmation_clicked', // Accessibility assistant dashboard assistantDashboardHistoryLogsButtonClicked: 'history_logs_button_clicked', diff --git a/assets/dev/js/utils/color-contrast-helpers.js b/assets/dev/js/utils/color-contrast-helpers.js new file mode 100644 index 00000000..1edbaefb --- /dev/null +++ b/assets/dev/js/utils/color-contrast-helpers.js @@ -0,0 +1,94 @@ +import postcss from 'postcss'; + +export const isValidCSS = (cssText) => { + try { + // Basic checks for common malicious patterns + if (!cssText || typeof cssText !== 'string') { + return false; + } + postcss.parse(cssText); + return true; + } catch (e) { + return false; + } +}; + +export const rgbOrRgbaToHex = (color) => { + const match = color.match( + /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i, + ); + if (!match) { + return null; + } // Not an RGB or RGBA string + + const r = parseInt(match[1]).toString(16).padStart(2, '0'); + const g = parseInt(match[2]).toString(16).padStart(2, '0'); + const b = parseInt(match[3]).toString(16).padStart(2, '0'); + + // If alpha present and less than 1, include it + if (match[4] !== undefined && parseFloat(match[4]) < 1) { + const a = Math.round(parseFloat(match[4]) * 255) + .toString(16) + .padStart(2, '0'); + return `#${r}${g}${b}${a}`.toUpperCase(); // 8-digit hex with alpha + } + + return `#${r}${g}${b}`.toUpperCase(); // 6-digit hex +}; + +export const getDataFromCss = (cssRules) => { + const result = { color: null, background: null }; + + const ruleMatches = cssRules.matchAll(/([^{]+)\s*\{([^}]+)\}/g); + + for (const [, selector, declarations] of ruleMatches) { + let element; + try { + element = document.querySelector(selector.trim()); + } catch { + continue; // Skip invalid selectors + } + + const colorMatch = declarations.match( + /(?%s', + 'https://go.elementor.com/ally-plugins-upgrade/', + esc_html__( 'Upgrade', 'pojo-accessibility' ) + ); + } else { $custom_links['connect'] = sprintf( '%s', admin_url( 'admin.php?page=' . Settings::SETTING_BASE_SLUG ), diff --git a/modules/remediation/actions/attribute.php b/modules/remediation/actions/attribute.php index 86385761..69567245 100644 --- a/modules/remediation/actions/attribute.php +++ b/modules/remediation/actions/attribute.php @@ -17,10 +17,15 @@ class Attribute extends Remediation_Base { public static string $type = 'attribute'; public function run() : ?DOMDocument { - $element_node = $this->get_element_by_xpath( $this->data['xpath'] ); + $element_node = $this->data['global'] + ? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] ) + : $this->get_element_by_xpath( $this->data['xpath'] ); + if ( ! $element_node ) { + $this->use_frontend = true; return null; } + switch ( $this->data['action'] ) { case 'update': case 'add': diff --git a/modules/remediation/actions/replace.php b/modules/remediation/actions/replace.php index d785e313..28e4df69 100644 --- a/modules/remediation/actions/replace.php +++ b/modules/remediation/actions/replace.php @@ -17,15 +17,20 @@ class Replace extends Remediation_Base { public static string $type = 'replace'; public function run() : ?DOMDocument { - $element_node = $this->get_element_by_xpath( $this->data['xpath'] ); + $element_node = $this->data['global'] + ? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] ) + : $this->get_element_by_xpath( $this->data['xpath'] ); + if ( ! $element_node instanceof \DOMElement ) { + $this->use_frontend = true; return null; // nothing to do } $outer_html = $this->dom->saveHTML( $element_node ); if ( stripos( $outer_html, $this->data['find'] ) === false ) { - return $this->dom; + $this->use_frontend = true; + return null; } $updated_html = str_ireplace( $this->data['find'], $this->data['replace'], $outer_html ); @@ -36,7 +41,7 @@ public function run() : ?DOMDocument { $tmp_dom = new DOMDocument( '1.0', 'UTF-8' ); $tmp_dom->loadHTML( - mb_convert_encoding( $updated_html, 'HTML-ENTITIES', 'UTF-8' ), + mb_encode_numericentity( $updated_html, [ 0x80, 0x10FFFF, 0, 0x1FFFFF ], 'UTF-8' ), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING ); diff --git a/modules/remediation/actions/styles.php b/modules/remediation/actions/styles.php index c709f0e8..fdba7c61 100644 --- a/modules/remediation/actions/styles.php +++ b/modules/remediation/actions/styles.php @@ -3,6 +3,7 @@ namespace EA11y\Modules\Remediation\Actions; use DOMDocument; +use DOMElement; use EA11y\Modules\Remediation\Classes\Remediation_Base; if ( ! defined( 'ABSPATH' ) ) { @@ -14,14 +15,145 @@ */ class Styles extends Remediation_Base { public static string $type = 'styles'; + public static string $style_id = 'ea11y-remediation-styles'; - public function __construct( DOMDocument $dom, $data ) { - parent::__construct( $dom, $data ); - $this->use_frontend = true; + /** + * Build a CSS selector string for a given DOMElement. + * + * @param DOMElement|null $element + * @return string|null + */ + public function get_element_css_selector( ?DOMElement $element ): ?string { + if ( ! $element ) { + return null; + } + + $parts = []; + + while ( $element && XML_ELEMENT_NODE === $element->nodeType ) { + $selector = strtolower( $element->tagName ); + + // If element has ID, stop here + if ( $element->hasAttribute( 'id' ) ) { + $selector .= '#' . $element->getAttribute( 'id' ); + array_unshift( $parts, $selector ); + break; + } + + // Add classes unless body + if ( $element->hasAttribute( 'class' ) && strtolower( $element->tagName ) !== 'body' ) { + $classes = preg_split( '/\s+/', trim( $element->getAttribute( 'class' ) ) ); + if ( ! empty( $classes ) ) { + $selector .= '.' . implode( '.', $classes ); + } + } + + // Add nth-of-type if needed + $parent = $element->parentNode; + if ( $parent instanceof DOMElement ) { + $tag_name = $element->tagName; + $siblings = []; + foreach ( $parent->childNodes as $child ) { + if ( $child instanceof DOMElement && $child->tagName === $tag_name ) { + $siblings[] = $child; + } + } + + if ( count( $siblings ) > 1 ) { + foreach ( $siblings as $i => $sibling ) { + if ( $sibling->isSameNode( $element ) ) { + $selector .= ':nth-of-type(' . ( $i + 1 ) . ')'; + break; + } + } + } + } + + array_unshift( $parts, $selector ); + $element = $element->parentNode instanceof DOMElement ? $element->parentNode : null; + } + + return implode( ' > ', $parts ); + } + + /** + * Replace CSS selectors for color and background-color rules. + * + * @param string $css The original CSS string + * @param string|null $color_selector New selector for color rule + * @param string|null $bg_selector New selector for background-color rule + * @return string The modified CSS + */ + public function replace_css_selectors( + string $css, + ?string $color_selector = null, + ?string $bg_selector = null + ): string { + // Match full CSS blocks like "selector { ... }" + preg_match_all( '/([^{]+)\{([^}]+)\}/', $css, $matches, PREG_SET_ORDER ); + + $result = ''; + + foreach ( $matches as $match ) { + $rules = trim( $match[2] ); + + // Find color value + if ( $color_selector && preg_match( '/(?data['rule']; + if ( $this->data['global'] ) { + $el_color = $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] ); + $el_bg = $this->get_element_by_xpath_with_snippet_fallback( $this->data['parentXPath'], $this->data['parentFind'] ); + $color_css_selector = $this->get_element_css_selector( $el_color ); + $bg_css_selector = $this->get_element_css_selector( $el_bg ); + + if ( ! $color_css_selector && ! $bg_css_selector ) { + $this->use_frontend = true; + return null; + } + + $rule = $this->replace_css_selectors( $rule, $color_css_selector, $bg_css_selector ); + } + + // Find or create element + $head = $this->dom->getElementsByTagName( 'head' )->item( 0 ); + if ( ! $head ) { + $head = $this->dom->createElement( 'head' ); + $html_element = $this->dom->getElementsByTagName( 'html' )->item( 0 ); + if ( $html_element ) { + $html_element->insertBefore( $head, $this->dom->getElementsByTagName( 'body' )->item( 0 ) ); + } else { + $this->dom->appendChild( $head ); + } + } + + // Create '; + + return $links; + } + public function __construct() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_action( 'admin_init', [ $this, 'register_base_data' ] ); add_action( 'rest_api_init', [ $this, 'register_settings' ] ); + add_filter( 'plugin_row_meta', array( $this, 'add_plugin_row_meta' ), 10, 2 ); $this->register_routes(); } diff --git a/modules/scanner/assets/js/api/APIScanner.js b/modules/scanner/assets/js/api/APIScanner.js index 2b0130b0..8b84b6c3 100644 --- a/modules/scanner/assets/js/api/APIScanner.js +++ b/modules/scanner/assets/js/api/APIScanner.js @@ -104,6 +104,38 @@ export class APIScanner extends API { }); } + static async setRemediationAsGlobal(data) { + return APIScanner.request({ + method: 'POST', + path: `${v1Prefix}/remediation/global-item`, + data, + }); + } + + static async updateGlobalRemediationForPage(data) { + return APIScanner.request({ + method: 'PUT', + path: `${v1Prefix}/remediation/global-item`, + data, + }); + } + + static async updateGlobalRemediationForAllPages(data) { + return APIScanner.request({ + method: 'PATCH', + path: `${v1Prefix}/remediation/global-item`, + data, + }); + } + + static async deleteGlobalRemediation(data) { + return APIScanner.request({ + method: 'DELETE', + path: `${v1Prefix}/remediation/global-item`, + data, + }); + } + static async updateRemediationStatusForPage(data) { return APIScanner.request({ method: 'PATCH', @@ -112,6 +144,38 @@ export class APIScanner extends API { }); } + static async updateGlobalRemediationGroupForPage(data) { + return APIScanner.request({ + method: 'PUT', + path: `${v1Prefix}/remediation/global-items-group`, + data, + }); + } + + static async updateAllGlobalRemediationForAllPages(data) { + return APIScanner.request({ + method: 'PATCH', + path: `${v1Prefix}/remediation/global-items`, + data, + }); + } + + static async updateAllGlobalRemediationForPage(data) { + return APIScanner.request({ + method: 'PUT', + path: `${v1Prefix}/remediation/global-items`, + data, + }); + } + + static async updateGlobalRemediationGroupForAllPages(data) { + return APIScanner.request({ + method: 'PATCH', + path: `${v1Prefix}/remediation/global-items-group`, + data, + }); + } + static async deleteRemediationForPage(data) { return APIScanner.request({ method: 'DELETE', @@ -120,6 +184,14 @@ export class APIScanner extends API { }); } + static async deleteGlobalRemediations(data) { + return APIScanner.request({ + method: 'DELETE', + path: `${v1Prefix}/remediation/global-items`, + data, + }); + } + static async setHeadingLevel(data) { return APIScanner.request({ method: 'POST', diff --git a/modules/scanner/assets/js/app.js b/modules/scanner/assets/js/app.js index 758eef4f..67c02e3f 100644 --- a/modules/scanner/assets/js/app.js +++ b/modules/scanner/assets/js/app.js @@ -1,28 +1,39 @@ +import Box from '@elementor/ui/Box'; import ErrorBoundary from '@elementor/ui/ErrorBoundary'; +import Tab from '@elementor/ui/Tab'; import { FocusTrap } from 'focus-trap-react'; -import { Notifications } from '@ea11y/components'; +import Notifications from '@ea11y-apps/global/components/notifications'; import { useNotificationSettings } from '@ea11y-apps/global/hooks/use-notifications'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { ErrorMessage } from '@ea11y-apps/scanner/components/error-message'; import Header from '@ea11y-apps/scanner/components/header'; +import AppHeader from '@ea11y-apps/scanner/components/header/app-header'; import { Loader } from '@ea11y-apps/scanner/components/list-loader'; import { NotConnectedMessage } from '@ea11y-apps/scanner/components/not-connected-message'; import { QuotaMessage } from '@ea11y-apps/scanner/components/quota-message'; import { ResolvedMessage } from '@ea11y-apps/scanner/components/resolved-message'; import { BLOCKS, PAGE_QUOTA_LIMIT } from '@ea11y-apps/scanner/constants'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useTabsContext } from '@ea11y-apps/scanner/context/tabs-context'; import { AltTextLayout, + ColorContrastLayout, + HeadingStructureLayout, MainLayout, + ManageAltTextLayout, + ManageColorContrastLayout, ManageMainLayout, + ManageManualLayout, ManualLayout, - RemediationLayout, } from '@ea11y-apps/scanner/layouts'; -import { ColorContrastLayout } from '@ea11y-apps/scanner/layouts/color-contrast-layout'; -import { HeadingStructureLayout } from '@ea11y-apps/scanner/layouts/heading-structure-layout'; -import { AppContainer } from '@ea11y-apps/scanner/styles/app.styles'; +import { + AppContainer, + AppsTabPanel, + AppsTabs, +} from '@ea11y-apps/scanner/styles/app.styles'; import { removeExistingFocus } from '@ea11y-apps/scanner/utils/focus-on-element'; import { useEffect, useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; const App = () => { const { notificationMessage, notificationType } = useNotificationSettings(); @@ -31,15 +42,18 @@ const App = () => { violation, resolved, openedBlock, - isManage, isError, + isManage, quotaExceeded, loading, } = useScannerWizardContext(); + + const { tabsProps, getTabProps, getTabPanelProps } = useTabsContext(); + const containerRef = useRef(null); const showResolvedMessage = Boolean( - (resolved > 0 && violation === resolved) || violation === 0, + !isManage && ((resolved > 0 && violation === resolved) || violation === 0), ); useEffect(() => { @@ -65,7 +79,7 @@ const App = () => { } }, [PAGE_QUOTA_LIMIT, quotaExceeded]); - const getBlock = () => { + const getMsgStateBlock = () => { if (!window.ea11yScannerData?.isConnected) { return ; } @@ -79,11 +93,16 @@ const App = () => { return ; } + return null; + }; + + const getIssuesBlock = () => { + if (showResolvedMessage) { + return ; + } switch (openedBlock) { case BLOCKS.main: return ; - case BLOCKS.management: - return ; case BLOCKS.altText: return ; case BLOCKS.colorContrast: @@ -91,7 +110,22 @@ const App = () => { case BLOCKS.headingStructure: return ; default: - return isManage ? : ; + return ; + } + }; + + const getManageBlock = () => { + switch (openedBlock) { + case BLOCKS.management: + return ; + case BLOCKS.altText: + return ; + case BLOCKS.colorContrast: + return ; + case BLOCKS.headingStructure: + return ; + default: + return ; } }; @@ -102,9 +136,35 @@ const App = () => { > }> + + + + + + + +
- {showResolvedMessage && !isManage ? : getBlock()} + + {getMsgStateBlock() || getIssuesBlock()} + + + {getMsgStateBlock() || getManageBlock()} + { +export const AltTextForm = ({ item, current, setCurrent, setIsEdit }) => { + const { isManage } = useScannerWizardContext(); const { error } = useToastNotification(); const { + isGlobal, + setIsGlobal, data, loadingAiText, isSubmitDisabled, @@ -36,12 +40,21 @@ export const AltTextForm = ({ items, current, setCurrent }) => { handleChange, handleCheck, handleSubmit, + handleUpdate, generateAltText, } = useAltTextForm({ current, - item: items[current], + item, }); + const onGlobalChange = (value) => { + setIsGlobal(value); + }; + + const onCancel = () => { + setIsEdit(false); + }; + const onSubmit = async () => { try { await handleSubmit(); @@ -51,6 +64,11 @@ export const AltTextForm = ({ items, current, setCurrent }) => { } }; + const onUpdate = async () => { + await handleUpdate(); + void (setIsEdit ? setIsEdit(false) : setCurrent(current + 1)); + }; + const onUpgradeHover = () => { mixpanelService.sendEvent(mixpanelEvents.upgradeSuggestionViewed, { current_plan: window.ea11yScannerData?.planData?.plan?.name, @@ -59,11 +77,15 @@ export const AltTextForm = ({ items, current, setCurrent }) => { }); }; + const applyBtnText = isManage + ? __('Apply changes', 'pojo-accessibility') + : __('Apply fix', 'pojo-accessibility'); + return ( - + { InputProps={{ endAdornment: ( - {IS_AI_ENABLED && AI_QUOTA_LIMIT ? ( + {IS_PRO_PLAN && AI_QUOTA_LIMIT ? ( { )} - + + {!isManage && ( + + )} + + + {isManage && ( + + )} + + + ); }; AltTextForm.propTypes = { - items: PropTypes.arrayOf(scannerItem).isRequired, + item: scannerItem, current: PropTypes.number.isRequired, setCurrent: PropTypes.func.isRequired, + setIsEdit: PropTypes.func, }; diff --git a/modules/scanner/assets/js/components/block-button/button-menu.js b/modules/scanner/assets/js/components/block-button/button-menu.js new file mode 100644 index 00000000..5fdc64d7 --- /dev/null +++ b/modules/scanner/assets/js/components/block-button/button-menu.js @@ -0,0 +1,126 @@ +import DotsVerticalIcon from '@elementor/icons/DotsVerticalIcon'; +import ReloadIcon from '@elementor/icons/ReloadIcon'; +import TrashIcon from '@elementor/icons/TrashIcon'; +import Box from '@elementor/ui/Box'; +import IconButton from '@elementor/ui/IconButton'; + +import Menu from '@elementor/ui/Menu'; +import MenuItem from '@elementor/ui/MenuItem'; +import MenuItemIcon from '@elementor/ui/MenuItemIcon'; +import MenuItemText from '@elementor/ui/MenuItemText'; +import PropTypes from 'prop-types'; +import { DeletePageRemediationModal } from '@ea11y-apps/scanner/components/remediation-confirmation-modal'; +import { BLOCK_TITLES } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useManageActions } from '@ea11y-apps/scanner/hooks/use-manage-actions'; +import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles'; +import { useRef, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +export const ButtonMenu = ({ group }) => { + const { sortedRemediation } = useScannerWizardContext(); + const { deleteAllRemediationForPage, updateAllRemediationForPage } = + useManageActions(); + const anchorEl = useRef(null); + + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [isOpened, setIsOpened] = useState(false); + + const count = sortedRemediation[group]?.length; + + const isAllDisabled = + count === + sortedRemediation[group]?.filter( + (remediation) => !Number(remediation.active), + )?.length; + + const handleOpen = () => { + setIsOpened(true); + }; + const handleClose = () => setIsOpened(false); + + const toggleDeleteModal = () => setShowDeleteModal(!showDeleteModal); + + const onDeleteRemediation = async () => { + setShowDeleteModal(false); + await deleteAllRemediationForPage(group); + }; + + return ( + + + + + + {isAllDisabled ? ( + + + + + + + {__('Enable all', 'pojo-accessibility')} + + + ) : ( + + + + + + + {__('Disable all', 'pojo-accessibility')} + + + )} + + + + + + {__('Remove all', 'pojo-accessibility')} + + + + + ); +}; + +ButtonMenu.propTypes = { + group: PropTypes.string.isRequired, +}; diff --git a/modules/scanner/assets/js/components/block-button/global-button-menu.js b/modules/scanner/assets/js/components/block-button/global-button-menu.js new file mode 100644 index 00000000..a04e2f98 --- /dev/null +++ b/modules/scanner/assets/js/components/block-button/global-button-menu.js @@ -0,0 +1,217 @@ +import DotsVerticalIcon from '@elementor/icons/DotsVerticalIcon'; +import ReloadIcon from '@elementor/icons/ReloadIcon'; +import TrashIcon from '@elementor/icons/TrashIcon'; +import Box from '@elementor/ui/Box'; +import IconButton from '@elementor/ui/IconButton'; + +import Menu from '@elementor/ui/Menu'; +import MenuItem from '@elementor/ui/MenuItem'; +import MenuItemIcon from '@elementor/ui/MenuItemIcon'; +import MenuItemText from '@elementor/ui/MenuItemText'; +import PropTypes from 'prop-types'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { + DeleteGlobalRemediationModal, + DisableGlobalRemediationModal, + EnableGlobalRemediationModal, +} from '@ea11y-apps/scanner/components/remediation-confirmation-modal'; +import { BLOCK_TITLES } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useGlobalManageActions } from '@ea11y-apps/scanner/hooks/use-global-manage-actions'; +import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles'; +import { useRef, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +export const GlobalButtonMenu = ({ group }) => { + const { sortedGlobalRemediation } = useScannerWizardContext(); + const { + updateGlobalRemediationGroupForPage, + updateGlobalRemediationGroupForAllPages, + deleteGlobalRemediations, + } = useGlobalManageActions(); + const anchorEl = useRef(null); + + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showDisableModal, setShowDisableModal] = useState(false); + const [showEnableModal, setShowEnableModal] = useState(false); + + const [isOpened, setIsOpened] = useState(false); + + const count = sortedGlobalRemediation[group]?.length; + + const isAllExcluded = + count === + sortedGlobalRemediation[group]?.filter( + (remediation) => !Number(remediation.active_for_page), + )?.length; + + const isAllDisabled = + count === + sortedGlobalRemediation[group]?.filter( + (remediation) => !Number(remediation.active), + )?.length; + + const handleClose = () => setIsOpened(false); + const handleOpen = () => { + setIsOpened(true); + }; + + const onShowDeleteModal = () => { + setShowDeleteModal(true); + mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, { + data: { + popupType: 'global_delete_confirmation', + buttonName: 'Remove across scans', + }, + }); + }; + const onHideDeleteModal = () => setShowDeleteModal(false); + const toggleDisableModal = () => setShowDisableModal(!showDisableModal); + const toggleEnableModal = () => setShowDisableModal(!showDisableModal); + + const onUpdateStatus = () => { + void (isAllDisabled ? setShowEnableModal(true) : setShowDisableModal(true)); + mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, { + data: { + popupType: isAllDisabled + ? 'global_enable_confirmation' + : 'global_disable_confirmation', + buttonName: isAllDisabled + ? __('Enable across scans', 'pojo-accessibility') + : __('Disable across scans', 'pojo-accessibility'), + }, + }); + }; + + const onUpdateForPage = () => { + void updateGlobalRemediationGroupForPage(isAllExcluded, group); + }; + + const onUpdateForAllPages = () => { + void updateGlobalRemediationGroupForAllPages(isAllDisabled, group); + }; + + const onDeleteRemediation = () => { + setShowDeleteModal(false); + void deleteGlobalRemediations(group); + }; + + return ( + + + + + + + {isAllExcluded ? ( + <> + + + + + + {__('Enable on this page', 'pojo-accessibility')} + + + ) : ( + <> + + + + + + {__('Disable on this page', 'pojo-accessibility')} + + + )} + + + + {isAllDisabled ? ( + <> + + + + + + {__('Enable across scans', 'pojo-accessibility')} + + + ) : ( + <> + + + + + + {__('Disable across scans', 'pojo-accessibility')} + + + )} + + + + + + + + + {__('Remove across scans', 'pojo-accessibility')} + + + + + + + + ); +}; + +GlobalButtonMenu.propTypes = { + group: PropTypes.string.isRequired, +}; diff --git a/modules/scanner/assets/js/components/block-button/index.js b/modules/scanner/assets/js/components/block-button/index.js index f6725056..b006890c 100644 --- a/modules/scanner/assets/js/components/block-button/index.js +++ b/modules/scanner/assets/js/components/block-button/index.js @@ -13,6 +13,8 @@ import { export const BlockButton = ({ title, count, block }) => { const { setOpenedBlock, isResolved } = useScannerWizardContext(); + const resolved = count === 0 || isResolved(block); + const handleClick = () => { setOpenedBlock(block); mixpanelService.sendEvent(mixpanelEvents.categoryClicked, { @@ -23,8 +25,6 @@ export const BlockButton = ({ title, count, block }) => { }); }; - const resolved = count === 0 || isResolved(block); - return ( { color={resolved ? 'success' : 'default'} > - + {title} diff --git a/modules/scanner/assets/js/components/block-button/manage-button.js b/modules/scanner/assets/js/components/block-button/manage-button.js index a36de740..33531fba 100644 --- a/modules/scanner/assets/js/components/block-button/manage-button.js +++ b/modules/scanner/assets/js/components/block-button/manage-button.js @@ -1,24 +1,28 @@ +import WorldIcon from '@elementor/icons/WorldIcon'; import Chip from '@elementor/ui/Chip'; import Typography from '@elementor/ui/Typography'; import PropTypes from 'prop-types'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; -import { DeleteButton } from '@ea11y-apps/scanner/components/manage-remediation-buttons/delete-button'; -import { DisableButton } from '@ea11y-apps/scanner/components/manage-remediation-buttons/disable-button'; -import { EnableButton } from '@ea11y-apps/scanner/components/manage-remediation-buttons/enable-button'; -import { BLOCKS } from '@ea11y-apps/scanner/constants'; +import { ButtonMenu } from '@ea11y-apps/scanner/components/block-button/button-menu'; +import { GlobalButtonMenu } from '@ea11y-apps/scanner/components/block-button/global-button-menu'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; import { ActionButton, - ManageButtonGroup, ManageButtonWrap, } from '@ea11y-apps/scanner/styles/app.styles'; import { __, sprintf } from '@wordpress/i18n'; -export const ManageButton = ({ title, count, block }) => { - const { sortedRemediation, setOpenedBlock } = useScannerWizardContext(); +export const ManageButton = ({ title, count, block, global = false }) => { + const { + sortedRemediation, + sortedGlobalRemediation, + setOpenedBlock, + setIsManageGlobal, + } = useScannerWizardContext(); const handleClick = () => { setOpenedBlock(block); + setIsManageGlobal(global); mixpanelService.sendEvent(mixpanelEvents.categoryClicked, { page_url: window.ea11yScannerData?.pageData?.url, issue_count: count, @@ -27,29 +31,24 @@ export const ManageButton = ({ title, count, block }) => { }); }; - const total = sortedRemediation[block].length; - const disabled = block === BLOCKS.colorContrast || block === BLOCKS.altText; - const isAllDisabled = - sortedRemediation[block]?.length === - sortedRemediation[block]?.filter( - (remediation) => !Number(remediation.active), - )?.length; + const remediations = global ? sortedGlobalRemediation : sortedRemediation; + const total = remediations[block].length; return ( - + - + {title} : null} label={sprintf( // Translators: %1$s - active, %2$s - total __('%1$s/%2$s active', 'pojo-accessibility'), @@ -61,14 +60,11 @@ export const ManageButton = ({ title, count, block }) => { size="tiny" disabled={count === 0} /> - - {isAllDisabled ? ( - - ) : ( - - )} - - + {global ? ( + + ) : ( + + )} ); }; @@ -77,4 +73,5 @@ ManageButton.propTypes = { title: PropTypes.string.isRequired, count: PropTypes.number.isRequired, block: PropTypes.string.isRequired, + global: PropTypes.bool, }; diff --git a/modules/scanner/assets/js/components/color-contrast-form/index.js b/modules/scanner/assets/js/components/color-contrast-form/index.js index 6176623e..09efdca8 100644 --- a/modules/scanner/assets/js/components/color-contrast-form/index.js +++ b/modules/scanner/assets/js/components/color-contrast-form/index.js @@ -1,36 +1,44 @@ import Alert from '@elementor/ui/Alert'; import AlertTitle from '@elementor/ui/AlertTitle'; +import Box from '@elementor/ui/Box'; import Button from '@elementor/ui/Button'; import Divider from '@elementor/ui/Divider'; import Typography from '@elementor/ui/Typography'; import PropTypes from 'prop-types'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { rgbOrRgbaToHex } from '@ea11y-apps/global/utils/color-contrast-helpers'; import { ColorSet } from '@ea11y-apps/scanner/components/color-contrast-form/color-set'; import { ColorSetDisabled } from '@ea11y-apps/scanner/components/color-contrast-form/color-set-disabled'; import { ParentSelector } from '@ea11y-apps/scanner/components/color-contrast-form/parent-selector'; +import { SetGlobal } from '@ea11y-apps/scanner/components/manage-footer-actions/page/set-global'; import { BLOCKS } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; import { useColorContrastForm } from '@ea11y-apps/scanner/hooks/use-color-contrast-form'; import { StyledBox } from '@ea11y-apps/scanner/styles/app.styles'; import { scannerItem } from '@ea11y-apps/scanner/types/scanner-item'; import { checkContrastAA } from '@ea11y-apps/scanner/utils/calc-color-ratio'; -import { rgbOrRgbaToHex } from '@ea11y-apps/scanner/utils/convert-colors'; import { useEffect, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -export const ColorContrastForm = ({ items, current, setCurrent }) => { - const item = items[current]; +export const ColorContrastForm = ({ item, current, setCurrent, setIsEdit }) => { + const { isManage } = useScannerWizardContext(); const { + isGlobal, + setIsGlobal, color, background, parents, + isDisabled, resolved, backgroundChanged, + parentChanged, loading, changeColor, changeBackground, setParentSmaller, setParentLarger, onSubmit, + onUpdate, } = useColorContrastForm({ item, current, @@ -47,10 +55,15 @@ export const ColorContrastForm = ({ items, current, setCurrent }) => { } }, [item]); - const isBackgroundEnabled = item.messageArgs[3] && item.messageArgs[4]; + const isBackgroundEnabled = + item.messageArgs[3] && item.messageArgs[4] && !item.isPotential; const colorData = checkContrastAA(item.node); + const onCancel = () => { + setIsEdit(false); + }; + const handleSubmit = async () => { await onSubmit(); mixpanelService.sendEvent(mixpanelEvents.applyFixButtonClicked, { @@ -60,9 +73,32 @@ export const ColorContrastForm = ({ items, current, setCurrent }) => { background_level: parents.length, category_name: BLOCKS.colorContrast, page_url: window.ea11yScannerData?.pageData?.url, + is_global: isGlobal ? 'yes' : 'no', }); }; + const onGlobalChange = (value) => { + setIsGlobal(value); + }; + + const handleUpdate = async () => { + await onUpdate(); + void (setIsEdit ? setIsEdit(false) : setCurrent(current + 1)); + }; + + const isSubmitDisabled = + !colorData.passesAA || + isDisabled || + loading || + (item.isEdit && + color === item.messageArgs[3] && + background === item.messageArgs[4] && + !parentChanged); + + const applyBtnText = isManage + ? __('Apply changes', 'pojo-accessibility') + : __('Apply fix', 'pojo-accessibility'); + return ( @@ -113,22 +149,41 @@ export const ColorContrastForm = ({ items, current, setCurrent }) => { {colorData.ratio} - + + {!isManage && ( + + )} + + {isManage && ( + + )} + + + ); }; ColorContrastForm.propTypes = { - items: PropTypes.arrayOf(scannerItem).isRequired, + item: scannerItem, current: PropTypes.number.isRequired, setCurrent: PropTypes.func.isRequired, + setIsEdit: PropTypes.func, }; diff --git a/modules/scanner/assets/js/components/empty-manage-message/index.js b/modules/scanner/assets/js/components/empty-manage-message/index.js new file mode 100644 index 00000000..032e59d5 --- /dev/null +++ b/modules/scanner/assets/js/components/empty-manage-message/index.js @@ -0,0 +1,41 @@ +import Button from '@elementor/ui/Button'; +import Typography from '@elementor/ui/Typography'; + +import { BLOCKS } from '@ea11y-apps/scanner/constants'; +import { useTabsContext } from '@ea11y-apps/scanner/context/tabs-context'; +import { EmptyImage } from '@ea11y-apps/scanner/images'; +import { StateContainer } from '@ea11y-apps/scanner/styles/app.styles'; +import { __ } from '@wordpress/i18n'; + +export const EmptyManageMessage = () => { + const { tabsProps } = useTabsContext(); + + const handleChangeTab = (event) => { + tabsProps.onChange(event, BLOCKS.main); + }; + + return ( + + + + + {__('No fixes yet', 'pojo-accessibility')} + + + + {__( + 'Once you start fixing issues for this page, they’ll appear here.', + 'pojo-accessibility', + )} + + + + ); +}; diff --git a/modules/scanner/assets/js/components/form-navigation/index.js b/modules/scanner/assets/js/components/form-navigation/index.js index 579b053e..ff77a70c 100644 --- a/modules/scanner/assets/js/components/form-navigation/index.js +++ b/modules/scanner/assets/js/components/form-navigation/index.js @@ -1,65 +1,40 @@ -import ChevronLeftIcon from '@elementor/icons/ChevronLeftIcon'; -import ChevronRightIcon from '@elementor/icons/ChevronRightIcon'; import Box from '@elementor/ui/Box'; import Divider from '@elementor/ui/Divider'; -import IconButton from '@elementor/ui/IconButton'; -import Typography from '@elementor/ui/Typography'; +import Pagination from '@elementor/ui/Pagination'; import { styled } from '@elementor/ui/styles'; import PropTypes from 'prop-types'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; -import { isRTL } from '@ea11y-apps/scanner/constants'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; -import { __, sprintf } from '@wordpress/i18n'; export const FormNavigation = ({ total, current, setCurrent }) => { const { openedBlock } = useScannerWizardContext(); - const previous = current - 1; - const next = current + 1; - const navigate = (index, direction) => () => { - setCurrent(index); - mixpanelService.sendEvent(mixpanelEvents.navigationImageClicked, { - direction, + const onChange = (event, index) => { + setCurrent(index - 1); + mixpanelService.sendEvent(mixpanelEvents.navigationChanged, { + page: index, category: openedBlock, }); }; return ( - - - - - - - - - {sprintf( - // Translators: %1$s - current, %2$s - total - __('%1$s of %2$s issues', 'pojo-accessibility'), - current + 1, - total, - )} - - - - - - - + + + + + + + + + + ); }; -const Navigation = styled(Box)` - position: absolute; +const StyledNavigationContainer = styled(Box)` + position: fixed; bottom: 0; - left: 0; - width: 100%; + right: ${({ theme }) => theme.spacing(2)}; + width: 393px; + background: ${({ theme }) => theme.palette.background.paper}; `; const StyledBox = styled(Box)` display: flex; @@ -74,10 +49,6 @@ const StyledNavigation = styled(Box)` gap: ${({ theme }) => theme.spacing(2)}; `; -const StyledIconButton = styled(IconButton)` - ${isRTL ? 'transform: rotate(180deg)' : ''} -`; - FormNavigation.propTypes = { total: PropTypes.number.isRequired, current: PropTypes.number.isRequired, diff --git a/modules/scanner/assets/js/components/header/header-container.js b/modules/scanner/assets/js/components/header/app-header.js similarity index 70% rename from modules/scanner/assets/js/components/header/header-container.js rename to modules/scanner/assets/js/components/header/app-header.js index 33f255ed..8e125269 100644 --- a/modules/scanner/assets/js/components/header/header-container.js +++ b/modules/scanner/assets/js/components/header/app-header.js @@ -1,27 +1,19 @@ -import ChevronLeftIcon from '@elementor/icons/ChevronLeftIcon'; import XIcon from '@elementor/icons/XIcon'; import Box from '@elementor/ui/Box'; import IconButton from '@elementor/ui/IconButton'; import Paper from '@elementor/ui/Paper'; import { styled } from '@elementor/ui/styles'; -import PropTypes from 'prop-types'; import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner'; import { DropdownMenu } from '@ea11y-apps/scanner/components/header/dropdown-menu'; -import HeaderTitle from '@ea11y-apps/scanner/components/header/title'; -import { BLOCKS, ROOT_ID } from '@ea11y-apps/scanner/constants'; +import AppTitle from '@ea11y-apps/scanner/components/header/title/app-title'; +import { ROOT_ID } from '@ea11y-apps/scanner/constants'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; import { StyledHeaderContent } from '@ea11y-apps/scanner/styles/app.styles'; import { closeWidget } from '@ea11y-apps/scanner/utils/close-widget'; import { __ } from '@wordpress/i18n'; -const HeaderContainer = ({ children }) => { - const { isChanged, setOpenedBlock, isManage, setIsManage } = - useScannerWizardContext(); - - const goBack = () => { - setIsManage(false); - setOpenedBlock(BLOCKS.main); - }; +const AppHeader = () => { + const { isChanged } = useScannerWizardContext(); const onClose = () => { const widget = document.getElementById(ROOT_ID); @@ -46,17 +38,8 @@ const HeaderContainer = ({ children }) => { alignItems="center" > - {isManage && ( - - - - )} - - + @@ -73,8 +56,6 @@ const HeaderContainer = ({ children }) => { - - {children} ); }; @@ -96,8 +77,4 @@ const StyledTitleWrapper = styled(Box)` } `; -HeaderContainer.propTypes = { - children: PropTypes.node, -}; - -export default HeaderContainer; +export default AppHeader; diff --git a/modules/scanner/assets/js/components/header/dropdown-menu.js b/modules/scanner/assets/js/components/header/dropdown-menu.js index 2628f507..1bf39bca 100644 --- a/modules/scanner/assets/js/components/header/dropdown-menu.js +++ b/modules/scanner/assets/js/components/header/dropdown-menu.js @@ -1,9 +1,9 @@ import CalendarDollarIcon from '@elementor/icons/CalendarDollarIcon'; +import ChecklistIcon from '@elementor/icons/ChecklistIcon'; import ClearIcon from '@elementor/icons/ClearIcon'; import DotsHorizontalIcon from '@elementor/icons/DotsHorizontalIcon'; import ExternalLinkIcon from '@elementor/icons/ExternalLinkIcon'; import RefreshIcon from '@elementor/icons/RefreshIcon'; -import SettingsIcon from '@elementor/icons/SettingsIcon'; import ThemeBuilderIcon from '@elementor/icons/ThemeBuilderIcon'; import Box from '@elementor/ui/Box'; import IconButton from '@elementor/ui/IconButton'; @@ -11,27 +11,22 @@ import Menu from '@elementor/ui/Menu'; import MenuItem from '@elementor/ui/MenuItem'; import MenuItemIcon from '@elementor/ui/MenuItemIcon'; import MenuItemText from '@elementor/ui/MenuItemText'; -import Tooltip from '@elementor/ui/Tooltip'; import { ELEMENTOR_URL } from '@ea11y-apps/global/constants'; import { useToastNotification } from '@ea11y-apps/global/hooks'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner'; import { BLOCKS } from '@ea11y-apps/scanner/constants'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import useScannerSettings from '@ea11y-apps/scanner/hooks/use-scanner-settings'; import { DisabledMenuItemText } from '@ea11y-apps/scanner/styles/app.styles'; import { areNoHeadingsDefined } from '@ea11y-apps/scanner/utils/page-headings'; import { useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; export const DropdownMenu = () => { - const { - remediations, - isManage, - openedBlock, - setOpenedBlock, - setIsManage, - runNewScan, - } = useScannerWizardContext(); + const { remediations, isManage, openedBlock, setOpenedBlock, runNewScan } = + useScannerWizardContext(); + const { dashboardUrl } = useScannerSettings(); const { error } = useToastNotification(); const [isOpened, setIsOpened] = useState(false); const [loading, setLoading] = useState(false); @@ -69,16 +64,8 @@ export const DropdownMenu = () => { } }; - const goToManagement = () => { - handleClose(); - setIsManage(true); - setOpenedBlock(BLOCKS.management); - sendOnClickEvent('Manage fixes'); - }; - const goToHeadingManager = () => { handleClose(); - setIsManage(true); setOpenedBlock(BLOCKS.headingStructure); }; @@ -120,6 +107,21 @@ export const DropdownMenu = () => { {__('Rescan', 'pojo-accessibility')} + + sendOnClickEvent('View all scans')} + dense + > + + + + + {__('View all scans', 'pojo-accessibility')} + + + {remediations.length > 0 ? ( { )} - {!remediations.length ? ( - - - - - - - {__('Manage fixes', 'pojo-accessibility')} - - - - ) : ( - - - - - - {__('Manage fixes', 'pojo-accessibility')} - - - )} - {!areNoHeadingsDefined() && ( { results, loading, isError, + isManage, violation: violationsCount, } = useScannerWizardContext(); const { pageData, isConnected } = useScannerSettings(); @@ -29,53 +30,48 @@ const Header = () => { openedBlock === BLOCKS.main && violationsCount > 0; - const showShortHeader = - !isConnected || - isError || - !PAGE_QUOTA_LIMIT || - (!violationsCount && !loading); + const hideHeader = + !isManage && + (!isConnected || + isError || + !PAGE_QUOTA_LIMIT || + (!violationsCount && !loading)); const showStatsBlock = openedBlock === BLOCKS.main || openedBlock === BLOCKS.management; - if (showShortHeader) { - return ; + if (hideHeader) { + return null; } if (showStatsBlock) { return ( - - - - - {pageData.title} - + + + + {pageData.title} + - {showViolationsChip && ( - - )} - + {showViolationsChip && ( + + )} + - - - + {isManage ? : } + ); } - return ( - - - - ); + return ; }; export default Header; diff --git a/modules/scanner/assets/js/components/header/stats/index.js b/modules/scanner/assets/js/components/header/stats/index.js deleted file mode 100644 index 908b1460..00000000 --- a/modules/scanner/assets/js/components/header/stats/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { ManagementStats } from '@ea11y-apps/scanner/components/header/stats/management-stats'; -import { ScanStats } from '@ea11y-apps/scanner/components/header/stats/scan-stats'; -import { BLOCKS } from '@ea11y-apps/scanner/constants'; -import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; - -const Stats = () => { - const { openedBlock } = useScannerWizardContext(); - - switch (openedBlock) { - case BLOCKS.main: - return ; - case BLOCKS.management: - return ; - default: - return false; - } -}; - -export default Stats; diff --git a/modules/scanner/assets/js/components/header/stats/management-stats.js b/modules/scanner/assets/js/components/header/stats/management-stats.js index 2e390cf4..02511b3b 100644 --- a/modules/scanner/assets/js/components/header/stats/management-stats.js +++ b/modules/scanner/assets/js/components/header/stats/management-stats.js @@ -5,21 +5,42 @@ import { StyledSkeleton } from '@ea11y-apps/scanner/styles/app.styles'; import { __, sprintf } from '@wordpress/i18n'; export const ManagementStats = () => { - const { loading, remediations } = useScannerWizardContext(); + const { loading, remediations, globalRemediations } = + useScannerWizardContext(); + + const pageRemediationsActive = remediations?.filter((remediation) => + Number(remediation.active), + ).length; + + const globalRemediationsActive = globalRemediations.filter( + ({ active, active_for_page: activeForPage }) => + Number(active) && (!activeForPage || Number(activeForPage)), + ).length; + + const active = pageRemediationsActive + globalRemediationsActive; + const total = remediations?.length + globalRemediations.length; + + const getStats = () => + active > 0 ? ( + + {sprintf( + // Translators: %1$s - active, %2$s - total + __('%1$s/%2$s fixes are currently active', 'pojo-accessibility'), + active, + total, + )} + + ) : ( + + {__('0 fixes are currently active', 'pojo-accessibility')} + + ); return loading ? ( ) : ( - - {sprintf( - // Translators: %1$s - active, %2$s - total - __('%1$s/%2$s fixes are currently active', 'pojo-accessibility'), - remediations?.filter((remediation) => Number(remediation.active)) - .length, - remediations?.length, - )} - + getStats() ); }; diff --git a/modules/scanner/assets/js/components/header/subheader/breadcrumbs.js b/modules/scanner/assets/js/components/header/subheader/breadcrumbs.js index da72d962..8fa23c24 100644 --- a/modules/scanner/assets/js/components/header/subheader/breadcrumbs.js +++ b/modules/scanner/assets/js/components/header/subheader/breadcrumbs.js @@ -17,9 +17,11 @@ import { __ } from '@wordpress/i18n'; const Breadcrumbs = () => { const { + isManageGlobal, openedBlock, sortedViolations, sortedRemediation, + sortedGlobalRemediation, setOpenedBlock, altTextData, manualData, @@ -31,16 +33,27 @@ const Breadcrumbs = () => { setOpenedBlock(isManage ? BLOCKS.management : BLOCKS.main); }; + const type = isManage ? 'manage' : 'main'; const itemsData = - openedBlock === BLOCKS.altText ? altTextData : manualData[openedBlock]; + openedBlock === BLOCKS.altText + ? altTextData[type] + : manualData[openedBlock]; const resolved = itemsData?.filter((item) => item?.resolved === true).length || 0; - const items = isManage ? sortedRemediation : sortedViolations; - const count = isManage - ? items[openedBlock].length - : items[openedBlock].length - resolved; + const remediations = isManageGlobal + ? sortedGlobalRemediation + : sortedRemediation; + const items = isManage ? remediations : sortedViolations; + const itemsResolved = + items[openedBlock]?.filter((item) => + item?.global === '1' + ? item.active_for_page === '1' + : item?.active === '1', + ).length || 0; + + const count = isManage ? itemsResolved : items[openedBlock].length - resolved; return ( @@ -72,16 +85,15 @@ const Breadcrumbs = () => { } > - + )} - {items[openedBlock].length > 0 && ( + {count > 0 && ( )} diff --git a/modules/scanner/assets/js/components/header/subheader/heading-structure/index.js b/modules/scanner/assets/js/components/header/subheader/heading-structure/index.js index 7b5d628a..be7fa2bb 100644 --- a/modules/scanner/assets/js/components/header/subheader/heading-structure/index.js +++ b/modules/scanner/assets/js/components/header/subheader/heading-structure/index.js @@ -1,7 +1,6 @@ import ArrowLeftIcon from '@elementor/icons/ArrowLeftIcon'; import InfoCircleIcon from '@elementor/icons/InfoCircleIcon'; import Box from '@elementor/ui/Box'; -import Divider from '@elementor/ui/Divider'; import IconButton from '@elementor/ui/IconButton'; import Infotip from '@elementor/ui/Infotip'; import Typography from '@elementor/ui/Typography'; @@ -27,18 +26,6 @@ const HeadingStructureSubheader = () => { setOpenedBlock(isManage ? BLOCKS.management : BLOCKS.main); }; - if (isManage) { - return ( - <> - - - ); - } - return ( <> @@ -72,15 +59,13 @@ const HeadingStructureSubheader = () => { } > - + )} - - {!areNoHeadingsDefined() && ( { const { openedBlock } = useScannerWizardContext(); - switch (openedBlock) { - case BLOCKS.headingStructure: - return ; - default: - return ; - } + return openedBlock === BLOCKS.headingStructure ? ( + + ) : ( + + ); }; export default Subheader; diff --git a/modules/scanner/assets/js/components/header/title/main-title.js b/modules/scanner/assets/js/components/header/title/app-title.js similarity index 78% rename from modules/scanner/assets/js/components/header/title/main-title.js rename to modules/scanner/assets/js/components/header/title/app-title.js index bdd2254a..70a2eaa4 100644 --- a/modules/scanner/assets/js/components/header/title/main-title.js +++ b/modules/scanner/assets/js/components/header/title/app-title.js @@ -1,9 +1,9 @@ import Chip from '@elementor/ui/Chip'; -import { StyledTitle } from '@ea11y/pages/pages.styles'; import { Logo } from '@ea11y-apps/scanner/images'; +import { StyledTitle } from '@ea11y-apps/scanner/styles/app.styles'; import { __ } from '@wordpress/i18n'; -const MainTitle = () => { +const AppTitle = () => { return ( <> @@ -23,4 +23,4 @@ const MainTitle = () => { ); }; -export default MainTitle; +export default AppTitle; diff --git a/modules/scanner/assets/js/components/header/title/index.js b/modules/scanner/assets/js/components/header/title/index.js deleted file mode 100644 index d7785cbb..00000000 --- a/modules/scanner/assets/js/components/header/title/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import MainTitle from '@ea11y-apps/scanner/components/header/title/main-title'; -import ManageFixesTitle from '@ea11y-apps/scanner/components/header/title/manage-fixes-title'; -import ManageHeadingsTitle from '@ea11y-apps/scanner/components/header/title/manage-headings-title'; -import { BLOCKS } from '@ea11y-apps/scanner/constants'; -import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; - -const HeaderTitle = () => { - const { openedBlock, isManage } = useScannerWizardContext(); - - if (!isManage) { - return ; - } - - if (BLOCKS.headingStructure === openedBlock) { - return ; - } - - return ; -}; - -export default HeaderTitle; diff --git a/modules/scanner/assets/js/components/header/title/manage-fixes-title.js b/modules/scanner/assets/js/components/header/title/manage-fixes-title.js deleted file mode 100644 index c88610b1..00000000 --- a/modules/scanner/assets/js/components/header/title/manage-fixes-title.js +++ /dev/null @@ -1,17 +0,0 @@ -import SettingsIcon from '@elementor/icons/SettingsIcon'; -import { StyledTitle } from '@ea11y/pages/pages.styles'; -import { __ } from '@wordpress/i18n'; - -const ManageFixesTitle = () => { - return ( - <> - - - - {__('Manage fixes', 'pojo-accessibility')} - - - ); -}; - -export default ManageFixesTitle; diff --git a/modules/scanner/assets/js/components/header/title/manage-headings-title.js b/modules/scanner/assets/js/components/header/title/manage-headings-title.js deleted file mode 100644 index 491a0944..00000000 --- a/modules/scanner/assets/js/components/header/title/manage-headings-title.js +++ /dev/null @@ -1,27 +0,0 @@ -import { ThemeBuilderIcon } from '@elementor/icons'; -import { StyledTitle } from '@ea11y/pages/pages.styles'; -import { useHeadingStructureContext } from '@ea11y-apps/scanner/context/heading-structure-context'; -import { __, sprintf } from '@wordpress/i18n'; - -const ManageHeadingsTitle = () => { - const { validationStats } = useHeadingStructureContext(); - const title = validationStats.total - ? sprintf( - // Translators: %s - headings count - __('Manage headings (%s)', 'pojo-accessibility'), - validationStats.total, - ) - : __('Manage headings', 'pojo-accessibility'); - - return ( - <> - - - - {title} - - - ); -}; - -export default ManageHeadingsTitle; diff --git a/modules/scanner/assets/js/components/main-list/index.js b/modules/scanner/assets/js/components/main-list/index.js index 2368677c..7d129580 100644 --- a/modules/scanner/assets/js/components/main-list/index.js +++ b/modules/scanner/assets/js/components/main-list/index.js @@ -19,7 +19,7 @@ export const MainList = () => { return []; } const itemsData = - key === BLOCKS.altText ? altTextData : manualData[key]; + key === BLOCKS.altText ? altTextData.main : manualData[key]; const resolved = itemsData?.filter((item) => item?.resolved === true).length || 0; diff --git a/modules/scanner/assets/js/components/manage-alt-text/index.js b/modules/scanner/assets/js/components/manage-alt-text/index.js new file mode 100644 index 00000000..72c6da5b --- /dev/null +++ b/modules/scanner/assets/js/components/manage-alt-text/index.js @@ -0,0 +1,84 @@ +import AlertTitle from '@elementor/ui/AlertTitle'; +import Box from '@elementor/ui/Box'; +import Divider from '@elementor/ui/Divider'; +import Typography from '@elementor/ui/Typography'; +import PropTypes from 'prop-types'; +import { ImagePreview } from '@ea11y-apps/scanner/components/alt-text-form/image-preview'; +import ManageFooterActions from '@ea11y-apps/scanner/components/manage-footer-actions'; +import { ManageItemHeader } from '@ea11y-apps/scanner/components/manage-item-header'; +import { + AlertWithDisabledState, + StyledBox, +} from '@ea11y-apps/scanner/styles/app.styles'; +import { remediationItem } from '@ea11y-apps/scanner/types/remediation-item'; +import { + focusOnElement, + removeExistingFocus, +} from '@ea11y-apps/scanner/utils/focus-on-element'; +import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; +import { useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export const ManageAltText = ({ item, current, openEdit }) => { + const data = JSON.parse(item?.content); + const node = getElementByXPath(data?.xpath); + const global = item.global === '1'; + const isActive = global ? item.active_for_page === '1' : item.active === '1'; + + useEffect(() => { + void (node ? focusOnElement(node) : removeExistingFocus()); + }, [current]); + + const isDecorative = + data.attribute_name === 'role' && data.attribute_value === 'presentation'; + + return ( + + + + + + + {isDecorative ? ( + + + + {__('Decorative image', 'pojo-accessibility')} + + {__( + "(decorative images don't require descriptions)", + 'pojo-accessibility', + )} + + + ) : ( + + + {__('Alt text', 'pojo-accessibility')} + + + {data.attribute_value} + + + )} + + + ); +}; + +ManageAltText.propTypes = { + item: remediationItem, + current: PropTypes.number.isRequired, + openEdit: PropTypes.func.isRequired, +}; diff --git a/modules/scanner/assets/js/components/manage-color-contrast/color-data.js b/modules/scanner/assets/js/components/manage-color-contrast/color-data.js new file mode 100644 index 00000000..98708f12 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-color-contrast/color-data.js @@ -0,0 +1,42 @@ +import Box from '@elementor/ui/Box'; +import Typography from '@elementor/ui/Typography'; +import { styled } from '@elementor/ui/styles'; +import { UnstableColorIndicator } from '@elementor/ui/unstable'; +import PropTypes from 'prop-types'; +import { SunIcon } from '@ea11y-apps/scanner/images'; +import { hexToHsl } from '@ea11y-apps/scanner/utils/convert-colors'; + +export const ColorData = ({ title, color, isActive }) => { + const hslColor = hexToHsl(color); + return ( + + + {title} + + + + {hslColor.l} + % + + + + ); +}; + +const StyledBox = styled(Box)` + color: ${({ isActive, theme }) => + isActive ? theme.palette.text.primary : theme.palette.text.disabled}; + + & svg * { + stroke: ${({ isActive, theme }) => + isActive + ? theme.palette.text.primary + : theme.palette.text.disabled}!important; + } +`; + +ColorData.propTypes = { + title: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, + isActive: PropTypes.bool.isRequired, +}; diff --git a/modules/scanner/assets/js/components/manage-color-contrast/index.js b/modules/scanner/assets/js/components/manage-color-contrast/index.js new file mode 100644 index 00000000..2c5321ec --- /dev/null +++ b/modules/scanner/assets/js/components/manage-color-contrast/index.js @@ -0,0 +1,86 @@ +import Box from '@elementor/ui/Box'; +import Divider from '@elementor/ui/Divider'; +import PropTypes from 'prop-types'; +import { ColorData } from '@ea11y-apps/scanner/components/manage-color-contrast/color-data'; +import ManageFooterActions from '@ea11y-apps/scanner/components/manage-footer-actions'; +import { ManageItemHeader } from '@ea11y-apps/scanner/components/manage-item-header'; +import { BACKGROUND_ELEMENT_CLASS } from '@ea11y-apps/scanner/constants'; +import { + ManageColorAlert, + StyledBox, +} from '@ea11y-apps/scanner/styles/app.styles'; +import { remediationItem } from '@ea11y-apps/scanner/types/remediation-item'; +import { + focusOnElement, + removeExistingFocus, +} from '@ea11y-apps/scanner/utils/focus-on-element'; +import { useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export const ManageColorContrast = ({ + item, + node, + color, + background, + current, + openEdit, +}) => { + useEffect(() => { + void (node ? focusOnElement(node) : removeExistingFocus()); + }, [current]); + + useEffect(() => { + if (background?.item) { + focusOnElement(background?.item, BACKGROUND_ELEMENT_CLASS); + } + }, [background?.item]); + + const global = item.global === '1'; + const isActive = global ? item.active_for_page === '1' : item.active === '1'; + + return ( + + + + + + + {color && ( + + )} + + {color && background && } + + {background && ( + + )} + + + + + + ); +}; + +ManageColorContrast.propTypes = { + item: remediationItem, + node: PropTypes.node.isRequired, + color: PropTypes.shape({ + item: PropTypes.node, + value: PropTypes.string, + }), + current: PropTypes.number.isRequired, + openEdit: PropTypes.func.isRequired, +}; diff --git a/modules/scanner/assets/js/components/manage-footer-actions/global/delete-global-item.js b/modules/scanner/assets/js/components/manage-footer-actions/global/delete-global-item.js new file mode 100644 index 00000000..2b602ad6 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-footer-actions/global/delete-global-item.js @@ -0,0 +1,55 @@ +import TrashIcon from '@elementor/icons/TrashIcon'; +import Divider from '@elementor/ui/Divider'; +import IconButton from '@elementor/ui/IconButton'; + +import Tooltip from '@elementor/ui/Tooltip'; +import PropTypes from 'prop-types'; +import { DeleteGlobalRemediationModal } from '@ea11y-apps/scanner/components/remediation-confirmation-modal'; +import { useGlobalManageActions } from '@ea11y-apps/scanner/hooks/use-global-manage-actions'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +const DeleteGlobalItem = ({ id, rule }) => { + const { activeRequest, deleteGlobalRemediation } = useGlobalManageActions(); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const toggleDeleteModal = () => setShowDeleteModal(!showDeleteModal); + const onDeleteRemediation = async () => { + setShowDeleteModal(false); + await deleteGlobalRemediation(id, rule); + }; + + return ( + <> + + + + + + + + + ); +}; + +DeleteGlobalItem.propTypes = { + id: PropTypes.number.isRequired, + rule: PropTypes.string.isRequired, +}; + +export default DeleteGlobalItem; diff --git a/modules/scanner/assets/js/components/manage-footer-actions/global/manage-global-actions.js b/modules/scanner/assets/js/components/manage-footer-actions/global/manage-global-actions.js new file mode 100644 index 00000000..fa298a36 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-footer-actions/global/manage-global-actions.js @@ -0,0 +1,97 @@ +import ChevronDownIcon from '@elementor/icons/ChevronDownIcon'; +import ReloadIcon from '@elementor/icons/ReloadIcon'; +import Box from '@elementor/ui/Box'; +import Button from '@elementor/ui/Button'; +import Menu from '@elementor/ui/Menu'; +import MenuItem from '@elementor/ui/MenuItem'; +import MenuItemText from '@elementor/ui/MenuItemText'; +import DeleteGlobalItem from '@ea11y-apps/scanner/components/manage-footer-actions/global/delete-global-item'; +import { useGlobalManageActions } from '@ea11y-apps/scanner/hooks/use-global-manage-actions'; +import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles'; +import { scannerItem } from '@ea11y-apps/scanner/types/scanner-item'; +import { useRef, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +const ManageGlobalActions = ({ item }) => { + const [isOpened, setIsOpened] = useState(false); + const anchorEl = useRef(null); + + const isDisabled = item.active !== '1'; + const isExcluded = item.active_for_page !== '1'; + + const { + activeRequest, + updateGlobalRemediationForPage, + updateGlobalRemediationForAllPages, + } = useGlobalManageActions(); + + const handleOpen = () => { + setIsOpened(true); + }; + const handleClose = () => setIsOpened(false); + + const onUpdateForPage = () => { + handleClose(); + void updateGlobalRemediationForPage(item.id, isExcluded, item.rule); + }; + const onUpdateForAllPages = () => { + handleClose(); + void updateGlobalRemediationForAllPages(item.id, isDisabled, item.rule); + }; + + return ( + + {isDisabled && isExcluded && ( + + )} + + + + + {isExcluded + ? __('Enable on this page', 'pojo-accessibility') + : __('Disable on this page', 'pojo-accessibility')} + + + + + + {isDisabled + ? __('Enable across scans', 'pojo-accessibility') + : __('Disable across scans', 'pojo-accessibility')} + + + + + ); +}; + +ManageGlobalActions.propTypes = { + item: scannerItem, +}; + +export default ManageGlobalActions; diff --git a/modules/scanner/assets/js/components/manage-footer-actions/index.js b/modules/scanner/assets/js/components/manage-footer-actions/index.js new file mode 100644 index 00000000..d988d696 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-footer-actions/index.js @@ -0,0 +1,12 @@ +import ManageGlobalActions from '@ea11y-apps/scanner/components/manage-footer-actions/global/manage-global-actions'; +import ManagePageActions from '@ea11y-apps/scanner/components/manage-footer-actions/page/manage-page-actions'; + +const ManageFooterActions = ({ item, isActive }) => { + return item.global === '1' ? ( + + ) : ( + + ); +}; + +export default ManageFooterActions; diff --git a/modules/scanner/assets/js/components/manage-footer-actions/page/manage-page-actions.js b/modules/scanner/assets/js/components/manage-footer-actions/page/manage-page-actions.js new file mode 100644 index 00000000..68769c2b --- /dev/null +++ b/modules/scanner/assets/js/components/manage-footer-actions/page/manage-page-actions.js @@ -0,0 +1,97 @@ +import ReloadIcon from '@elementor/icons/ReloadIcon'; +import TrashIcon from '@elementor/icons/TrashIcon'; +import Box from '@elementor/ui/Box'; +import Button from '@elementor/ui/Button'; +import Divider from '@elementor/ui/Divider'; +import IconButton from '@elementor/ui/IconButton'; +import Tooltip from '@elementor/ui/Tooltip'; +import PropTypes from 'prop-types'; +import { SetGlobal } from '@ea11y-apps/scanner/components/manage-footer-actions/page/set-global'; +import { DeletePageRemediationModal } from '@ea11y-apps/scanner/components/remediation-confirmation-modal'; +import { useManageActions } from '@ea11y-apps/scanner/hooks/use-manage-actions'; +import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles'; +import { remediationItem } from '@ea11y-apps/scanner/types/remediation-item'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +const ManagePageActions = ({ item, isActive }) => { + const { activeRequest, deleteRemediation, updateRemediation } = + useManageActions(item); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const toggleDeleteModal = () => setShowDeleteModal(!showDeleteModal); + const onDeleteRemediation = async () => { + setShowDeleteModal(false); + await deleteRemediation(); + }; + + const data = item?.content ? JSON.parse(item.content) : null; + + return ( + + {isActive ? ( + <> + {data?.find && } + + + ) : ( + + + + + + + + + + )} + + + ); +}; + +ManagePageActions.propTypes = { + item: remediationItem, + isActive: PropTypes.bool.isRequired, +}; + +export default ManagePageActions; diff --git a/modules/scanner/assets/js/components/manage-footer-actions/page/set-global-remediation-modal.js b/modules/scanner/assets/js/components/manage-footer-actions/page/set-global-remediation-modal.js new file mode 100644 index 00000000..1869bc91 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-footer-actions/page/set-global-remediation-modal.js @@ -0,0 +1,45 @@ +import Typography from '@elementor/ui/Typography'; +import PropTypes from 'prop-types'; +import ConfirmDialog from '@ea11y-apps/global/components/confirm-dialog'; +import { __ } from '@wordpress/i18n'; + +export const SetGlobalRemediationModal = ({ + open, + hideConfirmation, + onApprove, +}) => { + return ( + + + {__( + 'This fix will apply to past and future scans, making the issue resolved.', + 'pojo-accessibility', + )} + + + ); +}; + +SetGlobalRemediationModal.propTypes = { + open: PropTypes.bool.isRequired, + hideConfirmation: PropTypes.func.isRequired, + onApprove: PropTypes.func.isRequired, +}; diff --git a/modules/scanner/assets/js/components/manage-footer-actions/page/set-global.js b/modules/scanner/assets/js/components/manage-footer-actions/page/set-global.js new file mode 100644 index 00000000..d0bb6001 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-footer-actions/page/set-global.js @@ -0,0 +1,209 @@ +import InfoCircleIcon from '@elementor/icons/InfoCircleIcon'; +import WorldIcon from '@elementor/icons/WorldIcon'; +import Box from '@elementor/ui/Box'; +import Button from '@elementor/ui/Button'; +import Infotip from '@elementor/ui/Infotip'; +import Switch from '@elementor/ui/Switch'; +import Typography from '@elementor/ui/Typography'; +import PropTypes from 'prop-types'; +import { ProCrownIcon } from '@ea11y/icons'; +import CrownFilled from '@ea11y-apps/global/icons/crown-filled'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { SetGlobalRemediationModal } from '@ea11y-apps/scanner/components/manage-footer-actions/page/set-global-remediation-modal'; +import { IS_PRO_PLAN, UPGRADE_GLOBAL_URL } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useGlobalManageActions } from '@ea11y-apps/scanner/hooks/use-global-manage-actions'; +import infotipImageSrc from '@ea11y-apps/scanner/static/global-infotip-image.png'; +import { StyledProChip } from '@ea11y-apps/scanner/styles/app.styles'; +import { + InfotipBox, + InfotipImage, +} from '@ea11y-apps/scanner/styles/manual-fixes.styles'; +import { remediationItem } from '@ea11y-apps/scanner/types/remediation-item'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export const SetGlobal = ({ + item, + onGlobalChange = null, + isChecked = false, +}) => { + const { openedBlock } = useScannerWizardContext(); + const { setRemediationAsGlobal } = useGlobalManageActions(); + const [showModal, setShowModal] = useState(false); + + const onShowModal = () => { + setShowModal(true); + mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, { + data: { + popupType: 'set_as_global_confirmation', + buttonName: 'Apply across scans', + }, + }); + }; + + const changeIsGlobal = (value) => { + if (onGlobalChange) { + onGlobalChange(value); + } + + mixpanelService.sendEvent(mixpanelEvents.markAsGlobalToggled, { + toggle_status: value ? 'on' : 'off', + source: 'assistant', + category_name: openedBlock, + issue_type: item.message, + rule_id: item.ruleId, + }); + }; + + const onHideModal = () => setShowModal(false); + const onSwitchChange = (event, value) => + onGlobalChange ? changeIsGlobal(value) : onShowModal(); + + const onSetAsGlobal = () => { + setShowModal(false); + void setRemediationAsGlobal(item.id); + }; + + const onUpgrade = () => { + mixpanelService.sendEvent(mixpanelEvents.upgradeButtonClicked, { + current_plan: window.ea11yScannerData?.planData?.plan?.name, + feature_locked: 'globals', + }); + }; + + const UpgradeInfoTip = ({ children }) => { + return ( + + + + + {__('Upgrade to unlock cross-scan fixes', 'pojo-accessibility')} + + + {__( + 'Cross-scan fixes let you resolve the same issue on all scanned pages with a click.', + 'pojo-accessibility', + )} + + + + + + + } + > + {children} + + ); + }; + + return ( + + {IS_PRO_PLAN ? ( + + + + + + + {__('Apply across scans', 'pojo-accessibility')} + + + + + + {__('Fix once, apply everywhere', 'pojo-accessibility')} + + + {__( + 'Apply this fix automatically to pages already scanned and to future scans.', + 'pojo-accessibility', + )} + + + + } + > + + + + ) : ( + + + + + + + + + + + {__('Apply across scans', 'pojo-accessibility')} + + + } + size="small" + /> + + + )} + + + ); +}; + +SetGlobal.propTypes = { + item: remediationItem, + onGlobalChange: PropTypes.func, + isChecked: PropTypes.bool, +}; diff --git a/modules/scanner/assets/js/components/manage-item-header/index.js b/modules/scanner/assets/js/components/manage-item-header/index.js new file mode 100644 index 00000000..3a1114a7 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-item-header/index.js @@ -0,0 +1,73 @@ +import EditIcon from '@elementor/icons/EditIcon'; +import SettingsIcon from '@elementor/icons/SettingsIcon'; +import WorldIcon from '@elementor/icons/WorldIcon'; +import Box from '@elementor/ui/Box'; +import Chip from '@elementor/ui/Chip'; +import IconButton from '@elementor/ui/IconButton'; +import Tooltip from '@elementor/ui/Tooltip'; +import Typography from '@elementor/ui/Typography'; +import PropTypes from 'prop-types'; +import { + ItemHeader, + ItemTitle, +} from '@ea11y-apps/scanner/styles/manual-fixes.styles'; +import { __ } from '@wordpress/i18n'; + +export const ManageItemHeader = ({ isActive, openEdit, global }) => { + return ( + + + + + {isActive + ? __('Active fix', 'pojo-accessibility') + : __('Fix (disabled)', 'pojo-accessibility')} + + {global && ( + } + label={__('Cross-scan', 'pojo-accessibility')} + color="default" + variant="outlined" + size="tiny" + /> + )} + + + + {global ? ( + + + + ) : ( + + + + + + )} + + + ); +}; + +ManageItemHeader.propTypes = { + isActive: PropTypes.bool.isRequired, + openEdit: PropTypes.func.isRequired, + global: PropTypes.bool.isRequired, +}; diff --git a/modules/scanner/assets/js/components/manage-list/index.js b/modules/scanner/assets/js/components/manage-list/index.js deleted file mode 100644 index 34121e34..00000000 --- a/modules/scanner/assets/js/components/manage-list/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import { ManageButton } from '@ea11y-apps/scanner/components/block-button/manage-button'; -import { BLOCK_TITLES, BLOCKS } from '@ea11y-apps/scanner/constants'; -import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; -import { StyledBlockButtonsBox } from '@ea11y-apps/scanner/styles/app.styles'; - -export const ManageList = () => { - const { sortedRemediation } = useScannerWizardContext(); - - return ( - - {Object.keys(sortedRemediation).flatMap((key) => { - if (sortedRemediation[key].length < 1) { - return []; - } - - const resolved = - sortedRemediation[key].filter((item) => Number(item.active)).length || - 0; - - return ( - - ); - })} - - ); -}; diff --git a/modules/scanner/assets/js/components/manage-remediation-buttons/disable-button.js b/modules/scanner/assets/js/components/manage-remediation-buttons/disable-button.js deleted file mode 100644 index f679f2ee..00000000 --- a/modules/scanner/assets/js/components/manage-remediation-buttons/disable-button.js +++ /dev/null @@ -1,29 +0,0 @@ -import BanIcon from '@elementor/icons/BanIcon'; -import Button from '@elementor/ui/Button'; -import IconButton from '@elementor/ui/IconButton'; -import { useManageActions } from '@ea11y-apps/scanner/hooks/use-manage-actions'; -import { __ } from '@wordpress/i18n'; - -export const DisableButton = ({ group }) => { - const { updateAllRemediationForPage } = useManageActions(); - return group ? ( - - - - ) : ( - - ); -}; diff --git a/modules/scanner/assets/js/components/manage-remediation-list/index.js b/modules/scanner/assets/js/components/manage-remediation-list/index.js new file mode 100644 index 00000000..c9be6aeb --- /dev/null +++ b/modules/scanner/assets/js/components/manage-remediation-list/index.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import { ManageButton } from '@ea11y-apps/scanner/components/block-button/manage-button'; +import { BLOCK_TITLES, BLOCKS } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { StyledBlockButtonsBox } from '@ea11y-apps/scanner/styles/app.styles'; + +const ManageRemediationList = ({ global = false }) => { + const { sortedRemediation, sortedGlobalRemediation } = + useScannerWizardContext(); + + const remediations = global ? sortedGlobalRemediation : sortedRemediation; + + return ( + + {Object.keys(remediations).flatMap((key) => { + if (remediations[key].length < 1) { + return []; + } + + const resolved = remediations[key].filter( + ({ active, active_for_page: activeForPage }) => + global ? Number(activeForPage) : Number(active), + ).length; + + return ( + + ); + })} + + ); +}; + +ManageRemediationList.propTypes = { + global: PropTypes.bool, +}; + +export default ManageRemediationList; diff --git a/modules/scanner/assets/js/components/manage-remediation-main-controls/global/delete-global-button.js b/modules/scanner/assets/js/components/manage-remediation-main-controls/global/delete-global-button.js new file mode 100644 index 00000000..b5e26179 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-remediation-main-controls/global/delete-global-button.js @@ -0,0 +1,59 @@ +import TrashIcon from '@elementor/icons/TrashIcon'; +import IconButton from '@elementor/ui/IconButton'; +import Tooltip from '@elementor/ui/Tooltip'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { DeleteGlobalRemediationModal } from '@ea11y-apps/scanner/components/remediation-confirmation-modal'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useGlobalManageActions } from '@ea11y-apps/scanner/hooks/use-global-manage-actions'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +const DeleteGlobalButton = () => { + const { globalRemediations } = useScannerWizardContext(); + const { deleteGlobalRemediations } = useGlobalManageActions(); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const onShowDeleteModal = () => { + setShowDeleteModal(true); + mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, { + data: { + popupType: 'global_delete_confirmation', + buttonName: 'Remove all', + }, + }); + }; + + const onHideDeleteModal = () => setShowDeleteModal(false); + + const onDeleteRemediation = async () => { + setShowDeleteModal(false); + await deleteGlobalRemediations(); + }; + + return ( + <> + + + + + + + + + ); +}; + +export default DeleteGlobalButton; diff --git a/modules/scanner/assets/js/components/manage-remediation-main-controls/global/global-remediation-control-menu.js b/modules/scanner/assets/js/components/manage-remediation-main-controls/global/global-remediation-control-menu.js new file mode 100644 index 00000000..f6169dcf --- /dev/null +++ b/modules/scanner/assets/js/components/manage-remediation-main-controls/global/global-remediation-control-menu.js @@ -0,0 +1,126 @@ +import ChevronDownIcon from '@elementor/icons/ChevronDownIcon'; +import ReloadIcon from '@elementor/icons/ReloadIcon'; +import Box from '@elementor/ui/Box'; +import Button from '@elementor/ui/Button'; +import Menu from '@elementor/ui/Menu'; +import MenuItem from '@elementor/ui/MenuItem'; +import MenuItemText from '@elementor/ui/MenuItemText'; +import PropTypes from 'prop-types'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { + DisableGlobalRemediationModal, + EnableGlobalRemediationModal, +} from '@ea11y-apps/scanner/components/remediation-confirmation-modal'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useGlobalManageActions } from '@ea11y-apps/scanner/hooks/use-global-manage-actions'; +import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles'; +import { useRef, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +const GlobalRemediationControlMenu = ({ isAllExcluded, isAllDisabled }) => { + const { globalRemediations } = useScannerWizardContext(); + + const [isOpened, setIsOpened] = useState(false); + const anchorEl = useRef(null); + + const [showDisableModal, setShowDisableModal] = useState(false); + const [showEnableModal, setShowEnableModal] = useState(false); + + const { + updateAllGlobalRemediationForPage, + updateAllGlobalRemediationForAllPages, + } = useGlobalManageActions(); + + const handleOpen = () => { + setIsOpened(true); + }; + const handleClose = () => setIsOpened(false); + + const onUpdateStatus = () => { + void (isAllDisabled ? setShowEnableModal(true) : setShowDisableModal(true)); + mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, { + data: { + popupType: isAllDisabled + ? 'global_enable_confirmation' + : 'global_disable_confirmation', + buttonName: isAllDisabled + ? 'Enable across scans' + : 'Disable across scans', + }, + }); + }; + + const onUpdateForPage = () => { + void updateAllGlobalRemediationForPage(isAllExcluded); + }; + const onUpdateForAllPages = () => { + void updateAllGlobalRemediationForAllPages(isAllDisabled); + }; + + const toggleDisableModal = () => setShowDisableModal(!showDisableModal); + const toggleEnableModal = () => setShowDisableModal(!showDisableModal); + + return ( + + + + + + {isAllExcluded + ? __('Enable on this page', 'pojo-accessibility') + : __('Disable on this page', 'pojo-accessibility')} + + + + + + {isAllDisabled + ? __('Enable across scans', 'pojo-accessibility') + : __('Disable across scans', 'pojo-accessibility')} + + + + + + + ); +}; + +GlobalRemediationControlMenu.propTypes = { + isAllExcluded: PropTypes.bool.isRequired, + isAllDisabled: PropTypes.bool.isRequired, +}; + +export default GlobalRemediationControlMenu; diff --git a/modules/scanner/assets/js/components/manage-remediation-main-controls/global/manage-global-remediation-control.js b/modules/scanner/assets/js/components/manage-remediation-main-controls/global/manage-global-remediation-control.js new file mode 100644 index 00000000..9023e4f7 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-remediation-main-controls/global/manage-global-remediation-control.js @@ -0,0 +1,38 @@ +import Box from '@elementor/ui/Box'; +import Divider from '@elementor/ui/Divider'; +import DeleteGlobalButton from '@ea11y-apps/scanner/components/manage-remediation-main-controls/global/delete-global-button'; +import GlobalRemediationControlMenu from '@ea11y-apps/scanner/components/manage-remediation-main-controls/global/global-remediation-control-menu'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; + +const ManageGlobalRemediationControl = () => { + const { globalRemediations } = useScannerWizardContext(); + const count = globalRemediations?.length; + + const isAllExcluded = + count === + globalRemediations?.filter( + (remediation) => !Number(remediation.active_for_page), + )?.length; + + const isAllDisabled = + count === + globalRemediations?.filter((remediation) => !Number(remediation.active)) + ?.length; + + return ( + + {isAllExcluded && isAllDisabled && ( + <> + + + + )} + + + ); +}; + +export default ManageGlobalRemediationControl; diff --git a/modules/scanner/assets/js/components/manage-remediation-main-controls/index.js b/modules/scanner/assets/js/components/manage-remediation-main-controls/index.js new file mode 100644 index 00000000..5570b242 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-remediation-main-controls/index.js @@ -0,0 +1,2 @@ +export { default as ManageRemediationControl } from './page/manage-remediation-control'; +export { default as ManageGlobalRemediationControl } from './global/manage-global-remediation-control'; diff --git a/modules/scanner/assets/js/components/manage-remediation-buttons/delete-button.js b/modules/scanner/assets/js/components/manage-remediation-main-controls/page/delete-button.js similarity index 52% rename from modules/scanner/assets/js/components/manage-remediation-buttons/delete-button.js rename to modules/scanner/assets/js/components/manage-remediation-main-controls/page/delete-button.js index 5d2e0213..83bb6a0e 100644 --- a/modules/scanner/assets/js/components/manage-remediation-buttons/delete-button.js +++ b/modules/scanner/assets/js/components/manage-remediation-main-controls/page/delete-button.js @@ -1,12 +1,14 @@ import TrashIcon from '@elementor/icons/TrashIcon'; -import Button from '@elementor/ui/Button'; import IconButton from '@elementor/ui/IconButton'; -import { DeleteRemediationModal } from '@ea11y-apps/scanner/components/delete-remediation-modal'; +import Tooltip from '@elementor/ui/Tooltip'; +import { DeletePageRemediationModal } from '@ea11y-apps/scanner/components/remediation-confirmation-modal'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; import { useManageActions } from '@ea11y-apps/scanner/hooks/use-manage-actions'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -export const DeleteButton = ({ group }) => { +export const DeleteButton = () => { + const { remediations } = useScannerWizardContext(); const { deleteAllRemediationForPage } = useManageActions(); const [showDeleteModal, setShowDeleteModal] = useState(false); @@ -14,36 +16,29 @@ export const DeleteButton = ({ group }) => { const onDeleteRemediation = async () => { setShowDeleteModal(false); - await deleteAllRemediationForPage(group); + await deleteAllRemediationForPage(); }; return ( <> - {group ? ( - + + - ) : ( - - )} + - diff --git a/modules/scanner/assets/js/components/manage-remediation-main-controls/page/disable-button.js b/modules/scanner/assets/js/components/manage-remediation-main-controls/page/disable-button.js new file mode 100644 index 00000000..e077a7f7 --- /dev/null +++ b/modules/scanner/assets/js/components/manage-remediation-main-controls/page/disable-button.js @@ -0,0 +1,22 @@ +import Button from '@elementor/ui/Button'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useManageActions } from '@ea11y-apps/scanner/hooks/use-manage-actions'; +import { StyledBanIcon } from '@ea11y-apps/scanner/styles/app.styles'; +import { __ } from '@wordpress/i18n'; + +export const DisableButton = () => { + const { remediations } = useScannerWizardContext(); + const { updateAllRemediationForPage } = useManageActions(); + return ( + + ); +}; diff --git a/modules/scanner/assets/js/components/manage-remediation-buttons/enable-button.js b/modules/scanner/assets/js/components/manage-remediation-main-controls/page/enable-button.js similarity index 58% rename from modules/scanner/assets/js/components/manage-remediation-buttons/enable-button.js rename to modules/scanner/assets/js/components/manage-remediation-main-controls/page/enable-button.js index dea1bc44..36717d31 100644 --- a/modules/scanner/assets/js/components/manage-remediation-buttons/enable-button.js +++ b/modules/scanner/assets/js/components/manage-remediation-main-controls/page/enable-button.js @@ -1,21 +1,11 @@ import ReloadIcon from '@elementor/icons/ReloadIcon'; import Button from '@elementor/ui/Button'; -import IconButton from '@elementor/ui/IconButton'; import { useManageActions } from '@ea11y-apps/scanner/hooks/use-manage-actions'; import { __ } from '@wordpress/i18n'; -export const EnableButton = ({ group }) => { +export const EnableButton = () => { const { updateAllRemediationForPage } = useManageActions(); - return group ? ( - - - - ) : ( + return ( ) : ( - - - + )} - + {isEdit ? ( + + ) : ( + + )} @@ -257,7 +276,7 @@ export const ResolveWithAi = ({ item, current }) => { ) : ( - - - - - - {isActive ? ( - - ) : ( - - )} - + )} - ); }; diff --git a/modules/scanner/assets/js/components/resolved-message/index.js b/modules/scanner/assets/js/components/resolved-message/index.js index 1f41e23c..287a00a9 100644 --- a/modules/scanner/assets/js/components/resolved-message/index.js +++ b/modules/scanner/assets/js/components/resolved-message/index.js @@ -2,6 +2,7 @@ import Button from '@elementor/ui/Button'; import Typography from '@elementor/ui/Typography'; import { BLOCKS, ROOT_ID } from '@ea11y-apps/scanner/constants'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useTabsContext } from '@ea11y-apps/scanner/context/tabs-context'; import { ResolvedImage } from '@ea11y-apps/scanner/images'; import { ResolvedButtonsBox, @@ -11,19 +12,18 @@ import { closeWidget } from '@ea11y-apps/scanner/utils/close-widget'; import { __ } from '@wordpress/i18n'; export const ResolvedMessage = () => { - const { remediations, setOpenedBlock, setIsManage } = - useScannerWizardContext(); + const { remediations } = useScannerWizardContext(); + const { tabsProps } = useTabsContext(); + + const handleChangeTab = (event) => { + tabsProps.onChange(event, BLOCKS.management); + }; const onClose = () => { const widget = document.getElementById(ROOT_ID); closeWidget(widget); }; - const showIssues = () => { - setIsManage(true); - setOpenedBlock(BLOCKS.management); - }; - return ( @@ -45,7 +45,7 @@ export const ResolvedMessage = () => { size="small" variant="outlined" color="secondary" - onClick={showIssues} + onClick={handleChangeTab} > {__('Review fixes', 'pojo-accessibility')} diff --git a/modules/scanner/assets/js/components/upgrade-info-tip/upgrade-content.js b/modules/scanner/assets/js/components/upgrade-info-tip/upgrade-content.js index 861ca084..98e2e87e 100644 --- a/modules/scanner/assets/js/components/upgrade-info-tip/upgrade-content.js +++ b/modules/scanner/assets/js/components/upgrade-info-tip/upgrade-content.js @@ -3,11 +3,11 @@ import Button from '@elementor/ui/Button'; import IconButton from '@elementor/ui/IconButton'; import Typography from '@elementor/ui/Typography'; import PropTypes from 'prop-types'; -import CrownFilled from '@ea11y/icons/crown-filled'; +import CrownFilled from '@ea11y-apps/global/icons/crown-filled'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { COMPARE_PLAN_URL, - IS_AI_ENABLED, + IS_PRO_PLAN, UPGRADE_URL, } from '@ea11y-apps/scanner/constants'; import { UpgradeContentContainer } from '@ea11y-apps/scanner/styles/app.styles'; @@ -30,7 +30,7 @@ export const UpgradeContent = ({ closeUpgrade, isAlt = false }) => { - {IS_AI_ENABLED + {IS_PRO_PLAN ? __("You've reached the monthly limit", 'pojo-accessibility') : __('Resolve issues automatically with AI', 'pojo-accessibility')} @@ -46,7 +46,7 @@ export const UpgradeContent = ({ closeUpgrade, isAlt = false }) => { )} - {IS_AI_ENABLED + {IS_PRO_PLAN ? __( 'To work more magic with AI, upgrade your plan or wait until next month.', 'pojo-accessibility', @@ -61,13 +61,13 @@ export const UpgradeContent = ({ closeUpgrade, isAlt = false }) => { size="small" color="promotion" variant="contained" - href={IS_AI_ENABLED ? COMPARE_PLAN_URL : UPGRADE_URL} + href={IS_PRO_PLAN ? COMPARE_PLAN_URL : UPGRADE_URL} target="_blank" rel="noreferrer" - startIcon={!IS_AI_ENABLED ? : null} + startIcon={!IS_PRO_PLAN ? : null} onClick={onUpgrade} > - {IS_AI_ENABLED + {IS_PRO_PLAN ? __('Compare plans', 'pojo-accessibility') : __('Upgrade now', 'pojo-accessibility')} diff --git a/modules/scanner/assets/js/constants/index.js b/modules/scanner/assets/js/constants/index.js index c1c0d01b..847a389f 100644 --- a/modules/scanner/assets/js/constants/index.js +++ b/modules/scanner/assets/js/constants/index.js @@ -21,12 +21,14 @@ export const RULE_TEXT_CONTRAST = 'text_contrast_sufficient'; export const RATIO_EXCLUDED = 1; export const UPGRADE_URL = 'https://go.elementor.com/acc-free-no-AI-scanner'; +export const UPGRADE_GLOBAL_URL = + 'https://go.elementor.com/acc-global-remediation'; export const COMPARE_PLAN_URL = 'https://go.elementor.com/acc-AI-limit-scanner'; export const PAGE_LIMIT_URL = 'https://go.elementor.com/acc-URL-limit-scanner'; export const isRTL = Boolean(window.ea11yScannerData?.isRTL); -export const IS_AI_ENABLED = !window.ea11yScannerData?.planData?.plan?.name +export const IS_PRO_PLAN = !window.ea11yScannerData?.planData?.plan?.name ?.toLowerCase() .includes('free'); export const AI_QUOTA_LIMIT = @@ -34,6 +36,8 @@ export const AI_QUOTA_LIMIT = window.ea11yScannerData?.planData?.aiCredits?.used > 0; +export const HIDE_UPGRADE_KEY = 'ea11y-hide-upgrade-alert'; + export const PAGE_PER_PLAN = window.ea11yScannerData?.planData?.scannedPages?.allowed; diff --git a/modules/scanner/assets/js/context/scanner-wizard-context.js b/modules/scanner/assets/js/context/scanner-wizard-context.js index 9497b3e8..52d844e2 100644 --- a/modules/scanner/assets/js/context/scanner-wizard-context.js +++ b/modules/scanner/assets/js/context/scanner-wizard-context.js @@ -1,398 +1,23 @@ -import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; -import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner'; -import { - BLOCKS, - INITIAL_SORTED_VIOLATIONS, - LEVEL_POTENTIAL, - LEVEL_VIOLATION, - MANAGE_URL_PARAM, - MANUAL_GROUPS, - RATIO_EXCLUDED, - RULE_TEXT_CONTRAST, -} from '@ea11y-apps/scanner/constants'; -import useScannerSettings from '@ea11y-apps/scanner/hooks/use-scanner-settings'; -import { runAllEa11yRules } from '@ea11y-apps/scanner/rules'; -import { scannerWizard } from '@ea11y-apps/scanner/services/scanner-wizard'; -import { - focusOnElement, - removeExistingFocus, -} from '@ea11y-apps/scanner/utils/focus-on-element'; -import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; -import { getPageHeadingsTree } from '@ea11y-apps/scanner/utils/page-headings'; -import { - sortRemediation, - sortViolations, -} from '@ea11y-apps/scanner/utils/sort-violations'; -import { - calculateStats, - validateHeadings, -} from '@ea11y-apps/scanner/utils/validate-headings'; -import { - createContext, - useContext, - useEffect, - useState, -} from '@wordpress/element'; +import useScannerWizardActions from '@ea11y-apps/scanner/hooks/scanner-context/useScannerWizardActions'; +import useScannerWizardEffects from '@ea11y-apps/scanner/hooks/scanner-context/useScannerWizardEffects'; +import useScannerWizardState from '@ea11y-apps/scanner/hooks/scanner-context/useScannerWizardState'; +import { createContext, useContext } from '@wordpress/element'; -export const ScannerWizardContext = createContext({ - results: {}, - remediations: [], - currentScanId: null, - resolved: 0, - openedBlock: '', - loading: null, - isError: false, - quotaExceeded: false, - isManage: false, - isChanged: false, - sortedViolations: INITIAL_SORTED_VIOLATIONS, - sortedRemediation: INITIAL_SORTED_VIOLATIONS, - altTextData: [], - manualData: {}, - remediationData: {}, - violation: null, - openIndex: null, - setSortedRemediation: () => {}, - setOpenedBlock: () => {}, - setResolved: () => {}, - getResults: () => {}, - setAltTextData: () => {}, - setManualData: () => {}, - setLoading: () => {}, - setRemediationData: () => {}, - updateRemediationList: () => {}, - setIsManage: () => {}, - setIsManageChanged: () => {}, - isResolved: () => {}, - handleOpen: () => {}, - setOpenIndex: () => {}, - runNewScan: () => {}, -}); +export const ScannerWizardContext = createContext(null); -export const ScannerWizardContextProvider = ({ children }) => { - const { dismissedHeadingIssues } = useScannerSettings(); +const ScannerWizardContextProvider = ({ children }) => { + const state = useScannerWizardState(); + const actions = useScannerWizardActions(state); + useScannerWizardEffects(state, actions); - const [results, setResults] = useState(); - const [remediations, setRemediations] = useState([]); - const [sortedViolations, setSortedViolations] = useState( - INITIAL_SORTED_VIOLATIONS, - ); - const [sortedRemediation, setSortedRemediation] = useState( - INITIAL_SORTED_VIOLATIONS, - ); - const [resolved, setResolved] = useState(0); - const [currentScanId, setCurrentScanId] = useState(null); - const [openedBlock, setOpenedBlock] = useState(BLOCKS.main); - const [loading, setLoading] = useState(true); - const [isError, setIsError] = useState(false); - const [quotaExceeded, setQuotaExceeded] = useState(false); - const [isManage, setIsManage] = useState(false); - const [isManageChanged, setIsManageChanged] = useState(false); - const [altTextData, setAltTextData] = useState([]); - const [manualData, setManualData] = useState(structuredClone(MANUAL_GROUPS)); - const [remediationData, setRemediationData] = useState( - structuredClone(MANUAL_GROUPS), - ); - const [openIndex, setOpenIndex] = useState(null); - const [violation, setViolation] = useState(null); - - useEffect(() => { - const items = isManage - ? sortedRemediation[openedBlock] - : sortedViolations[openedBlock]; - if ( - openIndex !== null && - sortedViolations[openedBlock]?.length && - openIndex < items?.length - ) { - const element = isManage - ? getElementByXPath( - JSON.parse(sortedRemediation[openedBlock][openIndex].content) - ?.xpath, - ) - : sortedViolations[openedBlock][openIndex].node; - focusOnElement(element); - } else { - removeExistingFocus(); - } - }, [openIndex]); - - useEffect(() => { - if ( - Object.keys(sortedRemediation).every( - (key) => sortedRemediation[key].length === 0, - ) - ) { - setRemediations([]); - } - }, [sortedRemediation]); - - useEffect(() => { - if (results?.summary?.counts) { - const total = Object.values(sortedViolations).reduce( - (sum, arr) => sum + arr.length, - 0, - ); - setViolation(total); - } - }, [sortedViolations, results]); - - const updateRemediationList = async () => { - try { - const items = await APIScanner.getRemediations( - window.ea11yScannerData?.pageData?.url, - ); - - const sorted = sortRemediation(items.data); - - setRemediations(items.data); - setSortedRemediation(sorted); - } catch (error) { - setIsError(true); - } - }; - - const handleOpen = (index, item) => (event, isExpanded) => { - setOpenIndex(isExpanded ? index : null); - if (!isManage) { - mixpanelService.sendEvent(mixpanelEvents.issueSelected, { - issue_type: item.message, - rule_id: item.ruleId, - wcag_level: item.reasonCategory.match(/\((AAA?|AA?|A)\)/)?.[1] || '', - category_name: openedBlock, - }); - } - }; - - const handleOpenBlock = (block) => { - setOpenedBlock(block); - setOpenIndex(null); - }; - - const registerPage = async (data, sorted) => { - try { - if (window?.ea11yScannerData?.pageData?.unregistered) { - await APIScanner.registerPage( - window?.ea11yScannerData?.pageData, - data.summary, - ); - window.ea11yScannerData.pageData.unregistered = false; - } - - setResults(data); - setSortedViolations(sorted); - setAltTextData([]); - setManualData(structuredClone(MANUAL_GROUPS)); - } catch (e) { - if (e?.message === 'Quota exceeded') { - setQuotaExceeded(true); - } - setIsError(true); - } - }; - - const addScanResults = async (data) => { - try { - const response = await APIScanner.addScanResults({ - url: window?.ea11yScannerData?.pageData?.url, - summary: data.summary, - }); - void APIScanner.triggerSave({ - object_id: window?.ea11yScannerData?.pageData?.object_id, - object_type: window?.ea11yScannerData?.pageData?.object_type, - }); - - setCurrentScanId(response.scanId); - } catch (e) { - console.error(e); - setIsError(true); - } - }; - - const isViolation = (item) => item.level === LEVEL_VIOLATION; - const isContrastViolation = (item) => - item.ruleId === RULE_TEXT_CONTRAST && - item.level === LEVEL_POTENTIAL && - Number(item.messageArgs[0]) !== RATIO_EXCLUDED; - - const getResults = async () => { - setLoading(true); - - try { - const url = new URL(window.location.href); - const data = await window.ace.check(document); - - const filtered = data.results.filter( - (item) => isViolation(item) || isContrastViolation(item), - ); - - const customResults = runAllEa11yRules(document); - - const filteredCustomResults = customResults.filter( - (item) => - item.level === 'violation' && - !dismissedHeadingIssues.includes(item.path.dom), - ); - - const allResults = [...filtered, ...filteredCustomResults]; - - data.results = allResults; - - if (data?.summary?.counts) { - data.summary.counts.issuesResolved = 0; - data.summary.counts.violation += filteredCustomResults.filter( - (item) => item.level === 'violation', - ).length; - data.summary.counts.recommendation = - (data.summary.counts.recommendation || 0) + - filteredCustomResults.filter( - (item) => item.level === 'recommendation', - ).length; - } - - const sorted = sortViolations(allResults); - - await registerPage(data, sorted); - await addScanResults(data); - - mixpanelService.sendEvent(mixpanelEvents.scanTriggered, { - page_url: window.ea11yScannerData?.pageData?.url, - issue_count: data.summary?.counts?.violation, - source: url.searchParams.get('open-ea11y-assistant-src'), - }); - - return data.summary; - } catch (error) { - setIsError(true); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (window.ea11yScannerData?.isConnected) { - //Wait for apply FE remediation - setTimeout(() => { - scannerWizard - .load() - .then(() => { - void getResults(); - }) - .catch(() => { - setIsError(true); - setLoading(false); - }); - }, 500); - void updateRemediationList(); - } else { - setLoading(false); - } - }, [window.ea11yScannerData?.isConnected]); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - if (params.get(MANAGE_URL_PARAM) === '1') { - setIsManage(true); - handleOpenBlock(BLOCKS.management); - } - }, [window.location.search]); - - const isResolved = (block) => { - const indexes = Array.from( - { - length: sortedViolations[block]?.length || 0, - }, - (_, i) => i, - ); - switch (block) { - case BLOCKS.altText: - return ( - (altTextData?.length === sortedViolations[block]?.length && - indexes.every((index) => index in altTextData) && - altTextData.every((data) => data?.resolved)) || - sortedViolations[block]?.length === 0 - ); - case BLOCKS.headingStructure: - const stats = getHeadingsStats(); - return stats.error === 0; - default: - return ( - (manualData[block]?.length === sortedViolations[block]?.length && - indexes.every((index) => index in manualData[block]) && - manualData[block].every((data) => data?.resolved)) || - sortedViolations[block]?.length === 0 - ); - } - }; - - const isChanged = - altTextData.length > 0 || - Object.keys(manualData).some( - (key) => manualData[key] && manualData[key].length > 0, - ) || - isManageChanged; - - const runNewScan = () => { - const url = new URL(window.location.href); - url.searchParams.delete('open-ea11y-assistant'); - url.searchParams.delete('open-ea11y-assistant-src'); - url.searchParams.append('open-ea11y-assistant-src', 'rescan_button'); - url.searchParams.append('open-ea11y-assistant', '1'); - - window.location.assign(url); - }; - - const getHeadingsStats = () => { - const updatedHeadings = validateHeadings( - getPageHeadingsTree(), - dismissedHeadingIssues, - ); - return calculateStats(updatedHeadings); - }; + const value = { ...state, ...actions }; return ( - + {children} ); }; -export const useScannerWizardContext = () => { - return useContext(ScannerWizardContext); -}; +export default ScannerWizardContextProvider; +export const useScannerWizardContext = () => useContext(ScannerWizardContext); diff --git a/modules/scanner/assets/js/context/tabs-context.js b/modules/scanner/assets/js/context/tabs-context.js new file mode 100644 index 00000000..e31ad768 --- /dev/null +++ b/modules/scanner/assets/js/context/tabs-context.js @@ -0,0 +1,49 @@ +import useTabs from '@elementor/ui/useTabs'; +import PropTypes from 'prop-types'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { BLOCKS } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { getInitialTab } from '@ea11y-apps/scanner/utils/get-initial-tab'; +import { createContext, useContext } from '@wordpress/element'; + +export const TabsContext = createContext({}); + +export const TabsContextProvider = ({ children }) => { + const { setOpenedBlock, setIsManage } = useScannerWizardContext(); + const { getTabsProps, getTabProps, getTabPanelProps } = + useTabs(getInitialTab()); + + const tabsProps = getTabsProps(); + + const changeTab = (event, newValue) => { + setOpenedBlock(newValue); + setIsManage(newValue === BLOCKS.management); + tabsProps.onChange(event, newValue); + mixpanelService.sendEvent(mixpanelEvents.tabSelected, { + tab_name: newValue, + }); + }; + + return ( + + {children} + + ); +}; + +TabsContextProvider.propTypes = { + children: PropTypes.node, +}; + +export const useTabsContext = () => { + return useContext(TabsContext); +}; diff --git a/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardActions.js b/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardActions.js new file mode 100644 index 00000000..96148993 --- /dev/null +++ b/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardActions.js @@ -0,0 +1,253 @@ +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner'; +import { + BLOCKS, + LEVEL_POTENTIAL, + LEVEL_VIOLATION, + MANUAL_GROUPS, + RATIO_EXCLUDED, + RULE_TEXT_CONTRAST, +} from '@ea11y-apps/scanner/constants'; +import { runAllEa11yRules } from '@ea11y-apps/scanner/rules'; +import { getPageHeadingsTree } from '@ea11y-apps/scanner/utils/page-headings'; +import { + sortRemediation, + sortViolations, +} from '@ea11y-apps/scanner/utils/sort-violations'; +import { + calculateStats, + validateHeadings, +} from '@ea11y-apps/scanner/utils/validate-headings'; + +export default function useScannerWizardActions(state) { + const { + openedBlock, + isManage, + setResults, + setSortedViolations, + setAltTextData, + setManualData, + setCurrentScanId, + setIsError, + setQuotaExceeded, + setLoading, + setRemediations, + setSortedRemediation, + setGlobalRemediations, + setSortedGlobalRemediation, + setOpenIndex, + } = state; + + const updateRemediationList = async () => { + try { + const items = await APIScanner.getRemediations( + window.ea11yScannerData?.pageData?.url, + ); + const sorted = sortRemediation(items.data.page); + const sortedGlobal = sortRemediation(items.data.global); + setRemediations(items.data.page); + setSortedRemediation(sorted); + setGlobalRemediations(items.data.global); + setSortedGlobalRemediation(sortedGlobal); + } catch (error) { + setIsError(true); + } + }; + + const handleOpen = (index, item) => (event, isExpanded) => { + setOpenIndex(isExpanded ? index : null); + if (!isManage) { + mixpanelService.sendEvent(mixpanelEvents.issueSelected, { + issue_type: item.message, + rule_id: item.ruleId, + wcag_level: item.reasonCategory.match(/\((AAA?|AA?|A)\)/)?.[1] || '', + category_name: openedBlock, + }); + } + }; + + const handleOpenBlock = (block) => { + state.setOpenedBlock(block); + setOpenIndex(null); + }; + + const registerPage = async (data, sorted) => { + try { + if (window?.ea11yScannerData?.pageData?.unregistered) { + await APIScanner.registerPage( + window?.ea11yScannerData?.pageData, + data.summary, + ); + window.ea11yScannerData.pageData.unregistered = false; + } + + setResults(data); + setSortedViolations(sorted); + setAltTextData({ manage: [], main: [] }); + setManualData(structuredClone(MANUAL_GROUPS)); + } catch (e) { + if (e?.message === 'Quota exceeded') { + setQuotaExceeded(true); + } + setIsError(true); + } + }; + + const addScanResults = async (data) => { + try { + const response = await APIScanner.addScanResults({ + url: window?.ea11yScannerData?.pageData?.url, + summary: data.summary, + }); + void APIScanner.triggerSave({ + object_id: window?.ea11yScannerData?.pageData?.object_id, + object_type: window?.ea11yScannerData?.pageData?.object_type, + }); + + setCurrentScanId(response.scanId); + } catch (e) { + console.error(e); + setIsError(true); + } + }; + + const isViolation = (item) => item.level === LEVEL_VIOLATION; + const isContrastViolation = (item) => + item.ruleId === RULE_TEXT_CONTRAST && + item.level === LEVEL_POTENTIAL && + Number(item.messageArgs[0]) !== RATIO_EXCLUDED; + + const getResults = async () => { + setLoading(true); + + try { + const url = new URL(window.location.href); + const data = await window.ace.check(document); + + const filtered = data.results.filter( + (item) => isViolation(item) || isContrastViolation(item), + ); + + const customResults = runAllEa11yRules(document); + + const filteredCustomResults = customResults.filter( + (item) => + item.level === 'violation' && + !state.dismissedHeadingIssues?.includes(item.path.dom), + ); + + const allResults = [...filtered, ...filteredCustomResults]; + + data.results = allResults; + + if (data?.summary?.counts) { + data.summary.counts.issuesResolved = 0; + data.summary.counts.violation += filteredCustomResults.filter( + (item) => item.level === 'violation', + ).length; + data.summary.counts.recommendation = + (data.summary.counts.recommendation || 0) + + filteredCustomResults.filter( + (item) => item.level === 'recommendation', + ).length; + } + + const sorted = sortViolations(allResults); + + await registerPage(data, sorted); + await addScanResults(data); + + mixpanelService.sendEvent(mixpanelEvents.scanTriggered, { + page_url: window.ea11yScannerData?.pageData?.url, + issue_count: data.summary?.counts?.violation, + source: url.searchParams.get('open-ea11y-assistant-src'), + }); + + return data.summary; + } catch (error) { + setIsError(true); + } finally { + setLoading(false); + } + }; + + const runNewScan = () => { + const url = new URL(window.location.href); + + const keys = [...url.searchParams.keys()]; + for (const key of keys) { + url.searchParams.delete(key); + } + url.searchParams.append('open-ea11y-assistant-src', 'rescan_button'); + url.searchParams.append('open-ea11y-assistant', '1'); + + window.location.assign(url); + }; + + const getHeadingsStats = () => { + const updatedHeadings = validateHeadings( + getPageHeadingsTree(), + state.dismissedHeadingIssues, + ); + return calculateStats(updatedHeadings); + }; + + const isResolved = (block) => { + const type = state.isManage ? 'manage' : 'main'; + const { sortedViolations, altTextData, colorContrastData, manualData } = + state; + + const indexes = Array.from( + { length: sortedViolations[block]?.length || 0 }, + (_, i) => i, + ); + + switch (block) { + case BLOCKS.altText: + return ( + (altTextData?.[type]?.length === sortedViolations[block]?.length && + indexes.every((index) => index in altTextData.main) && + altTextData[type].every((data) => data?.resolved)) || + sortedViolations[block]?.length === 0 + ); + + case BLOCKS.colorContrast: + return ( + (colorContrastData?.[type]?.length === + sortedViolations[block]?.length && + indexes.every((index) => index in colorContrastData.main) && + colorContrastData[type].every((data) => data?.resolved)) || + sortedViolations[block]?.length === 0 + ); + + case BLOCKS.headingStructure: { + const updatedHeadings = validateHeadings( + getPageHeadingsTree(), + state.dismissedHeadingIssues, + ); + const stats = calculateStats(updatedHeadings); + return stats.error === 0; + } + + default: + return ( + (manualData[block]?.length === sortedViolations[block]?.length && + indexes.every((index) => index in manualData[block]) && + manualData[block].every((data) => data?.resolved)) || + sortedViolations[block]?.length === 0 + ); + } + }; + + return { + updateRemediationList, + handleOpen, + handleOpenBlock, + registerPage, + addScanResults, + getResults, + runNewScan, + getHeadingsStats, + isResolved, + }; +} diff --git a/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardEffects.js b/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardEffects.js new file mode 100644 index 00000000..237e4e50 --- /dev/null +++ b/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardEffects.js @@ -0,0 +1,95 @@ +import { BLOCKS, MANAGE_URL_PARAM } from '@ea11y-apps/scanner/constants'; +import { scannerWizard } from '@ea11y-apps/scanner/services/scanner-wizard'; +import { + focusOnElement, + removeExistingFocus, +} from '@ea11y-apps/scanner/utils/focus-on-element'; +import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; +import { useEffect } from '@wordpress/element'; + +export default function useScannerWizardEffects(state, actions) { + const { + sortedViolations, + sortedRemediation, + openIndex, + openedBlock, + isManage, + setRemediations, + setViolation, + setIsError, + setLoading, + } = state; + + // Focus management when openIndex changes + useEffect(() => { + const items = isManage + ? sortedRemediation[openedBlock] + : sortedViolations[openedBlock]; + if ( + openIndex !== null && + sortedViolations[openedBlock]?.length && + openIndex < items?.length + ) { + const element = isManage + ? getElementByXPath( + JSON.parse(sortedRemediation[openedBlock][openIndex].content) + ?.xpath, + ) + : sortedViolations[openedBlock][openIndex].node; + focusOnElement(element); + } else { + removeExistingFocus(); + } + }, [openIndex]); + + // Clear remediations array when no remediation items left + useEffect(() => { + if ( + Object.keys(sortedRemediation).every( + (key) => sortedRemediation[key].length === 0, + ) + ) { + setRemediations([]); + } + }, [sortedRemediation]); + + // compute violation count + useEffect(() => { + if (state.results?.summary?.counts) { + const total = Object.values(sortedViolations).reduce( + (sum, arr) => sum + arr.length, + 0, + ); + setViolation(total); + } + }, [sortedViolations, state.results]); + + // initial load and scannerWizard logic + useEffect(() => { + if (window.ea11yScannerData?.isConnected) { + setTimeout(() => { + scannerWizard + .load() + .then(() => { + void actions.getResults(); + }) + .catch(() => { + setIsError(true); + setLoading(false); + }); + }, 500); + void actions.updateRemediationList(); + } else { + setLoading(false); + } + }, [window.ea11yScannerData?.isConnected]); + + // handle manage param on url change + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get(MANAGE_URL_PARAM) === '1') { + state.setIsManage(true); + actions.handleOpenBlock(BLOCKS.management); + } + }, [window.location.search]); +} diff --git a/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardState.js b/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardState.js new file mode 100644 index 00000000..2c971bfd --- /dev/null +++ b/modules/scanner/assets/js/hooks/scanner-context/useScannerWizardState.js @@ -0,0 +1,90 @@ +import { + BLOCKS, + INITIAL_SORTED_VIOLATIONS, + MANUAL_GROUPS, +} from '@ea11y-apps/scanner/constants'; +import { useState } from '@wordpress/element'; + +const initialAltTextData = { manage: [], main: [] }; +const initialColorContrastData = { manage: [], main: [] }; + +export default function useScannerWizardState() { + const [results, setResults] = useState(); + const [remediations, setRemediations] = useState([]); + const [globalRemediations, setGlobalRemediations] = useState([]); + const [sortedViolations, setSortedViolations] = useState( + INITIAL_SORTED_VIOLATIONS, + ); + const [sortedRemediation, setSortedRemediation] = useState( + INITIAL_SORTED_VIOLATIONS, + ); + const [sortedGlobalRemediation, setSortedGlobalRemediation] = useState( + INITIAL_SORTED_VIOLATIONS, + ); + const [resolved, setResolved] = useState(0); + const [currentScanId, setCurrentScanId] = useState(null); + const [openedBlock, setOpenedBlock] = useState(BLOCKS.main); + const [loading, setLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [quotaExceeded, setQuotaExceeded] = useState(false); + const [isManage, setIsManage] = useState(false); + const [isManageGlobal, setIsManageGlobal] = useState(false); + const [isManageChanged, setIsManageChanged] = useState(false); + const [altTextData, setAltTextData] = useState(initialAltTextData); + const [colorContrastData, setColorContrastData] = useState( + initialColorContrastData, + ); + const [manualData, setManualData] = useState(structuredClone(MANUAL_GROUPS)); + const [remediationData, setRemediationData] = useState( + structuredClone(MANUAL_GROUPS), + ); + const [openIndex, setOpenIndex] = useState(null); + const [violation, setViolation] = useState(null); + + return { + // state values + results, + remediations, + sortedViolations, + sortedRemediation, + resolved, + currentScanId, + openedBlock, + loading, + isError, + quotaExceeded, + isManage, + isManageGlobal, + isManageChanged, + altTextData, + manualData, + colorContrastData, + remediationData, + openIndex, + violation, + globalRemediations, + sortedGlobalRemediation, + // setters + setResults, + setRemediations, + setSortedViolations, + setSortedRemediation, + setResolved, + setCurrentScanId, + setOpenedBlock, + setLoading, + setIsError, + setQuotaExceeded, + setIsManage, + setIsManageGlobal, + setIsManageChanged, + setAltTextData, + setManualData, + setColorContrastData, + setRemediationData, + setOpenIndex, + setViolation, + setGlobalRemediations, + setSortedGlobalRemediation, + }; +} diff --git a/modules/scanner/assets/js/hooks/use-alt-text-form.js b/modules/scanner/assets/js/hooks/use-alt-text-form.js index 03adecd6..0e220aa0 100644 --- a/modules/scanner/assets/js/hooks/use-alt-text-form.js +++ b/modules/scanner/assets/js/hooks/use-alt-text-form.js @@ -6,12 +6,13 @@ import { BLOCKS } from '@ea11y-apps/scanner/constants'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; import { scannerItem } from '@ea11y-apps/scanner/types/scanner-item'; import { removeExistingFocus } from '@ea11y-apps/scanner/utils/focus-on-element'; +import { getOuterHtmlByXpath } from '@ea11y-apps/scanner/utils/get-outer-html-by-xpath'; import { splitDescriptions } from '@ea11y-apps/scanner/utils/split-ai-response'; import { convertSvgToPngBase64, svgNodeToPngBase64, } from '@ea11y-apps/scanner/utils/svg-to-png-base64'; -import { useEffect, useState } from '@wordpress/element'; +import { useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; export const useAltTextForm = ({ current, item }) => { @@ -24,6 +25,8 @@ export const useAltTextForm = ({ current, item }) => { isResolved, setOpenedBlock, updateRemediationList, + isManage, + setIsManageChanged, } = useScannerWizardContext(); const { error } = useToastNotification(); @@ -31,81 +34,111 @@ export const useAltTextForm = ({ current, item }) => { const [loading, setLoading] = useState(false); const [firstOpen, setFirstOpen] = useState(true); - const isSubmitDisabled = - (!altTextData?.[current]?.makeDecorative && - !altTextData?.[current]?.altText) || - altTextData?.[current]?.resolved || - loading; + const type = isManage ? 'manage' : 'main'; + const isGlobal = + altTextData?.[type]?.[current]?.isGlobal || item.global || false; + + const isGlobalRef = useRef(null); useEffect(() => { - if (!firstOpen && isResolved(BLOCKS.altText)) { + if (item?.node) { + isGlobalRef.current = isGlobal; + } + }, [current]); + + useEffect(() => { + if (isManage) { + updateData({ + makeDecorative: item.data.attribute_name === 'role', + altText: + item.data.attribute_name !== 'role' ? item.data.attribute_value : '', + isGlobal, + }); + } else { + updateData({ isGlobal }); + } + }, [isManage, current]); + + useEffect(() => { + if (!isManage && !firstOpen && isResolved(BLOCKS.altText)) { removeExistingFocus(); setOpenedBlock(BLOCKS.main); } setFirstOpen(false); - }, [altTextData]); + }, [isResolved(BLOCKS.altText)]); + + const setIsGlobal = (value) => { + updateData({ + isGlobal: value, + }); + }; const updateData = (data) => { - const updData = [...altTextData]; - if (altTextData?.[current]?.resolved && !data.resolved) { + const updData = [...altTextData?.[type]]; + if (altTextData?.[type]?.[current]?.resolved && !data.resolved) { setResolved(resolved - 1); } updData[current] = { - ...(altTextData?.[current] || {}), + ...(altTextData?.[type]?.[current] || {}), ...data, }; - setAltTextData(updData); + setAltTextData({ + ...altTextData, + [type]: updData, + }); }; const makeAttributeData = () => { - if (altTextData?.[current]?.makeDecorative) { - item.node.setAttribute('role', 'presentation'); + if (altTextData?.[type]?.[current]?.makeDecorative) { return { attribute_name: 'role', attribute_value: 'presentation', }; } + if (item.node.tagName === 'svg') { - item.node.setAttribute('aria-label', altTextData?.[current]?.altText); return { attribute_name: 'aria-label', - attribute_value: altTextData?.[current]?.altText, + attribute_value: altTextData?.[type]?.[current]?.altText, }; } - item.node.setAttribute('alt', altTextData?.[current]?.altText); return { attribute_name: 'alt', - attribute_value: altTextData?.[current]?.altText, + attribute_value: altTextData?.[type]?.[current]?.altText, }; }; const updateAltText = async () => { const match = item.node.className.toString().match(/wp-image-(\d+)/); - const altText = !altTextData?.[current]?.makeDecorative - ? altTextData?.[current]?.altText + const altText = !altTextData?.[type]?.[current]?.makeDecorative + ? altTextData?.[type]?.[current]?.altText : ''; + const find = item.snippet; try { if (match && item.node.tagName !== 'svg') { void APIScanner.submitAltText(item.node.src, altText); } - await APIScanner.submitRemediation({ + const response = await APIScanner.submitRemediation({ url: window?.ea11yScannerData?.pageData.url, remediation: { ...makeAttributeData(), action: 'add', xpath: item.path.dom, + find, category: item.reasonCategory.match(/\((AAA?|AA?|A)\)/)?.[1] || '', type: 'ATTRIBUTE', }, + global: isGlobal, rule: item.ruleId, group: BLOCKS.altText, - apiId: altTextData?.[current]?.apiId, + apiId: altTextData?.[type]?.[current]?.apiId, }); await APIScanner.resolveIssue(currentScanId); void updateRemediationList(); + return response.remediation; } catch (e) { console.warn(e); } @@ -135,31 +168,70 @@ export const useAltTextForm = ({ current, item }) => { const handleSubmit = async () => { try { setLoading(true); - const fixMethod = altTextData?.[current]?.apiId + const fixMethod = altTextData?.[type]?.[current]?.apiId ? 'AI alt-text' : 'Manual alt-text'; - await updateAltText(item); - if (!altTextData?.[current]?.resolved) { - updateData({ resolved: true }); + const remediation = await updateAltText(item); + if (!altTextData?.[type]?.[current]?.resolved) { + updateData({ + remediation, + resolved: true, + }); + isGlobalRef.current = isGlobal; setResolved(resolved + 1); } - if (altTextData?.[current]?.apiId) { + if (altTextData?.[type]?.[current]?.apiId) { mixpanelService.sendEvent(mixpanelEvents.aiSuggestionAccepted, { element_selector: item.path.dom, image_src: item.node?.src, - final_text: altTextData?.[current]?.altText, + final_text: altTextData?.[type]?.[current]?.altText, credit_used: 1, }); } mixpanelService.sendEvent(mixpanelEvents.applyFixButtonClicked, { - fix_method: altTextData?.[current]?.makeDecorative + fix_method: altTextData?.[type]?.[current]?.makeDecorative ? 'Mark as decorative' : fixMethod, issue_type: item.message, category_name: BLOCKS.altText, page_url: window.ea11yScannerData?.pageData?.url, + is_global: isGlobal ? 'yes' : 'no', + }); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + const handleUpdate = async () => { + const find = getOuterHtmlByXpath( + item.path.dom, + item.data?.attribute_name + ? `${item.data.attribute_name}=" ${item.data.attribute_value}"` + : '', + ); + try { + setLoading(true); + const remediation = altTextData?.[type]?.[current]?.remediation; + const id = item.id || remediation.id; + const data = item.data || JSON.parse(remediation.content); + const strContent = JSON.stringify({ + ...data, + ...makeAttributeData(), + find, + }); + await APIScanner.updateRemediationContent({ + url: window?.ea11yScannerData?.pageData?.url, + id, + content: strContent, + global: isGlobal, }); + + isGlobalRef.current = isGlobal; + setIsManageChanged(true); + void updateRemediationList(); } catch (e) { console.error(e); } finally { @@ -212,33 +284,48 @@ export const useAltTextForm = ({ current, item }) => { }; const generateAltText = async () => { - if (altTextData?.[current]?.aiText?.length) { + if (altTextData?.[type]?.[current]?.aiText?.length) { const index = - altTextData?.[current]?.aiTextIndex + 1 < - altTextData?.[current]?.aiText?.length - ? altTextData?.[current]?.aiTextIndex + 1 + altTextData?.[type]?.[current]?.aiTextIndex + 1 < + altTextData?.[type]?.[current]?.aiText?.length + ? altTextData?.[type]?.[current]?.aiTextIndex + 1 : 0; updateData({ - altText: altTextData?.[current]?.aiText[index], + altText: altTextData?.[type]?.[current]?.aiText[index], aiTextIndex: index, resolved: false, }); - sendMixpanelEvent(altTextData?.[current]?.aiText[index]); + sendMixpanelEvent(altTextData?.[type]?.[current]?.aiText[index]); } else { await getAiText(); } }; + const attrData = makeAttributeData(); + + const isSubmitDisabled = isManage + ? attrData.attribute_value === item.data.attribute_value && + attrData.attribute_name === item.data.attribute_name && + isGlobal === item.global + : (!altTextData?.[type]?.[current]?.makeDecorative && + !altTextData?.[type]?.[current]?.altText) || + (altTextData?.[type]?.[current]?.resolved && + altTextData?.[type]?.[current]?.isGlobal === isGlobalRef.current) || + loading; + return { + isGlobal, + setIsGlobal, loadingAiText, - data: altTextData, + data: altTextData?.[type], isSubmitDisabled, loading, handleCheck, handleChange, handleSubmit, + handleUpdate, generateAltText, }; }; diff --git a/modules/scanner/assets/js/hooks/use-color-contrast-form.js b/modules/scanner/assets/js/hooks/use-color-contrast-form.js index a7161666..e5a7b87a 100644 --- a/modules/scanner/assets/js/hooks/use-color-contrast-form.js +++ b/modules/scanner/assets/js/hooks/use-color-contrast-form.js @@ -1,49 +1,79 @@ import getXPath from 'get-xpath'; import PropTypes from 'prop-types'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { + isValidCSS, + rgbOrRgbaToHex, +} from '@ea11y-apps/global/utils/color-contrast-helpers'; import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner'; import { BACKGROUND_ELEMENT_CLASS, BLOCKS, - DATA_INITIAL_BG, - DATA_INITIAL_COLOR, } from '@ea11y-apps/scanner/constants'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; import { scannerItem } from '@ea11y-apps/scanner/types/scanner-item'; -import { rgbOrRgbaToHex } from '@ea11y-apps/scanner/utils/convert-colors'; +import { buildPathToParent } from '@ea11y-apps/scanner/utils/build-path-to-parent'; import { focusOnElement, removeExistingFocus, } from '@ea11y-apps/scanner/utils/focus-on-element'; import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; import { getElementCSSSelector } from '@ea11y-apps/scanner/utils/get-element-css-selector'; -import { useEffect, useState } from '@wordpress/element'; +import { getOuterHtmlByXpath } from '@ea11y-apps/scanner/utils/get-outer-html-by-xpath'; +import { useEffect, useRef, useState } from '@wordpress/element'; export const useColorContrastForm = ({ item, current, setCurrent }) => { const { - openedBlock, - manualData, + colorContrastData, resolved: resolvedBlock, setResolved, isResolved, + isManage, setOpenedBlock, - setManualData, + setColorContrastData, + setIsManageChanged, updateRemediationList, currentScanId, } = useScannerWizardContext(); + const type = isManage ? 'manage' : 'main'; + const [loading, setLoading] = useState(false); const [firstOpen, setFirstOpen] = useState(true); + const [parentChanged, setParentChanged] = useState(false); - const updateData = (data) => { - const existing = manualData[openedBlock]?.[current] || {}; - const updated = [...(manualData[openedBlock] || [])]; - updated[current] = { ...existing, ...data }; + const isGlobal = + colorContrastData?.[type]?.[current]?.isGlobal || item.global || false; + + const isGlobalRef = useRef(null); + + useEffect(() => { + if (item?.node) { + isGlobalRef.current = isGlobal; + } + }, [current]); + + const setIsGlobal = (value) => { + updateData({ + isGlobal: value, + }); + }; - setManualData({ - ...manualData, - [openedBlock]: updated, + const updateData = (data) => { + const updData = [...colorContrastData?.[type]]; + updData[current] = { + ...(colorContrastData?.[type]?.[current] || {}), + ...data, + }; + setColorContrastData({ + ...colorContrastData, + [type]: updData, }); + + if (data.color || data.background) { + const rule = buildCSSRule(updData[current]); + updateCSS(rule); + } }; const sendEvent = (method) => { @@ -53,21 +83,12 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { }; useEffect(() => { - if (!firstOpen && isResolved(BLOCKS.colorContrast)) { + if (!isManage && !firstOpen && isResolved(BLOCKS.colorContrast)) { removeExistingFocus(); setOpenedBlock(BLOCKS.main); } setFirstOpen(false); - }, [manualData]); - - useEffect(() => { - if (!item?.node?.getAttribute(DATA_INITIAL_COLOR)) { - const initialColor = - manualData[openedBlock]?.[current]?.color || item.messageArgs[3]; - item.node.setAttribute(DATA_INITIAL_COLOR, initialColor); - item.node.style.setProperty('color', initialColor, 'important'); - } - }, [item]); + }, [isResolved(BLOCKS.colorContrast)]); const { color = item.messageArgs[3] || @@ -75,13 +96,23 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { window.getComputedStyle(item.node).getPropertyValue('color'), ), background = item.messageArgs[4], - parents = [item.path.dom], + parents = item.isEdit + ? buildPathToParent(item.node, item.parentNode) + : [item.path.dom], resolved = false, - backgroundChanged = false, - } = manualData[openedBlock]?.[current] || {}; + backgroundChanged = item.isEdit, + } = colorContrastData[type]?.[current] || {}; + + useEffect(() => { + if (isManage && parentChanged) { + const styles = document.getElementById('ea11y-remediation-styles'); + if (styles) { + styles.innerHTML = styles.innerHTML.replace(item.data.rule, ''); + } + } + }, [parentChanged]); const changeColor = (updColor) => { - item.node?.style?.setProperty('color', updColor, 'important'); updateData({ color: updColor, resolved: false }); }; @@ -91,14 +122,6 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { return; } - if (!element.getAttribute(DATA_INITIAL_BG)) { - const initial = window - .getComputedStyle(element) - .getPropertyValue('background-color'); - element.setAttribute(DATA_INITIAL_BG, initial); - } - - element.style?.setProperty('background-color', updBackground, 'important'); updateData({ background: updBackground, resolved: false, @@ -106,27 +129,6 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { }); }; - const setParentBackground = (nextElement, element) => { - if (!nextElement) { - return; - } - - if (!nextElement.getAttribute(DATA_INITIAL_BG)) { - const initial = window - .getComputedStyle(nextElement) - .getPropertyValue('background-color'); - nextElement.setAttribute(DATA_INITIAL_BG, initial); - } - - element?.style?.setProperty( - 'background-color', - element?.getAttribute(DATA_INITIAL_BG), - 'important', - ); - - nextElement.style?.setProperty('background-color', background, 'important'); - }; - const setParentLarger = () => { const element = getElementByXPath(parents.at(-1)); const parent = element?.parentElement; @@ -137,12 +139,11 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { try { const xpath = getXPath(parent, { ignoreId: true }); focusOnElement(parent, BACKGROUND_ELEMENT_CLASS); - setParentBackground(parent, element); - updateData({ parents: [...parents, xpath], resolved: false, }); + setParentChanged(true); sendEvent('plus'); } catch (error) { console.warn('Failed to get XPath for parent element:', error); @@ -162,74 +163,38 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { removeExistingFocus(BACKGROUND_ELEMENT_CLASS); } - setParentBackground(nextElement); updateData({ parents: newParents, resolved: false }); + setParentChanged(true); sendEvent('minus'); }; const isValidHexColor = (str) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(str.trim()); - const isValidCSS = (cssText) => { - try { - // Basic checks for common malicious patterns - if (!cssText || typeof cssText !== 'string') { - return false; - } - - // Check for basic CSS structure and disallow dangerous patterns - const dangerousPatterns = [ - /@import/i, - /javascript:/i, - /expression\s*\(/i, - /behavior\s*:/i, - /binding\s*:/i, - /-moz-binding/i, - ]; - - if (dangerousPatterns.some((pattern) => pattern.test(cssText))) { - return false; - } - - // More comprehensive CSS structure validation - const cssRegex = /^[\s\S]*\{\s*[\s\S]+:\s*[\s\S]+;\s*\}[\s\S]*$/; - const hasBasicStructure = cssRegex.test( - cssText.replace(/\s+/g, ' ').trim(), - ); + const buildCSSRule = (data) => { + const currentParents = data.parents || parents; + const currentParent = + currentParents.length > 0 ? currentParents.at(-1) : item.path.dom; - // Additional validation: check for balanced braces - const openBraces = (cssText.match(/\{/g) || []).length; - const closeBraces = (cssText.match(/\}/g) || []).length; - - return hasBasicStructure && openBraces === closeBraces && openBraces > 0; - } catch (e) { - return false; - } - }; - - const buildCSSRule = () => { if ( - !isValidHexColor(color) || - (background && !isValidHexColor(background)) + (data.color && !isValidHexColor(data.color)) || + (data.background && !isValidHexColor(data.background)) ) { throw new Error('Invalid hex color input detected'); } try { - const colorSelector = getElementCSSSelector(item.path.dom); - const bgSelector = getElementCSSSelector( - parents.length > 0 ? parents.at(-1) : item.path.dom, - ); + const bgElement = getElementByXPath(currentParent); + const colorSelector = getElementCSSSelector(item.node); + const bgSelector = getElementCSSSelector(bgElement); - const colorRule = - color !== item.messageArgs[3] - ? `${colorSelector} {color: ${color} !important;}` - : ''; + const colorRule = data.color + ? `${colorSelector} {color: ${data.color} !important;}` + : ''; - const bgRule = - background && background !== item.messageArgs[4] - ? `${bgSelector} {background-color: ${background} !important;}` - : ''; + const bgRule = data.background + ? `${bgSelector} {background-color: ${data.background} !important;}` + : ''; const css = `${colorRule}${bgRule}`; @@ -240,33 +205,86 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { } }; + const updateCSS = (rule) => { + let styles = document.getElementById('ea11y-remediation-styles-edit'); + if (!styles) { + styles = document.createElement('style'); + styles.id = 'ea11y-remediation-styles-edit'; + document.body.appendChild(styles); + } + styles.innerHTML = rule; + }; + + const onUpdate = async () => { + const rule = buildCSSRule(colorContrastData?.[type]?.[current]); + const find = getOuterHtmlByXpath(item.path.dom); + const parentXPath = parents.length > 0 ? parents.at(-1) : null; + const parentFind = getOuterHtmlByXpath(parentXPath); + + try { + setLoading(true); + const remediation = colorContrastData?.[type]?.[current]?.remediation; + const id = item.id || remediation.id; + const data = item.data || JSON.parse(remediation.content); + const updContent = JSON.stringify({ + ...data, + rule, + find, + parentFind, + parentXPath, + }); + await APIScanner.updateRemediationContent({ + url: window?.ea11yScannerData?.pageData?.url, + id, + content: updContent, + global: isGlobal, + }); + + setIsManageChanged(true); + removeExistingFocus(); + setParentChanged(false); + updateCSS(rule); + isGlobalRef.current = isGlobal; + void updateRemediationList(); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + const onSubmit = async () => { + const parentXPath = parents.length > 0 ? parents.at(-1) : null; + const parentFind = getOuterHtmlByXpath(parentXPath); setLoading(true); try { - await APIScanner.submitRemediation({ + const rule = buildCSSRule(colorContrastData?.[type]?.[current]); + const response = await APIScanner.submitRemediation({ url: window?.ea11yScannerData?.pageData?.url, remediation: { - rule: buildCSSRule(), + rule, category: item.reasonCategory.match(/\((AAA?|AA?|A)\)/)?.[1] || '', type: 'STYLES', xpath: item.path.dom, + find: item.snippet, + parentFind, + parentXPath, }, + global: isGlobal, rule: item.ruleId, group: BLOCKS.colorContrast, }); await APIScanner.resolveIssue(currentScanId); - updateData({ resolved: true }); - - item.node?.removeAttribute(DATA_INITIAL_COLOR); - getElementByXPath( - parents.length > 0 ? parents.at(-1) : item.path.dom, - )?.removeAttribute(DATA_INITIAL_BG); + updateData({ remediation: response.remediation, resolved: true }); removeExistingFocus(); setCurrent(current + 1); setResolved(resolvedBlock + 1); + setParentChanged(false); + updateCSS(rule); + isGlobalRef.current = isGlobal; void updateRemediationList(); } catch (error) { console.error('Failed to submit remediation:', error); @@ -275,18 +293,27 @@ export const useColorContrastForm = ({ item, current, setCurrent }) => { } }; + const isDisabled = + resolved && + colorContrastData?.[type]?.[current]?.isGlobal === isGlobalRef.current; + return { + isGlobal, + setIsGlobal, color, background, parents, + isDisabled, resolved, backgroundChanged, + parentChanged, loading, changeColor, changeBackground, setParentLarger, setParentSmaller, onSubmit, + onUpdate, }; }; diff --git a/modules/scanner/assets/js/hooks/use-global-manage-actions.js b/modules/scanner/assets/js/hooks/use-global-manage-actions.js new file mode 100644 index 00000000..b624bd04 --- /dev/null +++ b/modules/scanner/assets/js/hooks/use-global-manage-actions.js @@ -0,0 +1,234 @@ +import { useToastNotification } from '@ea11y-apps/global/hooks'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export const useGlobalManageActions = () => { + const { error } = useToastNotification(); + const { + openedBlock, + globalRemediations, + sortedGlobalRemediation, + setLoading, + updateRemediationList, + setIsManageChanged, + } = useScannerWizardContext(); + + const [activeRequest, setActiveRequest] = useState(false); + + const sendUpdateEvent = (active, group, context) => { + mixpanelService.sendEvent( + mixpanelEvents[active ? 'remediationEnabled' : 'remediationDisabled'], + { + action_type: active ? 'enable_all' : 'disable_all', + remediations_amount: group + ? sortedGlobalRemediation[group] + : globalRemediations?.length, + category: group || 'all', + context, + is_global: 'yes', + }, + ); + }; + + const sendSpecificEvent = (active, rule, context) => { + mixpanelService.sendEvent( + mixpanelEvents[active ? 'remediationEnabled' : 'remediationDisabled'], + { + action_type: active ? 'enable_specific' : 'disable_specific', + category_name: openedBlock, + issue_type: rule, + is_global: 'yes', + context, + }, + ); + }; + + const setRemediationAsGlobal = async (id) => { + try { + setLoading(true); + await APIScanner.setRemediationAsGlobal({ id }); + setIsManageChanged(true); + mixpanelService.sendEvent( + mixpanelEvents.applyGlobalFixConfirmationClicked, + ); + await updateRemediationList(); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setLoading(false); + } + }; + + const updateGlobalRemediationForPage = async (id, active, rule) => { + try { + setActiveRequest(true); + await APIScanner.updateGlobalRemediationForPage({ + url: window?.ea11yScannerData?.pageData?.url, + id, + active, + }); + setIsManageChanged(true); + await updateRemediationList(); + sendSpecificEvent(active, rule, 'current_page'); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setActiveRequest(false); + } + }; + + const updateGlobalRemediationForAllPages = async (id, active, rule) => { + try { + setActiveRequest(true); + await APIScanner.updateGlobalRemediationForAllPages({ + id, + active, + }); + setIsManageChanged(true); + await updateRemediationList(); + sendSpecificEvent(active, rule, 'all_pages'); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setActiveRequest(false); + } + }; + + const deleteGlobalRemediation = async (id, rule) => { + try { + setActiveRequest(true); + await APIScanner.deleteGlobalRemediation({ + id, + }); + + mixpanelService.sendEvent(mixpanelEvents.remediationRemoved, { + action_type: 'remove_specific', + category_name: openedBlock, + issue_type: rule, + is_global: 'yes', + }); + + await updateRemediationList(); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setActiveRequest(false); + } + }; + + const updateGlobalRemediationGroupForPage = async (active, group) => { + try { + setLoading(true); + await APIScanner.updateGlobalRemediationGroupForPage({ + url: window?.ea11yScannerData?.pageData?.url, + active, + group, + }); + setIsManageChanged(true); + await updateRemediationList(); + sendUpdateEvent(active, group, 'current_page'); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setLoading(false); + } + }; + + const updateGlobalRemediationGroupForAllPages = async (active, group) => { + try { + setLoading(true); + await APIScanner.updateGlobalRemediationGroupForAllPages({ + active, + group, + }); + setIsManageChanged(true); + await updateRemediationList(); + sendUpdateEvent(active, group, 'all_pages'); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setLoading(false); + } + }; + + const updateAllGlobalRemediationForPage = async (active) => { + try { + setLoading(true); + await APIScanner.updateAllGlobalRemediationForPage({ + url: window?.ea11yScannerData?.pageData?.url, + active, + }); + setIsManageChanged(true); + await updateRemediationList(); + sendUpdateEvent(active, 'all', 'current_page'); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setLoading(false); + } + }; + + const updateAllGlobalRemediationForAllPages = async (active) => { + try { + setLoading(true); + await APIScanner.updateAllGlobalRemediationForAllPages({ + active, + }); + setIsManageChanged(true); + await updateRemediationList(); + sendUpdateEvent(active, 'all', 'all_pages'); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setLoading(false); + } + }; + + const deleteGlobalRemediations = async (group) => { + try { + setLoading(true); + await APIScanner.deleteGlobalRemediations({ + group, + }); + + await updateRemediationList(); + mixpanelService.sendEvent(mixpanelEvents.remediationRemoved, { + action_type: 'remove_all', + remediations_amount: group + ? sortedGlobalRemediation[group] + : globalRemediations?.length, + category: group || 'all', + is_global: 'yes', + }); + } catch (e) { + console.error(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setLoading(false); + } + }; + + return { + activeRequest, + setRemediationAsGlobal, + updateGlobalRemediationForPage, + updateGlobalRemediationForAllPages, + updateGlobalRemediationGroupForPage, + updateGlobalRemediationGroupForAllPages, + updateAllGlobalRemediationForPage, + updateAllGlobalRemediationForAllPages, + deleteGlobalRemediation, + deleteGlobalRemediations, + }; +}; diff --git a/modules/scanner/assets/js/hooks/use-manage-actions.js b/modules/scanner/assets/js/hooks/use-manage-actions.js index 9961ffdf..9ad2e8b9 100644 --- a/modules/scanner/assets/js/hooks/use-manage-actions.js +++ b/modules/scanner/assets/js/hooks/use-manage-actions.js @@ -2,6 +2,7 @@ import { useToastNotification } from '@ea11y-apps/global/hooks'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { APIScanner } from '@ea11y-apps/scanner/api/APIScanner'; import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { removeExistingFocus } from '@ea11y-apps/scanner/utils/focus-on-element'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -37,6 +38,7 @@ export const useManageActions = (current = null) => { ? sortedRemediation[group] : remediations?.length, category: group || 'all', + is_global: 'no', }, ); } catch (e) { @@ -55,12 +57,13 @@ export const useManageActions = (current = null) => { group, }); - await mixpanelService.sendEvent(mixpanelEvents.remediationRemoved, { + mixpanelService.sendEvent(mixpanelEvents.remediationRemoved, { action_type: 'remove_all', remediations_amount: group ? sortedRemediation[group] : remediations?.length, category: group || 'all', + is_global: 'no', }); if (group) { @@ -89,13 +92,7 @@ export const useManageActions = (current = null) => { active, id: current.id, }); - const updated = sortedRemediation[openedBlock].map((item) => - item.id === current.id ? { ...item, active } : item, - ); - setSortedRemediation({ - ...sortedRemediation, - [openedBlock]: updated, - }); + await updateRemediationList(); setIsManageChanged(true); mixpanelService.sendEvent( mixpanelEvents[active ? 'remediationEnabled' : 'remediationDisabled'], @@ -120,23 +117,19 @@ export const useManageActions = (current = null) => { url: window?.ea11yScannerData?.pageData?.url, id: current.id, }); - const updated = sortedRemediation[openedBlock].flatMap((item) => - item.id !== current.id ? item : [], - ); - setSortedRemediation({ - ...sortedRemediation, - [openedBlock]: updated, - }); + await updateRemediationList(); setIsManageChanged(true); mixpanelService.sendEvent(mixpanelEvents.remediationRemoved, { action_type: 'remove_specific', category_name: openedBlock, issue_type: current.rule, + is_global: 'no', }); } catch (e) { console.error(e); error(__('An error occurred.', 'pojo-accessibility')); } finally { + removeExistingFocus(); setActiveRequest(false); } }; diff --git a/modules/scanner/assets/js/hooks/use-manual-fix-form.js b/modules/scanner/assets/js/hooks/use-manual-fix-form.js index 2e5f2ca9..f4469aee 100644 --- a/modules/scanner/assets/js/hooks/use-manual-fix-form.js +++ b/modules/scanner/assets/js/hooks/use-manual-fix-form.js @@ -7,7 +7,7 @@ import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wiz import { scannerItem } from '@ea11y-apps/scanner/types/scanner-item'; import { removeExistingFocus } from '@ea11y-apps/scanner/utils/focus-on-element'; import { getElementContext } from '@ea11y-apps/scanner/utils/get-element-context'; -import { useEffect, useState } from '@wordpress/element'; +import { useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; export const useManualFixForm = ({ item, current }) => { @@ -29,13 +29,35 @@ export const useManualFixForm = ({ item, current }) => { const [resolving, setResolving] = useState(false); const [firstOpen, setFirstOpen] = useState(true); + const [manualEdit, setManualEdit] = useState(false); + const [aiSuggestion, setAiSuggestion] = useState(null); + + const isGlobal = + manualData[openedBlock][current]?.isGlobal || item.global || false; + + const isGlobalRef = useRef(null); + + useEffect(() => { + if (item?.node) { + isGlobalRef.current = isGlobal; + } + }, [current]); + + useEffect(() => { + setAiSuggestion({ + ...manualData[openedBlock][current]?.aiSuggestion, + submitted: false, + }); + setManualEdit(manualData[openedBlock][current]?.aiSuggestion?.snippet); + }, [manualData[openedBlock][current]?.aiSuggestion]); + useEffect(() => { if (!firstOpen && isResolved(openedBlock)) { removeExistingFocus(); setOpenedBlock(BLOCKS.main); } setFirstOpen(false); - }, [manualData]); + }, [isResolved(openedBlock)]); const updateData = (data) => { const updData = [...manualData[openedBlock]]; @@ -49,6 +71,12 @@ export const useManualFixForm = ({ item, current }) => { }); }; + const setIsGlobal = (value) => { + updateData({ + isGlobal: value, + }); + }; + const getAISuggestion = async () => { try { setAiResponseLoading(true); @@ -71,20 +99,58 @@ export const useManualFixForm = ({ item, current }) => { } }; - const markResolved = () => { - updateData({ resolved: true }); + const markResolved = (remediation) => { + updateData({ remediation, resolved: true, isGlobal }); setResolved(resolved + 1); setOpenIndex(current + 1); }; - const resolveIssue = async (manualEdit) => { + const handleUpdate = async () => { + setResolving(true); + try { + const content = JSON.parse( + manualData[openedBlock][current]?.remediation.content, + ); + const replace = + manualEdit || manualData[openedBlock][current]?.aiSuggestion.snippet; + const strContent = JSON.stringify({ + ...content, + replace, + }); + await APIScanner.updateRemediationContent({ + url: window?.ea11yScannerData?.pageData?.url, + id: manualData[openedBlock][current]?.remediation.id, + content: strContent, + global: isGlobal, + }); + mixpanelService.sendEvent(mixpanelEvents.applyFixButtonClicked, { + fix_method: 'manual', + issue_type: item.message, + snippet_content: replace, + category_name: openedBlock, + source: 'assistant', + page_url: window.ea11yScannerData?.pageData?.url, + is_global: isGlobal ? 'yes' : 'no', + }); + void updateRemediationList(); + isGlobalRef.current = isGlobal; + setOpenIndex(current + 1); + } catch (e) { + console.log(e); + error(__('An error occurred.', 'pojo-accessibility')); + } finally { + setResolving(false); + } + }; + + const handleSubmit = async () => { setResolving(true); try { const replace = manualEdit || manualData[openedBlock][current]?.aiSuggestion.snippet; - await APIScanner.submitRemediation({ + const response = await APIScanner.submitRemediation({ url: window?.ea11yScannerData?.pageData.url, remediation: { find: item.snippet, @@ -93,13 +159,14 @@ export const useManualFixForm = ({ item, current }) => { category: item.reasonCategory.match(/\((AAA?|AA?|A)\)/)?.[1] || '', type: 'REPLACE', }, + global: isGlobal, rule: item.ruleId, group: BLOCKS[openedBlock], apiId: manualData[openedBlock]?.[current]?.apiId, }); await APIScanner.resolveIssue(currentScanId); - - markResolved(); + isGlobalRef.current = isGlobal; + markResolved(response.remediation); mixpanelService.sendEvent(mixpanelEvents.applyFixButtonClicked, { fix_method: manualEdit ? 'manual' : 'AI', @@ -108,6 +175,7 @@ export const useManualFixForm = ({ item, current }) => { category_name: openedBlock, source: 'assistant', page_url: window.ea11yScannerData?.pageData?.url, + is_global: isGlobal ? 'yes' : 'no', }); void updateRemediationList(); @@ -119,12 +187,25 @@ export const useManualFixForm = ({ item, current }) => { } }; + const isSubmitDisabled = + manualData[openedBlock][current]?.resolved && + manualData[openedBlock][current].isGlobal === isGlobalRef.current; + return { aiResponseLoading, resolving, + isGlobal, + isSubmitDisabled, + setIsGlobal, + manualEdit, + setManualEdit, + aiSuggestion, + setAiSuggestion, markResolved, getAISuggestion, - resolveIssue, + resolveIssue: manualData[openedBlock][current]?.resolved + ? handleUpdate + : handleSubmit, }; }; diff --git a/modules/scanner/assets/js/hooks/use-scanner-settings.js b/modules/scanner/assets/js/hooks/use-scanner-settings.js index 4f7064bd..8830a7aa 100644 --- a/modules/scanner/assets/js/hooks/use-scanner-settings.js +++ b/modules/scanner/assets/js/hooks/use-scanner-settings.js @@ -1,21 +1,24 @@ -import * as z from 'zod'; +import { z } from 'zod'; const ScannerSettings = z.object({ wpRestNonce: z.string(), dashboardUrl: z.url(), scannerUrl: z.url(), - initialScanResult: z.object({ - url: z.url(), - counts: z.object({ - issuesResolved: z.int(), - manual: z.int(), - pass: z.int(), - potentialRecommendation: z.int(), - potentialViolation: z.int(), - recommendation: z.int(), - violation: z.int(), + initialScanResult: z.union([ + z.object({ + url: z.string().url(), + counts: z.object({ + issuesResolved: z.number().int(), + manual: z.number().int(), + pass: z.number().int(), + potentialRecommendation: z.number().int(), + potentialViolation: z.number().int(), + recommendation: z.number().int(), + violation: z.number().int(), + }), }), - }), + z.tuple([]), + ]), pageData: z.object({ entry_id: z.string(), object_id: z.int(), @@ -34,7 +37,7 @@ const useScannerSettings = () => { const validationResult = ScannerSettings.safeParse(window.ea11yScannerData); if (!validationResult.success) { - console.error( + console.warn( 'Ea11y scanner: Validation error of `window.ea11yScannerData`', validationResult.error.issues, ); diff --git a/modules/scanner/assets/js/images/empty-image.js b/modules/scanner/assets/js/images/empty-image.js new file mode 100644 index 00000000..e50192a0 --- /dev/null +++ b/modules/scanner/assets/js/images/empty-image.js @@ -0,0 +1,67 @@ +export const EmptyImage = () => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/modules/scanner/assets/js/images/index.js b/modules/scanner/assets/js/images/index.js index ef88965f..8e0a50e8 100644 --- a/modules/scanner/assets/js/images/index.js +++ b/modules/scanner/assets/js/images/index.js @@ -4,3 +4,4 @@ export { ErrorImage } from './error-image'; export { NotConnectedImage } from './not-connected-image'; export { SunIcon } from './sun-icon'; export { SunOffIcon } from './sun-off-icon'; +export { EmptyImage } from './empty-image'; diff --git a/modules/scanner/assets/js/index.js b/modules/scanner/assets/js/index.js index 3eb363de..51eddf41 100644 --- a/modules/scanner/assets/js/index.js +++ b/modules/scanner/assets/js/index.js @@ -17,14 +17,25 @@ import { TOP_BAR_LINK, } from '@ea11y-apps/scanner/constants'; import { HeadingStructureContextProvider } from '@ea11y-apps/scanner/context/heading-structure-context'; -import { ScannerWizardContextProvider } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import ScannerWizardContextProvider from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { TabsContextProvider } from '@ea11y-apps/scanner/context/tabs-context'; import { closeWidget } from '@ea11y-apps/scanner/utils/close-widget'; import { createRoot, Fragment, StrictMode } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -document.addEventListener('DOMContentLoaded', function () { +window.addEventListener('load', () => { const params = new URLSearchParams(window.location.search); + if ( + params.get(SCANNER_URL_PARAM) === '1' || + params.get(MANAGE_URL_PARAM) === '1' + ) { + setTimeout(() => { + initApp(); + }, 500); + } +}); +document.addEventListener('DOMContentLoaded', function () { document .querySelector(CLEAR_CACHE_LINK) ?.addEventListener('click', async (event) => { @@ -55,12 +66,6 @@ document.addEventListener('DOMContentLoaded', function () { } }); }); - if ( - params.get(SCANNER_URL_PARAM) === '1' || - params.get(MANAGE_URL_PARAM) === '1' - ) { - initApp(); - } }); const initApp = () => { @@ -112,9 +117,11 @@ const initApp = () => { - - - + + + + + diff --git a/modules/scanner/assets/js/layouts/index.js b/modules/scanner/assets/js/layouts/index.js index ac27eaba..c6c732f3 100644 --- a/modules/scanner/assets/js/layouts/index.js +++ b/modules/scanner/assets/js/layouts/index.js @@ -1,5 +1,9 @@ -export { MainLayout } from './main-layout'; -export { AltTextLayout } from './alt-text-layout'; -export { ManualLayout } from './manual-layout'; -export { ManageMainLayout } from './manage-main-layout'; -export { RemediationLayout } from './remediation-layout'; +export { MainLayout } from './scanner/main-layout'; +export { AltTextLayout } from './scanner/alt-text-layout'; +export { ManualLayout } from './scanner/manual-layout'; +export { ColorContrastLayout } from './scanner/color-contrast-layout'; +export { HeadingStructureLayout } from './scanner/heading-structure-layout'; +export { ManageMainLayout } from './management/manage-main-layout'; +export { ManageManualLayout } from './management/manage-manual-layout'; +export { ManageAltTextLayout } from './management/manage-alt-text-layout'; +export { ManageColorContrastLayout } from './management/manage-color-contrast-layout'; diff --git a/modules/scanner/assets/js/layouts/manage-main-layout.js b/modules/scanner/assets/js/layouts/manage-main-layout.js deleted file mode 100644 index 9e7af361..00000000 --- a/modules/scanner/assets/js/layouts/manage-main-layout.js +++ /dev/null @@ -1,47 +0,0 @@ -import Box from '@elementor/ui/Box'; -import CardContent from '@elementor/ui/CardContent'; -import Typography from '@elementor/ui/Typography'; -import { styled } from '@elementor/ui/styles'; -import { ManageList } from '@ea11y-apps/scanner/components/manage-list'; -import { ManageRemediationButtons } from '@ea11y-apps/scanner/components/manage-remediation-buttons'; -import { BLOCKS } from '@ea11y-apps/scanner/constants'; -import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; -import { useEffect } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -export const ManageMainLayout = () => { - const { remediations, setIsManage, setOpenedBlock } = - useScannerWizardContext(); - - useEffect(() => { - if (remediations?.length === 0) { - setIsManage(false); - setOpenedBlock(BLOCKS.main); - } - }, [remediations?.length]); - - return ( - - - - {__('All issues', 'pojo-accessibility')} - - - - - - ); -}; - -const ManageHeader = styled(Box)` - display: flex; - align-items: center; - justify-content: space-between; -`; - -const StyledContent = styled(CardContent)` - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.spacing(2)}; - padding: 0 ${({ theme }) => theme.spacing(2)}; -`; diff --git a/modules/scanner/assets/js/layouts/management/manage-alt-text-layout.js b/modules/scanner/assets/js/layouts/management/manage-alt-text-layout.js new file mode 100644 index 00000000..bb5c0703 --- /dev/null +++ b/modules/scanner/assets/js/layouts/management/manage-alt-text-layout.js @@ -0,0 +1,75 @@ +import { AltTextForm } from '@ea11y-apps/scanner/components/alt-text-form'; +import { FormNavigation } from '@ea11y-apps/scanner/components/form-navigation'; +import { ManageAltText } from '@ea11y-apps/scanner/components/manage-alt-text'; +import { BLOCKS } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { StyledContent } from '@ea11y-apps/scanner/styles/app.styles'; +import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; +import { useState } from '@wordpress/element'; + +export const ManageAltTextLayout = () => { + const { + sortedRemediation, + sortedGlobalRemediation, + isManageGlobal, + setOpenedBlock, + } = useScannerWizardContext(); + const [current, setCurrent] = useState(0); + const [isEdit, setIsEdit] = useState(false); + + const remediations = isManageGlobal + ? sortedGlobalRemediation + : sortedRemediation; + + const item = remediations[BLOCKS.altText][current]; + + // Prevent to render empty list + if (!item) { + void (remediations[BLOCKS.altText].length > 0 + ? setCurrent(0) + : setOpenedBlock(BLOCKS.management)); + + return null; + } + + const openEdit = () => { + setIsEdit(true); + }; + + const changeNavigation = (index) => { + if (index > remediations[BLOCKS.altText].length - 1) { + setCurrent(0); + } else { + setCurrent(index); + } + }; + + const data = JSON.parse(item?.content || ''); + const node = getElementByXPath(data?.xpath); + + return ( + + {isEdit ? ( + + ) : ( + + )} + + + ); +}; diff --git a/modules/scanner/assets/js/layouts/management/manage-color-contrast-layout.js b/modules/scanner/assets/js/layouts/management/manage-color-contrast-layout.js new file mode 100644 index 00000000..c2f8dabd --- /dev/null +++ b/modules/scanner/assets/js/layouts/management/manage-color-contrast-layout.js @@ -0,0 +1,98 @@ +import { getDataFromCss } from '@ea11y-apps/global/utils/color-contrast-helpers'; +import { ColorContrastForm } from '@ea11y-apps/scanner/components/color-contrast-form'; +import { FormNavigation } from '@ea11y-apps/scanner/components/form-navigation'; +import { ManageColorContrast } from '@ea11y-apps/scanner/components/manage-color-contrast'; +import { BLOCKS } from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { StyledContent } from '@ea11y-apps/scanner/styles/app.styles'; +import { checkContrastAA } from '@ea11y-apps/scanner/utils/calc-color-ratio'; +import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; +import { useState } from '@wordpress/element'; + +export const ManageColorContrastLayout = () => { + const { + sortedRemediation, + sortedGlobalRemediation, + isManageGlobal, + setOpenedBlock, + } = useScannerWizardContext(); + + const [current, setCurrent] = useState(0); + const [isEdit, setIsEdit] = useState(false); + + const remediations = isManageGlobal + ? sortedGlobalRemediation + : sortedRemediation; + + const item = remediations[BLOCKS.colorContrast][current]; + + // Prevent to render empty list + if (!item) { + void (remediations[BLOCKS.colorContrast].length > 0 + ? setCurrent(0) + : setOpenedBlock(BLOCKS.management)); + + return null; + } + + const data = item?.content ? JSON.parse(item.content) : null; + const node = data?.xpath ? getElementByXPath(data.xpath) : null; + const cssData = data?.rule ? getDataFromCss(data.rule) : null; + + const openEdit = () => { + setIsEdit(true); + }; + + const changeNavigation = (index) => { + if (index > remediations[BLOCKS.colorContrast].length - 1) { + setCurrent(0); + } else { + setCurrent(index); + } + }; + + const colorData = node ? checkContrastAA(node) : null; + + return ( + + {isEdit ? ( + + ) : ( + + )} + + + ); +}; diff --git a/modules/scanner/assets/js/layouts/management/manage-main-layout.js b/modules/scanner/assets/js/layouts/management/manage-main-layout.js new file mode 100644 index 00000000..f3f731fa --- /dev/null +++ b/modules/scanner/assets/js/layouts/management/manage-main-layout.js @@ -0,0 +1,120 @@ +import Alert from '@elementor/ui/Alert'; +import Box from '@elementor/ui/Box'; +import Button from '@elementor/ui/Button'; +import CardContent from '@elementor/ui/CardContent'; +import Typography from '@elementor/ui/Typography'; +import { styled } from '@elementor/ui/styles'; +import CrownFilled from '@ea11y-apps/global/icons/crown-filled'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { EmptyManageMessage } from '@ea11y-apps/scanner/components/empty-manage-message'; +import ManageRemediationList from '@ea11y-apps/scanner/components/manage-remediation-list'; +import { + ManageGlobalRemediationControl, + ManageRemediationControl, +} from '@ea11y-apps/scanner/components/manage-remediation-main-controls'; +import { + HIDE_UPGRADE_KEY, + IS_PRO_PLAN, + UPGRADE_URL, +} from '@ea11y-apps/scanner/constants'; +import { useScannerWizardContext } from '@ea11y-apps/scanner/context/scanner-wizard-context'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export const ManageMainLayout = () => { + const { remediations, globalRemediations } = useScannerWizardContext(); + const [showUpgradeAlert, setShowUpgradeAlert] = useState(true); + + const hideUpgradeAlert = () => { + window.localStorage.setItem(HIDE_UPGRADE_KEY, Date.now().toString()); + setShowUpgradeAlert(false); + }; + + const isShowUpgradeAlert = () => { + const time = window.localStorage.getItem(HIDE_UPGRADE_KEY); + return !time && showUpgradeAlert; + }; + + const onUpgrade = () => { + mixpanelService.sendEvent(mixpanelEvents.upgradeButtonClicked, { + current_plan: window.ea11yScannerData?.planData?.plan?.name, + feature_locked: 'global_banner', + }); + }; + + const showPromo = + (remediations.length > 0 || globalRemediations.length > 0) && + !IS_PRO_PLAN && + isShowUpgradeAlert(); + + return ( + + {remediations.length > 0 && ( + <> + + + {__('Fixes on this page', 'pojo-accessibility')} + + + + + + )} + + {globalRemediations.length > 0 && ( + <> + + + {__('Cross-page fixes', 'pojo-accessibility')} + + + + + + )} + + {remediations.length === 0 && globalRemediations.length === 0 && ( + + )} + {showPromo && ( + } + color="error" + onClose={hideUpgradeAlert} + sx={{ mt: 2 }} + > + + {__( + 'Resolve these issues on all of your scanned pages with a click.', + 'pojo-accessibility', + )} + + + + )} + + ); +}; + +const ManageHeader = styled(Box)` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const StyledContent = styled(CardContent)` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + padding: 0 ${({ theme }) => theme.spacing(2)}; +`; diff --git a/modules/scanner/assets/js/layouts/remediation-layout.js b/modules/scanner/assets/js/layouts/management/manage-manual-layout.js similarity index 60% rename from modules/scanner/assets/js/layouts/remediation-layout.js rename to modules/scanner/assets/js/layouts/management/manage-manual-layout.js index 7fdb312e..0318e019 100644 --- a/modules/scanner/assets/js/layouts/remediation-layout.js +++ b/modules/scanner/assets/js/layouts/management/manage-manual-layout.js @@ -1,6 +1,8 @@ import CircleCheckFilledIcon from '@elementor/icons/CircleCheckFilledIcon'; +import WorldIcon from '@elementor/icons/WorldIcon'; import Box from '@elementor/ui/Box'; import Radio from '@elementor/ui/Radio'; +import Tooltip from '@elementor/ui/Tooltip'; import Typography from '@elementor/ui/Typography'; import { RemediationForm } from '@ea11y-apps/scanner/components/remediation-form'; import { BLOCKS } from '@ea11y-apps/scanner/constants'; @@ -11,25 +13,32 @@ import { StyledAccordionSummary, } from '@ea11y-apps/scanner/styles/manual-fixes.styles'; import { useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; -export const RemediationLayout = () => { +export const ManageManualLayout = () => { const { openIndex, handleOpen, openedBlock, sortedRemediation, + sortedGlobalRemediation, + isManageGlobal, setOpenedBlock, } = useScannerWizardContext(); + const remediations = isManageGlobal + ? sortedGlobalRemediation + : sortedRemediation; + useEffect(() => { - if (sortedRemediation[openedBlock]?.length === 0) { + if (remediations[openedBlock]?.length === 0) { setOpenedBlock(BLOCKS.management); } - }, [sortedRemediation[openedBlock]?.length]); + }, [remediations[openedBlock]?.length]); return ( - {sortedRemediation[openedBlock].map((item, index) => ( + {remediations[openedBlock].map((item, index) => ( { checked={Number(item.active)} role="presentation" /> - - {uxMessaging[item.rule]?.violationName ?? item.rule} - + + + {uxMessaging[item.rule]?.violationName ?? item.rule} + + {item.global === '1' && ( + + + + )} + diff --git a/modules/scanner/assets/js/layouts/alt-text-layout.js b/modules/scanner/assets/js/layouts/scanner/alt-text-layout.js similarity index 94% rename from modules/scanner/assets/js/layouts/alt-text-layout.js rename to modules/scanner/assets/js/layouts/scanner/alt-text-layout.js index c48d6167..06b0e55c 100644 --- a/modules/scanner/assets/js/layouts/alt-text-layout.js +++ b/modules/scanner/assets/js/layouts/scanner/alt-text-layout.js @@ -15,9 +15,9 @@ export const AltTextLayout = () => { const [current, setCurrent] = useState(0); const resolved = isResolved(BLOCKS.altText); + const item = sortedViolations.altText[current]; useEffect(() => { - const item = sortedViolations.altText[current]; if (!resolved && sortedViolations.altText.length) { focusOnElement(item.node); } else { @@ -43,7 +43,7 @@ export const AltTextLayout = () => { return ( diff --git a/modules/scanner/assets/js/layouts/color-contrast-layout.js b/modules/scanner/assets/js/layouts/scanner/color-contrast-layout.js similarity index 97% rename from modules/scanner/assets/js/layouts/color-contrast-layout.js rename to modules/scanner/assets/js/layouts/scanner/color-contrast-layout.js index 01fee795..d17fafd2 100644 --- a/modules/scanner/assets/js/layouts/color-contrast-layout.js +++ b/modules/scanner/assets/js/layouts/scanner/color-contrast-layout.js @@ -50,7 +50,7 @@ export const ColorContrastLayout = () => { return ( diff --git a/modules/scanner/assets/js/layouts/heading-structure-layout.js b/modules/scanner/assets/js/layouts/scanner/heading-structure-layout.js similarity index 100% rename from modules/scanner/assets/js/layouts/heading-structure-layout.js rename to modules/scanner/assets/js/layouts/scanner/heading-structure-layout.js diff --git a/modules/scanner/assets/js/layouts/main-layout.js b/modules/scanner/assets/js/layouts/scanner/main-layout.js similarity index 100% rename from modules/scanner/assets/js/layouts/main-layout.js rename to modules/scanner/assets/js/layouts/scanner/main-layout.js diff --git a/modules/scanner/assets/js/layouts/manual-layout.js b/modules/scanner/assets/js/layouts/scanner/manual-layout.js similarity index 78% rename from modules/scanner/assets/js/layouts/manual-layout.js rename to modules/scanner/assets/js/layouts/scanner/manual-layout.js index f9981ed9..b3626414 100644 --- a/modules/scanner/assets/js/layouts/manual-layout.js +++ b/modules/scanner/assets/js/layouts/scanner/manual-layout.js @@ -1,6 +1,8 @@ import CircleCheckFilledIcon from '@elementor/icons/CircleCheckFilledIcon'; +import WorldIcon from '@elementor/icons/WorldIcon'; import Box from '@elementor/ui/Box'; import Radio from '@elementor/ui/Radio'; +import Tooltip from '@elementor/ui/Tooltip'; import Typography from '@elementor/ui/Typography'; import { ManualFixForm } from '@ea11y-apps/scanner/components/manual-fix-form'; import { uxMessaging } from '@ea11y-apps/scanner/constants/ux-messaging'; @@ -44,9 +46,20 @@ export const ManualLayout = () => { aria-label={__('Resolved', 'pojo-accessibility')} /> - + {uxMessaging[item.ruleId]?.violationName ?? item.category} + {manualData[openedBlock][index]?.isGlobal && ( + + + + )} diff --git a/modules/scanner/assets/js/services/scanner-wizard.js b/modules/scanner/assets/js/services/scanner-wizard.js index 5a9f5940..1e30db51 100644 --- a/modules/scanner/assets/js/services/scanner-wizard.js +++ b/modules/scanner/assets/js/services/scanner-wizard.js @@ -1,8 +1,8 @@ const load = async () => { return new Promise((resolve, reject) => { - const { scannerUrl, planData } = window?.ea11yScannerData; + const { scannerUrl, planData, pageData } = window?.ea11yScannerData; - const scriptSrc = `${scannerUrl}?api_key=${planData?.public_api_key}`; + const scriptSrc = `${scannerUrl}?api_key=${planData?.public_api_key}&page_url=${pageData?.url}`; // Check if script already exists if (document.querySelector(`script[src="${scriptSrc}"]`)) { diff --git a/modules/scanner/assets/js/static/global-infotip-image.png b/modules/scanner/assets/js/static/global-infotip-image.png new file mode 100644 index 00000000..4eaeb76b Binary files /dev/null and b/modules/scanner/assets/js/static/global-infotip-image.png differ diff --git a/modules/scanner/assets/js/styles/app.styles.js b/modules/scanner/assets/js/styles/app.styles.js index 42d8e8d1..4e4c15f1 100644 --- a/modules/scanner/assets/js/styles/app.styles.js +++ b/modules/scanner/assets/js/styles/app.styles.js @@ -1,11 +1,15 @@ +import BanIcon from '@elementor/icons/BanIcon'; import Alert from '@elementor/ui/Alert'; import Box from '@elementor/ui/Box'; import Button from '@elementor/ui/Button'; import Card from '@elementor/ui/Card'; import CardContent from '@elementor/ui/CardContent'; +import Chip from '@elementor/ui/Chip'; import MenuItemText from '@elementor/ui/MenuItemText'; import Paper from '@elementor/ui/Paper'; import Skeleton from '@elementor/ui/Skeleton'; +import TabPanel from '@elementor/ui/TabPanel'; +import Tabs from '@elementor/ui/Tabs'; import Typography from '@elementor/ui/Typography'; import { styled } from '@elementor/ui/styles'; import { ColorPickerStyles } from '@ea11y-apps/scanner/styles/react-colourful.styles'; @@ -21,6 +25,21 @@ export const AppContainer = styled(Paper)` ${ColorPickerStyles} `; +export const AppsTabs = styled(Tabs)` + & .MuiTabs-indicator { + background-color: ${({ theme }) => theme.palette.info.main}; + } + + & .MuiTab-root.Mui-selected { + color: ${({ theme }) => theme.palette.info.main}; + font-weight: 400; + } +`; + +export const AppsTabPanel = styled(TabPanel)` + padding: 0; +`; + export const StyledStatsBlock = styled(Card)` margin: ${({ theme }) => `${theme.spacing(3)} ${theme.spacing(2)}`}; padding: ${({ theme }) => theme.spacing(2)}; @@ -73,7 +92,6 @@ export const StyledSkeleton = styled(Skeleton)` `; const disabledState = ` - opacity: .7; cursor: not-allowed; & * { pointer-events: none; @@ -83,15 +101,27 @@ const disabledState = ` export const StyledAlert = styled(Alert)` align-items: center; /* @noflip */ - padding-right: ${({ theme }) => theme.spacing(0.5)}; - /* @noflip */ direction: ltr; & .MuiAlert-icon, & .MuiAlert-content { padding-top: 0; display: flex; } +`; + +export const AlertWithDisabledState = styled(Alert)` ${({ disabled }) => (disabled ? disabledState : '')} + ${({ disabled, theme }) => + disabled + ? ` + & * { + color: ${theme.palette.text.disabled}; + } + & svg { + fill: ${theme.palette.text.disabled}; + } + ` + : ''} `; export const StateContainer = styled(Box)` @@ -153,8 +183,7 @@ export const ManageButtonWrap = styled(Box)` &:hover, &:focus .MuiPaper-root, &:focus-visible .MuiPaper-root { - background-color: ${({ theme, disabled }) => - !disabled ? theme.palette.action.hover : 'transparent'}; + background-color: ${({ theme }) => theme.palette.action.hover}; } `; @@ -170,12 +199,6 @@ export const ActionButton = styled(Button)` } `; -export const ManageButtonGroup = styled(Box)` - display: flex; - align-items: center; - gap: ${({ theme }) => theme.spacing(0.5)}; -`; - export const UpgradeContentContainer = styled(Box)` display: flex; justify-content: space-between; @@ -199,8 +222,32 @@ export const DisabledMenuItemText = styled(MenuItemText)` color: ${({ theme }) => theme.palette.text.disabled}; `; +export const StyledBanIcon = styled(BanIcon)` + transform: rotate(-45deg); +`; + export const StyledBox = styled(Box)` display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(3)}; `; + +export const ManageColorAlert = styled(Alert)` + padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(1.5)};`}; + & .MuiAlert-content { + padding-top: 0; + } + & .MuiAlert-content > div { + display: block; + } +`; + +export const StyledProChip = styled(Chip)` + margin-left: ${({ theme }) => theme.spacing(1)}; + height: 26px; + width: 26px; + + .MuiChip-label { + padding: 0; + } +`; diff --git a/modules/scanner/assets/js/styles/manual-fixes.styles.js b/modules/scanner/assets/js/styles/manual-fixes.styles.js index 31807c9c..fe2ab84c 100644 --- a/modules/scanner/assets/js/styles/manual-fixes.styles.js +++ b/modules/scanner/assets/js/styles/manual-fixes.styles.js @@ -37,7 +37,6 @@ export const StyledAccordionDetails = styled(AccordionDetails)` `; export const StyledSnippet = styled(Typography)` - width: 335px; word-break: break-word; `; @@ -47,20 +46,27 @@ export const InfotipBox = styled(Box)` white-space: normal; `; +export const InfotipImage = styled('img')` + max-width: 304px; + height: auto; + border-top-left-radius: ${({ theme }) => theme.spacing(0.5)}; + border-top-right-radius: ${({ theme }) => theme.spacing(0.5)}; +`; + export const InfotipFooter = styled(Box)` display: flex; justify-content: flex-end; margin-top: ${({ theme }) => theme.spacing(2)}; `; -export const AIHeader = styled(Box)` +export const ItemHeader = styled(Box)` display: flex; justify-content: space-between; margin-bottom: ${({ theme }) => theme.spacing(1)}; - min-height: 32px; + min-height: ${({ theme }) => theme.spacing(4)}; `; -export const AITitle = styled(Box)` +export const ItemTitle = styled(Box)` display: flex; gap: ${({ theme }) => theme.spacing(1)}; align-items: center; @@ -71,3 +77,11 @@ export const ManualTextField = styled(TextField)` font-size: 14px; } `; + +export const TitleBox = styled(Box)` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: ${({ theme }) => theme.spacing(0.5)}; + gap: ${({ theme }) => theme.spacing(0.5)}; +`; diff --git a/modules/scanner/assets/js/types/scanner-item.js b/modules/scanner/assets/js/types/scanner-item.js index 323893b8..3d9a8fcd 100644 --- a/modules/scanner/assets/js/types/scanner-item.js +++ b/modules/scanner/assets/js/types/scanner-item.js @@ -23,4 +23,8 @@ export const scannerItem = PropTypes.shape({ category: PropTypes.string.isRequired, level: PropTypes.string.isRequired, node: PropTypes.object, + parentNode: PropTypes.object, + isPotential: PropTypes.bool, + isEdit: PropTypes.bool, + global: PropTypes.bool, }); diff --git a/modules/scanner/assets/js/utils/build-path-to-parent.js b/modules/scanner/assets/js/utils/build-path-to-parent.js new file mode 100644 index 00000000..62d1501a --- /dev/null +++ b/modules/scanner/assets/js/utils/build-path-to-parent.js @@ -0,0 +1,26 @@ +import getXPath from 'get-xpath'; + +/** + * Builds an array of elements from a child up to a specific parent node. + * @param {Element} itemNode - Starting (child) element. + * @param {Element} parentNode - Ancestor element to stop at. + * @return {string[]} Array of elements from item → parent (inclusive) + */ +export const buildPathToParent = (itemNode, parentNode) => { + const path = []; + + if (!parentNode) { + return [getXPath(itemNode, { ignoreId: true })]; + } + + let current = itemNode; + while (current) { + path.push(getXPath(current, { ignoreId: true })); + if (current === parentNode) { + break; + } + current = current.parentElement; + } + + return path; +}; diff --git a/modules/scanner/assets/js/utils/calc-color-ratio.js b/modules/scanner/assets/js/utils/calc-color-ratio.js index 38859936..c4a83ed4 100644 --- a/modules/scanner/assets/js/utils/calc-color-ratio.js +++ b/modules/scanner/assets/js/utils/calc-color-ratio.js @@ -34,6 +34,7 @@ export const checkContrastAA = (el) => { //some exception occurred, or not able to get color combo for some reason throw new Error('unable to get color combo for element: ' + el.nodeName); } + const fg = colorCombo.fg; const bg = colorCombo.bg; const ratio = fg.contrastRatio(bg); @@ -43,5 +44,6 @@ export const checkContrastAA = (el) => { ratio: +ratio.toFixed(2), largeText: large, passesAA, + isPotential: colorCombo.hasBGImage || colorCombo.hasGradient, }; }; diff --git a/modules/scanner/assets/js/utils/convert-colors.js b/modules/scanner/assets/js/utils/convert-colors.js index f0edad97..666ecbc5 100644 --- a/modules/scanner/assets/js/utils/convert-colors.js +++ b/modules/scanner/assets/js/utils/convert-colors.js @@ -9,36 +9,6 @@ export const expandHex = (hex) => { return `#${hex}`; }; -export const rgbOrRgbaToHex = (color) => { - const match = color.match( - /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i, - ); - if (!match) { - return null; - } // Not an RGB or RGBA string - - const r = parseInt(match[1]).toString(16).padStart(2, '0'); - const g = parseInt(match[2]).toString(16).padStart(2, '0'); - const b = parseInt(match[3]).toString(16).padStart(2, '0'); - - // If alpha present and less than 1, include it - if (match[4] !== undefined && parseFloat(match[4]) < 1) { - const a = Math.round(parseFloat(match[4]) * 255) - .toString(16) - .padStart(2, '0'); - return `#${r}${g}${b}${a}`.toUpperCase(); // 8-digit hex with alpha - } - - return `#${r}${g}${b}`.toUpperCase(); // 6-digit hex -}; - -export const hexToRGB = (hex) => { - hex = expandHex(hex).replace(/^#/, ''); - const num = parseInt(hex, 16); - // eslint-disable-next-line no-bitwise - return [(num >> 16) & 255, (num >> 8) & 255, num & 255]; -}; - export const hexToHsl = (hex) => { hex = expandHex(hex).replace(/^#/, ''); diff --git a/modules/scanner/assets/js/utils/focus-on-element.js b/modules/scanner/assets/js/utils/focus-on-element.js index 0305f8bd..a3a0ddb5 100644 --- a/modules/scanner/assets/js/utils/focus-on-element.js +++ b/modules/scanner/assets/js/utils/focus-on-element.js @@ -2,8 +2,6 @@ import { BACKGROUND_ELEMENT_CLASS, COLOR_ELEMENT_CLASS, CURRENT_ELEMENT_CLASS, - DATA_INITIAL_BG, - DATA_INITIAL_COLOR, } from '@ea11y-apps/scanner/constants'; export const focusOnElement = (element, queryClass = null) => { @@ -16,20 +14,6 @@ export const focusOnElement = (element, queryClass = null) => { } }; -export const resetStyles = (element) => { - const bg = element.getAttribute(DATA_INITIAL_BG); - const color = element.getAttribute(DATA_INITIAL_COLOR); - - if (bg && element?.style) { - element.style['background-color'] = bg; - element.removeAttribute(DATA_INITIAL_BG); - } - if (color && element?.style) { - element.style.color = color; - element.removeAttribute(DATA_INITIAL_COLOR); - } -}; - export const removeExistingFocus = (queryClass = null) => { document .querySelectorAll( @@ -43,6 +27,5 @@ export const removeExistingFocus = (queryClass = null) => { COLOR_ELEMENT_CLASS, BACKGROUND_ELEMENT_CLASS, ); - resetStyles(element); }); }; diff --git a/modules/scanner/assets/js/utils/get-element-css-selector.js b/modules/scanner/assets/js/utils/get-element-css-selector.js index e2fd0457..2b3a2d88 100644 --- a/modules/scanner/assets/js/utils/get-element-css-selector.js +++ b/modules/scanner/assets/js/utils/get-element-css-selector.js @@ -3,10 +3,8 @@ import { COLOR_ELEMENT_CLASS, CURRENT_ELEMENT_CLASS, } from '@ea11y-apps/scanner/constants'; -import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; -export const getElementCSSSelector = (xpath) => { - let element = getElementByXPath(xpath); +export const getElementCSSSelector = (element) => { if (!element || !(element instanceof Element)) { return null; } diff --git a/modules/scanner/assets/js/utils/get-initial-tab.js b/modules/scanner/assets/js/utils/get-initial-tab.js new file mode 100644 index 00000000..54186437 --- /dev/null +++ b/modules/scanner/assets/js/utils/get-initial-tab.js @@ -0,0 +1,7 @@ +import { BLOCKS, MANAGE_URL_PARAM } from '@ea11y-apps/scanner/constants'; + +export const getInitialTab = () => { + const params = new URLSearchParams(window.location.search); + const isInitManage = params.get(MANAGE_URL_PARAM) === '1'; + return isInitManage ? BLOCKS.management : BLOCKS.main; +}; diff --git a/modules/scanner/assets/js/utils/get-outer-html-by-xpath.js b/modules/scanner/assets/js/utils/get-outer-html-by-xpath.js new file mode 100644 index 00000000..00be9c10 --- /dev/null +++ b/modules/scanner/assets/js/utils/get-outer-html-by-xpath.js @@ -0,0 +1,44 @@ +import { + BACKGROUND_ELEMENT_CLASS, + COLOR_ELEMENT_CLASS, + CURRENT_ELEMENT_CLASS, +} from '@ea11y-apps/scanner/constants'; +import { getElementByXPath } from '@ea11y-apps/scanner/utils/get-element-by-xpath'; + +/** + * Return outer html for el. Should be used only for global remediation. + * @param {string|null} xpath + * @param {string|null} attr + * @return {string} element outer html with removed classes + */ +export const getOuterHtmlByXpath = (xpath, attr = null) => { + if (!xpath) { + return ''; + } + const element = getElementByXPath(xpath); + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return ''; + } + + // Define ignored class constants (with dots) + const ignoredClasses = new Set([ + CURRENT_ELEMENT_CLASS, + COLOR_ELEMENT_CLASS, + BACKGROUND_ELEMENT_CLASS, + ]); + + // Clone the node so we don’t modify the real DOM + const clone = element.cloneNode(true); + let html = clone.outerHTML; + + ignoredClasses.forEach((item) => { + html = html.replaceAll(` ${item}`, ''); + html = html.replaceAll(item, ''); + html = html.replaceAll('class=""', ''); + if (attr) { + html = html.replaceAll(attr, ''); + } + }); + + return html; +}; diff --git a/modules/scanner/assets/js/utils/validate-headings.js b/modules/scanner/assets/js/utils/validate-headings.js index 253aed22..2b0c29ed 100644 --- a/modules/scanner/assets/js/utils/validate-headings.js +++ b/modules/scanner/assets/js/utils/validate-headings.js @@ -1,6 +1,6 @@ import { - toFlatTree, getHeadingXpath, + toFlatTree, } from '@ea11y-apps/scanner/utils/page-headings'; import { EA11Y_RULES } from '../rules'; import { HEADING_STATUS } from '../types/heading'; @@ -48,7 +48,7 @@ export const validateHeadings = (headingTree, dismissedHeadingIssues) => { */ const validateHierarchy = ( headings, - dismissedHeadingIssues, + dismissedHeadingIssues = [], parentLevel = 0, ) => { let previousLevel = parentLevel; diff --git a/modules/scanner/rest/scanner-results.php b/modules/scanner/rest/scanner-results.php index c86cbd56..70f955d9 100644 --- a/modules/scanner/rest/scanner-results.php +++ b/modules/scanner/rest/scanner-results.php @@ -44,7 +44,7 @@ public function GET() { foreach ( $pages_scanned as $page ) { $scans = Scan_Entry::get_scans( $page->url ); - $page_remediation_count = Remediation_Entry::get_page_remediations( $page->url, true ); + $page_remediation_count = Remediation_Entry::get_page_remediations_count( $page->url ); $scans = array_map(function( $scan ) { return [ @@ -71,7 +71,7 @@ public function GET() { 'page_url' => $page->url, 'scans' => $scans, 'last_scan' => $scans[0]['date'] ?: $page->created_at, - 'remediation_count' => isset( $page_remediation_count[0] ) ? $page_remediation_count[0]->total : 0, + 'remediation_count' => $page_remediation_count, 'issues_total' => $scans[0]['issues_total'], 'issues_fixed' => $scans[0]['issues_fixed'], ]; diff --git a/modules/settings/assets/js/app.js b/modules/settings/assets/js/app.js index f85b9a3c..c6d55461 100644 --- a/modules/settings/assets/js/app.js +++ b/modules/settings/assets/js/app.js @@ -5,18 +5,19 @@ import Grid from '@elementor/ui/Grid'; import { styled, ThemeProvider } from '@elementor/ui/styles'; import { ConnectModal, + GetStartedModal, MenuItems, - Notifications, + OnboardingModal, PostConnectModal, UrlMismatchModal, - OnboardingModal, } from '@ea11y/components'; import { useNotificationSettings, useSavedSettings, useSettings, } from '@ea11y/hooks'; -import { Sidebar, TopBar } from '@ea11y/layouts'; +import { QuotaNotices, Sidebar, TopBar } from '@ea11y/layouts'; +import Notifications from '@ea11y-apps/global/components/notifications'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { useEffect } from '@wordpress/element'; import { usePluginSettingsContext } from './contexts/plugin-settings'; @@ -54,6 +55,7 @@ const App = () => { {isConnected && !closePostConnectModal && } {isUrlMismatch && !isConnected && } + @@ -61,6 +63,7 @@ const App = () => { + { return ( - - mixpanelService.sendEvent(mixpanelEvents.helpButtonClicked, { - source: 'Header', - }) - } - > - - + ); diff --git a/modules/settings/assets/js/components/analytics/charts-list.js b/modules/settings/assets/js/components/analytics/charts-list.js index 6ae1aa8f..22356e56 100644 --- a/modules/settings/assets/js/components/analytics/charts-list.js +++ b/modules/settings/assets/js/components/analytics/charts-list.js @@ -1,4 +1,5 @@ -import { ChevronDownIcon, InfoCircleFilledIcon } from '@elementor/icons'; +import ChevronDownIcon from '@elementor/icons/ChevronDownIcon'; +import InfoCircleFilledIcon from '@elementor/icons/InfoCircleFilledIcon'; import Alert from '@elementor/ui/Alert'; import AlertTitle from '@elementor/ui/AlertTitle'; import Box from '@elementor/ui/Box'; diff --git a/modules/settings/assets/js/components/analytics/charts/line-chart.js b/modules/settings/assets/js/components/analytics/charts/line-chart.js index 5af4439d..908f552f 100644 --- a/modules/settings/assets/js/components/analytics/charts/line-chart.js +++ b/modules/settings/assets/js/components/analytics/charts/line-chart.js @@ -1,7 +1,7 @@ -import { useTheme } from '@elementor/ui'; import Card from '@elementor/ui/Card'; import CardHeader from '@elementor/ui/CardHeader'; import Typography from '@elementor/ui/Typography'; +import { useTheme } from '@elementor/ui/styles'; import { LineChart as MuiLineChart } from '@mui/x-charts/LineChart'; import { LineChartTitle } from '@ea11y/components/analytics/components/line-chart-title'; import { LineTooltip } from '@ea11y/components/analytics/components/line-tooltip'; diff --git a/modules/settings/assets/js/components/capabilities-item/pro-item-infotip.js b/modules/settings/assets/js/components/capabilities-item/pro-item-infotip.js index 504e3283..e41dea33 100644 --- a/modules/settings/assets/js/components/capabilities-item/pro-item-infotip.js +++ b/modules/settings/assets/js/components/capabilities-item/pro-item-infotip.js @@ -1,4 +1,4 @@ -import { CrownIcon } from '@elementor/icons'; +import CrownIcon from '@elementor/icons/CrownIcon'; import Button from '@elementor/ui/Button'; import Card from '@elementor/ui/Card'; import CardActions from '@elementor/ui/CardActions'; diff --git a/modules/settings/assets/js/components/connect-modal/check-icon.js b/modules/settings/assets/js/components/connect-modal/check-icon.js index e261224d..649c1307 100644 --- a/modules/settings/assets/js/components/connect-modal/check-icon.js +++ b/modules/settings/assets/js/components/connect-modal/check-icon.js @@ -1,4 +1,4 @@ -import { CircleCheckFilledIcon } from '@elementor/icons'; +import CircleCheckFilledIcon from '@elementor/icons/CircleCheckFilledIcon'; import ListItemIcon from '@elementor/ui/ListItemIcon'; const ConnectModalCheckIcon = () => { diff --git a/modules/settings/assets/js/components/copy-link/index.js b/modules/settings/assets/js/components/copy-link/index.js index 88a06c51..9401eb9a 100644 --- a/modules/settings/assets/js/components/copy-link/index.js +++ b/modules/settings/assets/js/components/copy-link/index.js @@ -1,4 +1,4 @@ -import { LinkIcon } from '@elementor/icons'; +import LinkIcon from '@elementor/icons/LinkIcon'; import IconButton from '@elementor/ui/IconButton'; import Tooltip from '@elementor/ui/Tooltip'; import clipboardCopy from 'clipboard-copy'; diff --git a/modules/settings/assets/js/components/edit-link/index.js b/modules/settings/assets/js/components/edit-link/index.js index 4533ecfd..8cef2de5 100644 --- a/modules/settings/assets/js/components/edit-link/index.js +++ b/modules/settings/assets/js/components/edit-link/index.js @@ -1,4 +1,4 @@ -import { EditIcon } from '@elementor/icons'; +import EditIcon from '@elementor/icons/EditIcon'; import IconButton from '@elementor/ui/IconButton'; import Tooltip from '@elementor/ui/Tooltip'; import { useSettings } from '@ea11y/hooks'; diff --git a/modules/settings/assets/js/components/help-menu/get-started-modal.js b/modules/settings/assets/js/components/help-menu/get-started-modal.js new file mode 100644 index 00000000..574916a5 --- /dev/null +++ b/modules/settings/assets/js/components/help-menu/get-started-modal.js @@ -0,0 +1,104 @@ +import Button from '@elementor/ui/Button'; +import Dialog from '@elementor/ui/Dialog'; +import DialogActions from '@elementor/ui/DialogActions'; +import DialogContent from '@elementor/ui/DialogContent'; +import DialogContentText from '@elementor/ui/DialogContentText'; +import DialogHeader from '@elementor/ui/DialogHeader'; +import DialogTitle from '@elementor/ui/DialogTitle'; +import { useSettings, useStorage } from '@ea11y/hooks'; +import { AppLogo } from '@ea11y/icons'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { usePluginSettingsContext } from '../../contexts/plugin-settings'; + +const GetStartedModal = () => { + const { save } = useStorage(); + const { isGetStartedModalOpen, setIsGetStartedModalOpen } = useSettings(); + const { + isConnected, + closePostConnectModal, + isUrlMismatch, + closeOnboardingModal, + closeGetStartedModal, + } = usePluginSettingsContext(); + + useEffect(() => { + if ( + isConnected && + closePostConnectModal && + !isUrlMismatch && + closeOnboardingModal && + !closeGetStartedModal && + !isGetStartedModalOpen + ) { + setIsGetStartedModalOpen(true); + } + }, [ + isConnected, + closePostConnectModal, + isUrlMismatch, + closeOnboardingModal, + closeGetStartedModal, + ]); + + const handleClose = () => { + setIsGetStartedModalOpen(false); + + save({ + ea11y_close_get_started_modal: true, + }).catch((error) => { + console.error('Failed to save get started modal state:', error); + }); + + mixpanelService + .sendEvent(mixpanelEvents.menuButtonClicked, { + buttonName: 'Get started with Ally - Modal Closed', + }) + .catch((error) => { + console.error('Failed to send mixpanel event:', error); + }); + }; + + return ( + + } onClose={handleClose}> + {__('Ally', 'pojo-accessibility')} + + + + + + {__('Getting started with Ally', 'pojo-accessibility')} + + + {__( + 'Watch this quick video to see how Ally helps you find, understand, and fix accessibility issues across your site with ease.', + 'pojo-accessibility', + )} + + + + + + + + ); +}; + +export default GetStartedModal; diff --git a/modules/settings/assets/js/components/help-menu/index.js b/modules/settings/assets/js/components/help-menu/index.js new file mode 100644 index 00000000..2eb120cd --- /dev/null +++ b/modules/settings/assets/js/components/help-menu/index.js @@ -0,0 +1,61 @@ +import ChevronDownIcon from '@elementor/icons/ChevronDownIcon'; +import HelpIcon from '@elementor/icons/HelpIcon'; +import Button from '@elementor/ui/Button'; +import { + bindMenu, + bindTrigger, + usePopupState, +} from '@elementor/ui/usePopupState'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { __ } from '@wordpress/i18n'; +import { HelpPopupMenu } from './popup-menu'; + +const HelpMenu = () => { + const helpMenuState = usePopupState({ + variant: 'popover', + popupId: 'helpMenu', + }); + + const triggerProps = bindTrigger(helpMenuState); + const handleHelpButtonClick = (e) => { + triggerProps.onClick(e); + mixpanelService.sendEvent(mixpanelEvents.menuButtonClicked, { + buttonName: 'Help', + }); + }; + + return ( + <> + + + + + ); +}; + +export default HelpMenu; diff --git a/modules/settings/assets/js/components/help-menu/popup-menu.js b/modules/settings/assets/js/components/help-menu/popup-menu.js new file mode 100644 index 00000000..294d2536 --- /dev/null +++ b/modules/settings/assets/js/components/help-menu/popup-menu.js @@ -0,0 +1,83 @@ +import Menu from '@elementor/ui/Menu'; +import MenuItem from '@elementor/ui/MenuItem'; +import Typography from '@elementor/ui/Typography'; +import { styled } from '@elementor/ui/styles'; +import { useSettings } from '@ea11y/hooks'; +import { GOLINKS } from '@ea11y-apps/global/constants'; +import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; +import { __ } from '@wordpress/i18n'; +import { openLink } from '../../utils'; + +export const HelpPopupMenu = (menuProps) => { + const { setIsGetStartedModalOpen } = useSettings(); + + const handleGetStartedClick = () => { + mixpanelService.sendEvent(mixpanelEvents.popupButtonClicked, { + popupType: 'get_started_with_ally', + buttonName: 'Get started with Ally', + }); + setIsGetStartedModalOpen(true); + + if (menuProps.closeAction) { + menuProps.closeAction(); + } + }; + + const handleHelpCenterClick = () => { + mixpanelService.sendEvent(mixpanelEvents.helpButtonClicked, { + source: 'Header', + }); + openLink(GOLINKS.HELP); + }; + + return ( + + + + {__('Get started with Ally', 'pojo-accessibility')} + + + + + + {__('Help center', 'pojo-accessibility')} + + + + ); +}; + +export default HelpPopupMenu; + +const StyledMenuItem = styled(MenuItem)` + &.MuiMenuItem-gutters:focus, + &.MuiMenuItem-gutters:focus-visible { + box-shadow: inset 0 0 0 3px #5e9ed6; + } +`; + +const StyledTypography = styled(Typography)` + color: ${({ theme }) => theme.palette.text.primary}; + font-size: 14px; +`; diff --git a/modules/settings/assets/js/components/index.js b/modules/settings/assets/js/components/index.js index 700f2914..c11c3128 100644 --- a/modules/settings/assets/js/components/index.js +++ b/modules/settings/assets/js/components/index.js @@ -1,7 +1,7 @@ export { default as ConnectModal } from './connect-modal'; -export { default as Notifications } from '../../../../../assets/dev/js/components/notifications'; export { default as MyAccountMenu } from './my-account-menu'; export { default as PopupMenu } from './my-account-menu/popup-menu'; +export { default as HelpMenu } from './help-menu'; export { default as SidebarAppBar } from './sidebar-app-bar'; export { default as SidebarMenu } from './sidebar-menu'; export { default as IconSize } from './icon-size'; @@ -37,3 +37,4 @@ export { default as MenuItem } from './sidebar-menu/menu-item'; export { default as QuotaBarPopupMenu } from './quota-bar/quota-popup-menu'; export { default as QuotaBarGroup } from './quota-bar/quota-bar-group'; export { default as OnboardingModal } from './onboarding-modal'; +export { default as GetStartedModal } from './help-menu/get-started-modal'; diff --git a/modules/settings/assets/js/components/media-uploader/media-uploader.js b/modules/settings/assets/js/components/media-uploader/media-uploader.js index 73cf46af..2637916c 100644 --- a/modules/settings/assets/js/components/media-uploader/media-uploader.js +++ b/modules/settings/assets/js/components/media-uploader/media-uploader.js @@ -1,4 +1,4 @@ -import { UploadIcon } from '@elementor/icons'; +import UploadIcon from '@elementor/icons/UploadIcon'; import Button from '@elementor/ui/Button'; import { ConfirmDialog } from '@ea11y/components'; import { useIconDesign, useStorage } from '@ea11y/hooks'; diff --git a/modules/settings/assets/js/components/my-account-menu/index.js b/modules/settings/assets/js/components/my-account-menu/index.js index 10977dbe..90343dfb 100644 --- a/modules/settings/assets/js/components/my-account-menu/index.js +++ b/modules/settings/assets/js/components/my-account-menu/index.js @@ -1,10 +1,8 @@ -import { - ChevronDownIcon, - ChevronUpIcon, - ExternalLinkIcon, - HelpIcon, - UserIcon, -} from '@elementor/icons'; +import ChevronDownIcon from '@elementor/icons/ChevronDownIcon'; +import ChevronUpIcon from '@elementor/icons/ChevronUpIcon'; +import ExternalLinkIcon from '@elementor/icons/ExternalLinkIcon'; +import HelpIcon from '@elementor/icons/HelpIcon'; +import UserIcon from '@elementor/icons/UserIcon'; import List from '@elementor/ui/List'; import ListItemButton from '@elementor/ui/ListItemButton'; import ListItemIcon from '@elementor/ui/ListItemIcon'; diff --git a/modules/settings/assets/js/components/my-account-menu/popup-menu.js b/modules/settings/assets/js/components/my-account-menu/popup-menu.js index 56ea2847..8c5e799a 100644 --- a/modules/settings/assets/js/components/my-account-menu/popup-menu.js +++ b/modules/settings/assets/js/components/my-account-menu/popup-menu.js @@ -1,8 +1,6 @@ -import { - CalendarDollarIcon, - ExternalLinkIcon, - UserIcon, -} from '@elementor/icons'; +import CalendarDollarIcon from '@elementor/icons/CalendarDollarIcon'; +import ExternalLinkIcon from '@elementor/icons/ExternalLinkIcon'; +import UserIcon from '@elementor/icons/UserIcon'; import Avatar from '@elementor/ui/Avatar'; import Box from '@elementor/ui/Box'; import Chip from '@elementor/ui/Chip'; diff --git a/modules/settings/assets/js/components/quota-bar/data.js b/modules/settings/assets/js/components/quota-bar/data.js index 18c37c94..2bbc815a 100644 --- a/modules/settings/assets/js/components/quota-bar/data.js +++ b/modules/settings/assets/js/components/quota-bar/data.js @@ -11,7 +11,7 @@ export const QuotaBarData = { scanner: { title: __('Pages scanned', 'pojo-accessibility'), infotipDescription: __( - 'This is how many URLs (like site pages, blog posts, or product pages) you’ve scanned for accessibility this month. Upgrade if you’re nearing your limit to keep scanning more.', + 'This shows how many URLs you’ve scanned for accessibility in total. If you’re reaching your limit, consider upgrading to keep scanning new pages.', 'pojo-accessibility', ), }, diff --git a/modules/settings/assets/js/components/quota-bar/index.js b/modules/settings/assets/js/components/quota-bar/index.js index 6e4c25c5..8d203467 100644 --- a/modules/settings/assets/js/components/quota-bar/index.js +++ b/modules/settings/assets/js/components/quota-bar/index.js @@ -1,9 +1,10 @@ -import { InfoCircleIcon, LockFilledIcon } from '@elementor/icons'; -import { styled } from '@elementor/ui'; +import InfoCircleIcon from '@elementor/icons/InfoCircleIcon'; +import LockFilledIcon from '@elementor/icons/LockFilledIcon'; import Box from '@elementor/ui/Box'; import Infotip from '@elementor/ui/Infotip'; import LinearProgress from '@elementor/ui/LinearProgress'; import Typography from '@elementor/ui/Typography'; +import { styled } from '@elementor/ui/styles'; import { QuotaBarData } from '@ea11y/components/quota-bar/data'; import { formatPlanValue } from '../../utils/index'; diff --git a/modules/settings/assets/js/components/quota-bar/quota-bar-group.js b/modules/settings/assets/js/components/quota-bar/quota-bar-group.js index fefb9c3b..3de43fa6 100644 --- a/modules/settings/assets/js/components/quota-bar/quota-bar-group.js +++ b/modules/settings/assets/js/components/quota-bar/quota-bar-group.js @@ -1,4 +1,5 @@ -import { ChevronUpIcon, CrownIcon } from '@elementor/icons'; +import ChevronUpIcon from '@elementor/icons/ChevronUpIcon'; +import CrownIcon from '@elementor/icons/CrownIcon'; import Box from '@elementor/ui/Box'; import Button from '@elementor/ui/Button'; import Card from '@elementor/ui/Card'; diff --git a/modules/settings/assets/js/components/quota-bar/quota-indicator.js b/modules/settings/assets/js/components/quota-bar/quota-indicator.js index 1918c46f..7cad8430 100644 --- a/modules/settings/assets/js/components/quota-bar/quota-indicator.js +++ b/modules/settings/assets/js/components/quota-bar/quota-indicator.js @@ -1,7 +1,5 @@ -import { - AlertOctagonFilledIcon, - AlertTriangleFilledIcon, -} from '@elementor/icons'; +import AlertOctagonFilledIcon from '@elementor/icons/AlertOctagonFilledIcon'; +import AlertTriangleFilledIcon from '@elementor/icons/AlertTriangleFilledIcon'; import Tooltip from '@elementor/ui/Tooltip'; import { useSettings } from '@ea11y/hooks'; import { __ } from '@wordpress/i18n'; diff --git a/modules/settings/assets/js/components/sidebar-menu/menu-item.js b/modules/settings/assets/js/components/sidebar-menu/menu-item.js index 204d0a26..641b1cf4 100644 --- a/modules/settings/assets/js/components/sidebar-menu/menu-item.js +++ b/modules/settings/assets/js/components/sidebar-menu/menu-item.js @@ -9,7 +9,7 @@ import ListSubheader from '@elementor/ui/ListSubheader'; import Tooltip from '@elementor/ui/Tooltip'; import { styled } from '@elementor/ui/styles'; import { useSettings } from '@ea11y/hooks'; -import CrownFilled from '@ea11y/icons/crown-filled'; +import CrownFilled from '@ea11y-apps/global/icons/crown-filled'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { Fragment, useState } from '@wordpress/element'; diff --git a/modules/settings/assets/js/components/sidebar-menu/menu.js b/modules/settings/assets/js/components/sidebar-menu/menu.js index 528bd422..f34bb772 100644 --- a/modules/settings/assets/js/components/sidebar-menu/menu.js +++ b/modules/settings/assets/js/components/sidebar-menu/menu.js @@ -1,11 +1,13 @@ -import { ChecklistIcon, PagesIcon, SettingsIcon } from '@elementor/icons'; -import { WidgetIcon, AnalyticsIcon } from '@ea11y/icons'; +import ChecklistIcon from '@elementor/icons/ChecklistIcon'; +import PagesIcon from '@elementor/icons/PagesIcon'; +import SettingsIcon from '@elementor/icons/SettingsIcon'; +import { AnalyticsIcon, WidgetIcon } from '@ea11y/icons'; import { + AccessibilityAssistant, AccessibilityStatement, - Menu, - IconSettings, Analytics, - AccessibilityAssistant, + IconSettings, + Menu, } from '@ea11y/pages'; import { __ } from '@wordpress/i18n'; import { AccessibilityAssistantContextProvider } from '../../contexts/accessibility-assistant-context'; diff --git a/modules/settings/assets/js/components/sidebar-menu/tooltips/accessibility-statement.js b/modules/settings/assets/js/components/sidebar-menu/tooltips/accessibility-statement.js index e954ce47..48118ac6 100644 --- a/modules/settings/assets/js/components/sidebar-menu/tooltips/accessibility-statement.js +++ b/modules/settings/assets/js/components/sidebar-menu/tooltips/accessibility-statement.js @@ -1,4 +1,4 @@ -import { InfoCircleIcon } from '@elementor/icons'; +import InfoCircleIcon from '@elementor/icons/InfoCircleIcon'; import Button from '@elementor/ui/Button'; import Card from '@elementor/ui/Card'; import CardActions from '@elementor/ui/CardActions'; diff --git a/modules/settings/assets/js/components/sidebar-menu/tooltips/analytics.js b/modules/settings/assets/js/components/sidebar-menu/tooltips/analytics.js index 9a015be8..c7877962 100644 --- a/modules/settings/assets/js/components/sidebar-menu/tooltips/analytics.js +++ b/modules/settings/assets/js/components/sidebar-menu/tooltips/analytics.js @@ -1,4 +1,4 @@ -import { InfoCircleIcon } from '@elementor/icons'; +import InfoCircleIcon from '@elementor/icons/InfoCircleIcon'; import Button from '@elementor/ui/Button'; import Card from '@elementor/ui/Card'; import CardActions from '@elementor/ui/CardActions'; diff --git a/modules/settings/assets/js/components/sitemap-settings/index.js b/modules/settings/assets/js/components/sitemap-settings/index.js index a8a7eb64..d95426f8 100644 --- a/modules/settings/assets/js/components/sitemap-settings/index.js +++ b/modules/settings/assets/js/components/sitemap-settings/index.js @@ -1,4 +1,4 @@ -import { InfoCircleIcon } from '@elementor/icons'; +import InfoCircleIcon from '@elementor/icons/InfoCircleIcon'; import Accordion from '@elementor/ui/Accordion'; import AccordionDetails from '@elementor/ui/AccordionDetails'; import AccordionSummary from '@elementor/ui/AccordionSummary'; diff --git a/modules/settings/assets/js/components/skip-to-content-settings/index.js b/modules/settings/assets/js/components/skip-to-content-settings/index.js index e7bb8e9b..e9b573d3 100644 --- a/modules/settings/assets/js/components/skip-to-content-settings/index.js +++ b/modules/settings/assets/js/components/skip-to-content-settings/index.js @@ -1,4 +1,4 @@ -import { InfoCircleIcon } from '@elementor/icons'; +import InfoCircleIcon from '@elementor/icons/InfoCircleIcon'; import Box from '@elementor/ui/Box'; import Card from '@elementor/ui/Card'; import FormLabel from '@elementor/ui/FormLabel'; diff --git a/modules/settings/assets/js/components/upgrade-modal/index.js b/modules/settings/assets/js/components/upgrade-modal/index.js index 649bf3e7..41e82d7c 100644 --- a/modules/settings/assets/js/components/upgrade-modal/index.js +++ b/modules/settings/assets/js/components/upgrade-modal/index.js @@ -4,9 +4,9 @@ import Modal from '@elementor/ui/Modal'; import Paper from '@elementor/ui/Paper'; import Typography from '@elementor/ui/Typography'; import { styled } from '@elementor/ui/styles'; -import CrownFilled from '@ea11y/icons/crown-filled'; import { StyledContainer } from '@ea11y/pages/pages.styles'; import { GOLINKS } from '@ea11y-apps/global/constants'; +import CrownFilled from '@ea11y-apps/global/icons/crown-filled'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { __ } from '@wordpress/i18n'; import imageUrl from '../../../img/upgrade.png'; diff --git a/modules/settings/assets/js/contexts/plugin-settings.js b/modules/settings/assets/js/contexts/plugin-settings.js index 15bf50ef..50efe327 100644 --- a/modules/settings/assets/js/contexts/plugin-settings.js +++ b/modules/settings/assets/js/contexts/plugin-settings.js @@ -35,6 +35,12 @@ export const PluginSettingsProvider = ({ children }) => { ); } + if ('closeGetStartedModal' in settings) { + settings.closeGetStartedModal = Boolean( + settings.closeGetStartedModal, + ); + } + if ('isUrlMismatch' in settings) { settings.isUrlMismatch = Boolean(settings.isUrlMismatch); } diff --git a/modules/settings/assets/js/hooks/use-saved-settings.js b/modules/settings/assets/js/hooks/use-saved-settings.js index ea32875d..f6301305 100644 --- a/modules/settings/assets/js/hooks/use-saved-settings.js +++ b/modules/settings/assets/js/hooks/use-saved-settings.js @@ -18,6 +18,7 @@ export const useSavedSettings = () => { setAccessibilityStatementData, setShowAccessibilityGeneratedInfotip, setSkipToContentSettings, + setDismissedQuotaNotices, } = useSettings(); const { setIsAnalyticsEnabled, setIsProVersion } = useAnalyticsContext(); @@ -70,12 +71,20 @@ export const useSavedSettings = () => { result?.data?.ea11y_plan_data?.plan?.features?.analytics, ); setPlanData(result.data.ea11y_plan_data); - setPlanUsage( - calculatePlanUsage( + setPlanUsage({ + aiCredits: calculatePlanUsage( + result?.data?.ea11y_plan_data?.aiCredits?.allowed, + result?.data?.ea11y_plan_data?.aiCredits?.used, + ), + scannedPages: calculatePlanUsage( + result?.data?.ea11y_plan_data?.scannedPages?.allowed, + result?.data?.ea11y_plan_data?.scannedPages?.used, + ), + visits: calculatePlanUsage( result?.data?.ea11y_plan_data?.visits?.allowed, result?.data?.ea11y_plan_data?.visits?.used, ), - ); + }); } if (result?.data?.ea11y_accessibility_statement_data) { @@ -96,6 +105,10 @@ export const useSavedSettings = () => { if (result?.data?.ea11y_analytics_enabled) { setIsAnalyticsEnabled(result?.data?.ea11y_analytics_enabled); } + + if (result?.data?.ea11y_dismissed_quota_notices) { + setDismissedQuotaNotices(result?.data?.ea11y_dismissed_quota_notices); + } } }, [result.hasFinishedResolution]); diff --git a/modules/settings/assets/js/hooks/use-settings.js b/modules/settings/assets/js/hooks/use-settings.js index c8152489..30903a07 100644 --- a/modules/settings/assets/js/hooks/use-settings.js +++ b/modules/settings/assets/js/hooks/use-settings.js @@ -152,6 +152,10 @@ export const SettingsProvider = ({ children }) => { const [planUsage, setPlanUsage] = useState(0); + const [dismissedQuotaNotices, setDismissedQuotaNotices] = useState([]); + + const [isGetStartedModalOpen, setIsGetStartedModalOpen] = useState(false); + return ( { setShowAccessibilityGeneratedInfotip, planUsage, setPlanUsage, + dismissedQuotaNotices, + setDismissedQuotaNotices, + isGetStartedModalOpen, + setIsGetStartedModalOpen, }} > {children} diff --git a/modules/settings/assets/js/layouts/position-settings.js b/modules/settings/assets/js/layouts/position-settings.js index e8172071..b894057e 100644 --- a/modules/settings/assets/js/layouts/position-settings.js +++ b/modules/settings/assets/js/layouts/position-settings.js @@ -1,4 +1,5 @@ -import { DesktopIcon, MobileIcon } from '@elementor/icons'; +import DesktopIcon from '@elementor/icons/DesktopIcon'; +import MobileIcon from '@elementor/icons/MobileIcon'; import Box from '@elementor/ui/Box'; import Grid from '@elementor/ui/Grid'; import Tab from '@elementor/ui/Tab'; diff --git a/modules/settings/assets/js/layouts/quota-bar.js b/modules/settings/assets/js/layouts/quota-bar.js index 50e1f323..aabe6291 100644 --- a/modules/settings/assets/js/layouts/quota-bar.js +++ b/modules/settings/assets/js/layouts/quota-bar.js @@ -1,4 +1,4 @@ -import { CalendarDollarIcon } from '@elementor/icons'; +import CalendarDollarIcon from '@elementor/icons/CalendarDollarIcon'; import Box from '@elementor/ui/Box'; import IconButton from '@elementor/ui/IconButton'; import Skeleton from '@elementor/ui/Skeleton'; diff --git a/modules/settings/assets/js/layouts/quota-notices.js b/modules/settings/assets/js/layouts/quota-notices.js index 3c9ee59c..cfdb7310 100644 --- a/modules/settings/assets/js/layouts/quota-notices.js +++ b/modules/settings/assets/js/layouts/quota-notices.js @@ -1,14 +1,22 @@ import Alert from '@elementor/ui/Alert'; import AlertAction from '@elementor/ui/AlertAction'; import AlertTitle from '@elementor/ui/AlertTitle'; -import { useSettings } from '@ea11y/hooks'; +import { styled } from '@elementor/ui/styles'; +import { useSettings, useStorage } from '@ea11y/hooks'; import { GOLINKS } from '@ea11y-apps/global/constants'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { __ } from '@wordpress/i18n'; import { openLink } from '../utils'; const QuotaNotices = () => { - const { planUsage } = useSettings(); + const { + planUsage, + planData, + dismissedQuotaNotices, + setDismissedQuotaNotices, + } = useSettings(); + const { save } = useStorage(); + const isFree = planData?.plan?.name === 'Free'; /** * Handle the click on the upgrade button. @@ -32,17 +40,49 @@ const QuotaNotices = () => { }); }; - if (planUsage < 80) { + /** + * Handle closing/dismissing a quota notice. + * @param {string} noticeId - The notice ID (e.g., 'quota-banner-80') + */ + const handleCloseNotice = async (noticeId) => { + const updatedDismissed = dismissedQuotaNotices.includes(noticeId) + ? dismissedQuotaNotices + : [...dismissedQuotaNotices, noticeId]; + + setDismissedQuotaNotices(updatedDismissed); + + try { + await save({ + ea11y_dismissed_quota_notices: updatedDismissed, + }); + } catch (error) { + console.error('Failed to save dismissed notice:', error); + } + }; + + if (planUsage.aiCredits < 80 && planUsage.scannedPages < 80) { return null; } - if (planUsage >= 80 && planUsage < 95) { + if ( + (planUsage.aiCredits >= 80 && planUsage.aiCredits < 95 && !isFree) || + (planUsage.scannedPages >= 80 && planUsage.scannedPages < 95 && !isFree) + ) { + const noticeId = 'quota-banner-80'; + if (dismissedQuotaNotices.includes(noticeId)) { + return null; + } + sendQuotaNoticeTriggeredEvent('80%'); return ( - + handleCloseNotice(noticeId)} + > {__( - 'You’ve reached 80% of your monthly plan usage', + "You've reached 80% of your monthly plan usage", 'pojo-accessibility', )} @@ -57,14 +97,26 @@ const QuotaNotices = () => { > {__('Upgrade now', 'pojo-accessibility')} - + ); } - if (planUsage >= 95 && planUsage < 100) { + if ( + (planUsage.aiCredits >= 95 && planUsage.aiCredits < 100 && !isFree) || + (planUsage.scannedPages >= 95 && planUsage.scannedPages < 100 && !isFree) + ) { + const noticeId = 'quota-banner-95'; + if (dismissedQuotaNotices.includes(noticeId)) { + return null; + } + sendQuotaNoticeTriggeredEvent('95%'); return ( - + handleCloseNotice(noticeId)} + > {__('Only 5% of your monthly plan usage left', 'pojo-accessibility')} @@ -79,14 +131,26 @@ const QuotaNotices = () => { > {__('Upgrade now', 'pojo-accessibility')} - + ); } - if (planUsage === 100) { + if ( + (planUsage.aiCredits === 100 || planUsage.scannedPages === 100) && + !isFree + ) { + const noticeId = 'quota-banner-100'; + if (dismissedQuotaNotices.includes(noticeId)) { + return null; + } + sendQuotaNoticeTriggeredEvent('100%'); return ( - + handleCloseNotice(noticeId)} + > {__("You've reached your monthly plan usage", 'pojo-accessibility')} @@ -101,9 +165,49 @@ const QuotaNotices = () => { > {__('Upgrade now', 'pojo-accessibility')} - + + ); + } + + if (planUsage.scannedPages === 100 && isFree) { + const noticeId = 'quota-banner-100-free'; + + if (dismissedQuotaNotices.includes(noticeId)) { + return null; + } + + sendQuotaNoticeTriggeredEvent('100%'); + return ( + handleCloseNotice(noticeId)} + > + + {__("You've reached your free plan limit", 'pojo-accessibility')} + + {__( + 'Upgrade to scan more pages, unlock AI fixes, and access all accessibility features.', + 'pojo-accessibility', + )} + handleUpgradeClick('100')} + sx={{ marginTop: 1 }} + > + {__('Upgrade now', 'pojo-accessibility')} + + ); } }; export default QuotaNotices; + +const StyledAlert = styled(Alert)` + .MuiAlert-content div { + display: flex; + flex-direction: column; + align-items: start; + } +`; diff --git a/modules/settings/assets/js/layouts/sidebar.js b/modules/settings/assets/js/layouts/sidebar.js index 4307497f..fe364ef5 100644 --- a/modules/settings/assets/js/layouts/sidebar.js +++ b/modules/settings/assets/js/layouts/sidebar.js @@ -1,4 +1,4 @@ -import { ChevronLeftIcon } from '@elementor/icons'; +import ChevronLeftIcon from '@elementor/icons/ChevronLeftIcon'; import Box from '@elementor/ui/Box'; import Divider from '@elementor/ui/Divider'; import Drawer from '@elementor/ui/Drawer'; diff --git a/modules/settings/assets/js/layouts/top-bar-menu.js b/modules/settings/assets/js/layouts/top-bar-menu.js index 7bc68e0a..ccd9ccbf 100644 --- a/modules/settings/assets/js/layouts/top-bar-menu.js +++ b/modules/settings/assets/js/layouts/top-bar-menu.js @@ -1,4 +1,5 @@ -import { HelpIcon, UserIcon, PointFilledIcon } from '@elementor/icons'; +import PointFilledIcon from '@elementor/icons/PointFilledIcon'; +import UserIcon from '@elementor/icons/UserIcon'; import Box from '@elementor/ui/Box'; import Button from '@elementor/ui/Button'; import Divider from '@elementor/ui/Divider'; @@ -18,6 +19,7 @@ import { GOLINKS } from '@ea11y-apps/global/constants'; import { mixpanelEvents, mixpanelService } from '@ea11y-apps/global/services'; import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import HelpMenu from '../components/help-menu'; import WhatsNewDrawer from '../components/whats-new/drawer'; import { usePluginSettingsContext } from '../contexts/plugin-settings'; import { openLink } from '../utils/index'; @@ -43,13 +45,6 @@ const TopBarMenu = () => { setShowWhatsNewDataHash(false); }; - const handleHelpButtonClick = () => { - mixpanelService.sendEvent(mixpanelEvents.helpButtonClicked, { - source: 'Header', - }); - openLink(GOLINKS.HELP); - }; - return ( <> @@ -87,21 +82,7 @@ const TopBarMenu = () => { {__("What's new", 'pojo-accessibility')} {showWhatsNewDataHash && } - + diff --git a/modules/settings/assets/js/pages/accessibility-statement.js b/modules/settings/assets/js/pages/accessibility-statement.js index a97091b8..23a3ec5f 100644 --- a/modules/settings/assets/js/pages/accessibility-statement.js +++ b/modules/settings/assets/js/pages/accessibility-statement.js @@ -1,4 +1,4 @@ -import { ChevronLeftIcon } from '@elementor/icons'; +import ChevronLeftIcon from '@elementor/icons/ChevronLeftIcon'; import Box from '@elementor/ui/Box'; import FormControl from '@elementor/ui/FormControl'; import FormLabel from '@elementor/ui/FormLabel'; diff --git a/modules/settings/assets/js/pages/assistant/stats/index.js b/modules/settings/assets/js/pages/assistant/stats/index.js index c4a9e607..3a19b3f8 100644 --- a/modules/settings/assets/js/pages/assistant/stats/index.js +++ b/modules/settings/assets/js/pages/assistant/stats/index.js @@ -56,7 +56,7 @@ const StyledStatsContainer = styled(Box)` display: grid; grid-template-columns: 1fr 1fr 2fr; - grid-template-rows: 104px 176px; + grid-template-rows: 104px 206px; grid-column-gap: ${({ theme }) => theme.spacing(2)}; grid-row-gap: ${({ theme }) => theme.spacing(2)}; diff --git a/modules/settings/banners/bf-sale-2025-banner.php b/modules/settings/banners/bf-sale-2025-banner.php new file mode 100644 index 00000000..1d6edda9 --- /dev/null +++ b/modules/settings/banners/bf-sale-2025-banner.php @@ -0,0 +1,172 @@ += $sale_start_time && $now_time <= $sale_end_time; + } + + public static function user_viewed_banner(): bool { + return Pointers::is_dismissed( self::BANNER_POINTER_NAME ); + } + + /** + * Get banner markup + * @throws Throwable + */ + public static function get_banner( string $link ) { + if ( ! self::is_sale_time() || self::user_viewed_banner() ) { + return; + } + + $img = plugins_url( '/images/bf-2025-banner.png', __FILE__ ); + $url = admin_url( 'admin-ajax.php' ); + $nonce = wp_create_nonce( self::POINTER_NONCE_KEY ); + ?> + +
+
+ Elementor black friday sale banner + + + Get discount + + + +
+
+ + + + + +
@@ -156,6 +158,7 @@ public static function get_plugin_settings(): array { 'isConnected' => Connect::is_connected(), 'closePostConnectModal' => Settings::get( Settings::CLOSE_POST_CONNECT_MODAL ), 'closeOnboardingModal' => Settings::get( Settings::CLOSE_ONBOARDING_MODAL ), + 'closeGetStartedModal' => Settings::get( Settings::CLOSE_GET_STARTED_MODAL ), 'isRTL' => is_rtl(), 'isUrlMismatch' => ! Connect_Utils::is_valid_home_url(), 'unfilteredUploads' => Svg::are_unfiltered_uploads_enabled(), @@ -492,6 +495,21 @@ public function register_settings(): void { 'close_onboarding_modal' => [ 'type' => 'boolean', ], + 'close_get_started_modal' => [ + 'type' => 'boolean', + ], + 'dismissed_quota_notices' => [ + 'type' => 'array', + 'show_in_rest' => [ + 'schema' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], + ], + 'default' => [], + ], ]; foreach ( $settings as $setting => $args ) { @@ -534,6 +552,11 @@ public static function get_upgrade_link( $campaign ) : string { * @param Notices $notice_manager */ public function register_notices( Notices $notice_manager ) { + + if( ! Connect::is_connected() && ! Settings::get( Settings::PLAN_DATA ) ) { + return; + } + $notices = [ 'Quota_80', 'Quota_100', @@ -555,11 +578,20 @@ public static function get_plan_usage() : float { return 0; } - if ( ! isset( $plan_data->visits ) ) { - return 0; + $usage_percentages = array(); + + // Calculate scanned pages usage percentage + if ( isset( $plan_data->scannedPages ) && isset( $plan_data->scannedPages->allowed ) && isset( $plan_data->scannedPages->used ) && $plan_data->scannedPages->allowed > 0 ) { + $usage_percentages[] = round( $plan_data->scannedPages->used / $plan_data->scannedPages->allowed * 100, 2 ); + } + + // Calculate AI credits usage percentage + if ( isset( $plan_data->aiCredits ) && isset( $plan_data->aiCredits->allowed ) && isset( $plan_data->aiCredits->used ) && $plan_data->aiCredits->allowed > 0 ) { + $usage_percentages[] = round( $plan_data->aiCredits->used / $plan_data->aiCredits->allowed * 100, 2 ); } - return round( $plan_data->visits->used / $plan_data->visits->allowed * 100, 2 ); + // Return the maximum usage percentage, or 0 if none exist + return empty( $usage_percentages ) ? 0 : max( $usage_percentages ); } /** @@ -597,7 +629,7 @@ public function __construct() { add_action( 'admin_head', [ $this, 'hide_admin_notices' ] ); // Register notices - //add_action( 'ea11y_register_notices', [ $this, 'register_notices' ] ); + add_action( 'ea11y_register_notices', [ $this, 'register_notices' ] ); add_action( 'admin_notices', [ $this, 'admin_banners' ] ); } } diff --git a/modules/settings/notices/quota-100.php b/modules/settings/notices/quota-100.php index d636d20f..75760718 100644 --- a/modules/settings/notices/quota-100.php +++ b/modules/settings/notices/quota-100.php @@ -21,9 +21,20 @@ class Quota_100 extends Notice_Base { public string $id = 'quota-banner-100'; public function content(): string { + $plan = Settings::get( Settings::PLAN_DATA )->plan->name; + + if ( $plan === 'Free' ) { + return sprintf( '

%s

%s

%s

', + __( 'You’ve reached your free plan limit', 'pojo-accessibility' ), + __( 'Upgrade to scan more pages, unlock AI fixes, and access all accessibility features.', 'pojo-accessibility' ), + SettingsModule::get_upgrade_link( 'acc-100-quota' ), + __( 'Upgrade Now', 'pojo-accessibility' ), + ); + } + return sprintf( '

%s

%s

%s

', - __( 'Oh no! Ally has reached the monthly widget visits limit.', 'pojo-accessibility' ), - __( 'Upgrade now to increase your plan\'s monthly widget visits limit and ensure all accessibility features remain available for every visitor.', 'pojo-accessibility' ), + __( 'You’ve reached your monthly plan usage', 'pojo-accessibility' ), + __( 'Upgrade now to raise your limit and maintain complete access to all accessibility features.', 'pojo-accessibility' ), SettingsModule::get_upgrade_link( 'acc-100-quota' ), __( 'Upgrade Now', 'pojo-accessibility' ), ); diff --git a/modules/settings/notices/quota-80.php b/modules/settings/notices/quota-80.php index 8464d94c..6821c917 100644 --- a/modules/settings/notices/quota-80.php +++ b/modules/settings/notices/quota-80.php @@ -22,8 +22,8 @@ class Quota_80 extends Notice_Base { public function content(): string { return sprintf( '

%s

%s

%s

', - __( 'You\'ve reached 80% of your widget monthly visits in Ally!', 'pojo-accessibility' ), - __( 'Upgrade now to increase your plan\'s monthly widget visits limit and ensure all accessibility features remain available for every visitor.', 'pojo-accessibility' ), + __( 'You\'ve reached 80% of your monthly plan usage!', 'pojo-accessibility' ), + __( 'Upgrade now to increase your limit and ensure all accessibility features stay fully available.', 'pojo-accessibility' ), SettingsModule::get_upgrade_link( 'acc-80-quota' ), __( 'Upgrade Now', 'pojo-accessibility' ), ); @@ -36,6 +36,10 @@ public function maybe_add_quota_80_notice() : void { $this->conditions = false; } + if ( $plan_data->plan->name === 'Free' ) { + $this->conditions = false; + } + $plan_usage = (int) SettingsModule::get_plan_usage(); if ( $plan_usage > 80 && $plan_usage < 100 ) { diff --git a/package-lock.json b/package-lock.json index bb086466..6a9888a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "pojo-accessibility", - "version": "3.8.1", + "version": "3.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pojo-accessibility", - "version": "3.8.1", + "version": "3.9.1", "dependencies": { "@elementor/design-tokens": "^1.1.4", "@elementor/icons": "^1.46.0", - "@elementor/ui": "^1.36.5", + "@elementor/ui": "^1.36.14", "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", "@mui/x-charts": "^7.27.0", @@ -32,6 +32,7 @@ "html-react-parser": "^5.2.2", "husky": "^9.1.6", "mixpanel-browser": "^2.58.0", + "postcss": "^8.5.6", "prop-types": "^15.8.1", "react-colorful": "^5.6.1", "react-content-loader": "^7.0.2", @@ -1963,40 +1964,40 @@ "dev": true }, "node_modules/@cacheable/memoize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.1.tgz", - "integrity": "sha512-WBLH37SynkCa39S6IrTSMQF3Wdv4/51WxuU5TuCNEqZcLgLGHme8NUxRTcDIO8ZZFXlslWbh9BD3DllixgPg6Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.3.tgz", + "integrity": "sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==", "dev": true, "dependencies": { - "@cacheable/utils": "^2.0.1" + "@cacheable/utils": "^2.0.3" } }, "node_modules/@cacheable/memory": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.1.tgz", - "integrity": "sha512-Ufc7iQnRKFC8gjZVGOTOsMwM/vZtmsw3LafvctVXPm835ElgK3DpMe1U5i9sd6OieSkyJhXbAT2Q2FosXBBbAQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.3.tgz", + "integrity": "sha512-R3UKy/CKOyb1LZG/VRCTMcpiMDyLH7SH3JrraRdK6kf3GweWCOU3sgvE13W3TiDRbxnDKylzKJvhUAvWl9LQOA==", "dev": true, "dependencies": { - "@cacheable/memoize": "^2.0.1", - "@cacheable/utils": "^2.0.1", - "@keyv/bigmap": "^1.0.0", - "hookified": "^1.12.0", - "keyv": "^5.5.1" + "@cacheable/memoize": "^2.0.3", + "@cacheable/utils": "^2.0.3", + "@keyv/bigmap": "^1.0.2", + "hookified": "^1.12.1", + "keyv": "^5.5.3" } }, "node_modules/@cacheable/memory/node_modules/keyv": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.1.tgz", - "integrity": "sha512-eF3cHZ40bVsjdlRi/RvKAuB0+B61Q1xWvohnrJrnaQslM3h1n79IV+mc9EGag4nrA9ZOlNyr3TUzW5c8uy8vNA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "dev": true, "dependencies": { "@keyv/serialize": "^1.1.1" } }, "node_modules/@cacheable/utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.0.1.tgz", - "integrity": "sha512-sxHjO6wKn4/0wHCFYbh6tljj+ciP9BKgyBi09NLsor3sN+nu/Rt3FwLw6bYp7bp8usHpmcwUozrB/u4RuSw/eg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-m7Rce68cMHlAUjvWBy9Ru1Nmw5gU0SjGGtQDdhpe6E0xnbcvrIY0Epy//JU1VYYBUTzrG9jvgmTauULGKzOkWA==", "dev": true }, "node_modules/@csstools/css-parser-algorithms": { @@ -2164,9 +2165,9 @@ } }, "node_modules/@elementor/ui": { - "version": "1.36.5", - "resolved": "https://registry.npmjs.org/@elementor/ui/-/ui-1.36.5.tgz", - "integrity": "sha512-QpnLrps5uC3Pago6JNXH67g0fhqYINKtetgMz2jZiRDzySLdZMXV5BUwLM4ak8wUkEE2e3IB0gEoZfMX5b6SNQ==", + "version": "1.36.14", + "resolved": "https://registry.npmjs.org/@elementor/ui/-/ui-1.36.14.tgz", + "integrity": "sha512-oC6wcme3O7ybmD3WeQizwTA5aPBqR86LeHJcfE8XJKOaaKU/jxpn+iT6H+ogR5KSgqyD7QveSUTHiwcmTpNXIA==", "dependencies": { "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", @@ -3801,15 +3802,15 @@ } }, "node_modules/@keyv/bigmap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.0.0.tgz", - "integrity": "sha512-N2UsRSXlWwbvYKdFVS7sKqj6oXGegELh+zr9VripWDc8grsq8KBNp8JHI+9AQuUEFiM1S7+tm6lLp/lmbBCqCw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.0.2.tgz", + "integrity": "sha512-KR03xkEZlAZNF4IxXgVXb+uNIVNvwdh8UwI0cnc7WI6a+aQcDp8GL80qVfeB4E5NpsKJzou5jU0r6yLSSbMOtA==", "dev": true, "dependencies": { - "hookified": "^1.10.0" + "hookified": "^1.12.1" }, "engines": { - "node": ">= 20" + "node": ">= 18" } }, "node_modules/@keyv/serialize": { @@ -5102,13 +5103,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", - "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", + "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", "dev": true, "peer": true, "dependencies": { - "playwright": "1.55.0" + "playwright": "1.55.1" }, "bin": { "playwright": "cli.js" @@ -7234,13 +7235,13 @@ } }, "node_modules/@wordpress/a11y": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.30.0.tgz", - "integrity": "sha512-FpKsURiJmlQXyNCYJXiPxdJLsfcVhDSvH4/+kjEsVLOYyqD+vcb9P/oYDmor4XNIIkMT0/Lna9OnwvwCZB8owQ==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.32.0.tgz", + "integrity": "sha512-FNoyQUO1wAf768MX2vMNNk1Il3bi/A7c1s9WKSaufwEZEViXjWeqqb9GO6stWkur4UP9MRcv8IpWoLXi1BePHA==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/dom-ready": "^4.30.0", - "@wordpress/i18n": "^6.3.0" + "@wordpress/dom-ready": "^4.32.0", + "@wordpress/i18n": "^6.5.0" }, "engines": { "node": ">=18.12.0", @@ -7259,13 +7260,13 @@ } }, "node_modules/@wordpress/a11y/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -7279,13 +7280,13 @@ } }, "node_modules/@wordpress/api-fetch": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-7.30.0.tgz", - "integrity": "sha512-KwI+ENWAd350Y5qd/Ok1bbSplDP4HFf9E/yh8QnRkhtsx4xAmFP5prNkPXktqK00NGPU8rRcTKzMdtrgQ2M/pA==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-7.32.0.tgz", + "integrity": "sha512-kTufX1lhb7AG7J3KMoDOKO9IKWVwWemf/TqaqiRYNC06uxXPl/VPBJC6AzInirsNw0BZknssje+g7Fc6WbrBFA==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/i18n": "^6.3.0", - "@wordpress/url": "^4.30.0" + "@wordpress/i18n": "^6.5.0", + "@wordpress/url": "^4.32.0" }, "engines": { "node": ">=18.12.0", @@ -7304,13 +7305,13 @@ } }, "node_modules/@wordpress/api-fetch/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -7324,9 +7325,9 @@ } }, "node_modules/@wordpress/autop": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-4.30.0.tgz", - "integrity": "sha512-0VGvgPdmbJDdVUgSYfwgM6aiBI/3G5bAtksQfue3HvNMMmZvsudopPgeOXH2S8jjkbbPLr3AIbx6lFSk4Autqw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-4.32.0.tgz", + "integrity": "sha512-JD1JmCE2gEWBikF9zHCFb8j6Az6AdXeuZjg3Ewyk9vlAfU8bADRXEwVslFM0p8UD/TtK3Zzw7Rj1B0B4Lf8W6g==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -7347,9 +7348,9 @@ } }, "node_modules/@wordpress/babel-preset-default": { - "version": "8.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.30.0.tgz", - "integrity": "sha512-DUEAseIg3Xqa4MroaFQEob4TYTGJv0zKRLsDrLHAgQCTtC4PcvUqU0gM7JZjG3zo20G9R5YCBNzx1353qd1t7Q==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.32.0.tgz", + "integrity": "sha512-K6sbRuLpLQWDnIhg0FRWuHOV68BMsrPrNeMNt1TcFDOMCqozI/mnfwCbejfIO7ZPqJtbfucnh1OQ9EkMWPibew==", "dev": true, "dependencies": { "@babel/core": "7.25.7", @@ -7358,8 +7359,8 @@ "@babel/preset-env": "7.25.7", "@babel/preset-typescript": "7.25.7", "@babel/runtime": "7.25.7", - "@wordpress/browserslist-config": "^6.30.0", - "@wordpress/warning": "^3.30.0", + "@wordpress/browserslist-config": "^6.32.0", + "@wordpress/warning": "^3.32.0", "browserslist": "^4.21.10", "core-js": "^3.31.0", "react": "^18.3.0" @@ -7412,9 +7413,9 @@ } }, "node_modules/@wordpress/base-styles": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-6.6.0.tgz", - "integrity": "sha512-72OqyskL5E8V7trJYt4xlNbOfgAtAFfpwa+8TR+wLwqIYxhFjsWmkZsT3aknE6cyfRAmKZgSUfEZr8zH6Skwyw==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-6.8.0.tgz", + "integrity": "sha512-x3LCQ4DuIOg58LyQRZtI6shmNKCk2zuKGwIEMH7h7MMri/Q95ehR6Sub8dKiUL4AHktdlweouJwbHaqrXPkd0Q==", "dev": true, "engines": { "node": ">=18.12.0", @@ -7422,9 +7423,9 @@ } }, "node_modules/@wordpress/blob": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/blob/-/blob-4.30.0.tgz", - "integrity": "sha512-kiGJFVNM8snw8r45s5cAv+qDaxOdeMWpNRB0scBFiV09yTLJtjtk7tJxVz0FfUHCX5cBA7NhwaOf+m8O4T2xZQ==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/blob/-/blob-4.32.0.tgz", + "integrity": "sha512-LcQMY5Rj0OczNnBU+w+kvJJ9htsOChpoq7uMdTMqz7Oj8QmC+laIkpkX9Ds72Vkck1uaPDpFofw6wcC5KY3h+A==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -7529,9 +7530,9 @@ } }, "node_modules/@wordpress/block-serialization-default-parser": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-5.30.0.tgz", - "integrity": "sha512-7bCeSqlkzkisn4MsVpkpNM8OsM8Orkulkg2lL6TKH0bYnDGZLYFmz2Jv5PC2MfUf7g0F1lkXu078Rg11s8aQjQ==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-5.32.0.tgz", + "integrity": "sha512-Rim243Fc2snGskZiKuBgi25MJQ9u81ngdMo7w1VZ7r/uqd6KrRr8CC1sY8hwop7ritCeGMFTEDAiD6ARuycmNw==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -7604,9 +7605,9 @@ } }, "node_modules/@wordpress/browserslist-config": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.30.0.tgz", - "integrity": "sha512-CjirkPIkMf72VQcKmhmQZUJGHHFEt80ITZVgnxEtyswWA6QPRXIwFhQOAElmfhWg2wS6pCncyg6k7DfgYX3bOg==", + "version": "6.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.32.0.tgz", + "integrity": "sha512-8NemosyPrey1yswd6LH8vX9mcRF6Xmqt2mKUCspnmEX4KvayyezmtJPh2EQo3GfySVHGWePzb0yfMYEqDdsC0Q==", "dev": true, "engines": { "node": ">=18.12.0", @@ -7614,18 +7615,18 @@ } }, "node_modules/@wordpress/commands": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/commands/-/commands-1.30.0.tgz", - "integrity": "sha512-1ErowcNAfn0sC6/KkFQje/F+pR1PGsoSbtxk8fxg2ZbDiINZnEMy/sc5yin+7nVLiC4QncAKnwdysbqnBAHb5Q==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/commands/-/commands-1.32.0.tgz", + "integrity": "sha512-csVqkLoGw73i4plSMVx1t5pXFdFk8D9vJQJRzJWtdWiy8aMM+/1Yx3YetTAY/YD62mYOGk4FtkkhF/3ijaQUqQ==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/components": "^30.3.0", - "@wordpress/data": "^10.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/icons": "^10.30.0", - "@wordpress/keyboard-shortcuts": "^5.30.0", - "@wordpress/private-apis": "^1.30.0", + "@wordpress/components": "^30.5.0", + "@wordpress/data": "^10.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/icons": "^10.32.0", + "@wordpress/keyboard-shortcuts": "^5.32.0", + "@wordpress/private-apis": "^1.32.0", "clsx": "^2.1.1", "cmdk": "^1.0.0" }, @@ -7667,9 +7668,9 @@ "integrity": "sha512-SaEcbgQscHtGJ1QL+ajgDTmmqU2f6T+00jZRcFlVHUW2Asivc84LNUev/UQFyu117AsdyrtI+qpwLvgjJXJxmw==" }, "node_modules/@wordpress/commands/node_modules/@wordpress/components": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-30.3.0.tgz", - "integrity": "sha512-tXe6ucxRThjMPYCk1ZZHAnL0MQqFpCq/PwME/ypBf5dOE3GHKMKXVTvgsJv5bHH6dbJoElkDWRm3ZwqwwQ5CVg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-30.5.0.tgz", + "integrity": "sha512-LIu96PI14RpwABd5iDyTI8OxlDVEbDfX/6UUctTKsSqfJWTAynIL/K/JIHhxxI2wYbtut9yu8nugwFCPdconvA==", "dependencies": { "@ariakit/react": "^0.4.15", "@babel/runtime": "7.25.7", @@ -7683,23 +7684,23 @@ "@types/gradient-parser": "1.1.0", "@types/highlight-words-core": "1.2.1", "@use-gesture/react": "^10.3.1", - "@wordpress/a11y": "^4.30.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/date": "^5.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/dom": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/escape-html": "^3.30.0", - "@wordpress/hooks": "^4.30.0", - "@wordpress/html-entities": "^4.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/icons": "^10.30.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/keycodes": "^4.30.0", - "@wordpress/primitives": "^4.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/rich-text": "^7.30.0", - "@wordpress/warning": "^3.30.0", + "@wordpress/a11y": "^4.32.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/date": "^5.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/dom": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/escape-html": "^3.32.0", + "@wordpress/hooks": "^4.32.0", + "@wordpress/html-entities": "^4.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/icons": "^10.32.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/keycodes": "^4.32.0", + "@wordpress/primitives": "^4.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/rich-text": "^7.32.0", + "@wordpress/warning": "^3.32.0", "change-case": "^4.1.2", "clsx": "^2.1.1", "colord": "^2.7.0", @@ -7728,13 +7729,13 @@ } }, "node_modules/@wordpress/commands/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -7843,19 +7844,19 @@ } }, "node_modules/@wordpress/compose": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-7.30.0.tgz", - "integrity": "sha512-2OkdbhGiNWI4A8VXrawZcnqU8dSty3B3KvSLV1YK/TktW4qCGDNvp4W++NO5BY0d/zaSMgDUJryxyk3ejKTfzA==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-7.32.0.tgz", + "integrity": "sha512-y4StIlClJiijBHduZ6Bx0tfFarsNi6hc+mvPk2ENIfNNLHf0P90f97XjbvUUr0U1J92x7silHliQfdF0ygbFQg==", "dependencies": { "@babel/runtime": "7.25.7", "@types/mousetrap": "^1.6.8", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/dom": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/keycodes": "^4.30.0", - "@wordpress/priority-queue": "^3.30.0", - "@wordpress/undo-manager": "^1.30.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/dom": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/keycodes": "^4.32.0", + "@wordpress/priority-queue": "^3.32.0", + "@wordpress/undo-manager": "^1.32.0", "change-case": "^4.1.2", "clipboard": "^2.0.11", "mousetrap": "^1.6.5", @@ -7881,27 +7882,27 @@ } }, "node_modules/@wordpress/core-data": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/core-data/-/core-data-7.30.0.tgz", - "integrity": "sha512-mElhJ3Oj/W65bmZhpZP8d4T9pIKrdXwiVqsUQGbfdVyQxgFIarjB7AL8SyvMkqX0MzBghZL5rAoNfvMg8CTRvQ==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/core-data/-/core-data-7.32.0.tgz", + "integrity": "sha512-cHcEhoucu5YhPTpo2I+NVlxINPMTnh8cXcfrBmrqAnwFA5MttMhF8av9lnz6k3ieY9eB99rY+BDcE09+jkRl7w==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/api-fetch": "^7.30.0", - "@wordpress/block-editor": "^15.3.0", - "@wordpress/blocks": "^15.3.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/data": "^10.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/html-entities": "^4.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/rich-text": "^7.30.0", - "@wordpress/sync": "^1.30.0", - "@wordpress/undo-manager": "^1.30.0", - "@wordpress/url": "^4.30.0", - "@wordpress/warning": "^3.30.0", + "@wordpress/api-fetch": "^7.32.0", + "@wordpress/block-editor": "^15.5.0", + "@wordpress/blocks": "^15.5.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/data": "^10.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/html-entities": "^4.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/rich-text": "^7.32.0", + "@wordpress/sync": "^1.32.0", + "@wordpress/undo-manager": "^1.32.0", + "@wordpress/url": "^4.32.0", + "@wordpress/warning": "^3.32.0", "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", @@ -7946,46 +7947,46 @@ "integrity": "sha512-SaEcbgQscHtGJ1QL+ajgDTmmqU2f6T+00jZRcFlVHUW2Asivc84LNUev/UQFyu117AsdyrtI+qpwLvgjJXJxmw==" }, "node_modules/@wordpress/core-data/node_modules/@wordpress/block-editor": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-15.3.0.tgz", - "integrity": "sha512-10ZtT4QupAMQCAVEP6uA5nB+vI19WJzZuqGTpQtIDygcD/L8xQ+k5/yHVnhM3Q1W++lfQJ3wTsIS5XdAZYceIw==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-15.5.0.tgz", + "integrity": "sha512-wyqhR7kE7vknEfxdHw5LIJrk7jR3I4WtO31TLKaIIK7gEaUS1eTAH6RNNu8UxJGhXP7PL7ogOb7q2bhd6eEmGA==", "dependencies": { "@babel/runtime": "7.25.7", "@emotion/react": "^11.7.1", "@emotion/styled": "^11.6.0", "@react-spring/web": "^9.4.5", - "@wordpress/a11y": "^4.30.0", - "@wordpress/api-fetch": "^7.30.0", - "@wordpress/blob": "^4.30.0", - "@wordpress/block-serialization-default-parser": "^5.30.0", - "@wordpress/blocks": "^15.3.0", - "@wordpress/commands": "^1.30.0", - "@wordpress/components": "^30.3.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/data": "^10.30.0", - "@wordpress/date": "^5.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/dom": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/escape-html": "^3.30.0", - "@wordpress/hooks": "^4.30.0", - "@wordpress/html-entities": "^4.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/icons": "^10.30.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/keyboard-shortcuts": "^5.30.0", - "@wordpress/keycodes": "^4.30.0", - "@wordpress/notices": "^5.30.0", - "@wordpress/preferences": "^4.30.0", - "@wordpress/priority-queue": "^3.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/rich-text": "^7.30.0", - "@wordpress/style-engine": "^2.30.0", - "@wordpress/token-list": "^3.30.0", - "@wordpress/upload-media": "^0.15.0", - "@wordpress/url": "^4.30.0", - "@wordpress/warning": "^3.30.0", - "@wordpress/wordcount": "^4.30.0", + "@wordpress/a11y": "^4.32.0", + "@wordpress/api-fetch": "^7.32.0", + "@wordpress/blob": "^4.32.0", + "@wordpress/block-serialization-default-parser": "^5.32.0", + "@wordpress/blocks": "^15.5.0", + "@wordpress/commands": "^1.32.0", + "@wordpress/components": "^30.5.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/data": "^10.32.0", + "@wordpress/date": "^5.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/dom": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/escape-html": "^3.32.0", + "@wordpress/hooks": "^4.32.0", + "@wordpress/html-entities": "^4.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/icons": "^10.32.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/keyboard-shortcuts": "^5.32.0", + "@wordpress/keycodes": "^4.32.0", + "@wordpress/notices": "^5.32.0", + "@wordpress/preferences": "^4.32.0", + "@wordpress/priority-queue": "^3.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/rich-text": "^7.32.0", + "@wordpress/style-engine": "^2.32.0", + "@wordpress/token-list": "^3.32.0", + "@wordpress/upload-media": "^0.17.0", + "@wordpress/url": "^4.32.0", + "@wordpress/warning": "^3.32.0", + "@wordpress/wordcount": "^4.32.0", "change-case": "^4.1.2", "clsx": "^2.1.1", "colord": "^2.7.0", @@ -8011,26 +8012,26 @@ } }, "node_modules/@wordpress/core-data/node_modules/@wordpress/blocks": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/blocks/-/blocks-15.3.0.tgz", - "integrity": "sha512-/ojVeDNhWzfA6fAKcF/CO1JFF1SiXcvcLemRrVq4oScC2EJh78Ft7PDbI+dnll+4u0wJnJ9+VUTuAKEBnmjioA==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/blocks/-/blocks-15.5.0.tgz", + "integrity": "sha512-RxJGBtjgyjDd79H4TEbGfy6N6JU1KLMyWzgjzrmScoZghFOBeWI4qHvqud9pr01A3i9IGhQ72j0QvvgQdtWhhA==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/autop": "^4.30.0", - "@wordpress/blob": "^4.30.0", - "@wordpress/block-serialization-default-parser": "^5.30.0", - "@wordpress/data": "^10.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/dom": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/hooks": "^4.30.0", - "@wordpress/html-entities": "^4.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/rich-text": "^7.30.0", - "@wordpress/shortcode": "^4.30.0", - "@wordpress/warning": "^3.30.0", + "@wordpress/autop": "^4.32.0", + "@wordpress/blob": "^4.32.0", + "@wordpress/block-serialization-default-parser": "^5.32.0", + "@wordpress/data": "^10.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/dom": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/hooks": "^4.32.0", + "@wordpress/html-entities": "^4.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/rich-text": "^7.32.0", + "@wordpress/shortcode": "^4.32.0", + "@wordpress/warning": "^3.32.0", "change-case": "^4.1.2", "colord": "^2.7.0", "fast-deep-equal": "^3.1.3", @@ -8052,9 +8053,9 @@ } }, "node_modules/@wordpress/core-data/node_modules/@wordpress/components": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-30.3.0.tgz", - "integrity": "sha512-tXe6ucxRThjMPYCk1ZZHAnL0MQqFpCq/PwME/ypBf5dOE3GHKMKXVTvgsJv5bHH6dbJoElkDWRm3ZwqwwQ5CVg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-30.5.0.tgz", + "integrity": "sha512-LIu96PI14RpwABd5iDyTI8OxlDVEbDfX/6UUctTKsSqfJWTAynIL/K/JIHhxxI2wYbtut9yu8nugwFCPdconvA==", "dependencies": { "@ariakit/react": "^0.4.15", "@babel/runtime": "7.25.7", @@ -8068,23 +8069,23 @@ "@types/gradient-parser": "1.1.0", "@types/highlight-words-core": "1.2.1", "@use-gesture/react": "^10.3.1", - "@wordpress/a11y": "^4.30.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/date": "^5.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/dom": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/escape-html": "^3.30.0", - "@wordpress/hooks": "^4.30.0", - "@wordpress/html-entities": "^4.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/icons": "^10.30.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/keycodes": "^4.30.0", - "@wordpress/primitives": "^4.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/rich-text": "^7.30.0", - "@wordpress/warning": "^3.30.0", + "@wordpress/a11y": "^4.32.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/date": "^5.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/dom": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/escape-html": "^3.32.0", + "@wordpress/hooks": "^4.32.0", + "@wordpress/html-entities": "^4.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/icons": "^10.32.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/keycodes": "^4.32.0", + "@wordpress/primitives": "^4.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/rich-text": "^7.32.0", + "@wordpress/warning": "^3.32.0", "change-case": "^4.1.2", "clsx": "^2.1.1", "colord": "^2.7.0", @@ -8113,13 +8114,13 @@ } }, "node_modules/@wordpress/core-data/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -8133,20 +8134,20 @@ } }, "node_modules/@wordpress/core-data/node_modules/@wordpress/upload-media": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/upload-media/-/upload-media-0.15.0.tgz", - "integrity": "sha512-bIQKxGczbI3DMam1+lLuQiQdP+quen1OATIoVJzW1v/pYfcumZUueOQTmkhFPB/8RpxnGVyFJonTyqMWg49vjw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@wordpress/upload-media/-/upload-media-0.17.0.tgz", + "integrity": "sha512-AyU/AQOIwjgWiCcI6K36up87RNXSjvIac8oqEgd/U29Srk7aD51griD43tbRB1T3xy9TMoyASiBrAgT1w33lHQ==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/api-fetch": "^7.30.0", - "@wordpress/blob": "^4.30.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/data": "^10.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/preferences": "^4.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/url": "^4.30.0", + "@wordpress/api-fetch": "^7.32.0", + "@wordpress/blob": "^4.32.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/data": "^10.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/preferences": "^4.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/url": "^4.32.0", "uuid": "^9.0.1" }, "engines": { @@ -8175,18 +8176,18 @@ } }, "node_modules/@wordpress/data": { - "version": "10.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.30.0.tgz", - "integrity": "sha512-lHvL78H6JI56sFVyLRuk+fJTIQ7XKiy14d0/kH2rWStmqKDQnniUXM4kVT3MUUOKtL0Mh59CVoiA9J4dlxJ6Ww==", + "version": "10.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.32.0.tgz", + "integrity": "sha512-7lReC2/qVxlQVYVlqIYfZ9Irbzo6W30iuiD67xaXqVxiD9BA8CePY2dTBpCsykBkczoT0ryerVp648SvY82R9Q==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/compose": "^7.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/priority-queue": "^3.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/redux-routine": "^5.30.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/priority-queue": "^3.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/redux-routine": "^5.32.0", "deepmerge": "^4.3.0", "equivalent-key-map": "^0.2.2", "is-plain-object": "^5.0.0", @@ -8215,12 +8216,12 @@ } }, "node_modules/@wordpress/date": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-5.30.0.tgz", - "integrity": "sha512-btCp3y33ykgZbOkbbbjK+PXXFNo2k3QJhh45e3NQpdSAQV4sFpsqhzRRu58ou/AeBdZRXMIzbhTpT2KcKUcIgw==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-5.32.0.tgz", + "integrity": "sha512-hWmsDHzzmhbWAwWzBM042eItGor1up9tV0nEvjn1qdERoI/MS3+78d8vF40sMdWnXLNcPTCR+JHjD6kqJVmuXQ==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/deprecated": "^4.30.0", + "@wordpress/deprecated": "^4.32.0", "moment": "^2.29.4", "moment-timezone": "^0.5.40" }, @@ -8241,9 +8242,9 @@ } }, "node_modules/@wordpress/dependency-extraction-webpack-plugin": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.30.0.tgz", - "integrity": "sha512-sms4yRJriS8vzlwbYHII/xqI64oSY5ALbfQy6HJBSCfLJCNxVOzC/2fCrhdV9ghd8nK3NMAJKhTCe09oMPCnIw==", + "version": "6.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.32.0.tgz", + "integrity": "sha512-05XLzLx+cTXgid6/qI6XzBclNvkLIi9F0oLAn4htL1ExIXLfc9yBSNcNkVVP7Tr5pmpMq0NUxfM58jqxxmFfCQ==", "dev": true, "dependencies": { "json2php": "^0.0.7" @@ -8263,12 +8264,12 @@ "dev": true }, "node_modules/@wordpress/deprecated": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.30.0.tgz", - "integrity": "sha512-QfzUOCANmcrdsViEpdqh4eD4OTkG2ZGpWDuA7lSJvnVB4rcupzHpDsukmKtz6gr/AZnBDXYBoHLUwZ44G4bx0g==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.32.0.tgz", + "integrity": "sha512-HfHXUWfe/lyXTvJLWjpMJ90+XzmC2l/9vcp05n2tD+nsxwF5nS0Hjf+38pQtFPBcw3d1bbzMTNahDjtNBLvKTQ==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/hooks": "^4.30.0" + "@wordpress/hooks": "^4.32.0" }, "engines": { "node": ">=18.12.0", @@ -8287,12 +8288,12 @@ } }, "node_modules/@wordpress/dom": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.30.0.tgz", - "integrity": "sha512-t+Q4ahm0joocIle1Wb9BpwykXjItk3xsAfIj8yNiCehBRC8z0b9uJHZLzcvs5iP4iDfRooDejAHDeNY26yPP2Q==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.32.0.tgz", + "integrity": "sha512-TphAq3bE34R5O0qW2q1SSBGdqfjTtHQSxzjKc0ufvTJf1nVZkJpCOqAP0Bue48AwfFYQSagdD3RgqYjcPPEMYg==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/deprecated": "^4.30.0" + "@wordpress/deprecated": "^4.32.0" }, "engines": { "node": ">=18.12.0", @@ -8300,9 +8301,9 @@ } }, "node_modules/@wordpress/dom-ready": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.30.0.tgz", - "integrity": "sha512-hD5mU0KkgNN4DzBDbBLfRKfZyja4CePK+nRlVTFn8Yo3dGdMAlOjJGaJS1szz7XflDv/ekngj72KoqvJGrrPmg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.32.0.tgz", + "integrity": "sha512-Ru+gF3J37wiz33yqVoSmwPmc5afvGyujxyLvkGI0N4Y6EBMUmEJbC6QUbTOVld8RANQ0Bqu1btXMZfFYEY9PIQ==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -8334,9 +8335,9 @@ } }, "node_modules/@wordpress/e2e-test-utils-playwright": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.30.0.tgz", - "integrity": "sha512-KN/q6359nlb+zh/eamQD0gBgi1616Px7v+03+Hz8HqKUPKozUab1ogxr6Ew751LCYGuh204eG7ImYVM6Aqta0Q==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.32.0.tgz", + "integrity": "sha512-w1hE7xDB06SQDfwtbggyU2xTyQ++GyU2wwEc648LL6OYLh7wlDuEzP/7XCrYbxA/GPGFDKCOL3uhU163F2N/Dw==", "dev": true, "dependencies": { "change-case": "^4.1.2", @@ -8355,14 +8356,14 @@ } }, "node_modules/@wordpress/element": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.30.0.tgz", - "integrity": "sha512-hPbnaPcnD1pbLqCmrjNnuFuAIAmyB8JPmD2GpywOXnWaHbtEVX0GaC6iPQSIHShnPqoUBESXXwS3Z8KFz8MBHA==", + "version": "6.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.32.0.tgz", + "integrity": "sha512-W/Bw6HXzRBJgYYUdoUBUvtjXNWh8dVK8aqFsqpnEJTAiXdU8Ii0wBQ+E49bI/08yGCwsaXrLbQLXqtAiV6leMw==", "dependencies": { "@babel/runtime": "7.25.7", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", - "@wordpress/escape-html": "^3.30.0", + "@wordpress/escape-html": "^3.32.0", "change-case": "^4.1.2", "is-plain-object": "^5.0.0", "react": "^18.3.0", @@ -8412,9 +8413,9 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.30.0.tgz", - "integrity": "sha512-qlyyPKiize/uEyqL1nRMce9GuReZyOum81tOTR14YAngvyRIGz3K3hGwWWf9VrITZrjwuZgqMJc2udSC1uKmKg==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.32.0.tgz", + "integrity": "sha512-pT5wZmg9ob/u8RuSXgfZv8Kfd8zpvtBcCdcFE/UHasjtxJSecxDHFb0uI4eXQrSiTrsthbDZDlK/GIAagmt75Q==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -8490,9 +8491,9 @@ } }, "node_modules/@wordpress/hooks": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.30.0.tgz", - "integrity": "sha512-uo3H4KneQSVxFU98jvbw1jN3aYQ/1fPTGqDXZh4NyZx5qras1hE3QhtQU0cO2A8o5/ZNbk/Oek06TnhwsXQPBQ==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.32.0.tgz", + "integrity": "sha512-aXCLsuOQJiVJDrVKV4MjGYeU2Nv8+pg2KSAzANs7OGXIl714Q968t5qODJiJ6ADsng3FnQ0pATVYBGBTGlW6Gg==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -8513,9 +8514,9 @@ } }, "node_modules/@wordpress/html-entities": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.30.0.tgz", - "integrity": "sha512-SlJ9HM3LYO5cDzLHAP5UytbnI3u2N770JwJkLK1mfCoEIjxa+3cl9faI2ClJp1DvIzGCE4f5LliHdit/D+pARg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.32.0.tgz", + "integrity": "sha512-IHHxBeMIQR7/+Fq27eWEzOuBi10guTRBNVZUrdk32ZyJL2ISpVYwMiHOwKBH6J/67ayBSon23gUEWBqUE6bC9g==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -8567,13 +8568,13 @@ } }, "node_modules/@wordpress/icons": { - "version": "10.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.30.0.tgz", - "integrity": "sha512-PT3VGWafx/+XnlftBK8uyrm8XSsFyOJPOnl6v87Om2/I5ihv5+UeCwMLyZP2j2axdSzhcRtkEyBnN7yiOcgBvA==", + "version": "10.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.32.0.tgz", + "integrity": "sha512-1WvJdT361X1LnetYBpBWUjAVXZzl+pBdIwHbYRAp8ej47EI/igPmNxmq81nFd40s8fer/9qtipielcqSI6H2rA==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/element": "^6.30.0", - "@wordpress/primitives": "^4.30.0" + "@wordpress/element": "^6.32.0", + "@wordpress/primitives": "^4.32.0" }, "engines": { "node": ">=18.12.0", @@ -8592,9 +8593,9 @@ } }, "node_modules/@wordpress/is-shallow-equal": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.30.0.tgz", - "integrity": "sha512-Q9xoIrxxsNuiaftBciAsHmUU3IVa7V1Es0pzKmwDa3ZFBFeBn3dgrY6iWzeQ4Fe4pBaiG3NagH0qE5Iz3PuG2g==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.32.0.tgz", + "integrity": "sha512-YabJ43zv30CU8kPhTrWQZhlatwO/fBo78/HvEU40CSGCRf+j9XKu9ZUidj3xDKgzLEkDCOKmj0vUY0+NyBbKzA==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -8615,9 +8616,9 @@ } }, "node_modules/@wordpress/jest-console": { - "version": "8.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.30.0.tgz", - "integrity": "sha512-Vw7iBqsueb9jCy6RnnjixjLosm+fMi+3iMQDiBB5Pw/yUpr0PUBCR11oQCE/nO3wDl7OOA5Nq3v2qo/wxLBLMg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.32.0.tgz", + "integrity": "sha512-Sq4Ss0gJNmLzpJ9K/5Nto/FTV5L7ALvmdfA7b8JpmuKh32CETN4uIGDSfqS2a5Pfcg4KepwnL6jF9790uM54QA==", "dev": true, "dependencies": { "@babel/runtime": "7.25.7", @@ -8644,12 +8645,12 @@ } }, "node_modules/@wordpress/jest-preset-default": { - "version": "12.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.30.0.tgz", - "integrity": "sha512-nxldSo9luQBfjIFF08hcVT1pEVABe213qBxYWCXMCx3+SPsENRF6pU/yoRb0i7nz6yrd/j5oD92EXXnbQoN8eA==", + "version": "12.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.32.0.tgz", + "integrity": "sha512-Y8X+0KCjUiB1MxxwridiY0Ku3iJVOTJSsutSReu+rM/hS/1azlBMctC0bqUyC0mQyBqPF0SNlpHjYdsAoVEznQ==", "dev": true, "dependencies": { - "@wordpress/jest-console": "^8.30.0", + "@wordpress/jest-console": "^8.32.0", "babel-jest": "29.7.0" }, "engines": { @@ -8662,14 +8663,14 @@ } }, "node_modules/@wordpress/keyboard-shortcuts": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/keyboard-shortcuts/-/keyboard-shortcuts-5.30.0.tgz", - "integrity": "sha512-X1LGeoqyWFH1lGXUjJ+QUXEBnf5+ty3iO9a5MQecCpSF94ZajPAcN/17LnaftftqF81PantfG2TkXNQ7HydnTg==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/keyboard-shortcuts/-/keyboard-shortcuts-5.32.0.tgz", + "integrity": "sha512-s33V1wNmlXTaUmVC8NOX06a7vU4UUuiwqaDeKZO6V3Y9U28b8NAr2tJeY8oFfctPX5aRWeOaYfAXqTMI4oU9KA==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/data": "^10.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/keycodes": "^4.30.0" + "@wordpress/data": "^10.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/keycodes": "^4.32.0" }, "engines": { "node": ">=18.12.0", @@ -8691,12 +8692,12 @@ } }, "node_modules/@wordpress/keycodes": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.30.0.tgz", - "integrity": "sha512-IoC7FPADDc5UfjasKRT8YtaLr9WUBDqb8GJpYtiuYjHpDBDgJJPnJwKUMBweCjMl93Zzf7JAHpF7LAkICHDE1A==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.32.0.tgz", + "integrity": "sha512-XzSc3uT+viVCdycT2W6/wu+d8NZaS2y0sdHZbPXIJ6hEbyyG7ncG+XDFhXckFggqXuajxkPTEJDwOtrSTxLYqw==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/i18n": "^6.3.0" + "@wordpress/i18n": "^6.5.0" }, "engines": { "node": ">=18.12.0", @@ -8715,13 +8716,13 @@ } }, "node_modules/@wordpress/keycodes/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -8735,16 +8736,16 @@ } }, "node_modules/@wordpress/media-utils": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/media-utils/-/media-utils-5.30.0.tgz", - "integrity": "sha512-2wehODR/rrtr9gnBxpcAwrV0aulYlckOV0lGwnASaZZn0vcxMFiy6GADfqQp/UI0hsOk8c/EOwQmnum5GTqVmA==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/media-utils/-/media-utils-5.32.0.tgz", + "integrity": "sha512-tDlOxyLSKbAZrVgQ92bp6MQHHD7gxTyfE8QmYjft55UaZDXOGo9Q5aLsQ+5PoU+ZuTwImvOpt0EBiO5I/PWa4A==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/api-fetch": "^7.30.0", - "@wordpress/blob": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/private-apis": "^1.30.0" + "@wordpress/api-fetch": "^7.32.0", + "@wordpress/blob": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/private-apis": "^1.32.0" }, "engines": { "node": ">=18.12.0", @@ -8763,13 +8764,13 @@ } }, "node_modules/@wordpress/media-utils/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -8783,13 +8784,13 @@ } }, "node_modules/@wordpress/notices": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-5.30.0.tgz", - "integrity": "sha512-iEIBi7r+3dFPPOXE76WKP6f1Y9gVRr+jqca6LK3erGABvETVOYhrtFVoq0aAyDcpONc1c80TvFdO5OudCepafQ==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-5.32.0.tgz", + "integrity": "sha512-iyjAtp/UJUT46zKpBi/oX3iR8y1P7W/VqsvTitGUUeZIH9yBwaKgk5mroTABjgIgqUcC7p64i5cOGi9c23L5kA==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/a11y": "^4.30.0", - "@wordpress/data": "^10.30.0" + "@wordpress/a11y": "^4.32.0", + "@wordpress/data": "^10.32.0" }, "engines": { "node": ">=18.12.0", @@ -8811,9 +8812,9 @@ } }, "node_modules/@wordpress/npm-package-json-lint-config": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.30.0.tgz", - "integrity": "sha512-pJ5XMmj2Osk05/TFrCpf6l6VuxWDU837ISKW1bXAmBfpnLTlUuVUl6pTWbIE4gNZqb2faSBQSN3OPeQqt4RNJg==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.32.0.tgz", + "integrity": "sha512-X3D9g0m0OIM6FfdHfmyGENOZG53kc+i49WuHnDnC1gZWV5Ai0SD/CNfRcPZOYw7Ld9CScrM3kqXAKG7nd1fGFg==", "dev": true, "engines": { "node": ">=18.12.0", @@ -8824,12 +8825,12 @@ } }, "node_modules/@wordpress/postcss-plugins-preset": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.30.0.tgz", - "integrity": "sha512-3mB+tqN9uIQ6h1SXtQUFowNeCv/Cy6vWsUBhPkgU3hj7LQrGbCAmMWefDAQeYHyG0lQfSrAD6jctZ2wZmNXwsQ==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.32.0.tgz", + "integrity": "sha512-cJGgp8Z3o8dKfUWQZtQ/gNceYwLQET1z7RqkOAaSI5O0lelGit8gqpWeA7qYyZb9K1kWwhz8GGVWUIsQhCo2sQ==", "dev": true, "dependencies": { - "@wordpress/base-styles": "^6.6.0", + "@wordpress/base-styles": "^6.8.0", "autoprefixer": "^10.4.20" }, "engines": { @@ -8841,20 +8842,20 @@ } }, "node_modules/@wordpress/preferences": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/preferences/-/preferences-4.30.0.tgz", - "integrity": "sha512-QzBm0pjBbDH6GufTAPYsi5c4CfJnefS7u7AsRFRVCy0hQScXu+q/hTPrGWg5nsV1xuwLR0KGwlF2p/bX1JUnYg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/preferences/-/preferences-4.32.0.tgz", + "integrity": "sha512-K2umcScb2efx028aFR9mbjW+p88kWp7C3CmdHOL00Gl8p4Hcl8N3eGUlYxs5CEaS0cCcwzL1eG9axL1TiskwOw==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/a11y": "^4.30.0", - "@wordpress/components": "^30.3.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/data": "^10.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/icons": "^10.30.0", - "@wordpress/private-apis": "^1.30.0", + "@wordpress/a11y": "^4.32.0", + "@wordpress/components": "^30.5.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/data": "^10.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/icons": "^10.32.0", + "@wordpress/private-apis": "^1.32.0", "clsx": "^2.1.1" }, "engines": { @@ -8895,9 +8896,9 @@ "integrity": "sha512-SaEcbgQscHtGJ1QL+ajgDTmmqU2f6T+00jZRcFlVHUW2Asivc84LNUev/UQFyu117AsdyrtI+qpwLvgjJXJxmw==" }, "node_modules/@wordpress/preferences/node_modules/@wordpress/components": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-30.3.0.tgz", - "integrity": "sha512-tXe6ucxRThjMPYCk1ZZHAnL0MQqFpCq/PwME/ypBf5dOE3GHKMKXVTvgsJv5bHH6dbJoElkDWRm3ZwqwwQ5CVg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-30.5.0.tgz", + "integrity": "sha512-LIu96PI14RpwABd5iDyTI8OxlDVEbDfX/6UUctTKsSqfJWTAynIL/K/JIHhxxI2wYbtut9yu8nugwFCPdconvA==", "dependencies": { "@ariakit/react": "^0.4.15", "@babel/runtime": "7.25.7", @@ -8911,23 +8912,23 @@ "@types/gradient-parser": "1.1.0", "@types/highlight-words-core": "1.2.1", "@use-gesture/react": "^10.3.1", - "@wordpress/a11y": "^4.30.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/date": "^5.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/dom": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/escape-html": "^3.30.0", - "@wordpress/hooks": "^4.30.0", - "@wordpress/html-entities": "^4.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/icons": "^10.30.0", - "@wordpress/is-shallow-equal": "^5.30.0", - "@wordpress/keycodes": "^4.30.0", - "@wordpress/primitives": "^4.30.0", - "@wordpress/private-apis": "^1.30.0", - "@wordpress/rich-text": "^7.30.0", - "@wordpress/warning": "^3.30.0", + "@wordpress/a11y": "^4.32.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/date": "^5.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/dom": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/escape-html": "^3.32.0", + "@wordpress/hooks": "^4.32.0", + "@wordpress/html-entities": "^4.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/icons": "^10.32.0", + "@wordpress/is-shallow-equal": "^5.32.0", + "@wordpress/keycodes": "^4.32.0", + "@wordpress/primitives": "^4.32.0", + "@wordpress/private-apis": "^1.32.0", + "@wordpress/rich-text": "^7.32.0", + "@wordpress/warning": "^3.32.0", "change-case": "^4.1.2", "clsx": "^2.1.1", "colord": "^2.7.0", @@ -8956,13 +8957,13 @@ } }, "node_modules/@wordpress/preferences/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -8992,9 +8993,9 @@ } }, "node_modules/@wordpress/prettier-config": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.30.0.tgz", - "integrity": "sha512-b0tOy/H0A1ilsjAGUKqMJ3idMQbe1XS7K2ViqG62ZMJRUYBEZ1x3t+ne3Z2fVbyNVhrMqq3eZK9BSEuxr67cSg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.32.0.tgz", + "integrity": "sha512-fS4Z5hXewajLKxbMQY4rdq3fSpUnwHHfLWqOJHXlu3u4rjqVf0VMJsLsSsSM8tV78rU6x6dWIEugG6CnDygYjA==", "dev": true, "engines": { "node": ">=18.12.0", @@ -9005,12 +9006,12 @@ } }, "node_modules/@wordpress/primitives": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.30.0.tgz", - "integrity": "sha512-xKstIDGv0dG35X9kX65ZPCtAe4s06gUqJVbOf3oFD9b5PXoL2vjC3/kc5GG2jO56pLahKPLoqitH9maCJcGQEQ==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.32.0.tgz", + "integrity": "sha512-pf46CU5qQaGOULlAMNQTi+Jkwf1vwfrGYmkRtuTP68/Y8yOI19v5JZg/Vwq5nCHOs/L750mX1wMp4WvGWoPhFg==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/element": "^6.30.0", + "@wordpress/element": "^6.32.0", "clsx": "^2.1.1" }, "engines": { @@ -9041,9 +9042,9 @@ } }, "node_modules/@wordpress/priority-queue": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.30.0.tgz", - "integrity": "sha512-XScxqCjlLnijcZdoon9tUpBqw7BDBqNZQT1pgLcy0Pe661hDDvhsQ7fSapZ/KjrNnecE1uuxKQXQBXDNoJA0Eg==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.32.0.tgz", + "integrity": "sha512-LXlkiXxRSv35FBvjfAqn+rHH7KF4mw2wVl57SVzWglZAUdfvrcjrinRlEsqgMZxeAVeLPiutRV1qlkueZl7E8w==", "dependencies": { "@babel/runtime": "7.25.7", "requestidlecallback": "^0.3.0" @@ -9065,9 +9066,9 @@ } }, "node_modules/@wordpress/private-apis": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.30.0.tgz", - "integrity": "sha512-0EaWB+JxJ4t3Bylzk3xXu7Khqi0TOXYX9qDOm9W0Wy+J6xGzlkz8rSF73MvT+6JQTNNwietWHqkkHcn6dFGIXQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.32.0.tgz", + "integrity": "sha512-xmc+U8tve6QmGKiYTwVutkPkqqJkvB2fvrimjMkw8TGpnzBcmSlCtwIoLwJOBesD6liDdRFtBPpf6PM0jIRcIA==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -9088,9 +9089,9 @@ } }, "node_modules/@wordpress/redux-routine": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.30.0.tgz", - "integrity": "sha512-1cabT7u4pryjnsAe5uG6hV1Rwhh1HC+VIMz41wuMQCZ3Te1xYIjj4uWAf+u1H2Vrcz6an0pijh4OLXq/S/AB5A==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.32.0.tgz", + "integrity": "sha512-DE/UCpBF7PxznAOOlf2/Tq1aQKvqU07aaFhgGaCBd7sBn6QtBjA5SvtOvKcpr+09awdcS1AeLl2DdPGnRUZkog==", "dependencies": { "@babel/runtime": "7.25.7", "is-plain-object": "^5.0.0", @@ -9117,19 +9118,19 @@ } }, "node_modules/@wordpress/rich-text": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.30.0.tgz", - "integrity": "sha512-2aQRtxWslGd7FUqKbBHFHuVefwi3jnFp5WWJD/gxSVGEWJ3i48XuLtXLGK6V9XiutuoUbG+8C1Bx2lq3WaPMXg==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.32.0.tgz", + "integrity": "sha512-CUiKYCkuoAqfyMPkw4HRcWcK8bKdOCfMiHUfZvAaapjWB6qGhSsL4MRlmQV8IGtbHj5tUVkBAzTm5V5tIV2/yQ==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/a11y": "^4.30.0", - "@wordpress/compose": "^7.30.0", - "@wordpress/data": "^10.30.0", - "@wordpress/deprecated": "^4.30.0", - "@wordpress/element": "^6.30.0", - "@wordpress/escape-html": "^3.30.0", - "@wordpress/i18n": "^6.3.0", - "@wordpress/keycodes": "^4.30.0", + "@wordpress/a11y": "^4.32.0", + "@wordpress/compose": "^7.32.0", + "@wordpress/data": "^10.32.0", + "@wordpress/deprecated": "^4.32.0", + "@wordpress/element": "^6.32.0", + "@wordpress/escape-html": "^3.32.0", + "@wordpress/i18n": "^6.5.0", + "@wordpress/keycodes": "^4.32.0", "colord": "2.9.3", "memize": "^2.1.0" }, @@ -9153,13 +9154,13 @@ } }, "node_modules/@wordpress/rich-text/node_modules/@wordpress/i18n": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.3.0.tgz", - "integrity": "sha512-5Dw5JOWlF8JM0hOZnli7MbVshHi9wFPrBkB4CeeJm//arGGYYxw5IYaPJN5jnlMxFAVtCOaXgrxGJoC9mxQBng==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.5.0.tgz", + "integrity": "sha512-VJ4QwkgKaVBk2+u6NYTMJ4jc/fave0Q2DAmoTN1AoSaHnK1Yuq9qJtBHAdkLUo7bBpRORBTl8OFFJTFLxgc9eA==", "dependencies": { "@babel/runtime": "7.25.7", "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.30.0", + "@wordpress/hooks": "^4.32.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "tannin": "^1.2.0" @@ -9173,24 +9174,24 @@ } }, "node_modules/@wordpress/scripts": { - "version": "30.23.0", - "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.23.0.tgz", - "integrity": "sha512-IVMv4GvSxZQuj/JybHMHA0BbScv2//tELUQSHMe7IHRxvaqzd8WDJgMfnwaqQWOmtw8d4739w7kAEo26kh6zWA==", + "version": "30.25.0", + "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.25.0.tgz", + "integrity": "sha512-sUL6R4aYzdvrg8V7W49RxLsLavv7ZuQ4/6gIb/ZCYZDwhzT4feqdMXyViwmlyaQMIxTMJy93zD+7jFrUGy07Sw==", "dev": true, "dependencies": { "@babel/core": "7.25.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@svgr/webpack": "^8.0.1", - "@wordpress/babel-preset-default": "^8.30.0", - "@wordpress/browserslist-config": "^6.30.0", - "@wordpress/dependency-extraction-webpack-plugin": "^6.30.0", - "@wordpress/e2e-test-utils-playwright": "^1.30.0", - "@wordpress/eslint-plugin": "^22.16.0", - "@wordpress/jest-preset-default": "^12.30.0", - "@wordpress/npm-package-json-lint-config": "^5.30.0", - "@wordpress/postcss-plugins-preset": "^5.30.0", - "@wordpress/prettier-config": "^4.30.0", - "@wordpress/stylelint-config": "^23.22.0", + "@wordpress/babel-preset-default": "^8.32.0", + "@wordpress/browserslist-config": "^6.32.0", + "@wordpress/dependency-extraction-webpack-plugin": "^6.32.0", + "@wordpress/e2e-test-utils-playwright": "^1.32.0", + "@wordpress/eslint-plugin": "^22.18.0", + "@wordpress/jest-preset-default": "^12.32.0", + "@wordpress/npm-package-json-lint-config": "^5.32.0", + "@wordpress/postcss-plugins-preset": "^5.32.0", + "@wordpress/prettier-config": "^4.32.0", + "@wordpress/stylelint-config": "^23.24.0", "adm-zip": "^0.5.9", "babel-jest": "29.7.0", "babel-loader": "9.2.1", @@ -9288,16 +9289,16 @@ } }, "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin": { - "version": "22.16.0", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.16.0.tgz", - "integrity": "sha512-1z3rXq2uanCY0m2D1BgimeNGxZOZy87VPwzKRjaf2aPLw/ezoQckiaVGAKYKhbHLt6HFP2EkdKfuD3pmbTJ57g==", + "version": "22.18.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.18.0.tgz", + "integrity": "sha512-1w4bhax+vg3xFDXs/z2jNRaCO/kagpK0ZZ6PGWH5Anlp+e9MuzQHMbcU6Xpd/+ZU118z0rJeXDvjb3QgEfvEOg==", "dev": true, "dependencies": { "@babel/eslint-parser": "7.25.7", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", - "@wordpress/babel-preset-default": "^8.30.0", - "@wordpress/prettier-config": "^4.30.0", + "@wordpress/babel-preset-default": "^8.32.0", + "@wordpress/prettier-config": "^4.32.0", "cosmiconfig": "^7.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.25.2", @@ -9359,9 +9360,9 @@ } }, "node_modules/@wordpress/shortcode": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.30.0.tgz", - "integrity": "sha512-x7bw8NX+4tw3XauuvkM4s8SvgYVF81FiaS31YmFQ3J+g8oKokx6x+7WnrO4HHELTUNbUw6RSI/cfFSw+Xua/pg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.32.0.tgz", + "integrity": "sha512-z8es//Y7eMk8hDjeIo1PJ4auXZ0JN87n/CC6GD3uW5wgOvpRqiLjT5QpxCBqfKzOisUnNkYbrEjeChpgOT1gMw==", "dependencies": { "@babel/runtime": "7.25.7", "memize": "^2.0.1" @@ -9383,9 +9384,9 @@ } }, "node_modules/@wordpress/style-engine": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/style-engine/-/style-engine-2.30.0.tgz", - "integrity": "sha512-YpMuMx76x2IhYjOZbljVgxJEgX/FduBJleMsMMvciPGCBsvcjfPxqliwGaauofnk7/kN4E7LFyT1o9cErqiw3g==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/style-engine/-/style-engine-2.32.0.tgz", + "integrity": "sha512-qjXu4Dncch1UL4AUC3zJAOCj8mRy4+kAt+Rz3OoZV4vSsccUXKutxSCc12HGYS9IO+cjhj9yiWrSpGWCdMD/Sw==", "dependencies": { "@babel/runtime": "7.25.7", "change-case": "^4.1.2" @@ -9407,9 +9408,9 @@ } }, "node_modules/@wordpress/stylelint-config": { - "version": "23.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.22.0.tgz", - "integrity": "sha512-d1aEVn6jbMFFJh3SqpGKoNsnm0DcYD6TwgzLLlIL11kslyFEn6mfiKJVeVNIgWQe7sBDEtUkE7h4qEOSDbxO8A==", + "version": "23.24.0", + "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.24.0.tgz", + "integrity": "sha512-qF0zRv/fLK29/jEjQnY0xy1hrP5Cv6ReG10QkVAtSlaBvu07Hu+RWNIVrSznwlrbzzP+2JM61lO2oAeX/3LD+g==", "dev": true, "dependencies": { "@stylistic/stylelint-plugin": "^3.0.1", @@ -9426,13 +9427,13 @@ } }, "node_modules/@wordpress/sync": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/sync/-/sync-1.30.0.tgz", - "integrity": "sha512-gbCnWHsrgGA4G2rdCTldrkauFBeB1kZCj6Rvzpf97FVX2xiagt3/LP5u8tW96qBB5qsJhx/uJ1Wv0uhuiBX/tg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/sync/-/sync-1.32.0.tgz", + "integrity": "sha512-yKahpmYRm1VbXvU/6M2eXiQgMWl45VCcxDgMO+Y/2Wh1C/HyVZdgsNpg5c2ikXUolyKRKd7vuSMIn+agSceHTw==", "dependencies": { "@babel/runtime": "7.25.7", "@types/simple-peer": "^9.11.5", - "@wordpress/url": "^4.30.0", + "@wordpress/url": "^4.32.0", "import-locals": "^2.0.0", "lib0": "^0.2.42", "simple-peer": "^9.11.0", @@ -9458,9 +9459,9 @@ } }, "node_modules/@wordpress/token-list": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/token-list/-/token-list-3.30.0.tgz", - "integrity": "sha512-zK4MJd2y7bvzuyMhGPeiErbwZDvXjQwodkTwUM/KiJVgfXjR98O2YBc70ZGOrhpzPeIb81PZljwiNTIx7NI6dA==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/token-list/-/token-list-3.32.0.tgz", + "integrity": "sha512-FgA06HzE0dESSXAQIgzKfLa9Z2Qv1Gv4Blcskyko7wNTcQfPq53oByvCiA0aMFh/LmKHNIa2FR1vn3Y3ck/Ewg==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -9481,12 +9482,12 @@ } }, "node_modules/@wordpress/undo-manager": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.30.0.tgz", - "integrity": "sha512-pbvkzMLJrozAoaB+KOVSiRFvKvfxDrSt0jWmWCDnobbDRsmrSJRQaANw4RcoNShHuMOzd1wJ1KruK4Dtypf5MQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.32.0.tgz", + "integrity": "sha512-pEnsf9zvk61ijX28wmJ7HM2Xb2Dbdg80feF0QVFAsngFiS34r9/K1JE+y56OdyYY20ZGPbmbHLpIHX0ghjhpoQ==", "dependencies": { "@babel/runtime": "7.25.7", - "@wordpress/is-shallow-equal": "^5.30.0" + "@wordpress/is-shallow-equal": "^5.32.0" }, "engines": { "node": ">=18.12.0", @@ -9542,9 +9543,9 @@ } }, "node_modules/@wordpress/url": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.30.0.tgz", - "integrity": "sha512-oFiQQBy/2G9fct1Df52ypu3d2nYiq2k2GkOdfMHRFYI1N3k0JP7rinwyePU+37mHPCzYuJK6TuMDxoKh8oMpKg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.32.0.tgz", + "integrity": "sha512-B7Q6sKzBqSLUdQiW8oL69LFuky/IRrXDbBhPpOJruwV4l6eH6UhTlnY4QZYi1Ke91c/VJZRjUKx1fNWPJx5d9w==", "dependencies": { "@babel/runtime": "7.25.7", "remove-accents": "^0.5.0" @@ -9566,18 +9567,18 @@ } }, "node_modules/@wordpress/warning": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.30.0.tgz", - "integrity": "sha512-ZtkpSe3DhtUzIrwf+5slGkJJCxy1xn56fZ6atUaJWRbjsKnIZlTcPgahPUJZ2bugsGS5BlmDEuVI8C4NUdbwvQ==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.32.0.tgz", + "integrity": "sha512-6dPNKfJAOXijIMi9k/QdS/IQvHXcl5ErNM10y5dIhhLDuGmsZlQER06VrVmQIVAkbsmL49OfrqkqMOQidp61JA==", "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" } }, "node_modules/@wordpress/wordcount": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/wordcount/-/wordcount-4.30.0.tgz", - "integrity": "sha512-WGALjfdw0/Cl8BiJiTCM9hlGRmSErubqM9IWSKkcHNwxFV5FoOdZOzWB6xxMhxR4u9TL0QXCDkR9p6DoJEzBYw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/wordcount/-/wordcount-4.32.0.tgz", + "integrity": "sha512-bVxNPejXkKo7s2HE2kkN5K1WqnkFp9yGRe40MNuq9/iLS0hXKqjMo3Ae0Gq2LQLbgVC/VSDXqB8B+vjFRZt3CQ==", "dependencies": { "@babel/runtime": "7.25.7" }, @@ -10741,16 +10742,16 @@ } }, "node_modules/cacheable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.0.1.tgz", - "integrity": "sha512-MSKxcybpxB5kcWKpj+1tPBm2os4qKKGxDovsZmLhZmWIDYp8EgtC45C5zk1fLe1IC9PpI4ZE4eyryQH0N10PKA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.0.3.tgz", + "integrity": "sha512-nZF80J3d8RMrroMSYm1E9pBllVDXWPuECZgEZxH+vusCY4MAXAJVrY0jutcHSgh3xYX3G2EUNnmtWGZVVjWCXw==", "dev": true, "dependencies": { - "@cacheable/memoize": "^2.0.1", - "@cacheable/memory": "^2.0.1", - "@cacheable/utils": "^2.0.1", - "hookified": "^1.12.0", - "keyv": "^5.5.1" + "@cacheable/memoize": "^2.0.3", + "@cacheable/memory": "^2.0.3", + "@cacheable/utils": "^2.0.3", + "hookified": "^1.12.1", + "keyv": "^5.5.3" } }, "node_modules/cacheable-lookup": { @@ -10781,9 +10782,9 @@ } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.1.tgz", - "integrity": "sha512-eF3cHZ40bVsjdlRi/RvKAuB0+B61Q1xWvohnrJrnaQslM3h1n79IV+mc9EGag4nrA9ZOlNyr3TUzW5c8uy8vNA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "dev": true, "dependencies": { "@keyv/serialize": "^1.1.1" @@ -11041,9 +11042,9 @@ } }, "node_modules/chrome-launcher": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz", - "integrity": "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.1.tgz", + "integrity": "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==", "dev": true, "dependencies": { "@types/node": "*", @@ -15881,9 +15882,9 @@ } }, "node_modules/hookified": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.0.tgz", - "integrity": "sha512-hMr1Y9TCLshScrBbV2QxJ9BROddxZ12MX9KsCtuGGy/3SmmN5H1PllKerrVlSotur9dlE8hmUKAOSa3WDzsZmQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.1.tgz", + "integrity": "sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q==", "dev": true }, "node_modules/hosted-git-info": { @@ -16304,9 +16305,9 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", - "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.4.tgz", + "integrity": "sha512-eWjxh735SJLFJJDs5X82JQ2405OdJeAHDBnaoFCfdr5GVc7AWc9xU7KbrF+3Xd5F2ccP1aQFKtY+65X6EfKZ7A==", "dev": true, "dependencies": { "acorn": "^8.14.0", @@ -18156,17 +18157,17 @@ } }, "node_modules/lighthouse/node_modules/puppeteer-core": { - "version": "24.21.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.21.0.tgz", - "integrity": "sha512-WR4FehOs4XJ8OSp7MkGyVB4mfMs9Q6t8Y48TxiTCRxc8G2lJ5OKYPJvgU80dtKl+aIqIbdcNTgIooY49S5SsmA==", + "version": "24.23.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.23.0.tgz", + "integrity": "sha512-yl25C59gb14sOdIiSnJ08XiPP+O2RjuyZmEG+RjYmCXO7au0jcLf7fRiyii96dXGUBW7Zwei/mVKfxMx/POeFw==", "dev": true, "dependencies": { "@puppeteer/browsers": "2.10.10", - "chromium-bidi": "8.0.0", + "chromium-bidi": "9.1.0", "debug": "^4.4.3", - "devtools-protocol": "0.0.1495869", + "devtools-protocol": "0.0.1508733", "typed-query-selector": "^2.12.0", - "webdriver-bidi-protocol": "0.2.11", + "webdriver-bidi-protocol": "0.3.6", "ws": "^8.18.3" }, "engines": { @@ -18174,9 +18175,9 @@ } }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/chromium-bidi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-8.0.0.tgz", - "integrity": "sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz", + "integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==", "dev": true, "dependencies": { "mitt": "^3.0.1", @@ -18187,9 +18188,9 @@ } }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1495869", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1495869.tgz", - "integrity": "sha512-i+bkd9UYFis40RcnkW7XrOprCujXRAHg62IVh/Ah3G8MmNXpCGt1m0dTFhSdx/AVs8XEMbdOGRwdkR1Bcta8AA==", + "version": "0.0.1508733", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", + "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", "dev": true }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/ws": { @@ -20283,13 +20284,13 @@ } }, "node_modules/playwright": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", - "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", + "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", "dev": true, "peer": true, "dependencies": { - "playwright-core": "1.55.0" + "playwright-core": "1.55.1" }, "bin": { "playwright": "cli.js" @@ -20302,9 +20303,9 @@ } }, "node_modules/playwright-core": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", - "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", + "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", "dev": true, "peer": true, "bin": { @@ -21451,9 +21452,9 @@ } }, "node_modules/react-day-picker": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.10.0.tgz", - "integrity": "sha512-tedecLSd+fpSN+J08601MaMsf122nxtqZXxB6lwX37qFoLtuPNuRJN8ylxFjLhyJS1kaLfAqL1GUkSLd2BMrpQ==", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.0.tgz", + "integrity": "sha512-L4FYOaPrr3+AEROeP6IG2mCORZZfxJDkJI2df8mv1jyPrNYeccgmFPZDaHyAuPCBCddQFozkxbikj2NhMEYfDQ==", "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", @@ -23809,9 +23810,9 @@ } }, "node_modules/stylelint": { - "version": "16.24.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.24.0.tgz", - "integrity": "sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==", + "version": "16.25.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz", + "integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==", "dev": true, "funding": [ { @@ -23828,13 +23829,13 @@ "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3", "@csstools/selector-specificity": "^5.0.0", - "@dual-bundle/import-meta-resolve": "^4.1.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", - "debug": "^4.4.1", + "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.1.4", @@ -24055,12 +24056,12 @@ } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.14", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.14.tgz", - "integrity": "sha512-ExZSCSV9e7v/Zt7RzCbX57lY2dnPdxzU/h3UE6WJ6NtEMfwBd8jmi1n4otDEUfz+T/R+zxrFDpICFdjhD3H/zw==", + "version": "6.1.17", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.17.tgz", + "integrity": "sha512-Jzse4YoiUJBVYTwz5Bwl4h/2VQM7e2KK3MVAMlXzX9uamIHAH/TXUlRKU1AQGQOryQhN0EsmufiiF40G057YXA==", "dev": true, "dependencies": { - "cacheable": "^2.0.1", + "cacheable": "^2.0.3", "flatted": "^3.3.3", "hookified": "^1.12.0" } @@ -24374,9 +24375,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", - "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "dependencies": { "pump": "^3.0.0", @@ -24608,18 +24609,18 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "node_modules/tldts-core": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.14.tgz", - "integrity": "sha512-viZGNK6+NdluOJWwTO9olaugx0bkKhscIdriQQ+lNNhwitIKvb+SvhbYgnCz6j9p7dX3cJntt4agQAKMXLjJ5g==", + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", + "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", "dev": true }, "node_modules/tldts-icann": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.0.14.tgz", - "integrity": "sha512-UZPOb6KYSFBH8QD6QkK1FH2WGM7opD0s0KGafoHA4UnOQirBHtNs45kOfgy0E5a/a9fx8ukPS4E+nl7Dp3oYAw==", + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.0.16.tgz", + "integrity": "sha512-WS/pPasPs2cx6orcxCcIz01SlG3dwYlgjLAnQt7vLAusTuTLqdI8zmkqbM8TWYEf3Z0o1S4BzM3oSRFPk/6WnA==", "dev": true, "dependencies": { - "tldts-core": "^7.0.14" + "tldts-core": "^7.0.16" } }, "node_modules/tmpl": { @@ -25396,9 +25397,9 @@ "dev": true }, "node_modules/webdriver-bidi-protocol": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.2.11.tgz", - "integrity": "sha512-Y9E1/oi4XMxcR8AT0ZC4OvYntl34SPgwjmELH+owjBr0korAX4jKgZULBWILGCVGdVCQ0dodTToIETozhG8zvA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.6.tgz", + "integrity": "sha512-mlGndEOA9yK9YAbvtxaPTqdi/kaCWYYfwrZvGzcmkr/3lWM+tQj53BxtpVd6qbC6+E5OnHXgCcAhre6AkXzxjA==", "dev": true }, "node_modules/webidl-conversions": { diff --git a/package.json b/package.json index 11bdbb3b..c3198acc 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "slug": "pojo-accessibility", "homepage": "http://pojo.me/", "description": "", - "version": "3.8.1", + "version": "3.9.1", "scripts": { "build": "NODE_ENV=production wp-scripts build", "start": "NODE_ENV=development wp-scripts start", @@ -41,7 +41,7 @@ "dependencies": { "@elementor/design-tokens": "^1.1.4", "@elementor/icons": "^1.46.0", - "@elementor/ui": "^1.36.5", + "@elementor/ui": "^1.36.14", "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", "@mui/x-charts": "^7.27.0", @@ -63,6 +63,7 @@ "html-react-parser": "^5.2.2", "husky": "^9.1.6", "mixpanel-browser": "^2.58.0", + "postcss": "^8.5.6", "prop-types": "^15.8.1", "react-colorful": "^5.6.1", "react-content-loader": "^7.0.2", diff --git a/pojo-accessibility.php b/pojo-accessibility.php index c5ac49d1..dbd9e3dc 100644 --- a/pojo-accessibility.php +++ b/pojo-accessibility.php @@ -5,7 +5,7 @@ * Description: Improve your website’s accessibility with ease. Customize capabilities such as text resizing, contrast modes, link highlights, and easily generate an accessibility statement to demonstrate your commitment to inclusivity. * Author: Elementor.com * Author URI: https://elementor.com/ - * Version: 3.8.1 + * Version: 3.9.1 * Text Domain: pojo-accessibility * Domain Path: /languages/ */ @@ -15,7 +15,7 @@ // Legacy define( 'POJO_A11Y_CUSTOMIZER_OPTIONS', 'pojo_a11y_customizer_options' ); -define( 'EA11Y_VERSION', '3.8.1' ); +define( 'EA11Y_VERSION', '3.9.1' ); define( 'EA11Y_MAIN_FILE', __FILE__ ); define( 'EA11Y_BASE', plugin_basename( EA11Y_MAIN_FILE ) ); define( 'EA11Y_PATH', plugin_dir_path( __FILE__ ) ); diff --git a/readme.txt b/readme.txt index aa713262..ab161aa5 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: Web Accessibility, Accessibility, A11Y, WCAG, Accessibility Statement Requires at least: 6.6 Tested up to: 6.8 Requires PHP: 7.4 -Stable tag: 3.8.1 +Stable tag: 3.9.1 License: GPLv2 or later Ally: Make your site more inclusive by scanning for accessibility violations, fixing them easily, and adding a usability widget and accessibility statement. @@ -191,6 +191,21 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro 7. Scanner dashboard: Track your site’s accessibility scans, monitor open issues, and follow progress over time. == Changelog == += 3.9.1 – 2025-12-08 = +New: Added the Walkthrough video to assist during Ally onboarding. + Tweak: Updated the tooltip for Site-Wide Fixes for better clarity. + Tweak: Made the Ally widget footer more prominent. + Fix: Resolved an issue where the quota-limit indication did not appear correctly. + Fix: Fixed a blink issue that occurred when triggering a scan from the admin. + += 3.9.0 – 2025-11-11 = +New: You can now apply fixes across all scanned pages in one click for repeating elements such as headers, footers, and more. +New: Added the ability to manage global Alt Text and Color Contrast remediations. +Tweak: Improved the Reviews component experience. +Fix: Resolved a bug causing the Search page in the Dashboard not to load properly. +Fix: Corrected an error message that appeared improperly when the quota limit was reached. +Fix: Fixed a compatibility issue with the Divi plugin. + = 3.8.1 – 2025-10-15 = * Tweak: Code refactor