@elementor/editor-audits 4.3.4 → 4.3.5

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 (52) hide show
  1. package/dist/index.d.mts +2 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +789 -1098
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +731 -1040
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +17 -17
  8. package/src/audits/accessibility-statement.ts +2 -2
  9. package/src/audits/cookie-policy.ts +1 -0
  10. package/src/audits/images-alt-text.ts +6 -4
  11. package/src/audits/images-inefficient-format.ts +8 -9
  12. package/src/audits/images-too-large.ts +8 -9
  13. package/src/audits/nested-boxed-containers.ts +5 -0
  14. package/src/components/audit-feedback.tsx +4 -1
  15. package/src/components/audit-panel.tsx +13 -13
  16. package/src/components/audit-toggle-icon.tsx +22 -0
  17. package/src/components/fix-violation-with-angie.tsx +18 -7
  18. package/src/components/pages/category-page.tsx +27 -3
  19. package/src/components/pages/overview-page.tsx +24 -29
  20. package/src/components/report-shell.tsx +10 -91
  21. package/src/components/rescan-button.tsx +38 -0
  22. package/src/components/severity-icons.tsx +5 -1
  23. package/src/components/severity-summary-cards.tsx +52 -0
  24. package/src/components/violation-cta-button.tsx +10 -2
  25. package/src/components/violation-icons.tsx +2 -2
  26. package/src/components/violation-row.tsx +118 -69
  27. package/src/hooks/use-audit-report.ts +5 -4
  28. package/src/hooks/use-audit-toggle-props.ts +2 -2
  29. package/src/hooks/use-single-expanded-card.ts +11 -0
  30. package/src/init.ts +2 -0
  31. package/src/register-audits.ts +0 -2
  32. package/src/register-promotions.ts +0 -23
  33. package/src/runner.ts +6 -2
  34. package/src/store/selectors.ts +4 -0
  35. package/src/store/slice.ts +10 -1
  36. package/src/sync-report-staleness.ts +14 -0
  37. package/src/types.ts +2 -0
  38. package/src/utils/all-image-sources.ts +17 -0
  39. package/src/utils/alt-text-image-sources.ts +24 -0
  40. package/src/utils/atomic-image-sources.ts +8 -4
  41. package/src/utils/audit-status-summary.ts +0 -57
  42. package/src/utils/image-like-sources.ts +0 -12
  43. package/src/utils/page-attachments.ts +3 -11
  44. package/src/audits/too-many-widgets.ts +0 -53
  45. package/src/components/audit-statuses-section.tsx +0 -65
  46. package/src/components/count-summary-circle.tsx +0 -66
  47. package/src/components/pages/all-audits-page.tsx +0 -65
  48. package/src/components/pages/issues-page.tsx +0 -65
  49. package/src/components/score-bar.tsx +0 -56
  50. package/src/components/score-circle.tsx +0 -60
  51. package/src/components/severity-filter-chips.tsx +0 -57
  52. package/src/utils/score-thresholds.ts +0 -23
@@ -0,0 +1,38 @@
1
+ import * as React from 'react';
2
+ import { useFloatingPanelZIndex } from '@elementor/editor-floating-panels';
3
+ import { ReloadIcon } from '@elementor/icons';
4
+ import { Badge, Button, Tooltip } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { AUDIT_PANEL_ID } from '../constants';
8
+
9
+ type Props = {
10
+ hasScanned: boolean;
11
+ isStale: boolean;
12
+ disabled: boolean;
13
+ onClick: () => void;
14
+ };
15
+
16
+ export default function RescanButton( { hasScanned, isStale, disabled, onClick }: Props ) {
17
+ const panelZIndex = useFloatingPanelZIndex( AUDIT_PANEL_ID );
18
+
19
+ return (
20
+ <Tooltip
21
+ title={ __( 'Page content changed - rescan advised', 'elementor' ) }
22
+ disableHoverListener={ ! isStale }
23
+ PopperProps={ { sx: { zIndex: panelZIndex } } }
24
+ >
25
+ <Badge variant="dot" color="warning" invisible={ ! isStale } overlap="rectangular">
26
+ <Button
27
+ variant="contained"
28
+ size="small"
29
+ startIcon={ hasScanned ? <ReloadIcon fontSize="small" /> : undefined }
30
+ onClick={ onClick }
31
+ disabled={ disabled }
32
+ >
33
+ { hasScanned ? __( 'Rescan', 'elementor' ) : __( 'Run page audit', 'elementor' ) }
34
+ </Button>
35
+ </Badge>
36
+ </Tooltip>
37
+ );
38
+ }
@@ -20,7 +20,11 @@ type Props = {
20
20
  severity: AuditSeverity;
21
21
  };
