@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,66 @@
1
+ import * as React from 'react';
2
+ import { Box, Chip, type ChipProps, type Theme, Typography } from '@elementor/ui';
3
+
4
+ type Props = {
5
+ ariaLabel: string;
6
+ color: ChipProps[ 'color' ];
7
+ count: number;
8
+ label: string;
9
+ onClick: () => void;
10
+ };
11
+
12
+ const COUNT_SUMMARY_CIRCLE_SIZE = 64;
13
+ const COUNT_SUMMARY_BORDER_WIDTH = 4;
14
+
15
+ function chipBorderColor( theme: Theme, color: ChipProps[ 'color' ] ): string {
16
+ if ( color === 'default' || ! color ) {
17
+ return theme.palette.text.secondary;
18
+ }
19
+
20
+ return theme.palette[ color ].main;
21
+ }
22
+
23
+ export default function CountSummaryCircle( { ariaLabel, color, count, label, onClick }: Props ) {
24
+ return (
25
+ <Box
26
+ aria-label={ ariaLabel }
27
+ component="button"
28
+ onClick={ onClick }
29
+ type="button"
30
+ sx={ {
31
+ alignItems: 'center',
32
+ background: 'none',
33
+ border: 'none',
34
+ cursor: 'pointer',
35
+ display: 'flex',
36
+ flex: 1,
37
+ flexDirection: 'column',
38
+ gap: 0.5,
39
+ padding: 0,
40
+ } }
41
+ >
42
+ <Chip
43
+ color={ color }
44
+ label={ count }
45
+ size="small"
46
+ variant="standard"
47
+ sx={ ( theme: Theme ) => ( {
48
+ border: `${ COUNT_SUMMARY_BORDER_WIDTH }px solid ${ chipBorderColor( theme, color ) }`,
49
+ borderRadius: '50%',
50
+ height: COUNT_SUMMARY_CIRCLE_SIZE,
51
+ maxWidth: COUNT_SUMMARY_CIRCLE_SIZE,
52
+ width: COUNT_SUMMARY_CIRCLE_SIZE,
53
+ '& .MuiChip-label': {
54
+ fontSize: theme.typography.body2.fontSize,
55
+ fontWeight: 700,
56
+ lineHeight: 1,
57
+ padding: 0,
58
+ },
59
+ } ) }
60
+ />
61
+ <Typography color="text.secondary" sx={ { textAlign: 'center' } } variant="caption">
62
+ { label }
63
+ </Typography>
64
+ </Box>
65
+ );
66
+ }
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { isAngieAvailable, sendPromptToAngie } from '@elementor/editor-mcp';
3
+ import { AngieIcon } from '@elementor/icons';
4
+ import { IconButton, Tooltip } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { ANGIE_FIX_ENTRY_POINT, CREATE_WIDGET_EVENT } from '../constants';
8
+
9
+ type Props = {
10
+ prompt: string;
11
+ };
12
+
13
+ export default function FixViolationWithAngie( { prompt }: Props ) {
14
+ const label = __( 'Fix with Angie', 'elementor' );
15
+ const href = `#angie-prompt=${ encodeURIComponent( prompt ) }`;
16
+
17
+ const handleClick = ( event: React.MouseEvent< HTMLAnchorElement > ) => {
18
+ event.stopPropagation();
19
+ event.preventDefault();
20
+
21
+ if ( isAngieAvailable() ) {
22
+ sendPromptToAngie( prompt );
23
+ return;
24
+ }
25
+
26
+ window.dispatchEvent(
27
+ new CustomEvent( CREATE_WIDGET_EVENT, {
28
+ detail: {
29
+ entry_point: ANGIE_FIX_ENTRY_POINT,
30
+ prompt,
31
+ },
32
+ } )
33
+ );
34
+ };
35
+
36
+ return (
37
+ <Tooltip title={ label }>
38
+ <IconButton
39
+ className="violation-hover-icon"
40
+ component="a"
41
+ href={ href }
42
+ size="small"
43
+ aria-label={ label }
44
+ onClick={ handleClick }
45
+ >
46
+ <AngieIcon fontSize="tiny" />
47
+ </IconButton>
48
+ </Tooltip>
49
+ );
50
+ }
@@ -0,0 +1,62 @@
1
+ import * as React from 'react';
2
+ import { ChevronRightIcon } from '@elementor/icons';
3
+ import { Box, Rotate, Typography, useTheme } from '@elementor/ui';
4
+
5
+ import { type AuditCategory } from '../types';
6
+ import { onKeyboardClick } from '../utils/keyboard-click';
7
+ import { ALL_SEVERITIES, type SeverityCounts } from '../utils/severity-counts';
8
+ import { CATEGORY_ICONS } from './category-icons';
9
+ import SeverityIcon from './severity-icons';
10
+
11
+ type Props = {
12
+ category: AuditCategory;
13
+ label: string;
14
+ counts: SeverityCounts;
15
+ onClick: () => void;
16
+ };
17
+
18
+ export default function IssuesCategoryRow( { category, label, counts, onClick }: Props ) {
19
+ const isRtl = 'rtl' === useTheme().direction;
20
+ const Icon = CATEGORY_ICONS[ category ];
21
+
22
+ return (
23
+ <Box
24
+ role="button"
25
+ tabIndex={ 0 }
26
+ onClick={ onClick }
27
+ onKeyDown={ onKeyboardClick( onClick ) }
28
+ sx={ {
29
+ display: 'flex',
30
+ alignItems: 'center',
31
+ gap: 1,
32
+ px: 2,
33
+ py: 1.5,
34
+ cursor: 'pointer',
35
+ borderRadius: 1,
36
+ border: 1,
37
+ borderColor: 'divider',
38
+ '&:hover': { bgcolor: 'action.hover' },
39
+ outline: 'none',
40
+ '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main' },
41
+ } }
42
+ >
43
+ <Icon fontSize="small" color="action" />
44
+ <Typography variant="body2" fontWeight="bold" sx={ { flex: 1 } }>
45
+ { label }
46
+ </Typography>
47
+ <Box sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
48
+ { ALL_SEVERITIES.filter( ( s ) => counts[ s ] > 0 ).map( ( severity ) => (
49
+ <Box key={ severity } sx={ { display: 'flex', alignItems: 'center', gap: 0.25 } }>
50
+ <SeverityIcon severity={ severity } />
51
+ <Typography variant="caption" color="text.primary" fontWeight="bold">
52
+ { counts[ severity ] }
53
+ </Typography>
54
+ </Box>
55
+ ) ) }
56
+ </Box>
57
+ <Rotate in={ isRtl }>
58
+ <ChevronRightIcon fontSize="small" color="action" />
59
+ </Rotate>
60
+ </Box>
61
+ );
62
+ }
@@ -0,0 +1,65 @@
1
+ import * as React from 'react';
2
+ import { Box } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { type PageAuditReport } from '../../types';
6
+ import {
7
+ auditStatusColor,
8
+ type AuditStatusGroup,
9
+ auditStatusLabel,
10
+ partitionAuditResults,
11
+ } from '../../utils/audit-status-summary';
12
+ import StatusSection from '../status-section';
13
+ import SubpageHeader from '../subpage-header';
14
+ import ViolationRow from '../violation-row';
15
+
16
+ type Props = {
17
+ initialExpandedStatus?: AuditStatusGroup;
18
+ onBack: () => void;
19
+ report: PageAuditReport;
20
+ };
21
+
22
+ export default function AllAuditsPage( { initialExpandedStatus, onBack, report }: Props ) {
23
+ const { failed, passed, skipped, totalViolations } = partitionAuditResults( report );
24
+ const expandFail = ! initialExpandedStatus || initialExpandedStatus === 'fail';
25
+ const expandPass = initialExpandedStatus === 'pass';
26
+ const expandSkipped = initialExpandedStatus === 'skipped';
27
+
28
+ return (
29
+ <Box key={ initialExpandedStatus ?? 'default' }>
30
+ <SubpageHeader title={ __( 'All audits', 'elementor' ) } onBack={ onBack } />
31
+ <Box sx={ { p: 1 } }>
32
+ <StatusSection
33
+ label={ auditStatusLabel( 'fail' ) }
34
+ count={ totalViolations }
35
+ color={ auditStatusColor( 'fail' ) }
36
+ defaultExpanded={ expandFail }
37
+ >
38
+ { failed.map( ( r ) => (
39
+ <ViolationRow key={ r.audit.id } audit={ r.audit } violations={ r.result.violations } />
40
+ ) ) }
41
+ </StatusSection>
42
+ <StatusSection
43
+ label={ auditStatusLabel( 'pass' ) }
44
+ count={ passed.length }
45
+ color={ auditStatusColor( 'pass' ) }
46
+ defaultExpanded={ expandPass }
47
+ >
48
+ { passed.map( ( r ) => (
49
+ <ViolationRow key={ r.audit.id } audit={ r.audit } />
50
+ ) ) }
51
+ </StatusSection>
52
+ <StatusSection
53
+ label={ auditStatusLabel( 'skipped' ) }
54
+ count={ skipped.length }
55
+ color={ auditStatusColor( 'skipped' ) }
56
+ defaultExpanded={ expandSkipped }
57
+ >
58
+ { skipped.map( ( r ) => (
59
+ <ViolationRow key={ r.audit.id } audit={ r.audit } skipReason={ r.result.reason } />
60
+ ) ) }
61
+ </StatusSection>
62
+ </Box>
63
+ </Box>
64
+ );
65
+ }
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { Box } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { CATEGORY_LABELS } from '../../constants';
6
+ import { type AuditCategory, type PageAuditReport } from '../../types';
7
+ import { partitionAuditResults } from '../../utils/audit-status-summary';
8
+ import { CATEGORY_ICONS } from '../category-icons';
9
+ import StatusSection from '../status-section';
10
+ import SubpageHeader from '../subpage-header';
11
+ import ViolationRow from '../violation-row';
12
+
13
+ type Props = {
14
+ category: AuditCategory;
15
+ report: PageAuditReport;
16
+ onBack: () => void;
17
+ };
18
+
19
+ export default function CategoryPage( { category, report, onBack }: Props ) {
20
+ const Icon = CATEGORY_ICONS[ category ];
21
+ const { failed, passed, totalViolations } = partitionAuditResults( report, { category } );
22
+
23
+ return (
24
+ <>
25
+ <SubpageHeader
26
+ title={ CATEGORY_LABELS[ category ] }
27
+ onBack={ onBack }
28
+ backLabel={ __( 'Back to all issues', 'elementor' ) }
29
+ icon={ <Icon fontSize="small" color="action" /> }
30
+ />
31
+ <Box sx={ { p: 1 } }>
32
+ <StatusSection
33
+ label={ __( 'Failed audits', 'elementor' ) }
34
+ count={ totalViolations }
35
+ color="error"
36
+ defaultExpanded
37
+ >
38
+ { failed.map( ( r ) => (
39
+ <ViolationRow key={ r.audit.id } audit={ r.audit } violations={ r.result.violations } />
40
+ ) ) }
41
+ </StatusSection>
42
+ <StatusSection label={ __( 'Passed audits', 'elementor' ) } count={ passed.length } color="success">
43
+ { passed.map( ( r ) => (
44
+ <ViolationRow key={ r.audit.id } audit={ r.audit } />
45
+ ) ) }
46
+ </StatusSection>
47
+ </Box>
48
+ </>
49
+ );
50
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+ import { Box, Button, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ type Props = {
6
+ message: string;
7
+ onRetry: () => void;
8
+ };
9
+
10
+ export default function ErrorPage( { message, onRetry }: Props ) {
11
+ return (
12
+ <Box sx={ { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1.5, p: 4 } }>
13
+ <Typography variant="body2" color="error" textAlign="center">
14
+ { message }
15
+ </Typography>
16
+ <Button variant="contained" size="small" onClick={ onRetry }>
17
+ { __( 'Try again', 'elementor' ) }
18
+ </Button>
19
+ </Box>
20
+ );
21
+ }
@@ -0,0 +1,48 @@
1
+ import * as React from 'react';
2
+ import { Box, Link, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { ALL_CATEGORIES, CATEGORY_LABELS } from '../../constants';
6
+ import { type AuditCategory, type PageAuditReport } from '../../types';
7
+ import { getPopulatedCategories } from '../../utils/audit-status-summary';
8
+ import { countSeverities } from '../../utils/severity-counts';
9
+ import IssuesCategoryRow from '../issues-category-row';
10
+ import Promotions from '../promotions';
11
+
12
+ type Props = {
13
+ report: PageAuditReport;
14
+ onCategoryClick: ( category: AuditCategory ) => void;
15
+ onAllAuditsClick: () => void;
16
+ };
17
+
18
+ export default function IssuesPage( { report, onCategoryClick, onAllAuditsClick }: Props ) {
19
+ const populatedCategories = getPopulatedCategories( report.categories, ALL_CATEGORIES );
20
+
21
+ return (
22
+ <Box sx={ { display: 'flex', flexDirection: 'column', gap: 4, p: 2 } }>
23
+ <Link
24
+ component="button"
25
+ underline="none"
26
+ color="inherit"
27
+ onClick={ onAllAuditsClick }
28
+ sx={ { textAlign: 'start' } }
29
+ >
30
+ <Typography variant="subtitle1" component="h2">
31
+ { __( 'All issues', 'elementor' ) }
32
+ </Typography>
33
+ </Link>
34
+ <Box sx={ { display: 'flex', flexDirection: 'column', gap: 1 } }>
35
+ { populatedCategories.map( ( category ) => (
36
+ <IssuesCategoryRow
37
+ key={ category }
38
+ category={ category }
39
+ label={ CATEGORY_LABELS[ category ] }
40
+ counts={ countSeverities( report, category ) }
41
+ onClick={ () => onCategoryClick( category ) }
42
+ />
43
+ ) ) }
44
+ </Box>
45
+ <Promotions report={ report } />
46
+ </Box>
47
+ );
48
+ }
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import { Box, CircularProgress, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ export default function LoadingPage() {
6
+ return (
7
+ <Box sx={ { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1.5, p: 4 } }>
8
+ <CircularProgress size={ 32 } />
9
+ <Typography variant="body2">{ __( 'Audit in progress.', 'elementor' ) }</Typography>
10
+ </Box>
11
+ );
12
+ }
@@ -0,0 +1,140 @@
1
+ import * as React from 'react';
2
+ import { Box, Chip, Divider, Typography } from '@elementor/ui';
3
+ import { __, sprintf } from '@wordpress/i18n';
4
+
5
+ import { ALL_CATEGORIES, CATEGORY_LABELS } from '../../constants';
6
+ import { type AuditCategory, type PageAuditReport } from '../../types';
7
+ import {
8
+ auditStatusColor,
9
+ auditStatusDisplayCounts,
10
+ type AuditStatusGroup,
11
+ auditStatusLabel,
12
+ getPopulatedCategories,
13
+ } from '../../utils/audit-status-summary';
14
+ import { getScoreTier } from '../../utils/score-thresholds';
15
+ import {
16
+ ALL_SEVERITIES,
17
+ countSeverities,
18
+ severityPluralLabel,
19
+ severityRemainingCountLabel,
20
+ } from '../../utils/severity-counts';
21
+ import CountSummaryCircle from '../count-summary-circle';
22
+ import ScoreBar from '../score-bar';
23
+ import ScoreCircle from '../score-circle';
24
+ import SeverityIcon from '../severity-icons';
25
+
26
+ type Props = {
27
+ onCategoryClick: ( category: AuditCategory ) => void;
28
+ onStatusClick: ( status: AuditStatusGroup ) => void;
29
+ report: PageAuditReport;
30
+ };
31
+
32
+ const STATUS_ARIA_LABELS: Record< AuditStatusGroup, ( count: number ) => string > = {
33
+ pass: ( count ) =>
34
+ sprintf(
35
+ /* translators: %d: number of passed audits. */
36
+ __( '%d passed audits, view all', 'elementor' ),
37
+ count
38
+ ),
39
+ fail: ( count ) =>
40
+ sprintf(
41
+ /* translators: %d: number of failed audit violations. */
42
+ __( '%d failed audits, view all', 'elementor' ),
43
+ count
44
+ ),
45
+ skipped: ( count ) =>
46
+ sprintf(
47
+ /* translators: %d: number of skipped audits. */
48
+ __( '%d skipped audits, view all', 'elementor' ),
49
+ count
50
+ ),
51
+ };
52
+
53
+ const STATUS_GROUPS = [ 'fail', 'pass', 'skipped' ] as const;
54
+
55
+ export default function OverviewPage( { onCategoryClick, onStatusClick, report }: Props ) {
56
+ const populatedCategories = getPopulatedCategories( report.categories, ALL_CATEGORIES );
57
+ const severityCounts = countSeverities( report );
58
+ const statusCounts = auditStatusDisplayCounts( report );
59
+ const overallScore = getScoreTier( report.overall );
60
+
61
+ return (
62
+ <Box sx={ { display: 'flex', flexDirection: 'column', gap: 4, p: 2 } }>
63
+ <Box sx={ { display: 'flex', alignItems: 'center', gap: 2 } }>
64
+ <ScoreCircle color={ overallScore.color } score={ report.overall } />
65
+ <Box sx={ { display: 'flex', flexDirection: 'column', gap: 0.5 } }>
66
+ <Chip
67
+ label={ overallScore.label }
68
+ color={ overallScore.color }
69
+ variant="standard"
70
+ size="small"
71
+ sx={ { fontWeight: 600, alignSelf: 'flex-start' } }
72
+ />
73
+ <Typography variant="body2" color="text.secondary">
74
+ { __( 'Overall score', 'elementor' ) }
75
+ </Typography>
76
+ </Box>
77
+ </Box>
78
+ <Box sx={ { display: 'flex', flexDirection: 'column', gap: 2 } }>
79
+ { populatedCategories.map( ( category ) => (
80
+ <ScoreBar
81
+ key={ category }
82
+ label={ CATEGORY_LABELS[ category ] }
83
+ score={ report.categories[ category ].score }
84
+ onClick={ () => onCategoryClick( category ) }
85
+ />
86
+ ) ) }
87
+ </Box>
88
+ <Divider />
89
+ <Typography variant="subtitle1" fontWeight="bold">
90
+ { __( 'Audit statuses', 'elementor' ) }
91
+ </Typography>
92
+ <Box sx={ { display: 'flex', justifyContent: 'space-around', gap: 2 } }>
93
+ { STATUS_GROUPS.map( ( status ) => (
94
+ <CountSummaryCircle
95
+ key={ status }
96
+ ariaLabel={ STATUS_ARIA_LABELS[ status ]( statusCounts[ status ] ) }
97
+ color={ auditStatusColor( status ) }
98
+ count={ statusCounts[ status ] }
99
+ label={ auditStatusLabel( status ) }
100
+ onClick={ () => onStatusClick( status ) }
101
+ />
102
+ ) ) }
103
+ </Box>
104
+ <Divider />
105
+ <Typography variant="subtitle1" fontWeight="bold">
106
+ { __( 'Remaining issues', 'elementor' ) }
107
+ </Typography>
108
+ <Box
109
+ component="ul"
110
+ sx={ {
111
+ display: 'flex',
112
+ flexDirection: 'column',
113
+ gap: 1,
114
+ listStyle: 'none',
115
+ m: 0,
116
+ p: 0,
117
+ } }
118
+ >
119
+ { ALL_SEVERITIES.map( ( severity ) => {
120
+ const count = severityCounts[ severity ];
121
+
122
+ return (
123
+ <Box
124
+ aria-label={ severityRemainingCountLabel( severity, count ) }
125
+ component="li"
126
+ key={ severity }
127
+ sx={ { alignItems: 'center', display: 'flex', gap: 0.5 } }
128
+ >
129
+ <SeverityIcon severity={ severity } />
130
+ <Typography variant="body2" fontWeight="bold">
131
+ { count }
132
+ </Typography>
133
+ <Typography variant="body2">{ severityPluralLabel( severity ) }</Typography>
134
+ </Box>
135
+ );
136
+ } ) }
137
+ </Box>
138
+ </Box>
139
+ );
140
+ }
@@ -0,0 +1,48 @@
1
+ import * as React from 'react';
2
+ import { Box, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ const INFORMATION_IMAGE_PATH = 'images/information.svg';
6
+
7
+ type ElementorCommonWindow = Window & {
8
+ elementorCommon?: {
9
+ config?: {
10
+ urls?: {
11
+ assets?: string;
12
+ };
13
+ };
14
+ };
15
+ };
16
+
17
+ export default function WelcomePage() {
18
+ const assetsUrl = ( window as ElementorCommonWindow ).elementorCommon?.config?.urls?.assets ?? '';
19
+
20
+ return (
21
+ <Box
22
+ sx={ {
23
+ display: 'flex',
24
+ flexDirection: 'column',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ gap: 1,
28
+ p: 3,
29
+ } }
30
+ >
31
+ <img
32
+ className="elementor-nerd-box-icon"
33
+ src={ `${ assetsUrl }${ INFORMATION_IMAGE_PATH }` }
34
+ loading="lazy"
35
+ alt={ __( 'Elementor', 'elementor' ) }
36
+ />
37
+ <Typography variant="subtitle1" component="h3" sx={ { marginBlockStart: 2 } }>
38
+ { __( 'Audit your page!', 'elementor' ) }
39
+ </Typography>
40
+ <Typography variant="caption" textAlign="center">
41
+ { __(
42
+ 'Check SEO, accessibility, performance, and best practices. Click "Run page audit" to begin.',
43
+ 'elementor'
44
+ ) }
45
+ </Typography>
46
+ </Box>
47
+ );
48
+ }
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ import { Box, Button, Typography } from '@elementor/ui';
3
+
4
+ import { type PromotionIcon } from '../register-promotions';
5
+
6
+ type Props = {
7
+ ctaDisabled: boolean;
8
+ ctaLabel: string;
9
+ icon: PromotionIcon;
10
+ onCtaClick: () => void;
11
+ subtitle: string;
12
+ title: string;
13
+ };
14
+
15
+ export default function PromotionCard( { ctaDisabled, ctaLabel, icon: Icon, onCtaClick, subtitle, title }: Props ) {
16
+ return (
17
+ <Box
18
+ sx={ {
19
+ alignItems: 'center',
20
+ border: 1,
21
+ borderColor: 'divider',
22
+ borderRadius: 1,
23
+ display: 'flex',
24
+ gap: 1,
25
+ px: 2,
26
+ py: 1.5,
27
+ } }
28
+ >
29
+ <Icon fontSize="small" color="action" />
30
+ <Box sx={ { display: 'flex', flex: 1, flexDirection: 'column', gap: 0.25, minWidth: 0 } }>
31
+ <Typography variant="body2" fontWeight="bold">
32
+ { title }
33
+ </Typography>
34
+ <Typography variant="caption" color="text.secondary">
35
+ { subtitle }
36
+ </Typography>
37
+ </Box>
38
+ <Button variant="outlined" color="secondary" size="small" disabled={ ctaDisabled } onClick={ onCtaClick }>
39
+ { ctaLabel }
40
+ </Button>
41
+ </Box>
42
+ );
43
+ }
@@ -0,0 +1,69 @@
1
+ import * as React from 'react';
2
+ import { Box, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { PROMOTIONS } from '../register-promotions';
6
+ import { type PageAuditReport } from '../types';
7
+ import { findAuditRun } from '../utils/find-audit-run';
8
+ import PromotionCard from './promotion-card';
9
+
10
+ type Props = {
11
+ report: PageAuditReport;
12
+ };
13
+
14
+ export default function Promotions( { report }: Props ) {
15
+ const cards = PROMOTIONS.flatMap( ( config ) => {
16
+ const run = findAuditRun( report, config.auditId );
17
+
18
+ if ( ! run || run.result.status !== 'fail' ) {
19
+ return [];
20
+ }
21
+
22
+ const subtitle = config.formatSubtitle( run );
23
+
24
+ if ( ! subtitle ) {
25
+ return [];
26
+ }
27
+
28
+ const ctaUrl = config.getCtaUrl( run );
29
+
30
+ return [
31
+ {
32
+ config,
33
+ ctaUrl,
34
+ key: config.auditId,
35
+ run,
36
+ subtitle,
37
+ },
38
+ ];
39
+ } );
40
+
41
+ if ( cards.length === 0 ) {
42
+ return null;
43
+ }
44
+
45
+ return (
46
+ <>
47
+ <Typography variant="subtitle1" fontWeight="bold">
48
+ { __( 'Quick wins', 'elementor' ) }
49
+ </Typography>
50
+ <Box sx={ { display: 'flex', flexDirection: 'column', gap: 1 } }>
51
+ { cards.map( ( { config, ctaUrl, key, run, subtitle } ) => (
52
+ <PromotionCard
53
+ key={ key }
54
+ icon={ config.icon }
55
+ title={ run.audit.title }
56
+ subtitle={ subtitle }
57
+ ctaLabel={ config.ctaLabel }
58
+ ctaDisabled={ ! ctaUrl }
59
+ onCtaClick={ () => {
60
+ if ( ctaUrl ) {
61
+ window.open( ctaUrl, '_blank' );
62
+ }
63
+ } }
64
+ />
65
+ ) ) }
66
+ </Box>
67
+ </>
68
+ );
69
+ }