@elementor/editor-audits 4.3.0-1036

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/index.d.mts +122 -0
  3. package/dist/index.d.ts +122 -0
  4. package/dist/index.js +2822 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +2801 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +65 -0
  9. package/src/api/page-context-client.ts +19 -0
  10. package/src/audits/accessibility-policy.ts +32 -0
  11. package/src/audits/cookie-policy.ts +32 -0
  12. package/src/audits/deep-nesting.ts +45 -0
  13. package/src/audits/default-design-system.ts +32 -0
  14. package/src/audits/deprecated-widgets.ts +57 -0
  15. package/src/audits/heading-structure.ts +294 -0
  16. package/src/audits/hidden-elements.ts +41 -0
  17. package/src/audits/images-alt-text.ts +47 -0
  18. package/src/audits/images-too-large.ts +61 -0
  19. package/src/audits/nested-boxed-containers.ts +44 -0
  20. package/src/audits/page-excerpt.ts +30 -0
  21. package/src/audits/page-featured-image.ts +29 -0
  22. package/src/audits/page-title.ts +48 -0
  23. package/src/audits/prefer-global-colors.ts +52 -0
  24. package/src/audits/prefer-global-fonts.ts +50 -0
  25. package/src/audits/privacy-policy.ts +35 -0
  26. package/src/audits/robots-noindex.ts +35 -0
  27. package/src/audits/sections-and-columns.ts +39 -0
  28. package/src/audits/site-identity.ts +62 -0
  29. package/src/audits/too-many-widgets.ts +43 -0
  30. package/src/components/audit-feedback.tsx +178 -0
  31. package/src/components/audit-panel.tsx +55 -0
  32. package/src/components/category-icons.ts +17 -0
  33. package/src/components/count-summary-circle.tsx +66 -0
  34. package/src/components/fix-violation-with-angie.tsx +50 -0
  35. package/src/components/issues-category-row.tsx +62 -0
  36. package/src/components/pages/all-audits-page.tsx +65 -0
  37. package/src/components/pages/category-page.tsx +50 -0
  38. package/src/components/pages/error-page.tsx +21 -0
  39. package/src/components/pages/issues-page.tsx +48 -0
  40. package/src/components/pages/loading-page.tsx +12 -0
  41. package/src/components/pages/overview-page.tsx +140 -0
  42. package/src/components/pages/welcome-page.tsx +48 -0
  43. package/src/components/promotion-card.tsx +43 -0
  44. package/src/components/promotions.tsx +69 -0
  45. package/src/components/report-shell.tsx +110 -0
  46. package/src/components/score-bar.tsx +56 -0
  47. package/src/components/score-circle.tsx +60 -0
  48. package/src/components/severity-icons.tsx +26 -0
  49. package/src/components/status-section.tsx +47 -0
  50. package/src/components/subpage-header.tsx +29 -0
  51. package/src/components/violation-icons.tsx +33 -0
  52. package/src/components/violation-row.tsx +169 -0
  53. package/src/constants.ts +35 -0
  54. package/src/editor-app-bar.tsx +13 -0
  55. package/src/editor-panel.tsx +19 -0
  56. package/src/hooks/focus-violation.ts +53 -0
  57. package/src/hooks/use-audit-report.ts +43 -0
  58. package/src/hooks/use-audit-toggle-props.ts +17 -0
  59. package/src/index.ts +14 -0
  60. package/src/init.ts +14 -0
  61. package/src/register-audits.ts +56 -0
  62. package/src/register-promotions.ts +75 -0
  63. package/src/registry.ts +19 -0
  64. package/src/runner.ts +44 -0
  65. package/src/store/index.ts +2 -0
  66. package/src/store/selectors.ts +15 -0
  67. package/src/store/slice.ts +42 -0
  68. package/src/types.ts +112 -0
  69. package/src/utils/audit-status-summary.ts +112 -0
  70. package/src/utils/build-angie-prompt.ts +9 -0
  71. package/src/utils/collect-hardcoded-colors.ts +74 -0
  72. package/src/utils/collect-hardcoded-fonts.ts +70 -0
  73. package/src/utils/compute-report.ts +51 -0
  74. package/src/utils/find-audit-run.ts +5 -0
  75. package/src/utils/image-alt.ts +19 -0
  76. package/src/utils/image-like-sources.ts +70 -0
  77. package/src/utils/keyboard-click.ts +10 -0
  78. package/src/utils/match-global-by-value.ts +5 -0
  79. package/src/utils/page-attachments.ts +14 -0
  80. package/src/utils/read-kit-snapshot.ts +149 -0
  81. package/src/utils/report-storage.ts +16 -0
  82. package/src/utils/score-thresholds.ts +23 -0
  83. package/src/utils/severity-counts.ts +59 -0
  84. package/src/utils/sort-failed-audits.ts +19 -0
  85. package/src/utils/v1-snapshot.ts +27 -0
  86. package/src/utils/walk.ts +17 -0
  87. package/src/utils/window-config.ts +20 -0
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { Box, Divider, Tab, Tabs } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { type AuditCategory, type PageAuditReport } from '../types';
7
+ import { type AuditStatusGroup } from '../utils/audit-status-summary';
8
+ import AllAuditsPage from './pages/all-audits-page';
9
+ import CategoryPage from './pages/category-page';
10
+ import IssuesPage from './pages/issues-page';
11
+ import OverviewPage from './pages/overview-page';
12
+
13
+ type TabId = 'overview' | 'issues';
14
+
15
+ type ActivePage =
16
+ | 'overview'
17
+ | 'issues'
18
+ | { backTo: TabId; category: AuditCategory }
19
+ | { backTo: TabId; expand?: AuditStatusGroup; page: 'all-audits' };
20
+
21
+ type Props = {
22
+ report: PageAuditReport;
23
+ };
24
+
25
+ function activeTab( page: ActivePage ): TabId {
26
+ if ( page === 'overview' ) {
27
+ return 'overview';
28
+ }
29
+
30
+ if ( page === 'issues' ) {
31
+ return 'issues';
32
+ }
33
+
34
+ return page.backTo;
35
+ }
36
+
37
+ function isAllAuditsPage( page: ActivePage ): page is { backTo: TabId; expand?: AuditStatusGroup; page: 'all-audits' } {
38
+ return typeof page === 'object' && 'page' in page && page.page === 'all-audits';
39
+ }
40
+
41
+ function isCategoryPage( page: ActivePage ): page is { backTo: TabId; category: AuditCategory } {
42
+ return typeof page === 'object' && 'category' in page;
43
+ }
44
+
45
+ export default function ReportShell( { report }: Props ) {
46
+ const [ activePage, setActivePage ] = useState< ActivePage >( 'overview' );
47
+
48
+ const currentTab = activeTab( activePage );
49
+
50
+ const handleTabChange = ( _: React.SyntheticEvent, value: TabId ) => {
51
+ setActivePage( value );
52
+ };
53
+
54
+ const openCategory = ( category: AuditCategory, backTo: TabId ) => {
55
+ setActivePage( { category, backTo } );
56
+ };
57
+
58
+ const openAllAudits = ( expand?: AuditStatusGroup, backTo: TabId = 'issues' ) => {
59
+ setActivePage( { page: 'all-audits', expand, backTo } );
60
+ };
61
+
62
+ const backFromSubPage = () => {
63
+ if ( isAllAuditsPage( activePage ) || isCategoryPage( activePage ) ) {
64
+ setActivePage( activePage.backTo );
65
+ }
66
+ };
67
+
68
+ return (
69
+ <Box>
70
+ <Tabs
71
+ aria-label={ __( 'Audit navigation', 'elementor' ) }
72
+ value={ currentTab }
73
+ onChange={ handleTabChange }
74
+ textColor="secondary"
75
+ indicatorColor="secondary"
76
+ size="small"
77
+ centered={ true }
78
+ variant="fullWidth"
79
+ >
80
+ <Tab value="overview" label={ __( 'Overview', 'elementor' ) } />
81
+ <Tab value="issues" label={ __( 'Issues', 'elementor' ) } />
82
+ </Tabs>
83
+ <Divider />
84
+ { activePage === 'overview' && (
85
+ <OverviewPage
86
+ report={ report }
87
+ onCategoryClick={ ( category ) => openCategory( category, 'overview' ) }
88
+ onStatusClick={ ( status ) => openAllAudits( status, 'overview' ) }
89
+ />
90
+ ) }
91
+ { activePage === 'issues' && (
92
+ <IssuesPage
93
+ report={ report }
94
+ onCategoryClick={ ( category ) => openCategory( category, 'issues' ) }
95
+ onAllAuditsClick={ () => openAllAudits() }
96
+ />
97
+ ) }
98
+ { isAllAuditsPage( activePage ) && (
99
+ <AllAuditsPage
100
+ report={ report }
101
+ initialExpandedStatus={ activePage.expand }
102
+ onBack={ backFromSubPage }
103
+ />
104
+ ) }
105
+ { isCategoryPage( activePage ) && (
106
+ <CategoryPage category={ activePage.category } report={ report } onBack={ backFromSubPage } />
107
+ ) }
108
+ </Box>
109
+ );
110
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+ import { ChevronRightIcon } from '@elementor/icons';
3
+ import { Box, LinearProgress, Rotate, Typography, useTheme } from '@elementor/ui';
4
+
5
+ import { onKeyboardClick } from '../utils/keyboard-click';
6
+ import { getScoreTier } from '../utils/score-thresholds';
7
+
8
+ type Props = {
9
+ label: string;
10
+ score: number;
11
+ onClick?: () => void;
12
+ };
13
+
14
+ export default function ScoreBar( { label, score, onClick }: Props ) {
15
+ const isRtl = 'rtl' === useTheme().direction;
16
+
17
+ return (
18
+ <Box
19
+ role={ onClick ? 'button' : undefined }
20
+ tabIndex={ onClick ? 0 : undefined }
21
+ onClick={ onClick }
22
+ onKeyDown={ onClick ? onKeyboardClick( onClick ) : undefined }
23
+ sx={ {
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ gap: 1,
27
+ cursor: onClick ? 'pointer' : 'default',
28
+ outline: 'none',
29
+ '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', borderRadius: 1 },
30
+ py: 0.5,
31
+ } }
32
+ >
33
+ <Typography variant="body2" sx={ { minWidth: 96 } }>
34
+ { label }
35
+ </Typography>
36
+ <LinearProgress
37
+ variant="determinate"
38
+ value={ score }
39
+ color={ getScoreTier( score ).color }
40
+ sx={ { flex: 1, height: 6, borderRadius: 4, bgcolor: 'action.disabledBackground' } }
41
+ />
42
+ <Typography
43
+ variant="body2"
44
+ color="text.primary"
45
+ sx={ { minWidth: 24, textAlign: 'right', fontWeight: 900 } }
46
+ >
47
+ { score }
48
+ </Typography>
49
+ { onClick && (
50
+ <Rotate in={ isRtl }>
51
+ <ChevronRightIcon fontSize="small" color="action" />
52
+ </Rotate>
53
+ ) }
54
+ </Box>
55
+ );
56
+ }
@@ -0,0 +1,60 @@
1
+ import * as React from 'react';
2
+ import { Box, CircularProgress, Typography } from '@elementor/ui';
3
+
4
+ import { getScoreTier, type ScoreColor } from '../utils/score-thresholds';
5
+
6
+ type Props = {
7
+ color?: ScoreColor;
8
+ score: number;
9
+ };
10
+
11
+ const SCORE_CIRCLE_SIZE = 88;
12
+ const SCORE_CIRCLE_THICKNESS = 3;
13
+
14
+ export default function ScoreCircle( { color, score }: Props ) {
15
+ const progressColor = color ?? getScoreTier( score ).color;
16
+
17
+ return (
18
+ <Box
19
+ role="progressbar"
20
+ aria-valuenow={ score }
21
+ aria-valuemin={ 0 }
22
+ aria-valuemax={ 100 }
23
+ sx={ { position: 'relative', display: 'inline-flex' } }
24
+ >
25
+ <CircularProgress
26
+ variant="determinate"
27
+ value={ 100 }
28
+ size={ SCORE_CIRCLE_SIZE }
29
+ thickness={ SCORE_CIRCLE_THICKNESS }
30
+ sx={ { color: 'action.disabledBackground' } }
31
+ />
32
+ <CircularProgress
33
+ variant="determinate"
34
+ value={ score }
35
+ size={ SCORE_CIRCLE_SIZE }
36
+ thickness={ SCORE_CIRCLE_THICKNESS }
37
+ color={ progressColor }
38
+ sx={ { position: 'absolute', left: 0 } }
39
+ />
40
+ <Box
41
+ sx={ {
42
+ position: 'absolute',
43
+ inset: 0,
44
+ display: 'flex',
45
+ alignItems: 'center',
46
+ justifyContent: 'center',
47
+ } }
48
+ >
49
+ <Typography
50
+ variant="h4"
51
+ component="span"
52
+ color="text.primary"
53
+ sx={ { fontWeight: 700, lineHeight: 1 } }
54
+ >
55
+ { score }
56
+ </Typography>
57
+ </Box>
58
+ </Box>
59
+ );
60
+ }
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ import { AlertCircleIcon, AlertTriangleFilledIcon, InfoCircleIcon } from '@elementor/icons';
3
+
4
+ import { type AuditSeverity } from '../types';
5
+
6
+ type IconComponent = typeof AlertTriangleFilledIcon;
7
+
8
+ type SeverityConfig = {
9
+ Icon: IconComponent;
10
+ color: 'error' | 'warning' | 'info';
11
+ };
12
+
13
+ const SEVERITY_CONFIG: Record< AuditSeverity, SeverityConfig > = {
14
+ error: { Icon: AlertTriangleFilledIcon, color: 'error' },
15
+ warning: { Icon: AlertCircleIcon, color: 'warning' },
16
+ info: { Icon: InfoCircleIcon, color: 'info' },
17
+ };
18
+
19
+ type Props = {
20
+ severity: AuditSeverity;
21
+ };
22
+
23
+ export default function SeverityIcon( { severity }: Props ) {
24
+ const { Icon, color } = SEVERITY_CONFIG[ severity ];
25
+ return <Icon fontSize="small" color={ color } />;
26
+ }
@@ -0,0 +1,47 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { ChevronDownIcon } from '@elementor/icons';
4
+ import { Box, Chip, type ChipProps, Collapse, IconButton } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ type Props = {
8
+ label: string;
9
+ count: number;
10
+ color?: ChipProps[ 'color' ];
11
+ defaultExpanded?: boolean;
12
+ children: React.ReactNode;
13
+ };
14
+
15
+ export default function StatusSection( { label, count, color = 'default', defaultExpanded = false, children }: Props ) {
16
+ const [ expanded, setExpanded ] = useState( defaultExpanded );
17
+
18
+ if ( count === 0 ) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <Box sx={ { paddingBlock: 1 } }>
24
+ <Box
25
+ sx={ { display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'pointer' } }
26
+ onClick={ () => setExpanded( ( v ) => ! v ) }
27
+ >
28
+ <IconButton
29
+ size="small"
30
+ aria-label={ expanded ? __( 'Collapse', 'elementor' ) : __( 'Expand', 'elementor' ) }
31
+ >
32
+ <ChevronDownIcon
33
+ fontSize="small"
34
+ sx={ {
35
+ transform: expanded ? 'rotate(180deg)' : undefined,
36
+ transition: 'transform .2s',
37
+ } }
38
+ />
39
+ </IconButton>
40
+ <Chip label={ `${ label } (${ count })` } size="small" color={ color } variant="standard" />
41
+ </Box>
42
+ <Collapse in={ expanded }>
43
+ <Box sx={ { paddingInlineStart: 4.25, paddingBlock: 2 } }>{ children }</Box>
44
+ </Collapse>
45
+ </Box>
46
+ );
47
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+ import { ArrowLeftIcon } from '@elementor/icons';
3
+ import { Box, IconButton, Rotate, Typography, useTheme } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ type Props = {
7
+ title: string;
8
+ onBack: () => void;
9
+ backLabel?: string;
10
+ icon?: React.ReactNode;
11
+ };
12
+
13
+ export default function SubpageHeader( { title, onBack, backLabel, icon }: Props ) {
14
+ const isRtl = 'rtl' === useTheme().direction;
15
+
16
+ return (
17
+ <Box sx={ { display: 'flex', alignItems: 'center', gap: 0.5, p: 1 } }>
18
+ <IconButton size="small" onClick={ onBack } aria-label={ backLabel ?? __( 'Back', 'elementor' ) }>
19
+ <Rotate in={ isRtl }>
20
+ <ArrowLeftIcon fontSize="small" />
21
+ </Rotate>
22
+ </IconButton>
23
+ { icon }
24
+ <Typography variant="subtitle2" fontWeight="bold">
25
+ { title }
26
+ </Typography>
27
+ </Box>
28
+ );
29
+ }
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ import { FileSettingsIcon, SettingsIcon, ShieldCheckIcon } from '@elementor/icons';
3
+ import { Box } from '@elementor/ui';
4
+
5
+ import { type AuditViolation } from '../types';
6
+
7
+ type Props = {
8
+ violation: AuditViolation;
9
+ widgetIcon: string | null;
10
+ };
11
+
12
+ export default function ViolationIcon( { violation, widgetIcon }: Props ) {
13
+ if ( widgetIcon ) {
14
+ return (
15
+ <Box
16
+ component="i"
17
+ className={ widgetIcon }
18
+ aria-hidden={ true }
19
+ sx={ { fontSize: 'inherit', width: '1em', textAlign: 'center' } }
20
+ />
21
+ );
22
+ }
23
+
24
+ if ( violation.targetHint === 'page-settings' ) {
25
+ return <FileSettingsIcon fontSize="inherit" aria-hidden={ true } />;
26
+ }
27
+
28
+ if ( violation.targetHint === 'site-settings' || violation.targetHint === 'site-identity-settings' ) {
29
+ return <SettingsIcon fontSize="inherit" aria-hidden={ true } />;
30
+ }
31
+
32
+ return <ShieldCheckIcon fontSize="inherit" aria-hidden={ true } />;
33
+ }
@@ -0,0 +1,169 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { getElementIcon, getElementTitle } from '@elementor/editor-elements';
4
+ import { AlertCircleIcon, BulbIcon, CheckIcon, ChevronDownIcon, EyeIcon, HelpIcon } from '@elementor/icons';
5
+ import { Alert, AlertTitle, Box, Collapse, IconButton, Tooltip, Typography } from '@elementor/ui';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { focusViolation } from '../hooks/focus-violation';
9
+ import { type AuditMeta, type AuditViolation } from '../types';
10
+ import { buildAngiePrompt } from '../utils/build-angie-prompt';
11
+ import { onKeyboardClick } from '../utils/keyboard-click';
12
+ import FixViolationWithAngie from './fix-violation-with-angie';
13
+ import SeverityIcon from './severity-icons';
14
+ import ViolationIcon from './violation-icons';
15
+
16
+ type Props = {
17
+ audit: AuditMeta;
18
+ skipReason?: string;
19
+ violations?: AuditViolation[];
20
+ };
21
+
22
+ function SkipReasonTooltip( { reason }: { reason: string } ) {
23
+ return (
24
+ <Tooltip title={ reason } placement="top">
25
+ <Box aria-label={ reason } component="span" sx={ { display: 'inline-flex', alignItems: 'center' } }>
26
+ <HelpIcon fontSize="small" color="action" />
27
+ </Box>
28
+ </Tooltip>
29
+ );
30
+ }
31
+
32
+ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violations' > ) {
33
+ if ( violations ) {
34
+ return (
35
+ <>
36
+ <Typography variant="caption" color="text.secondary" fontWeight="bold">
37
+ { violations.length }
38
+ </Typography>
39
+ <SeverityIcon severity={ audit.severity } />
40
+ </>
41
+ );
42
+ }
43
+
44
+ return <CheckIcon fontSize="small" color="success" />;
45
+ }
46
+
47
+ export default function ViolationRow( { audit, skipReason, violations }: Props ) {
48
+ const [ expanded, setExpanded ] = useState( false );
49
+
50
+ const toggleExpanded = () => setExpanded( ( value ) => ! value );
51
+
52
+ return (
53
+ <Box sx={ { borderBottom: 1, borderColor: 'divider', paddingBlock: 0.5 } }>
54
+ <Box sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
55
+ <Box
56
+ sx={ {
57
+ alignItems: 'center',
58
+ cursor: 'pointer',
59
+ display: 'flex',
60
+ flex: 1,
61
+ gap: 0.5,
62
+ minWidth: 0,
63
+ } }
64
+ onClick={ toggleExpanded }
65
+ >
66
+ <Typography variant="body2" sx={ { flex: 1 } }>
67
+ { audit.title }
68
+ </Typography>
69
+ { ! skipReason && <StatusIndicator audit={ audit } violations={ violations } /> }
70
+ </Box>
71
+ { skipReason && <SkipReasonTooltip reason={ skipReason } /> }
72
+ <IconButton
73
+ size="small"
74
+ aria-label={ expanded ? __( 'Collapse', 'elementor' ) : __( 'Expand', 'elementor' ) }
75
+ onClick={ toggleExpanded }
76
+ >
77
+ <ChevronDownIcon
78
+ fontSize="small"
79
+ sx={ {
80
+ transform: expanded ? 'rotate(180deg)' : undefined,
81
+ transition: 'transform .2s',
82
+ } }
83
+ />
84
+ </IconButton>
85
+ </Box>
86
+ <Collapse in={ expanded }>
87
+ <Box sx={ { display: 'flex', flexDirection: 'column', gap: 1, paddingBlock: 1 } }>
88
+ <Alert
89
+ severity="secondary"
90
+ sx={ { p: 1 } }
91
+ icon={ <AlertCircleIcon fontSize="small" color="secondary" aria-hidden={ true } /> }
92
+ >
93
+ <AlertTitle>
94
+ <Typography variant="caption" component="p" color="text.primary" fontWeight="bold">
95
+ { __( "What's the issue", 'elementor' ) }
96
+ </Typography>
97
+ </AlertTitle>
98
+ <Typography variant="caption" component="p" color="text.secondary">
99
+ { audit.description }
100
+ </Typography>
101
+ </Alert>
102
+ <Alert
103
+ severity="info"
104
+ sx={ { p: 1 } }
105
+ icon={ <BulbIcon fontSize="small" color="info" aria-hidden={ true } /> }
106
+ >
107
+ <AlertTitle>
108
+ <Typography variant="caption" component="p" color="text.primary" fontWeight="bold">
109
+ { __( 'How to resolve', 'elementor' ) }
110
+ </Typography>
111
+ </AlertTitle>
112
+ <Typography variant="caption" component="p" color="text.secondary">
113
+ { audit.fixHint }
114
+ </Typography>
115
+ </Alert>
116
+ </Box>
117
+ { violations && violations.length > 0 && (
118
+ <Box role="list" sx={ { paddingBlockEnd: 1, paddingInlineStart: 2 } }>
119
+ { violations.map( ( violation, idx ) => {
120
+ const widgetIcon = violation.elementId ? getElementIcon( violation.elementId ) : null;
121
+ const elementTitle = violation.elementId ? getElementTitle( violation.elementId ) : null;
122
+ const rowLabel = elementTitle
123
+ ? `${ elementTitle } - ${ violation.label }`
124
+ : violation.label;
125
+
126
+ return (
127
+ <Box
128
+ key={ idx }
129
+ role="button"
130
+ tabIndex={ 0 }
131
+ onClick={ () => focusViolation( violation ) }
132
+ onKeyDown={ onKeyboardClick( () => focusViolation( violation ) ) }
133
+ sx={ {
134
+ display: 'flex',
135
+ alignItems: 'center',
136
+ gap: 1,
137
+ paddingBlock: 0.5,
138
+ paddingInline: 2,
139
+ borderRadius: 1,
140
+ cursor: 'pointer',
141
+ '& .violation-hover-icon': { opacity: 0, transition: 'opacity .15s' },
142
+ '&:hover': { bgcolor: 'action.hover' },
143
+ '&:hover .violation-hover-icon, &:focus-visible .violation-hover-icon': {
144
+ opacity: 1,
145
+ },
146
+ } }
147
+ >
148
+ <ViolationIcon violation={ violation } widgetIcon={ widgetIcon } />
149
+ <Box sx={ { flex: 1 } }>
150
+ <Typography variant="caption">{ rowLabel }</Typography>
151
+ { violation.detail && (
152
+ <Typography variant="caption" color="text.secondary">
153
+ { violation.detail }
154
+ </Typography>
155
+ ) }
156
+ </Box>
157
+ { violation.angieFix && (
158
+ <FixViolationWithAngie prompt={ buildAngiePrompt( rowLabel ) } />
159
+ ) }
160
+ <EyeIcon className="violation-hover-icon" fontSize="tiny" aria-hidden={ true } />
161
+ </Box>
162
+ );
163
+ } ) }
164
+ </Box>
165
+ ) }
166
+ </Collapse>
167
+ </Box>
168
+ );
169
+ }
@@ -0,0 +1,35 @@
1
+ import { __ } from '@wordpress/i18n';
2
+
3
+ import { type AuditCategory } from './types';
4
+
5
+ export const AUDIT_PANEL_ID = 'audit-panel';
6
+
7
+ export const REPORT_STORAGE_KEY_PREFIX = 'elementor/audits/report';
8
+
9
+ export const CREATE_WIDGET_EVENT = 'elementor/editor/create-widget';
10
+
11
+ export const ANGIE_FIX_ENTRY_POINT = 'audit_violation';
12
+
13
+ export const FEEDBACK_EXPERIMENT_NAME = 'in_editor_feedback';
14
+ export const FEEDBACK_ENTRY_POINT = 'audit_panel';
15
+
16
+ export const FEEDBACK_CLICKED_EVENT = 'audit_feedback_clicked';
17
+ export const FEEDBACK_SENT_EVENT = 'audit_feedback_sent';
18
+ export const FEEDBACK_CANCELLED_EVENT = 'audit_feedback_cancelled';
19
+ export const FEEDBACK_CLOSED_EVENT = 'audit_feedback_closed';
20
+
21
+ export const ALL_CATEGORIES: AuditCategory[] = [
22
+ 'best-practices',
23
+ 'seo',
24
+ 'accessibility',
25
+ 'performance',
26
+ 'compliance',
27
+ ];
28
+
29
+ export const CATEGORY_LABELS: Record< AuditCategory, string > = {
30
+ 'best-practices': __( 'Best Practices', 'elementor' ),
31
+ seo: __( 'SEO', 'elementor' ),
32
+ accessibility: __( 'Accessibility', 'elementor' ),
33
+ performance: __( 'Performance', 'elementor' ),
34
+ compliance: __( 'Compliance', 'elementor' ),
35
+ };
@@ -0,0 +1,13 @@
1
+ import { utilitiesMenu } from '@elementor/editor-app-bar';
2
+
3
+ import { useAuditToggleProps } from './hooks/use-audit-toggle-props';
4
+
5
+ const AUDIT_TOGGLE_PRIORITY = 24;
6
+
7
+ export function registerAppBarAuditsToggle(): void {
8
+ utilitiesMenu.registerToggleAction( {
9
+ id: 'toggle-audit-panel',
10
+ priority: AUDIT_TOGGLE_PRIORITY,
11
+ useProps: useAuditToggleProps,
12
+ } );
13
+ }
@@ -0,0 +1,19 @@
1
+ import { createFloatingPanel } from '@elementor/editor-floating-panels';
2
+
3
+ import AuditPanel from './components/audit-panel';
4
+ import { AUDIT_PANEL_ID } from './constants';
5
+
6
+ export const auditPanel = createFloatingPanel( {
7
+ id: AUDIT_PANEL_ID,
8
+ title: 'Audit',
9
+ icon: () => null,
10
+ component: AuditPanel,
11
+ isDraggable: true,
12
+ isResizable: true,
13
+ defaults: {
14
+ width: 360,
15
+ height: 600,
16
+ minWidth: 280,
17
+ minHeight: 400,
18
+ },
19
+ } );
@@ -0,0 +1,53 @@
1
+ import { getContainer, selectElement } from '@elementor/editor-elements';
2
+ import { __privateOpenRoute as openRoute, __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
3
+
4
+ import { type AuditViolation } from '../types';
5
+
6
+ const SCROLL_TO_VIEW_DELAY_MS = 200;
7
+
8
+ export function focusViolation( violation: AuditViolation ): void {
9
+ if ( violation.elementId ) {
10
+ const container = getContainer( violation.elementId );
11
+
12
+ if ( container ) {
13
+ selectElement( violation.elementId );
14
+
15
+ const domElement = container.view?.getDomElement?.();
16
+
17
+ if ( domElement ) {
18
+ (
19
+ window as unknown as {
20
+ elementor?: { helpers?: { scrollToView?: ( element: unknown, timeout?: number ) => void } };
21
+ }
22
+ ).elementor?.helpers?.scrollToView?.( domElement, SCROLL_TO_VIEW_DELAY_MS );
23
+ }
24
+ }
25
+
26
+ if ( violation.targetHint === 'element-settings' ) {
27
+ runCommand( 'panel/editor/open' );
28
+ }
29
+
30
+ return;
31
+ }
32
+
33
+ if ( violation.externalUrl ) {
34
+ window.open( violation.externalUrl, '_blank' );
35
+ return;
36
+ }
37
+
38
+ if ( violation.targetHint === 'page-settings' ) {
39
+ openRoute( 'panel/page-settings/settings' );
40
+ return;
41
+ }
42
+
43
+ if ( violation.targetHint === 'site-settings' ) {
44
+ runCommand( 'panel/global/open' );
45
+ return;
46
+ }
47
+
48
+ if ( violation.targetHint === 'site-identity-settings' ) {
49
+ void runCommand( 'panel/global/open' ).then( () => {
50
+ openRoute( 'panel/global/settings-site-identity' );
51
+ } );
52
+ }
53
+ }