22
22
 
23
+ const INFO_ICON_VERTICAL_NUDGE = 'translateY(-2px)';
24
+
23
25
  export default function SeverityIcon( { severity }: Props ) {
24
26
  const { Icon, color } = SEVERITY_CONFIG[ severity ];
25
- return <Icon fontSize="small" color={ color } />;
27
+ const sx = 'info' === severity ? { transform: INFO_ICON_VERTICAL_NUDGE } : undefined;
28
+
29
+ return <Icon fontSize="small" color={ color } sx={ sx } />;
26
30
  }
@@ -0,0 +1,52 @@
1
+ import * as React from 'react';
2
+ import { Box, Typography } from '@elementor/ui';
3
+
4
+ import { type AuditSeverity, type PageAuditReport } from '../types';
5
+ import { ALL_SEVERITIES, countSeverities, severityPluralLabel } from '../utils/severity-counts';
6
+ import SeverityIcon from './severity-icons';
7
+
8
+ type Props = {
9
+ report: PageAuditReport;
10
+ };
11
+
12
+ export default function SeveritySummaryCards( { report }: Props ) {
13
+ const counts = countSeverities( report );
14
+
15
+ return (
16
+ <Box display="flex" gap={ 1.5 }>
17
+ { ALL_SEVERITIES.map( ( severity ) => (
18
+ <SeverityCard key={ severity } severity={ severity } count={ counts[ severity ] } />
19
+ ) ) }
20
+ </Box>
21
+ );
22
+ }
23
+
24
+ type SeverityCardProps = {
25
+ severity: AuditSeverity;
26
+ count: number;
27
+ };
28
+
29
+ function SeverityCard( { severity, count }: SeverityCardProps ) {
30
+ return (
31
+ <Box
32
+ display="flex"
33
+ flex={ 1 }
34
+ flexDirection="column"
35
+ gap={ 1 }
36
+ bgcolor="action.hover"
37
+ borderRadius={ 1.5 }
38
+ px={ 1.5 }
39
+ py={ 1 }
40
+ >
41
+ <SeverityIcon severity={ severity } />
42
+ <Box display="flex" flexDirection="column">
43
+ <Typography variant="h5" fontWeight={ 700 } component="span">
44
+ { count }
45
+ </Typography>
46
+ <Typography variant="caption" color="text.secondary">
47
+ { severityPluralLabel( severity ) }
48
+ </Typography>
49
+ </Box>
50
+ </Box>
51
+ );
52
+ }
@@ -1,13 +1,15 @@
1
1
  import * as React from 'react';
2
+ import { WandIcon } from '@elementor/icons';
2
3
  import { Button } from '@elementor/ui';
3
4
 
4
5
  type Props = {
5
6
  ctaLabel: string;
6
7
  externalUrl: string;
7
8
  variant?: 'outlined' | 'text';
9
+ withIcon?: boolean;
8
10
  };
9
11
 
10
- export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined' }: Props ) {
12
+ export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined', withIcon = false }: Props ) {
11
13
  const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
12
14
  event.stopPropagation();
13
15
  event.preventDefault();
@@ -16,7 +18,13 @@ export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = '
16
18
  };
17
19
 
18
20
  return (
19
- <Button variant={ variant } color="secondary" size="small" onClick={ handleClick }>
21
+ <Button
22
+ variant={ variant }
23
+ color="secondary"
24
+ size="small"
25
+ startIcon={ withIcon ? <WandIcon fontSize="tiny" aria-hidden={ true } /> : undefined }
26
+ onClick={ handleClick }
27
+ >
20
28
  { ctaLabel }
21
29
  </Button>
22
30
  );
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { FileSettingsIcon, SettingsIcon, ShieldCheckIcon } from '@elementor/icons';
2
+ import { FileSettingsIcon, SettingsIcon } from '@elementor/icons';
3
3
  import { Box } from '@elementor/ui';
4
4
 
5
5
  import { type AuditViolation } from '../types';
@@ -29,5 +29,5 @@ export default function ViolationIcon( { violation, widgetIcon }: Props ) {
29
29
  return <SettingsIcon fontSize="inherit" aria-hidden={ true } />;
30
30
  }
31
31
 
32
- return <ShieldCheckIcon fontSize="inherit" aria-hidden={ true } />;
32
+ return <SettingsIcon fontSize="inherit" aria-hidden={ true } />;
33
33
  }
@@ -1,10 +1,11 @@
1
1
  import * as React from 'react';
2
- import { useState } from 'react';
3
2
  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';
3
+ import { useFloatingPanelZIndex } from '@elementor/editor-floating-panels';
4
+ import { CheckIcon, ChevronDownIcon, HelpIcon, InfoCircleIcon } from '@elementor/icons';
5
+ import { Alert, Box, Collapse, IconButton, Tooltip, Typography } from '@elementor/ui';
6
6
  import { __ } from '@wordpress/i18n';
7
7
 
8
+ import { AUDIT_PANEL_ID } from '../constants';
8
9
  import { focusViolation } from '../hooks/focus-violation';
9
10
  import { type AuditMeta, type AuditViolation } from '../types';
10
11
  import { buildAngiePrompt } from '../utils/build-angie-prompt';
@@ -17,6 +18,8 @@ import ViolationIcon from './violation-icons';
17
18
 
18
19
  type Props = {
19
20
  audit: AuditMeta;
21
+ expanded: boolean;
22
+ onToggleExpand: () => void;
20
23
  skipReason?: string;
21
24
  violations?: AuditViolation[];
22
25
  };
@@ -35,12 +38,12 @@ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violati
35
38
  if ( violations ) {
36
39
  return (
37
40
  <>
41
+ <SeverityIcon severity={ audit.severity } />
38
42
  { isScoredAudit( audit ) && (
39
43
  <Typography variant="caption" color="text.secondary" fontWeight="bold">
40
44
  { violations.length }
41
45
  </Typography>
42
46
  ) }
43
- <SeverityIcon severity={ audit.severity } />
44
47
  </>
45
48
  );
46
49
  }
@@ -48,10 +51,73 @@ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violati
48
51
  return <CheckIcon fontSize="small" color="success" />;
49
52
  }
50
53
 
51
- export default function ViolationRow( { audit, skipReason, violations }: Props ) {
52
- const [ expanded, setExpanded ] = useState( false );
54
+ function GuidanceAction( {
55
+ violation,
56
+ sharedAngiePrompt,
57
+ panelZIndex,
58
+ }: {
59
+ violation: AuditViolation;
60
+ sharedAngiePrompt?: string;
61
+ panelZIndex: number;
62
+ } ) {
63
+ const hasPrimaryCta = !! ( violation.ctaLabel && violation.externalUrl );
53
64
 
54
- const toggleExpanded = () => setExpanded( ( value ) => ! value );
65
+ if ( ! hasPrimaryCta && ! sharedAngiePrompt ) {
66
+ return null;
67
+ }
68
+
69
+ return (
70
+ <Box display="flex" alignItems="center" gap={ 1 } mt={ 1 }>
71
+ { hasPrimaryCta && (
72
+ <ViolationCtaButton
73
+ ctaLabel={ violation.ctaLabel as string }
74
+ externalUrl={ violation.externalUrl as string }
75
+ withIcon
76
+ />
77
+ ) }
78
+ { hasPrimaryCta && violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
79
+ <ViolationCtaButton
80
+ ctaLabel={ violation.secondaryCtaLabel }
81
+ externalUrl={ violation.secondaryCtaUrl }
82
+ variant="text"
83
+ />
84
+ ) }
85
+ { sharedAngiePrompt && (
86
+ <FixViolationWithAngie prompt={ sharedAngiePrompt } panelZIndex={ panelZIndex } variant="button" />
87
+ ) }
88
+ </Box>
89
+ );
90
+ }
91
+
92
+ function getSharedAngiePrompt( violations?: AuditViolation[] ): string | undefined {
93
+ if ( ! violations || violations.length < 2 ) {
94
+ return undefined;
95
+ }
96
+
97
+ const [ first, ...rest ] = violations;
98
+
99
+ if ( ! first.angieFix || ! first.angiePrompt ) {
100
+ return undefined;
101
+ }
102
+
103
+ const allShareTheSameFix = rest.every(
104
+ ( violation ) => violation.angieFix && violation.angiePrompt === first.angiePrompt
105
+ );
106
+
107
+ return allShareTheSameFix ? first.angiePrompt : undefined;
108
+ }
109
+
110
+ export default function ViolationRow( { audit, expanded, onToggleExpand, skipReason, violations }: Props ) {
111
+ const primaryViolation = violations?.[ 0 ];
112
+ const isGuidanceFocusable = !! primaryViolation;
113
+ const panelZIndex = useFloatingPanelZIndex( AUDIT_PANEL_ID );
114
+ const sharedAngiePrompt = getSharedAngiePrompt( violations );
115
+
116
+ const handleGuidanceClick = () => {
117
+ if ( isGuidanceFocusable && primaryViolation ) {
118
+ focusViolation( primaryViolation );
119
+ }
120
+ };
55
121
 
56
122
  return (
57
123
  <Box sx={ { borderBottom: 1, borderColor: 'divider', paddingBlock: 0.5 } }>
@@ -65,9 +131,9 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
65
131
  gap: 0.5,
66
132
  minWidth: 0,
67
133
  } }
68
- onClick={ toggleExpanded }
134
+ onClick={ onToggleExpand }
69
135
  >
70
- <Typography variant="body2" sx={ { flex: 1 } }>
136
+ <Typography variant="subtitle2" color="text.secondary" sx={ { flex: 1 } }>
71
137
  { audit.title }
72
138
  </Typography>
73
139
  { ! skipReason && <StatusIndicator audit={ audit } violations={ violations } /> }
@@ -76,7 +142,7 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
76
142
  <IconButton
77
143
  size="small"
78
144
  aria-label={ expanded ? __( 'Collapse', 'elementor' ) : __( 'Expand', 'elementor' ) }
79
- onClick={ toggleExpanded }
145
+ onClick={ onToggleExpand }
80
146
  >
81
147
  <ChevronDownIcon
82
148
  fontSize="small"
@@ -88,44 +154,40 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
88
154
  </IconButton>
89
155
  </Box>
90
156
  <Collapse in={ expanded }>
91
- <Box sx={ { display: 'flex', flexDirection: 'column', gap: 1, paddingBlock: 1 } }>
92
- <Alert
93
- severity="secondary"
94
- sx={ { p: 1 } }
95
- icon={ <AlertCircleIcon fontSize="small" color="secondary" aria-hidden={ true } /> }
96
- >
97
- <AlertTitle>
98
- <Typography variant="caption" component="p" color="text.primary" fontWeight="bold">
99
- { __( "What's the issue", 'elementor' ) }
157
+ <Box paddingBlock={ 1 }>
158
+ <Tooltip title={ audit.fixHint } placement="top">
159
+ <Alert
160
+ severity="secondary"
161
+ sx={ { p: 1, cursor: isGuidanceFocusable ? 'pointer' : undefined } }
162
+ icon={ <InfoCircleIcon fontSize="small" aria-hidden={ true } /> }
163
+ role={ isGuidanceFocusable ? 'button' : undefined }
164
+ tabIndex={ isGuidanceFocusable ? 0 : undefined }
165
+ onClick={ isGuidanceFocusable ? handleGuidanceClick : undefined }
166
+ onKeyDown={ isGuidanceFocusable ? onKeyboardClick( handleGuidanceClick ) : undefined }
167
+ >
168
+ <Typography variant="caption" component="p" color="text.secondary">
169
+ { audit.description }
100
170
  </Typography>
101
- </AlertTitle>
102
- <Typography variant="caption" component="p" color="text.secondary">
103
- { audit.description }
104
- </Typography>
105
- </Alert>
106
- <Alert
107
- severity="info"
108
- sx={ { p: 1 } }
109
- icon={ <BulbIcon fontSize="small" color="info" aria-hidden={ true } /> }
110
- >
111
- <AlertTitle>
112
- <Typography variant="caption" component="p" color="text.primary" fontWeight="bold">
113
- { __( 'How to resolve', 'elementor' ) }
114
- </Typography>
115
- </AlertTitle>
116
- <Typography variant="caption" component="p" color="text.secondary">
117
- { audit.fixHint }
118
- </Typography>
119
- </Alert>
171
+ { primaryViolation && (
172
+ <GuidanceAction
173
+ violation={ primaryViolation }
174
+ sharedAngiePrompt={ sharedAngiePrompt }
175
+ panelZIndex={ panelZIndex }
176
+ />
177
+ ) }
178
+ </Alert>
179
+ </Tooltip>
120
180
  </Box>
121
181
  { violations && violations.length > 0 && (
122
- <Box role="list" sx={ { paddingBlockEnd: 1, paddingInlineStart: 2 } }>
182
+ <Box role="list" sx={ { paddingBlockEnd: 1 } }>
123
183
  { violations.map( ( violation, idx ) => {
124
184
  const widgetIcon = violation.elementId ? getElementIcon( violation.elementId ) : null;
125
185
  const elementTitle = violation.elementId ? getElementTitle( violation.elementId ) : null;
126
- const rowLabel = elementTitle
186
+ const displayLabel = elementTitle ?? violation.label;
187
+ const angiePromptContext = elementTitle
127
188
  ? `${ elementTitle } - ${ violation.label }`
128
189
  : violation.label;
190
+ const showsOwnFixAction = !! violation.angieFix && ! sharedAngiePrompt;
129
191
 
130
192
  return (
131
193
  <Box
@@ -139,50 +201,37 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
139
201
  alignItems: 'center',
140
202
  gap: 1,
141
203
  paddingBlock: 0.5,
142
- paddingInline: 2,
204
+ paddingInline: 1,
143
205
  borderRadius: 1,
144
206
  cursor: 'pointer',
145
- '& .violation-hover-icon': { opacity: 0, transition: 'opacity .15s' },
146
207
  '&:hover': { bgcolor: 'action.hover' },
147
- '&:hover .violation-hover-icon, &:focus-visible .violation-hover-icon': {
148
- opacity: 1,
149
- },
208
+ '&:hover .violation-row-fix-action, &:focus-visible .violation-row-fix-action':
209
+ {
210
+ opacity: 1,
211
+ },
150
212
  } }
151
213
  >
152
214
  <ViolationIcon violation={ violation } widgetIcon={ widgetIcon } />
153
215
  <Box sx={ { flex: 1 } }>
154
- <Typography variant="caption">{ rowLabel }</Typography>
216
+ <Typography variant="caption">{ displayLabel }</Typography>
155
217
  { violation.detail && (
156
218
  <Typography variant="caption" color="text.secondary">
157
219
  { violation.detail }
158
220
  </Typography>
159
221
  ) }
160
222
  </Box>
161
- { violation.angieFix && (
162
- <FixViolationWithAngie
163
- prompt={ violation.angiePrompt ?? buildAngiePrompt( rowLabel ) }
164
- />
165
- ) }
166
- { violation.ctaLabel && violation.externalUrl ? (
167
- <Box sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
168
- <ViolationCtaButton
169
- ctaLabel={ violation.ctaLabel }
170
- externalUrl={ violation.externalUrl }
223
+ { showsOwnFixAction && (
224
+ <Box
225
+ className="violation-row-fix-action"
226
+ sx={ { opacity: 0, transition: 'opacity .15s' } }
227
+ >
228
+ <FixViolationWithAngie
229
+ prompt={
230
+ violation.angiePrompt ?? buildAngiePrompt( angiePromptContext )
231
+ }
232
+ panelZIndex={ panelZIndex }
171
233
  />
172
- { violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
173
- <ViolationCtaButton
174
- ctaLabel={ violation.secondaryCtaLabel }
175
- externalUrl={ violation.secondaryCtaUrl }
176
- variant="text"
177
- />
178
- ) }
179
234
  </Box>
180
- ) : (
181
- <EyeIcon
182
- className="violation-hover-icon"
183
- fontSize="tiny"
184
- aria-hidden={ true }
185
- />
186
235
  ) }
187
236
  </Box>
188
237
  );
@@ -1,9 +1,9 @@
1
1
  import { useEffect } from 'react';
2
- import { getCurrentDocumentId } from '@elementor/editor-elements';
2
+ import { getHostDocumentId } from '@elementor/editor-elements';
3
3
  import { __useDispatch as useDispatch, __useSelector as useSelector } from '@elementor/store';
4
4
 
5
5
  import { runPageAudit } from '../runner';
6
- import { type GlobalState, selectError, selectReport, selectStatus, slice } from '../store';
6
+ import { type GlobalState, selectError, selectIsStale, selectReport, selectStatus, slice } from '../store';
7
7
  import { getPersistedReport, persistReport } from '../utils/report-storage';
8
8
  import { SessionExpiredError } from '../utils/session-expiration';
9
9
 
@@ -11,8 +11,9 @@ export function useAuditReport() {
11
11
  const status = useSelector( ( state: GlobalState ) => selectStatus( state ) );
12
12
  const report = useSelector( ( state: GlobalState ) => selectReport( state ) );
13
13
  const error = useSelector( ( state: GlobalState ) => selectError( state ) );
14
+ const isStale = useSelector( ( state: GlobalState ) => selectIsStale( state ) );
14
15
  const dispatch = useDispatch();
15
- const documentId = getCurrentDocumentId() ?? 0;
16
+ const documentId = getHostDocumentId() ?? 0;
16
17
 
17
18
  useEffect( () => {
18
19
  if ( ! documentId || report?.documentId === documentId ) {
@@ -45,5 +46,5 @@ export function useAuditReport() {
45
46
  }
46
47
  };
47
48
 
48
- return { status, report, error, run };
49
+ return { status, report, error, isStale, run };
49
50
  }
@@ -1,7 +1,7 @@
1
1
  import { type ToggleActionProps } from '@elementor/editor-app-bar';
2
- import { ShieldCheckIcon } from '@elementor/icons';
3
2
  import { __ } from '@wordpress/i18n';
4
3
 
4
+ import AuditToggleIcon from '../components/audit-toggle-icon';
5
5
  import { auditPanel } from '../editor-panel';
6
6
 
7
7
  export function useAuditToggleProps(): ToggleActionProps {
@@ -10,7 +10,7 @@ export function useAuditToggleProps(): ToggleActionProps {
10
10
 
11
11
  return {
12
12
  title: __( 'Audit Page', 'elementor' ),
13
- icon: ShieldCheckIcon,
13
+ icon: AuditToggleIcon,
14
14
  selected: isOpen,
15
15
  onClick: () => toggle(),
16
16
  };
@@ -0,0 +1,11 @@
1
+ import { useState } from 'react';
2
+
3
+ export function useSingleExpandedCard( initialId: string | null = null ) {
4
+ const [ expandedId, setExpandedId ] = useState< string | null >( initialId );
5
+
6
+ const toggle = ( id: string ) => {
7
+ setExpandedId( ( current ) => ( current === id ? null : id ) );
8
+ };
9
+
10
+ return { expandedId, toggle };
11
+ }
package/src/init.ts CHANGED
@@ -5,10 +5,12 @@ import { registerAppBarAuditsToggle } from './editor-app-bar';
5
5
  import { auditPanel } from './editor-panel';
6
6
  import { registerAllAudits } from './register-audits';
7
7
  import { slice } from './store/slice';
8
+ import { syncReportStaleness } from './sync-report-staleness';
8
9
 
9
10
  export function init(): void {
10
11
  registerSlice( slice );
11
12
  registerAllAudits();
12
13
  registerFloatingPanel( auditPanel.panel );
13
14
  registerAppBarAuditsToggle();
15
+ syncReportStaleness();
14
16
  }
@@ -22,7 +22,6 @@ import * as robotsNoindex from './audits/robots-noindex';
22
22
  import * as scanForCookies from './audits/scan-for-cookies';
23
23
  import * as sectionsAndColumns from './audits/sections-and-columns';
24
24
  import * as siteIdentity from './audits/site-identity';
25
- import * as tooManyWidgets from './audits/too-many-widgets';
26
25
  import { registerAudit } from './registry';
27
26
  import { type Audit } from './types';
28
27
 
@@ -33,7 +32,6 @@ const AUDITS: Audit[] = [
33
32
 
34
33
  hiddenElements.audit,
35
34
  deprecatedWidgets.audit,
36
- tooManyWidgets.audit,
37
35
  sectionsAndColumns.audit,
38
36
  deepNesting.audit,
39
37
  nestedBoxedContainers.audit,
@@ -80,27 +80,4 @@ export const PROMOTIONS: PromotionConfig[] = [
80
80
  },
81
81
  getCtaUrl: firstFailExternalUrl,
82
82
  },
83
- {
84
- auditId: 'audits/images-inefficient-format',
85
- icon: ShieldHalfFilledIcon,
86
- ctaLabel: __( 'Optimize all', 'elementor' ),
87
- formatSubtitle: ( run ) => {
88
- if ( run.result.status !== 'fail' ) {
89
- return null;
90
- }
91
-
92
- const count = run.result.metadata?.inefficientImageFormatCount ?? 0;
93
-
94
- if ( count === 0 ) {
95
- return null;
96
- }
97
-
98
- return sprintf(
99
- /* translators: %d: number of images served in an inefficient format. */
100
- _n( '%d image', '%d images', count, 'elementor' ),
101
- count
102
- );
103
- },
104
- getCtaUrl: firstFailExternalUrl,
105
- },
106
83
  ];
package/src/runner.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getElements } from '@elementor/editor-elements';
1
+ import { getHostDocumentElements, getHostDocumentId } from '@elementor/editor-elements';
2
2
 
3
3
  import { fetchPageContext } from './api/page-context-client';
4
4
  import { getRegisteredAudits } from './registry';
@@ -18,7 +18,11 @@ import { readKitSnapshot } from './utils/read-kit-snapshot';
18
18
  import { buildSnapshotTree } from './utils/v1-snapshot';
19
19
 
20
20
  export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
21
- const tree = buildSnapshotTree( getElements() );
21
+ if ( documentId !== getHostDocumentId() ) {
22
+ throw new Error( 'runPageAudit: documentId must match the host document.' );
23
+ }
24
+
25
+ const tree = buildSnapshotTree( getHostDocumentElements() );
22
26
  const imageSizeRequests = extractImageSizeRequests( tree );
23
27
  const pageContext = await fetchPageContext( documentId, imageSizeRequests );
24
28
 
@@ -13,3 +13,7 @@ export function selectReport( state: GlobalState ) {
13
13
  export function selectError( state: GlobalState ) {
14
14
  return state.audits.error;
15
15
  }
16
+
17
+ export function selectIsStale( state: GlobalState ) {
18
+ return state.audits.isStale;
19
+ }
@@ -6,9 +6,10 @@ type SliceState = {
6
6
  status: 'idle' | 'loading' | 'error' | 'ready';
7
7
  report: PageAuditReport | null;
8
8
  error: string | null;
9
+ isStale: boolean;
9
10
  };
10
11
 
11
- const initialState: SliceState = { status: 'idle', report: null, error: null };
12
+ const initialState: SliceState = { status: 'idle', report: null, error: null, isStale: false };
12
13
 
13
14
  export const slice = __createSlice( {
14
15
  name: 'audits',
@@ -21,6 +22,7 @@ export const slice = __createSlice( {
21
22
  runSucceeded( state, action: PayloadAction< PageAuditReport > ) {
22
23
  state.status = 'ready';
23
24
  state.report = action.payload;
25
+ state.isStale = false;
24
26
  },
25
27
  runFailed( state, action: PayloadAction< string > ) {
26
28
  state.status = 'error';
@@ -33,11 +35,18 @@ export const slice = __createSlice( {
33
35
  state.status = 'ready';
34
36
  state.report = action.payload;
35
37
  state.error = null;
38
+ state.isStale = false;
36
39
  },
37
40
  reportCleared( state ) {
38
41
  state.status = 'idle';
39
42
  state.report = null;
40
43
  state.error = null;
44
+ state.isStale = false;
45
+ },
46
+ reportStale( state ) {
47
+ if ( state.report ) {
48
+ state.isStale = true;
49
+ }
41
50
  },
42
51
  },
43
52
  } );
@@ -0,0 +1,14 @@
1
+ import { __privateListenTo as listenTo, commandEndEvent, type CommandEvent } from '@elementor/editor-v1-adapters';
2
+ import { __dispatch as dispatch } from '@elementor/store';
3
+
4
+ import { slice } from './store/slice';
5
+
6
+ export function syncReportStaleness() {
7
+ listenTo( commandEndEvent( 'document/save/set-is-modified' ), ( e ) => {
8
+ const event = e as CommandEvent< { status: boolean } >;
9
+
10
+ if ( event.args?.status ) {
11
+ dispatch( slice.actions.reportStale() );
12
+ }
13
+ } );
14
+ }
package/src/types.ts CHANGED
@@ -72,6 +72,7 @@ export type PageContextResponse = {
72
72
  ally_accessibility_statement_created: boolean;
73
73
  ally_accessibility_statement_url: string;
74
74
  ally_widget_settings_url: string;
75
+ ally_scan_url: string;
75
76
  cookiez_plugin_active: boolean;
76
77
  cookiez_plugin_url: string;
77
78
  cookiez_plugin_installed: boolean;
@@ -80,6 +81,7 @@ export type PageContextResponse = {
80
81
  cookiez_consent_mode_settings_url: string;
81
82
  image_optimization_plugin_active: boolean;
82
83
  image_optimization_plugin_url: string;
84
+ image_optimization_settings_url: string;
83
85
  frontend_url: string | null;
84
86
  site_identity: {
85
87
  site_name_set: boolean;
@@ -0,0 +1,17 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { walkAtomicBackgroundImageSources } from './atomic-background-image-sources';
3
+ import { walkAtomicImageSources } from './atomic-image-sources';
4
+ import { walkBackgroundImageSources } from './background-image-sources';
5
+ import { type ImageLikeMedia, walkImageLikeSources } from './image-like-sources';
6
+
7
+ export type ImageSourceVisit = {
8
+ node: ElementSnapshotNode;
9
+ media: ImageLikeMedia;
10
+ };
11
+
12
+ export function walkAllImageSources( tree: ElementSnapshotNode[], visit: ( source: ImageSourceVisit ) => void ): void {
13
+ walkImageLikeSources( tree, visit );
14
+ walkBackgroundImageSources( tree, visit );
15
+ walkAtomicImageSources( tree, visit );
16
+ walkAtomicBackgroundImageSources( tree, visit );
17
+ }