@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
package/dist/index.mjs ADDED
@@ -0,0 +1,2801 @@
1
+ // src/init.ts
2
+ import { registerFloatingPanel } from "@elementor/editor-floating-panels";
3
+ import { __registerSlice as registerSlice } from "@elementor/store";
4
+
5
+ // src/editor-app-bar.tsx
6
+ import { utilitiesMenu } from "@elementor/editor-app-bar";
7
+
8
+ // src/hooks/use-audit-toggle-props.ts
9
+ import { ShieldCheckIcon as ShieldCheckIcon2 } from "@elementor/icons";
10
+ import { __ as __22 } from "@wordpress/i18n";
11
+
12
+ // src/editor-panel.tsx
13
+ import { createFloatingPanel } from "@elementor/editor-floating-panels";
14
+
15
+ // src/components/audit-panel.tsx
16
+ import * as React22 from "react";
17
+ import { getCurrentDocumentId as getCurrentDocumentId2 } from "@elementor/editor-elements";
18
+ import { FloatingPanelBody, FloatingPanelFooter, FloatingPanelHeader } from "@elementor/editor-floating-panels";
19
+ import { Box as Box19, Button as Button4, Typography as Typography15 } from "@elementor/ui";
20
+ import { __ as __21 } from "@wordpress/i18n";
21
+
22
+ // src/hooks/use-audit-report.ts
23
+ import { useEffect } from "react";
24
+ import { getCurrentDocumentId } from "@elementor/editor-elements";
25
+ import { __useDispatch as useDispatch, __useSelector as useSelector } from "@elementor/store";
26
+
27
+ // src/runner.ts
28
+ import { getElements } from "@elementor/editor-elements";
29
+
30
+ // src/api/page-context-client.ts
31
+ import { httpService } from "@elementor/http-client";
32
+
33
+ // src/utils/window-config.ts
34
+ function getWindowConfig() {
35
+ const config = window.elementorAudits;
36
+ if (!config) {
37
+ return { restNamespace: "elementor/v1", nonce: "" };
38
+ }
39
+ return config;
40
+ }
41
+
42
+ // src/api/page-context-client.ts
43
+ async function fetchPageContext(documentId, attachmentIds) {
44
+ const { restNamespace, nonce } = getWindowConfig();
45
+ const url = `${restNamespace}/audits/page-context`;
46
+ const response = await httpService().get(url, {
47
+ params: {
48
+ document_id: documentId,
49
+ attachment_ids: attachmentIds
50
+ },
51
+ headers: { "X-WP-Nonce": nonce }
52
+ });
53
+ return response.data;
54
+ }
55
+
56
+ // src/registry.ts
57
+ var registry = /* @__PURE__ */ new Map();
58
+ function registerAudit(audit21) {
59
+ registry.set(audit21.id, audit21);
60
+ }
61
+ function getRegisteredAudits() {
62
+ return Array.from(registry.values());
63
+ }
64
+
65
+ // src/constants.ts
66
+ import { __ } from "@wordpress/i18n";
67
+ var AUDIT_PANEL_ID = "audit-panel";
68
+ var REPORT_STORAGE_KEY_PREFIX = "elementor/audits/report";
69
+ var CREATE_WIDGET_EVENT = "elementor/editor/create-widget";
70
+ var ANGIE_FIX_ENTRY_POINT = "audit_violation";
71
+ var FEEDBACK_EXPERIMENT_NAME = "in_editor_feedback";
72
+ var FEEDBACK_ENTRY_POINT = "audit_panel";
73
+ var FEEDBACK_CLICKED_EVENT = "audit_feedback_clicked";
74
+ var FEEDBACK_SENT_EVENT = "audit_feedback_sent";
75
+ var FEEDBACK_CANCELLED_EVENT = "audit_feedback_cancelled";
76
+ var FEEDBACK_CLOSED_EVENT = "audit_feedback_closed";
77
+ var ALL_CATEGORIES = [
78
+ "best-practices",
79
+ "seo",
80
+ "accessibility",
81
+ "performance",
82
+ "compliance"
83
+ ];
84
+ var CATEGORY_LABELS = {
85
+ "best-practices": __("Best Practices", "elementor"),
86
+ seo: __("SEO", "elementor"),
87
+ accessibility: __("Accessibility", "elementor"),
88
+ performance: __("Performance", "elementor"),
89
+ compliance: __("Compliance", "elementor")
90
+ };
91
+
92
+ // src/utils/audit-status-summary.ts
93
+ import { __ as __2 } from "@wordpress/i18n";
94
+
95
+ // src/utils/sort-failed-audits.ts
96
+ var SEVERITY_SORTING_RANK = {
97
+ error: 0,
98
+ warning: 1,
99
+ info: 2
100
+ };
101
+ function sortFailedAuditResults(results) {
102
+ return [...results].sort((a, b) => {
103
+ const rankDiff = SEVERITY_SORTING_RANK[a.audit.severity] - SEVERITY_SORTING_RANK[b.audit.severity];
104
+ if (rankDiff !== 0) {
105
+ return rankDiff;
106
+ }
107
+ return a.audit.title.localeCompare(b.audit.title);
108
+ });
109
+ }
110
+
111
+ // src/utils/audit-status-summary.ts
112
+ function partitionAuditResults(report, options = {}) {
113
+ const { category, sortFailed = true } = options;
114
+ const failed = [];
115
+ const passed = [];
116
+ const skipped = [];
117
+ let totalViolations = 0;
118
+ for (const run of report.auditResults) {
119
+ if (category && !run.audit.categories.includes(category)) {
120
+ continue;
121
+ }
122
+ switch (run.result.status) {
123
+ case "fail":
124
+ failed.push({ ...run, result: run.result });
125
+ totalViolations += run.result.violations.length;
126
+ break;
127
+ case "pass":
128
+ passed.push({ ...run, result: run.result });
129
+ break;
130
+ case "skipped":
131
+ skipped.push({ ...run, result: run.result });
132
+ break;
133
+ }
134
+ }
135
+ return {
136
+ failed: sortFailed ? sortFailedAuditResults(failed) : failed,
137
+ passed,
138
+ skipped,
139
+ totalViolations
140
+ };
141
+ }
142
+ function auditStatusDisplayCounts(report) {
143
+ let pass = 0;
144
+ let skipped = 0;
145
+ let totalViolations = 0;
146
+ for (const { result } of report.auditResults) {
147
+ switch (result.status) {
148
+ case "fail":
149
+ totalViolations += result.violations.length;
150
+ break;
151
+ case "pass":
152
+ pass++;
153
+ break;
154
+ case "skipped":
155
+ skipped++;
156
+ break;
157
+ }
158
+ }
159
+ return {
160
+ fail: totalViolations,
161
+ pass,
162
+ skipped
163
+ };
164
+ }
165
+ function auditStatusColor(status) {
166
+ switch (status) {
167
+ case "fail":
168
+ return "error";
169
+ case "pass":
170
+ return "success";
171
+ case "skipped":
172
+ return "default";
173
+ }
174
+ }
175
+ function auditStatusLabel(status) {
176
+ switch (status) {
177
+ case "fail":
178
+ return __2("Failed audits", "elementor");
179
+ case "pass":
180
+ return __2("Passed audits", "elementor");
181
+ case "skipped":
182
+ return __2("Skipped audits", "elementor");
183
+ }
184
+ }
185
+ function getPopulatedCategories(categoryTotals, categories) {
186
+ return categories.filter((category) => categoryTotals[category].total > 0);
187
+ }
188
+
189
+ // src/utils/compute-report.ts
190
+ function computeReport(documentId, results) {
191
+ const accumulators = Object.fromEntries(
192
+ ALL_CATEGORIES.map((c) => [c, { totalWeight: 0, passedWeight: 0, total: 0, failed: 0 }])
193
+ );
194
+ for (const { audit: audit21, result } of results) {
195
+ if (result.status === "skipped") {
196
+ continue;
197
+ }
198
+ for (const category of audit21.categories) {
199
+ const acc = accumulators[category];
200
+ acc.total++;
201
+ acc.totalWeight += audit21.weight;
202
+ if (result.status === "pass") {
203
+ acc.passedWeight += audit21.weight;
204
+ } else {
205
+ acc.failed++;
206
+ }
207
+ }
208
+ }
209
+ const categories = Object.fromEntries(
210
+ ALL_CATEGORIES.map((c) => {
211
+ const acc = accumulators[c];
212
+ const score = acc.totalWeight === 0 ? 0 : Math.round(acc.passedWeight / acc.totalWeight * 100);
213
+ return [c, { score, failed: acc.failed, total: acc.total }];
214
+ })
215
+ );
216
+ const populated = getPopulatedCategories(categories, ALL_CATEGORIES);
217
+ const overall = populated.length === 0 ? 0 : Math.round(populated.reduce((sum, c) => sum + categories[c].score, 0) / populated.length);
218
+ return {
219
+ documentId,
220
+ runAt: Date.now(),
221
+ overall,
222
+ categories,
223
+ auditResults: results
224
+ };
225
+ }
226
+
227
+ // src/utils/walk.ts
228
+ function walkElements(tree, visit) {
229
+ const parents = [];
230
+ recurse(tree, parents, visit);
231
+ }
232
+ function recurse(nodes, parents, visit) {
233
+ for (const node of nodes) {
234
+ visit(node, parents);
235
+ parents.push(node);
236
+ recurse(node.elements, parents, visit);
237
+ parents.pop();
238
+ }
239
+ }
240
+
241
+ // src/utils/image-like-sources.ts
242
+ var IMAGE_LIKE_WIDGETS = /* @__PURE__ */ new Set(["image", "image-box", "image-carousel", "gallery", "image-gallery"]);
243
+ function hasPageImages(tree) {
244
+ let found = false;
245
+ walkImageLikeSources(tree, ({ media }) => {
246
+ if (media.id || media.url) {
247
+ found = true;
248
+ }
249
+ });
250
+ return found;
251
+ }
252
+ function walkImageLikeSources(tree, visit) {
253
+ walkElements(tree, (node) => {
254
+ if (node.elType !== "widget") {
255
+ return;
256
+ }
257
+ if (!IMAGE_LIKE_WIDGETS.has(node.widgetType ?? "")) {
258
+ return;
259
+ }
260
+ for (const media of collectMediaFromNode(node)) {
261
+ visit({ node, media });
262
+ }
263
+ });
264
+ }
265
+ function collectMediaFromNode(node) {
266
+ const sources = [];
267
+ const image = node.settings.image;
268
+ if (image?.id || image?.url) {
269
+ sources.push(image);
270
+ }
271
+ const gallery = node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery;
272
+ if (Array.isArray(gallery)) {
273
+ for (const item of gallery) {
274
+ if (item?.id || item?.url) {
275
+ sources.push(item);
276
+ }
277
+ }
278
+ }
279
+ return sources;
280
+ }
281
+
282
+ // src/utils/page-attachments.ts
283
+ function extractAttachmentIds(tree) {
284
+ const ids = /* @__PURE__ */ new Set();
285
+ walkImageLikeSources(tree, ({ media }) => {
286
+ if (media.id) {
287
+ ids.add(media.id);
288
+ }
289
+ });
290
+ return Array.from(ids).sort((a, b) => a - b);
291
+ }
292
+
293
+ // src/utils/read-kit-snapshot.ts
294
+ function hasNonEmptyGlobalValue(item) {
295
+ return item.value.length > 0;
296
+ }
297
+ async function readKitSnapshot(kitId) {
298
+ const [colorsFromApi, fontsFromApi] = await Promise.all([
299
+ readGlobalsFromApi("globals/colors", mapApiColor, hasNonEmptyGlobalValue),
300
+ readGlobalsFromApi("globals/typography", mapApiTypography, hasNonEmptyGlobalValue)
301
+ ]);
302
+ const documentSettings = colorsFromApi.length === 0 || fontsFromApi.length === 0 ? getKitDocumentSettings(kitId) : null;
303
+ const colors = colorsFromApi.length > 0 ? colorsFromApi : readColorsFromKitDocumentSettings(documentSettings);
304
+ const fonts = fontsFromApi.length > 0 ? fontsFromApi : readFontsFromKitDocumentSettings(documentSettings);
305
+ return { id: kitId, globals: { colors, fonts } };
306
+ }
307
+ async function readGlobalsFromApi(command, mapItem, hasValue) {
308
+ const $e = window.$e;
309
+ if (!$e?.data?.get) {
310
+ return [];
311
+ }
312
+ try {
313
+ const result = await $e.data.get(command);
314
+ const data = result?.data ?? {};
315
+ return Object.values(data).map((item) => mapItem(item)).filter(hasValue);
316
+ } catch {
317
+ return [];
318
+ }
319
+ }
320
+ function mapApiColor(item) {
321
+ return {
322
+ id: item.id,
323
+ value: item.value ?? "",
324
+ title: item.title ?? item.id
325
+ };
326
+ }
327
+ function mapApiTypography(item) {
328
+ return {
329
+ id: item.id,
330
+ value: item.value?.typography_font_family ?? "",
331
+ title: item.title ?? item.id
332
+ };
333
+ }
334
+ function readColorsFromKitDocumentSettings(settings) {
335
+ if (!settings) {
336
+ return [];
337
+ }
338
+ const rows = [...settings.system_colors ?? [], ...settings.custom_colors ?? []];
339
+ return rows.map((row) => ({
340
+ id: row._id,
341
+ value: row.color,
342
+ title: row.title ?? row._id
343
+ })).filter(hasNonEmptyGlobalValue);
344
+ }
345
+ function readFontsFromKitDocumentSettings(settings) {
346
+ if (!settings) {
347
+ return [];
348
+ }
349
+ const rows = [...settings.system_typography ?? [], ...settings.custom_typography ?? []];
350
+ return rows.map((row) => ({
351
+ id: row._id,
352
+ value: row.typography_font_family ?? "",
353
+ title: row.title ?? row._id
354
+ })).filter(hasNonEmptyGlobalValue);
355
+ }
356
+ function getKitDocumentSettings(kitId) {
357
+ const document = window.elementor?.documents?.get?.(kitId);
358
+ return document?.config?.settings?.settings ?? null;
359
+ }
360
+
361
+ // src/utils/v1-snapshot.ts
362
+ function buildSnapshotTree(elements) {
363
+ if (elements.length === 0) {
364
+ return [];
365
+ }
366
+ const root = elements[0];
367
+ const children = root.children ?? [];
368
+ return children.map(toSnapshot);
369
+ }
370
+ function toSnapshot(element) {
371
+ const model = element.model;
372
+ const settings = element.settings?.toJSON?.() ?? {};
373
+ return {
374
+ id: element.id,
375
+ elType: model.get("elType") ?? "",
376
+ widgetType: model.get("widgetType"),
377
+ settings,
378
+ elements: (element.children ?? []).map(toSnapshot)
379
+ };
380
+ }
381
+
382
+ // src/runner.ts
383
+ async function runPageAudit(documentId) {
384
+ const tree = buildSnapshotTree(getElements());
385
+ const attachmentIds = extractAttachmentIds(tree);
386
+ const pageContext = await fetchPageContext(documentId, attachmentIds);
387
+ const elements = { documentId, tree };
388
+ const kit = await readKitSnapshot(pageContext.kit_id);
389
+ const ctx = { documentId, elements, pageContext, kit };
390
+ const registered = getRegisteredAudits();
391
+ const auditResults = await Promise.all(
392
+ registered.map(async (audit21) => {
393
+ const { evaluate: _evaluate, ...meta } = audit21;
394
+ try {
395
+ const result = await audit21.evaluate(ctx);
396
+ return { audit: meta, result };
397
+ } catch (error) {
398
+ const reason = error instanceof Error ? error.message : "unknown-error";
399
+ return { audit: meta, result: { status: "skipped", reason } };
400
+ }
401
+ })
402
+ );
403
+ return computeReport(documentId, auditResults);
404
+ }
405
+
406
+ // src/store/slice.ts
407
+ import { __createSlice } from "@elementor/store";
408
+ var initialState = { status: "idle", report: null, error: null };
409
+ var slice = __createSlice({
410
+ name: "audits",
411
+ initialState,
412
+ reducers: {
413
+ runStarted(state) {
414
+ state.status = "loading";
415
+ state.error = null;
416
+ },
417
+ runSucceeded(state, action) {
418
+ state.status = "ready";
419
+ state.report = action.payload;
420
+ },
421
+ runFailed(state, action) {
422
+ state.status = "error";
423
+ state.error = action.payload;
424
+ },
425
+ reportRestored(state, action) {
426
+ state.status = "ready";
427
+ state.report = action.payload;
428
+ state.error = null;
429
+ },
430
+ reportCleared(state) {
431
+ state.status = "idle";
432
+ state.report = null;
433
+ state.error = null;
434
+ }
435
+ }
436
+ });
437
+
438
+ // src/store/selectors.ts
439
+ function selectStatus(state) {
440
+ return state.audits.status;
441
+ }
442
+ function selectReport(state) {
443
+ return state.audits.report;
444
+ }
445
+ function selectError(state) {
446
+ return state.audits.error;
447
+ }
448
+
449
+ // src/utils/report-storage.ts
450
+ import { getSessionStorageItem, setSessionStorageItem } from "@elementor/session";
451
+ function getStorageKey(documentId) {
452
+ return `${REPORT_STORAGE_KEY_PREFIX}/${documentId}`;
453
+ }
454
+ function getPersistedReport(documentId) {
455
+ return getSessionStorageItem(getStorageKey(documentId)) ?? null;
456
+ }
457
+ function persistReport(documentId, report) {
458
+ setSessionStorageItem(getStorageKey(documentId), report);
459
+ }
460
+
461
+ // src/hooks/use-audit-report.ts
462
+ function useAuditReport() {
463
+ const status = useSelector((state) => selectStatus(state));
464
+ const report = useSelector((state) => selectReport(state));
465
+ const error = useSelector((state) => selectError(state));
466
+ const dispatch = useDispatch();
467
+ const documentId = getCurrentDocumentId() ?? 0;
468
+ useEffect(() => {
469
+ if (!documentId || report?.documentId === documentId) {
470
+ return;
471
+ }
472
+ const persisted = getPersistedReport(documentId);
473
+ if (persisted) {
474
+ dispatch(slice.actions.reportRestored(persisted));
475
+ } else if (report) {
476
+ dispatch(slice.actions.reportCleared());
477
+ }
478
+ }, [documentId, report, dispatch]);
479
+ const run = async (documentIdToRun) => {
480
+ dispatch(slice.actions.runStarted());
481
+ try {
482
+ const nextReport = await runPageAudit(documentIdToRun);
483
+ dispatch(slice.actions.runSucceeded(nextReport));
484
+ persistReport(documentIdToRun, nextReport);
485
+ } catch (e) {
486
+ dispatch(slice.actions.runFailed(e instanceof Error ? e.message : "Unknown error"));
487
+ }
488
+ };
489
+ return { status, report, error, run };
490
+ }
491
+
492
+ // src/components/audit-feedback.tsx
493
+ import * as React from "react";
494
+ import { useState } from "react";
495
+ import { notify } from "@elementor/editor-notifications";
496
+ import { isExperimentActive } from "@elementor/editor-v1-adapters";
497
+ import { useMixpanel } from "@elementor/events";
498
+ import { httpService as httpService2 } from "@elementor/http-client";
499
+ import { MessageLinesIcon } from "@elementor/icons";
500
+ import {
501
+ Button,
502
+ Dialog,
503
+ DialogActions,
504
+ DialogContent,
505
+ DialogHeader,
506
+ DialogTitle,
507
+ IconButton,
508
+ Stack,
509
+ TextField,
510
+ Tooltip,
511
+ Typography
512
+ } from "@elementor/ui";
513
+ import { __ as __3 } from "@wordpress/i18n";
514
+ var FEEDBACK_SUBJECT = "Page Audit Tool";
515
+ var FEEDBACK_SUBJECT_LABEL = __3("Page Audit Tool", "elementor");
516
+ var SUCCESS_NOTIFICATION_ID = "audit-feedback-success";
517
+ var ERROR_NOTIFICATION_ID = "audit-feedback-error";
518
+ var DIALOG_TITLE_ID = "audit-feedback-dialog-title";
519
+ var DIALOG_DESCRIPTION_ID = "audit-feedback-dialog-description";
520
+ var isUserConnected = () => Boolean(window.elementorCommon?.config?.library_connect?.is_connected || window.elementorPro?.config?.isActive);
521
+ var notifySuccess = () => notify({
522
+ id: SUCCESS_NOTIFICATION_ID,
523
+ type: "success",
524
+ message: __3("Feedback sent. Thanks for helping us out.", "elementor")
525
+ });
526
+ var notifyError = () => notify({
527
+ id: ERROR_NOTIFICATION_ID,
528
+ type: "error",
529
+ message: __3("Something went wrong. Please try sending your feedback again.", "elementor")
530
+ });
531
+ function AuditFeedback() {
532
+ const [isOpen, setIsOpen] = useState(false);
533
+ const [feedbackText, setFeedbackText] = useState("");
534
+ const [isSubmitting, setIsSubmitting] = useState(false);
535
+ const { dispatchEvent: trackEvent = (...args) => void args } = useMixpanel();
536
+ if (!isExperimentActive(FEEDBACK_EXPERIMENT_NAME)) {
537
+ return null;
538
+ }
539
+ const connected = isUserConnected();
540
+ const connectUrl = window.elementor?.config?.user?.top_bar?.connect_url;
541
+ if (!connected && !connectUrl) {
542
+ return null;
543
+ }
544
+ const triggerLabel = __3("Give feedback", "elementor");
545
+ const handleOpen = () => {
546
+ if (!connected) {
547
+ window.open(connectUrl, "_blank", "noopener");
548
+ trackEvent(FEEDBACK_CLICKED_EVENT, { entry_point: FEEDBACK_ENTRY_POINT, connected: false });
549
+ return;
550
+ }
551
+ setIsOpen(true);
552
+ trackEvent(FEEDBACK_CLICKED_EVENT, { entry_point: FEEDBACK_ENTRY_POINT, connected: true });
553
+ };
554
+ const dismiss = (eventName) => {
555
+ setIsOpen(false);
556
+ setFeedbackText("");
557
+ trackEvent(eventName, { entry_point: FEEDBACK_ENTRY_POINT });
558
+ };
559
+ const handleCancel = () => dismiss(FEEDBACK_CANCELLED_EVENT);
560
+ const handleClose = () => dismiss(FEEDBACK_CLOSED_EVENT);
561
+ const handleSubmit = () => {
562
+ setIsSubmitting(true);
563
+ httpService2().post("elementor/v1/feedback/submit", {
564
+ subject: FEEDBACK_SUBJECT,
565
+ description: feedbackText.trim()
566
+ }).then((response) => {
567
+ if (!response.data.success) {
568
+ notifyError();
569
+ return;
570
+ }
571
+ setIsOpen(false);
572
+ setFeedbackText("");
573
+ notifySuccess();
574
+ trackEvent(FEEDBACK_SENT_EVENT, { entry_point: FEEDBACK_ENTRY_POINT });
575
+ }).catch(notifyError).finally(() => setIsSubmitting(false));
576
+ };
577
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Tooltip, { title: triggerLabel, placement: "top" }, /* @__PURE__ */ React.createElement(IconButton, { size: "small", "aria-label": triggerLabel, onClick: handleOpen }, /* @__PURE__ */ React.createElement(MessageLinesIcon, { fontSize: "small" }))), /* @__PURE__ */ React.createElement(
578
+ Dialog,
579
+ {
580
+ open: isOpen,
581
+ onClose: handleClose,
582
+ "aria-labelledby": DIALOG_TITLE_ID,
583
+ "aria-describedby": DIALOG_DESCRIPTION_ID,
584
+ PaperProps: { sx: { borderRadius: 1 } }
585
+ },
586
+ /* @__PURE__ */ React.createElement(DialogHeader, { logo: false, onClose: handleClose }, /* @__PURE__ */ React.createElement(DialogTitle, { id: DIALOG_TITLE_ID }, __3("Let us know what you think", "elementor"))),
587
+ /* @__PURE__ */ React.createElement(DialogContent, { dividers: true }, /* @__PURE__ */ React.createElement(Stack, { direction: "column", gap: 2 }, /* @__PURE__ */ React.createElement(TextField, { value: FEEDBACK_SUBJECT_LABEL, disabled: true, fullWidth: true, size: "small" }), /* @__PURE__ */ React.createElement(
588
+ TextField,
589
+ {
590
+ placeholder: __3(
591
+ "Tell us what you had in mind, what we can improve or fix to make this feature better.",
592
+ "elementor"
593
+ ),
594
+ fullWidth: true,
595
+ multiline: true,
596
+ rows: 4,
597
+ disabled: isSubmitting,
598
+ onChange: (event) => setFeedbackText(event.target.value),
599
+ value: feedbackText
600
+ }
601
+ ), /* @__PURE__ */ React.createElement(Typography, { id: DIALOG_DESCRIPTION_ID, variant: "caption", color: "text.secondary" }, __3(
602
+ "We appreciate your feedback! While we review all submissions, we can't guarantee that every suggestion will result in a change or update.",
603
+ "elementor"
604
+ )))),
605
+ /* @__PURE__ */ React.createElement(DialogActions, null, /* @__PURE__ */ React.createElement(Button, { variant: "text", color: "secondary", onClick: handleCancel }, __3("Cancel", "elementor")), /* @__PURE__ */ React.createElement(
606
+ Button,
607
+ {
608
+ variant: "contained",
609
+ color: "primary",
610
+ disabled: isSubmitting || feedbackText.trim().length === 0,
611
+ onClick: handleSubmit
612
+ },
613
+ __3("Send", "elementor")
614
+ ))
615
+ ));
616
+ }
617
+
618
+ // src/components/pages/error-page.tsx
619
+ import * as React2 from "react";
620
+ import { Box, Button as Button2, Typography as Typography2 } from "@elementor/ui";
621
+ import { __ as __4 } from "@wordpress/i18n";
622
+ function ErrorPage({ message, onRetry }) {
623
+ return /* @__PURE__ */ React2.createElement(Box, { sx: { display: "flex", flexDirection: "column", alignItems: "center", gap: 1.5, p: 4 } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "error", textAlign: "center" }, message), /* @__PURE__ */ React2.createElement(Button2, { variant: "contained", size: "small", onClick: onRetry }, __4("Try again", "elementor")));
624
+ }
625
+
626
+ // src/components/pages/loading-page.tsx
627
+ import * as React3 from "react";
628
+ import { Box as Box2, CircularProgress, Typography as Typography3 } from "@elementor/ui";
629
+ import { __ as __5 } from "@wordpress/i18n";
630
+ function LoadingPage() {
631
+ return /* @__PURE__ */ React3.createElement(Box2, { sx: { display: "flex", flexDirection: "column", alignItems: "center", gap: 1.5, p: 4 } }, /* @__PURE__ */ React3.createElement(CircularProgress, { size: 32 }), /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2" }, __5("Audit in progress.", "elementor")));
632
+ }
633
+
634
+ // src/components/pages/welcome-page.tsx
635
+ import * as React4 from "react";
636
+ import { Box as Box3, Typography as Typography4 } from "@elementor/ui";
637
+ import { __ as __6 } from "@wordpress/i18n";
638
+ var INFORMATION_IMAGE_PATH = "images/information.svg";
639
+ function WelcomePage() {
640
+ const assetsUrl = window.elementorCommon?.config?.urls?.assets ?? "";
641
+ return /* @__PURE__ */ React4.createElement(
642
+ Box3,
643
+ {
644
+ sx: {
645
+ display: "flex",
646
+ flexDirection: "column",
647
+ alignItems: "center",
648
+ justifyContent: "center",
649
+ gap: 1,
650
+ p: 3
651
+ }
652
+ },
653
+ /* @__PURE__ */ React4.createElement(
654
+ "img",
655
+ {
656
+ className: "elementor-nerd-box-icon",
657
+ src: `${assetsUrl}${INFORMATION_IMAGE_PATH}`,
658
+ loading: "lazy",
659
+ alt: __6("Elementor", "elementor")
660
+ }
661
+ ),
662
+ /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle1", component: "h3", sx: { marginBlockStart: 2 } }, __6("Audit your page!", "elementor")),
663
+ /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", textAlign: "center" }, __6(
664
+ 'Check SEO, accessibility, performance, and best practices. Click "Run page audit" to begin.',
665
+ "elementor"
666
+ ))
667
+ );
668
+ }
669
+
670
+ // src/components/report-shell.tsx
671
+ import * as React21 from "react";
672
+ import { useState as useState4 } from "react";
673
+ import { Box as Box18, Divider as Divider2, Tab, Tabs } from "@elementor/ui";
674
+ import { __ as __20 } from "@wordpress/i18n";
675
+
676
+ // src/components/pages/all-audits-page.tsx
677
+ import * as React11 from "react";
678
+ import { Box as Box8 } from "@elementor/ui";
679
+ import { __ as __12 } from "@wordpress/i18n";
680
+
681
+ // src/components/status-section.tsx
682
+ import * as React5 from "react";
683
+ import { useState as useState2 } from "react";
684
+ import { ChevronDownIcon } from "@elementor/icons";
685
+ import { Box as Box4, Chip, Collapse, IconButton as IconButton2 } from "@elementor/ui";
686
+ import { __ as __7 } from "@wordpress/i18n";
687
+ function StatusSection({ label, count, color = "default", defaultExpanded = false, children }) {
688
+ const [expanded, setExpanded] = useState2(defaultExpanded);
689
+ if (count === 0) {
690
+ return null;
691
+ }
692
+ return /* @__PURE__ */ React5.createElement(Box4, { sx: { paddingBlock: 1 } }, /* @__PURE__ */ React5.createElement(
693
+ Box4,
694
+ {
695
+ sx: { display: "flex", alignItems: "center", gap: 0.5, cursor: "pointer" },
696
+ onClick: () => setExpanded((v) => !v)
697
+ },
698
+ /* @__PURE__ */ React5.createElement(
699
+ IconButton2,
700
+ {
701
+ size: "small",
702
+ "aria-label": expanded ? __7("Collapse", "elementor") : __7("Expand", "elementor")
703
+ },
704
+ /* @__PURE__ */ React5.createElement(
705
+ ChevronDownIcon,
706
+ {
707
+ fontSize: "small",
708
+ sx: {
709
+ transform: expanded ? "rotate(180deg)" : void 0,
710
+ transition: "transform .2s"
711
+ }
712
+ }
713
+ )
714
+ ),
715
+ /* @__PURE__ */ React5.createElement(Chip, { label: `${label} (${count})`, size: "small", color, variant: "standard" })
716
+ ), /* @__PURE__ */ React5.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React5.createElement(Box4, { sx: { paddingInlineStart: 4.25, paddingBlock: 2 } }, children)));
717
+ }
718
+
719
+ // src/components/subpage-header.tsx
720
+ import * as React6 from "react";
721
+ import { ArrowLeftIcon } from "@elementor/icons";
722
+ import { Box as Box5, IconButton as IconButton3, Rotate, Typography as Typography5, useTheme } from "@elementor/ui";
723
+ import { __ as __8 } from "@wordpress/i18n";
724
+ function SubpageHeader({ title, onBack, backLabel, icon }) {
725
+ const isRtl = "rtl" === useTheme().direction;
726
+ return /* @__PURE__ */ React6.createElement(Box5, { sx: { display: "flex", alignItems: "center", gap: 0.5, p: 1 } }, /* @__PURE__ */ React6.createElement(IconButton3, { size: "small", onClick: onBack, "aria-label": backLabel ?? __8("Back", "elementor") }, /* @__PURE__ */ React6.createElement(Rotate, { in: isRtl }, /* @__PURE__ */ React6.createElement(ArrowLeftIcon, { fontSize: "small" }))), icon, /* @__PURE__ */ React6.createElement(Typography5, { variant: "subtitle2", fontWeight: "bold" }, title));
727
+ }
728
+
729
+ // src/components/violation-row.tsx
730
+ import * as React10 from "react";
731
+ import { useState as useState3 } from "react";
732
+ import { getElementIcon, getElementTitle } from "@elementor/editor-elements";
733
+ import { AlertCircleIcon as AlertCircleIcon2, BulbIcon, CheckIcon, ChevronDownIcon as ChevronDownIcon2, EyeIcon, HelpIcon } from "@elementor/icons";
734
+ import { Alert, AlertTitle, Box as Box7, Collapse as Collapse2, IconButton as IconButton5, Tooltip as Tooltip3, Typography as Typography6 } from "@elementor/ui";
735
+ import { __ as __11 } from "@wordpress/i18n";
736
+
737
+ // src/hooks/focus-violation.ts
738
+ import { getContainer, selectElement } from "@elementor/editor-elements";
739
+ import { __privateOpenRoute as openRoute, __privateRunCommand as runCommand } from "@elementor/editor-v1-adapters";
740
+ var SCROLL_TO_VIEW_DELAY_MS = 200;
741
+ function focusViolation(violation) {
742
+ if (violation.elementId) {
743
+ const container = getContainer(violation.elementId);
744
+ if (container) {
745
+ selectElement(violation.elementId);
746
+ const domElement = container.view?.getDomElement?.();
747
+ if (domElement) {
748
+ window.elementor?.helpers?.scrollToView?.(domElement, SCROLL_TO_VIEW_DELAY_MS);
749
+ }
750
+ }
751
+ if (violation.targetHint === "element-settings") {
752
+ runCommand("panel/editor/open");
753
+ }
754
+ return;
755
+ }
756
+ if (violation.externalUrl) {
757
+ window.open(violation.externalUrl, "_blank");
758
+ return;
759
+ }
760
+ if (violation.targetHint === "page-settings") {
761
+ openRoute("panel/page-settings/settings");
762
+ return;
763
+ }
764
+ if (violation.targetHint === "site-settings") {
765
+ runCommand("panel/global/open");
766
+ return;
767
+ }
768
+ if (violation.targetHint === "site-identity-settings") {
769
+ void runCommand("panel/global/open").then(() => {
770
+ openRoute("panel/global/settings-site-identity");
771
+ });
772
+ }
773
+ }
774
+
775
+ // src/utils/build-angie-prompt.ts
776
+ import { __ as __9, sprintf } from "@wordpress/i18n";
777
+ function buildAngiePrompt(violationText) {
778
+ return sprintf(
779
+ /* translators: %s: audit violation description. */
780
+ __9("Help me fix: %s", "elementor"),
781
+ violationText
782
+ );
783
+ }
784
+
785
+ // src/utils/keyboard-click.ts
786
+ function onKeyboardClick(callback) {
787
+ return (event) => {
788
+ if (event.key === "Enter" || event.key === " ") {
789
+ event.preventDefault();
790
+ callback();
791
+ }
792
+ };
793
+ }
794
+
795
+ // src/components/fix-violation-with-angie.tsx
796
+ import * as React7 from "react";
797
+ import { isAngieAvailable, sendPromptToAngie } from "@elementor/editor-mcp";
798
+ import { AngieIcon } from "@elementor/icons";
799
+ import { IconButton as IconButton4, Tooltip as Tooltip2 } from "@elementor/ui";
800
+ import { __ as __10 } from "@wordpress/i18n";
801
+ function FixViolationWithAngie({ prompt }) {
802
+ const label = __10("Fix with Angie", "elementor");
803
+ const href = `#angie-prompt=${encodeURIComponent(prompt)}`;
804
+ const handleClick = (event) => {
805
+ event.stopPropagation();
806
+ event.preventDefault();
807
+ if (isAngieAvailable()) {
808
+ sendPromptToAngie(prompt);
809
+ return;
810
+ }
811
+ window.dispatchEvent(
812
+ new CustomEvent(CREATE_WIDGET_EVENT, {
813
+ detail: {
814
+ entry_point: ANGIE_FIX_ENTRY_POINT,
815
+ prompt
816
+ }
817
+ })
818
+ );
819
+ };
820
+ return /* @__PURE__ */ React7.createElement(Tooltip2, { title: label }, /* @__PURE__ */ React7.createElement(
821
+ IconButton4,
822
+ {
823
+ className: "violation-hover-icon",
824
+ component: "a",
825
+ href,
826
+ size: "small",
827
+ "aria-label": label,
828
+ onClick: handleClick
829
+ },
830
+ /* @__PURE__ */ React7.createElement(AngieIcon, { fontSize: "tiny" })
831
+ ));
832
+ }
833
+
834
+ // src/components/severity-icons.tsx
835
+ import * as React8 from "react";
836
+ import { AlertCircleIcon, AlertTriangleFilledIcon, InfoCircleIcon } from "@elementor/icons";
837
+ var SEVERITY_CONFIG = {
838
+ error: { Icon: AlertTriangleFilledIcon, color: "error" },
839
+ warning: { Icon: AlertCircleIcon, color: "warning" },
840
+ info: { Icon: InfoCircleIcon, color: "info" }
841
+ };
842
+ function SeverityIcon({ severity }) {
843
+ const { Icon, color } = SEVERITY_CONFIG[severity];
844
+ return /* @__PURE__ */ React8.createElement(Icon, { fontSize: "small", color });
845
+ }
846
+
847
+ // src/components/violation-icons.tsx
848
+ import * as React9 from "react";
849
+ import { FileSettingsIcon, SettingsIcon, ShieldCheckIcon } from "@elementor/icons";
850
+ import { Box as Box6 } from "@elementor/ui";
851
+ function ViolationIcon({ violation, widgetIcon }) {
852
+ if (widgetIcon) {
853
+ return /* @__PURE__ */ React9.createElement(
854
+ Box6,
855
+ {
856
+ component: "i",
857
+ className: widgetIcon,
858
+ "aria-hidden": true,
859
+ sx: { fontSize: "inherit", width: "1em", textAlign: "center" }
860
+ }
861
+ );
862
+ }
863
+ if (violation.targetHint === "page-settings") {
864
+ return /* @__PURE__ */ React9.createElement(FileSettingsIcon, { fontSize: "inherit", "aria-hidden": true });
865
+ }
866
+ if (violation.targetHint === "site-settings" || violation.targetHint === "site-identity-settings") {
867
+ return /* @__PURE__ */ React9.createElement(SettingsIcon, { fontSize: "inherit", "aria-hidden": true });
868
+ }
869
+ return /* @__PURE__ */ React9.createElement(ShieldCheckIcon, { fontSize: "inherit", "aria-hidden": true });
870
+ }
871
+
872
+ // src/components/violation-row.tsx
873
+ function SkipReasonTooltip({ reason }) {
874
+ return /* @__PURE__ */ React10.createElement(Tooltip3, { title: reason, placement: "top" }, /* @__PURE__ */ React10.createElement(Box7, { "aria-label": reason, component: "span", sx: { display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React10.createElement(HelpIcon, { fontSize: "small", color: "action" })));
875
+ }
876
+ function StatusIndicator({ audit: audit21, violations }) {
877
+ if (violations) {
878
+ return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, violations.length), /* @__PURE__ */ React10.createElement(SeverityIcon, { severity: audit21.severity }));
879
+ }
880
+ return /* @__PURE__ */ React10.createElement(CheckIcon, { fontSize: "small", color: "success" });
881
+ }
882
+ function ViolationRow({ audit: audit21, skipReason, violations }) {
883
+ const [expanded, setExpanded] = useState3(false);
884
+ const toggleExpanded = () => setExpanded((value) => !value);
885
+ return /* @__PURE__ */ React10.createElement(Box7, { sx: { borderBottom: 1, borderColor: "divider", paddingBlock: 0.5 } }, /* @__PURE__ */ React10.createElement(Box7, { sx: { display: "flex", alignItems: "center", gap: 0.5 } }, /* @__PURE__ */ React10.createElement(
886
+ Box7,
887
+ {
888
+ sx: {
889
+ alignItems: "center",
890
+ cursor: "pointer",
891
+ display: "flex",
892
+ flex: 1,
893
+ gap: 0.5,
894
+ minWidth: 0
895
+ },
896
+ onClick: toggleExpanded
897
+ },
898
+ /* @__PURE__ */ React10.createElement(Typography6, { variant: "body2", sx: { flex: 1 } }, audit21.title),
899
+ !skipReason && /* @__PURE__ */ React10.createElement(StatusIndicator, { audit: audit21, violations })
900
+ ), skipReason && /* @__PURE__ */ React10.createElement(SkipReasonTooltip, { reason: skipReason }), /* @__PURE__ */ React10.createElement(
901
+ IconButton5,
902
+ {
903
+ size: "small",
904
+ "aria-label": expanded ? __11("Collapse", "elementor") : __11("Expand", "elementor"),
905
+ onClick: toggleExpanded
906
+ },
907
+ /* @__PURE__ */ React10.createElement(
908
+ ChevronDownIcon2,
909
+ {
910
+ fontSize: "small",
911
+ sx: {
912
+ transform: expanded ? "rotate(180deg)" : void 0,
913
+ transition: "transform .2s"
914
+ }
915
+ }
916
+ )
917
+ )), /* @__PURE__ */ React10.createElement(Collapse2, { in: expanded }, /* @__PURE__ */ React10.createElement(Box7, { sx: { display: "flex", flexDirection: "column", gap: 1, paddingBlock: 1 } }, /* @__PURE__ */ React10.createElement(
918
+ Alert,
919
+ {
920
+ severity: "secondary",
921
+ sx: { p: 1 },
922
+ icon: /* @__PURE__ */ React10.createElement(AlertCircleIcon2, { fontSize: "small", color: "secondary", "aria-hidden": true })
923
+ },
924
+ /* @__PURE__ */ React10.createElement(AlertTitle, null, /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption", component: "p", color: "text.primary", fontWeight: "bold" }, __11("What's the issue", "elementor"))),
925
+ /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption", component: "p", color: "text.secondary" }, audit21.description)
926
+ ), /* @__PURE__ */ React10.createElement(
927
+ Alert,
928
+ {
929
+ severity: "info",
930
+ sx: { p: 1 },
931
+ icon: /* @__PURE__ */ React10.createElement(BulbIcon, { fontSize: "small", color: "info", "aria-hidden": true })
932
+ },
933
+ /* @__PURE__ */ React10.createElement(AlertTitle, null, /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption", component: "p", color: "text.primary", fontWeight: "bold" }, __11("How to resolve", "elementor"))),
934
+ /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption", component: "p", color: "text.secondary" }, audit21.fixHint)
935
+ )), violations && violations.length > 0 && /* @__PURE__ */ React10.createElement(Box7, { role: "list", sx: { paddingBlockEnd: 1, paddingInlineStart: 2 } }, violations.map((violation, idx) => {
936
+ const widgetIcon = violation.elementId ? getElementIcon(violation.elementId) : null;
937
+ const elementTitle = violation.elementId ? getElementTitle(violation.elementId) : null;
938
+ const rowLabel = elementTitle ? `${elementTitle} - ${violation.label}` : violation.label;
939
+ return /* @__PURE__ */ React10.createElement(
940
+ Box7,
941
+ {
942
+ key: idx,
943
+ role: "button",
944
+ tabIndex: 0,
945
+ onClick: () => focusViolation(violation),
946
+ onKeyDown: onKeyboardClick(() => focusViolation(violation)),
947
+ sx: {
948
+ display: "flex",
949
+ alignItems: "center",
950
+ gap: 1,
951
+ paddingBlock: 0.5,
952
+ paddingInline: 2,
953
+ borderRadius: 1,
954
+ cursor: "pointer",
955
+ "& .violation-hover-icon": { opacity: 0, transition: "opacity .15s" },
956
+ "&:hover": { bgcolor: "action.hover" },
957
+ "&:hover .violation-hover-icon, &:focus-visible .violation-hover-icon": {
958
+ opacity: 1
959
+ }
960
+ }
961
+ },
962
+ /* @__PURE__ */ React10.createElement(ViolationIcon, { violation, widgetIcon }),
963
+ /* @__PURE__ */ React10.createElement(Box7, { sx: { flex: 1 } }, /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption" }, rowLabel), violation.detail && /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption", color: "text.secondary" }, violation.detail)),
964
+ violation.angieFix && /* @__PURE__ */ React10.createElement(FixViolationWithAngie, { prompt: buildAngiePrompt(rowLabel) }),
965
+ /* @__PURE__ */ React10.createElement(EyeIcon, { className: "violation-hover-icon", fontSize: "tiny", "aria-hidden": true })
966
+ );
967
+ }))));
968
+ }
969
+
970
+ // src/components/pages/all-audits-page.tsx
971
+ function AllAuditsPage({ initialExpandedStatus, onBack, report }) {
972
+ const { failed, passed, skipped, totalViolations } = partitionAuditResults(report);
973
+ const expandFail = !initialExpandedStatus || initialExpandedStatus === "fail";
974
+ const expandPass = initialExpandedStatus === "pass";
975
+ const expandSkipped = initialExpandedStatus === "skipped";
976
+ return /* @__PURE__ */ React11.createElement(Box8, { key: initialExpandedStatus ?? "default" }, /* @__PURE__ */ React11.createElement(SubpageHeader, { title: __12("All audits", "elementor"), onBack }), /* @__PURE__ */ React11.createElement(Box8, { sx: { p: 1 } }, /* @__PURE__ */ React11.createElement(
977
+ StatusSection,
978
+ {
979
+ label: auditStatusLabel("fail"),
980
+ count: totalViolations,
981
+ color: auditStatusColor("fail"),
982
+ defaultExpanded: expandFail
983
+ },
984
+ failed.map((r) => /* @__PURE__ */ React11.createElement(ViolationRow, { key: r.audit.id, audit: r.audit, violations: r.result.violations }))
985
+ ), /* @__PURE__ */ React11.createElement(
986
+ StatusSection,
987
+ {
988
+ label: auditStatusLabel("pass"),
989
+ count: passed.length,
990
+ color: auditStatusColor("pass"),
991
+ defaultExpanded: expandPass
992
+ },
993
+ passed.map((r) => /* @__PURE__ */ React11.createElement(ViolationRow, { key: r.audit.id, audit: r.audit }))
994
+ ), /* @__PURE__ */ React11.createElement(
995
+ StatusSection,
996
+ {
997
+ label: auditStatusLabel("skipped"),
998
+ count: skipped.length,
999
+ color: auditStatusColor("skipped"),
1000
+ defaultExpanded: expandSkipped
1001
+ },
1002
+ skipped.map((r) => /* @__PURE__ */ React11.createElement(ViolationRow, { key: r.audit.id, audit: r.audit, skipReason: r.result.reason }))
1003
+ )));
1004
+ }
1005
+
1006
+ // src/components/pages/category-page.tsx
1007
+ import * as React12 from "react";
1008
+ import { Box as Box9 } from "@elementor/ui";
1009
+ import { __ as __13 } from "@wordpress/i18n";
1010
+
1011
+ // src/components/category-icons.ts
1012
+ import {
1013
+ ElementorAccessibilityIcon,
1014
+ FilterIcon,
1015
+ HeartHandShakeIcon,
1016
+ Settings2Icon,
1017
+ ShieldHalfFilledIcon
1018
+ } from "@elementor/icons";
1019
+ var CATEGORY_ICONS = {
1020
+ "best-practices": Settings2Icon,
1021
+ seo: FilterIcon,
1022
+ accessibility: ElementorAccessibilityIcon,
1023
+ performance: ShieldHalfFilledIcon,
1024
+ compliance: HeartHandShakeIcon
1025
+ };
1026
+
1027
+ // src/components/pages/category-page.tsx
1028
+ function CategoryPage({ category, report, onBack }) {
1029
+ const Icon = CATEGORY_ICONS[category];
1030
+ const { failed, passed, totalViolations } = partitionAuditResults(report, { category });
1031
+ return /* @__PURE__ */ React12.createElement(React12.Fragment, null, /* @__PURE__ */ React12.createElement(
1032
+ SubpageHeader,
1033
+ {
1034
+ title: CATEGORY_LABELS[category],
1035
+ onBack,
1036
+ backLabel: __13("Back to all issues", "elementor"),
1037
+ icon: /* @__PURE__ */ React12.createElement(Icon, { fontSize: "small", color: "action" })
1038
+ }
1039
+ ), /* @__PURE__ */ React12.createElement(Box9, { sx: { p: 1 } }, /* @__PURE__ */ React12.createElement(
1040
+ StatusSection,
1041
+ {
1042
+ label: __13("Failed audits", "elementor"),
1043
+ count: totalViolations,
1044
+ color: "error",
1045
+ defaultExpanded: true
1046
+ },
1047
+ failed.map((r) => /* @__PURE__ */ React12.createElement(ViolationRow, { key: r.audit.id, audit: r.audit, violations: r.result.violations }))
1048
+ ), /* @__PURE__ */ React12.createElement(StatusSection, { label: __13("Passed audits", "elementor"), count: passed.length, color: "success" }, passed.map((r) => /* @__PURE__ */ React12.createElement(ViolationRow, { key: r.audit.id, audit: r.audit })))));
1049
+ }
1050
+
1051
+ // src/components/pages/issues-page.tsx
1052
+ import * as React16 from "react";
1053
+ import { Box as Box13, Link, Typography as Typography10 } from "@elementor/ui";
1054
+ import { __ as __17 } from "@wordpress/i18n";
1055
+
1056
+ // src/utils/severity-counts.ts
1057
+ import { __ as __14, sprintf as sprintf2 } from "@wordpress/i18n";
1058
+ var ALL_SEVERITIES = ["error", "warning", "info"];
1059
+ function countSeverities(report, category) {
1060
+ const counts = { error: 0, warning: 0, info: 0 };
1061
+ for (const { audit: audit21, result } of report.auditResults) {
1062
+ if (result.status !== "fail") {
1063
+ continue;
1064
+ }
1065
+ if (category && !audit21.categories.includes(category)) {
1066
+ continue;
1067
+ }
1068
+ counts[audit21.severity] += result.violations.length;
1069
+ }
1070
+ return counts;
1071
+ }
1072
+ function severityPluralLabel(severity) {
1073
+ switch (severity) {
1074
+ case "error":
1075
+ return __14("Errors", "elementor");
1076
+ case "warning":
1077
+ return __14("Warnings", "elementor");
1078
+ case "info":
1079
+ return __14("Info", "elementor");
1080
+ }
1081
+ }
1082
+ function severityRemainingCountLabel(severity, count) {
1083
+ switch (severity) {
1084
+ case "error":
1085
+ return sprintf2(
1086
+ /* translators: %d: number of remaining error violations. */
1087
+ __14("%d errors", "elementor"),
1088
+ count
1089
+ );
1090
+ case "warning":
1091
+ return sprintf2(
1092
+ /* translators: %d: number of remaining warning violations. */
1093
+ __14("%d warnings", "elementor"),
1094
+ count
1095
+ );
1096
+ case "info":
1097
+ return sprintf2(
1098
+ /* translators: %d: number of remaining info violations. */
1099
+ __14("%d info", "elementor"),
1100
+ count
1101
+ );
1102
+ }
1103
+ }
1104
+
1105
+ // src/components/issues-category-row.tsx
1106
+ import * as React13 from "react";
1107
+ import { ChevronRightIcon } from "@elementor/icons";
1108
+ import { Box as Box10, Rotate as Rotate2, Typography as Typography7, useTheme as useTheme2 } from "@elementor/ui";
1109
+ function IssuesCategoryRow({ category, label, counts, onClick }) {
1110
+ const isRtl = "rtl" === useTheme2().direction;
1111
+ const Icon = CATEGORY_ICONS[category];
1112
+ return /* @__PURE__ */ React13.createElement(
1113
+ Box10,
1114
+ {
1115
+ role: "button",
1116
+ tabIndex: 0,
1117
+ onClick,
1118
+ onKeyDown: onKeyboardClick(onClick),
1119
+ sx: {
1120
+ display: "flex",
1121
+ alignItems: "center",
1122
+ gap: 1,
1123
+ px: 2,
1124
+ py: 1.5,
1125
+ cursor: "pointer",
1126
+ borderRadius: 1,
1127
+ border: 1,
1128
+ borderColor: "divider",
1129
+ "&:hover": { bgcolor: "action.hover" },
1130
+ outline: "none",
1131
+ "&:focus-visible": { outline: "2px solid", outlineColor: "primary.main" }
1132
+ }
1133
+ },
1134
+ /* @__PURE__ */ React13.createElement(Icon, { fontSize: "small", color: "action" }),
1135
+ /* @__PURE__ */ React13.createElement(Typography7, { variant: "body2", fontWeight: "bold", sx: { flex: 1 } }, label),
1136
+ /* @__PURE__ */ React13.createElement(Box10, { sx: { display: "flex", alignItems: "center", gap: 0.5 } }, ALL_SEVERITIES.filter((s) => counts[s] > 0).map((severity) => /* @__PURE__ */ React13.createElement(Box10, { key: severity, sx: { display: "flex", alignItems: "center", gap: 0.25 } }, /* @__PURE__ */ React13.createElement(SeverityIcon, { severity }), /* @__PURE__ */ React13.createElement(Typography7, { variant: "caption", color: "text.primary", fontWeight: "bold" }, counts[severity])))),
1137
+ /* @__PURE__ */ React13.createElement(Rotate2, { in: isRtl }, /* @__PURE__ */ React13.createElement(ChevronRightIcon, { fontSize: "small", color: "action" }))
1138
+ );
1139
+ }
1140
+
1141
+ // src/components/promotions.tsx
1142
+ import * as React15 from "react";
1143
+ import { Box as Box12, Typography as Typography9 } from "@elementor/ui";
1144
+ import { __ as __16 } from "@wordpress/i18n";
1145
+
1146
+ // src/register-promotions.ts
1147
+ import { ElementorCookieIcon, PhotoIcon, ShieldHalfFilledIcon as ShieldHalfFilledIcon2 } from "@elementor/icons";
1148
+ import { __ as __15, _n, sprintf as sprintf3 } from "@wordpress/i18n";
1149
+ function firstFailExternalUrl(run) {
1150
+ return run.result.status === "fail" ? run.result.violations[0]?.externalUrl : void 0;
1151
+ }
1152
+ var PROMOTIONS = [
1153
+ {
1154
+ auditId: "audits/images-alt-text",
1155
+ icon: PhotoIcon,
1156
+ ctaLabel: __15("Fix with Ally", "elementor"),
1157
+ formatSubtitle: (run) => {
1158
+ if (run.result.status !== "fail") {
1159
+ return null;
1160
+ }
1161
+ const count = run.result.metadata?.missingAltImageCount ?? 0;
1162
+ if (count === 0) {
1163
+ return null;
1164
+ }
1165
+ return sprintf3(
1166
+ /* translators: %d: number of images missing alt text. */
1167
+ _n("%d image", "%d images", count, "elementor"),
1168
+ count
1169
+ );
1170
+ },
1171
+ getCtaUrl: firstFailExternalUrl
1172
+ },
1173
+ {
1174
+ auditId: "audits/cookie-policy",
1175
+ icon: ElementorCookieIcon,
1176
+ ctaLabel: __15("Fix with Cookiez", "elementor"),
1177
+ formatSubtitle: (run) => run.result.status === "fail" ? __15("Generate cookie policy", "elementor") : null,
1178
+ getCtaUrl: firstFailExternalUrl
1179
+ },
1180
+ {
1181
+ auditId: "audits/images-too-large",
1182
+ icon: ShieldHalfFilledIcon2,
1183
+ ctaLabel: __15("Optimize all", "elementor"),
1184
+ formatSubtitle: (run) => {
1185
+ if (run.result.status !== "fail") {
1186
+ return null;
1187
+ }
1188
+ const count = run.result.metadata?.oversizedImageCount ?? 0;
1189
+ if (count === 0) {
1190
+ return null;
1191
+ }
1192
+ return sprintf3(
1193
+ /* translators: %d: number of oversized images. */
1194
+ _n("%d image", "%d images", count, "elementor"),
1195
+ count
1196
+ );
1197
+ },
1198
+ getCtaUrl: firstFailExternalUrl
1199
+ }
1200
+ ];
1201
+
1202
+ // src/utils/find-audit-run.ts
1203
+ function findAuditRun(report, auditId) {
1204
+ return report.auditResults.find((run) => run.audit.id === auditId);
1205
+ }
1206
+
1207
+ // src/components/promotion-card.tsx
1208
+ import * as React14 from "react";
1209
+ import { Box as Box11, Button as Button3, Typography as Typography8 } from "@elementor/ui";
1210
+ function PromotionCard({ ctaDisabled, ctaLabel, icon: Icon, onCtaClick, subtitle, title }) {
1211
+ return /* @__PURE__ */ React14.createElement(
1212
+ Box11,
1213
+ {
1214
+ sx: {
1215
+ alignItems: "center",
1216
+ border: 1,
1217
+ borderColor: "divider",
1218
+ borderRadius: 1,
1219
+ display: "flex",
1220
+ gap: 1,
1221
+ px: 2,
1222
+ py: 1.5
1223
+ }
1224
+ },
1225
+ /* @__PURE__ */ React14.createElement(Icon, { fontSize: "small", color: "action" }),
1226
+ /* @__PURE__ */ React14.createElement(Box11, { sx: { display: "flex", flex: 1, flexDirection: "column", gap: 0.25, minWidth: 0 } }, /* @__PURE__ */ React14.createElement(Typography8, { variant: "body2", fontWeight: "bold" }, title), /* @__PURE__ */ React14.createElement(Typography8, { variant: "caption", color: "text.secondary" }, subtitle)),
1227
+ /* @__PURE__ */ React14.createElement(Button3, { variant: "outlined", color: "secondary", size: "small", disabled: ctaDisabled, onClick: onCtaClick }, ctaLabel)
1228
+ );
1229
+ }
1230
+
1231
+ // src/components/promotions.tsx
1232
+ function Promotions({ report }) {
1233
+ const cards = PROMOTIONS.flatMap((config) => {
1234
+ const run = findAuditRun(report, config.auditId);
1235
+ if (!run || run.result.status !== "fail") {
1236
+ return [];
1237
+ }
1238
+ const subtitle = config.formatSubtitle(run);
1239
+ if (!subtitle) {
1240
+ return [];
1241
+ }
1242
+ const ctaUrl = config.getCtaUrl(run);
1243
+ return [
1244
+ {
1245
+ config,
1246
+ ctaUrl,
1247
+ key: config.auditId,
1248
+ run,
1249
+ subtitle
1250
+ }
1251
+ ];
1252
+ });
1253
+ if (cards.length === 0) {
1254
+ return null;
1255
+ }
1256
+ return /* @__PURE__ */ React15.createElement(React15.Fragment, null, /* @__PURE__ */ React15.createElement(Typography9, { variant: "subtitle1", fontWeight: "bold" }, __16("Quick wins", "elementor")), /* @__PURE__ */ React15.createElement(Box12, { sx: { display: "flex", flexDirection: "column", gap: 1 } }, cards.map(({ config, ctaUrl, key, run, subtitle }) => /* @__PURE__ */ React15.createElement(
1257
+ PromotionCard,
1258
+ {
1259
+ key,
1260
+ icon: config.icon,
1261
+ title: run.audit.title,
1262
+ subtitle,
1263
+ ctaLabel: config.ctaLabel,
1264
+ ctaDisabled: !ctaUrl,
1265
+ onCtaClick: () => {
1266
+ if (ctaUrl) {
1267
+ window.open(ctaUrl, "_blank");
1268
+ }
1269
+ }
1270
+ }
1271
+ ))));
1272
+ }
1273
+
1274
+ // src/components/pages/issues-page.tsx
1275
+ function IssuesPage({ report, onCategoryClick, onAllAuditsClick }) {
1276
+ const populatedCategories = getPopulatedCategories(report.categories, ALL_CATEGORIES);
1277
+ return /* @__PURE__ */ React16.createElement(Box13, { sx: { display: "flex", flexDirection: "column", gap: 4, p: 2 } }, /* @__PURE__ */ React16.createElement(
1278
+ Link,
1279
+ {
1280
+ component: "button",
1281
+ underline: "none",
1282
+ color: "inherit",
1283
+ onClick: onAllAuditsClick,
1284
+ sx: { textAlign: "start" }
1285
+ },
1286
+ /* @__PURE__ */ React16.createElement(Typography10, { variant: "subtitle1", component: "h2" }, __17("All issues", "elementor"))
1287
+ ), /* @__PURE__ */ React16.createElement(Box13, { sx: { display: "flex", flexDirection: "column", gap: 1 } }, populatedCategories.map((category) => /* @__PURE__ */ React16.createElement(
1288
+ IssuesCategoryRow,
1289
+ {
1290
+ key: category,
1291
+ category,
1292
+ label: CATEGORY_LABELS[category],
1293
+ counts: countSeverities(report, category),
1294
+ onClick: () => onCategoryClick(category)
1295
+ }
1296
+ ))), /* @__PURE__ */ React16.createElement(Promotions, { report }));
1297
+ }
1298
+
1299
+ // src/components/pages/overview-page.tsx
1300
+ import * as React20 from "react";
1301
+ import { Box as Box17, Chip as Chip3, Divider, Typography as Typography14 } from "@elementor/ui";
1302
+ import { __ as __19, sprintf as sprintf4 } from "@wordpress/i18n";
1303
+
1304
+ // src/utils/score-thresholds.ts
1305
+ import { __ as __18 } from "@wordpress/i18n";
1306
+ var GOOD_THRESHOLD = 90;
1307
+ var OK_THRESHOLD = 50;
1308
+ function getScoreTier(score) {
1309
+ if (score >= GOOD_THRESHOLD) {
1310
+ return { color: "success", label: __18("Good", "elementor") };
1311
+ }
1312
+ if (score >= OK_THRESHOLD) {
1313
+ return { color: "warning", label: __18("Needs work", "elementor") };
1314
+ }
1315
+ return { color: "error", label: __18("At risk", "elementor") };
1316
+ }
1317
+
1318
+ // src/components/count-summary-circle.tsx
1319
+ import * as React17 from "react";
1320
+ import { Box as Box14, Chip as Chip2, Typography as Typography11 } from "@elementor/ui";
1321
+ var COUNT_SUMMARY_CIRCLE_SIZE = 64;
1322
+ var COUNT_SUMMARY_BORDER_WIDTH = 4;
1323
+ function chipBorderColor(theme, color) {
1324
+ if (color === "default" || !color) {
1325
+ return theme.palette.text.secondary;
1326
+ }
1327
+ return theme.palette[color].main;
1328
+ }
1329
+ function CountSummaryCircle({ ariaLabel, color, count, label, onClick }) {
1330
+ return /* @__PURE__ */ React17.createElement(
1331
+ Box14,
1332
+ {
1333
+ "aria-label": ariaLabel,
1334
+ component: "button",
1335
+ onClick,
1336
+ type: "button",
1337
+ sx: {
1338
+ alignItems: "center",
1339
+ background: "none",
1340
+ border: "none",
1341
+ cursor: "pointer",
1342
+ display: "flex",
1343
+ flex: 1,
1344
+ flexDirection: "column",
1345
+ gap: 0.5,
1346
+ padding: 0
1347
+ }
1348
+ },
1349
+ /* @__PURE__ */ React17.createElement(
1350
+ Chip2,
1351
+ {
1352
+ color,
1353
+ label: count,
1354
+ size: "small",
1355
+ variant: "standard",
1356
+ sx: (theme) => ({
1357
+ border: `${COUNT_SUMMARY_BORDER_WIDTH}px solid ${chipBorderColor(theme, color)}`,
1358
+ borderRadius: "50%",
1359
+ height: COUNT_SUMMARY_CIRCLE_SIZE,
1360
+ maxWidth: COUNT_SUMMARY_CIRCLE_SIZE,
1361
+ width: COUNT_SUMMARY_CIRCLE_SIZE,
1362
+ "& .MuiChip-label": {
1363
+ fontSize: theme.typography.body2.fontSize,
1364
+ fontWeight: 700,
1365
+ lineHeight: 1,
1366
+ padding: 0
1367
+ }
1368
+ })
1369
+ }
1370
+ ),
1371
+ /* @__PURE__ */ React17.createElement(Typography11, { color: "text.secondary", sx: { textAlign: "center" }, variant: "caption" }, label)
1372
+ );
1373
+ }
1374
+
1375
+ // src/components/score-bar.tsx
1376
+ import * as React18 from "react";
1377
+ import { ChevronRightIcon as ChevronRightIcon2 } from "@elementor/icons";
1378
+ import { Box as Box15, LinearProgress, Rotate as Rotate3, Typography as Typography12, useTheme as useTheme3 } from "@elementor/ui";
1379
+ function ScoreBar({ label, score, onClick }) {
1380
+ const isRtl = "rtl" === useTheme3().direction;
1381
+ return /* @__PURE__ */ React18.createElement(
1382
+ Box15,
1383
+ {
1384
+ role: onClick ? "button" : void 0,
1385
+ tabIndex: onClick ? 0 : void 0,
1386
+ onClick,
1387
+ onKeyDown: onClick ? onKeyboardClick(onClick) : void 0,
1388
+ sx: {
1389
+ display: "flex",
1390
+ alignItems: "center",
1391
+ gap: 1,
1392
+ cursor: onClick ? "pointer" : "default",
1393
+ outline: "none",
1394
+ "&:focus-visible": { outline: "2px solid", outlineColor: "primary.main", borderRadius: 1 },
1395
+ py: 0.5
1396
+ }
1397
+ },
1398
+ /* @__PURE__ */ React18.createElement(Typography12, { variant: "body2", sx: { minWidth: 96 } }, label),
1399
+ /* @__PURE__ */ React18.createElement(
1400
+ LinearProgress,
1401
+ {
1402
+ variant: "determinate",
1403
+ value: score,
1404
+ color: getScoreTier(score).color,
1405
+ sx: { flex: 1, height: 6, borderRadius: 4, bgcolor: "action.disabledBackground" }
1406
+ }
1407
+ ),
1408
+ /* @__PURE__ */ React18.createElement(
1409
+ Typography12,
1410
+ {
1411
+ variant: "body2",
1412
+ color: "text.primary",
1413
+ sx: { minWidth: 24, textAlign: "right", fontWeight: 900 }
1414
+ },
1415
+ score
1416
+ ),
1417
+ onClick && /* @__PURE__ */ React18.createElement(Rotate3, { in: isRtl }, /* @__PURE__ */ React18.createElement(ChevronRightIcon2, { fontSize: "small", color: "action" }))
1418
+ );
1419
+ }
1420
+
1421
+ // src/components/score-circle.tsx
1422
+ import * as React19 from "react";
1423
+ import { Box as Box16, CircularProgress as CircularProgress2, Typography as Typography13 } from "@elementor/ui";
1424
+ var SCORE_CIRCLE_SIZE = 88;
1425
+ var SCORE_CIRCLE_THICKNESS = 3;
1426
+ function ScoreCircle({ color, score }) {
1427
+ const progressColor = color ?? getScoreTier(score).color;
1428
+ return /* @__PURE__ */ React19.createElement(
1429
+ Box16,
1430
+ {
1431
+ role: "progressbar",
1432
+ "aria-valuenow": score,
1433
+ "aria-valuemin": 0,
1434
+ "aria-valuemax": 100,
1435
+ sx: { position: "relative", display: "inline-flex" }
1436
+ },
1437
+ /* @__PURE__ */ React19.createElement(
1438
+ CircularProgress2,
1439
+ {
1440
+ variant: "determinate",
1441
+ value: 100,
1442
+ size: SCORE_CIRCLE_SIZE,
1443
+ thickness: SCORE_CIRCLE_THICKNESS,
1444
+ sx: { color: "action.disabledBackground" }
1445
+ }
1446
+ ),
1447
+ /* @__PURE__ */ React19.createElement(
1448
+ CircularProgress2,
1449
+ {
1450
+ variant: "determinate",
1451
+ value: score,
1452
+ size: SCORE_CIRCLE_SIZE,
1453
+ thickness: SCORE_CIRCLE_THICKNESS,
1454
+ color: progressColor,
1455
+ sx: { position: "absolute", left: 0 }
1456
+ }
1457
+ ),
1458
+ /* @__PURE__ */ React19.createElement(
1459
+ Box16,
1460
+ {
1461
+ sx: {
1462
+ position: "absolute",
1463
+ inset: 0,
1464
+ display: "flex",
1465
+ alignItems: "center",
1466
+ justifyContent: "center"
1467
+ }
1468
+ },
1469
+ /* @__PURE__ */ React19.createElement(
1470
+ Typography13,
1471
+ {
1472
+ variant: "h4",
1473
+ component: "span",
1474
+ color: "text.primary",
1475
+ sx: { fontWeight: 700, lineHeight: 1 }
1476
+ },
1477
+ score
1478
+ )
1479
+ )
1480
+ );
1481
+ }
1482
+
1483
+ // src/components/pages/overview-page.tsx
1484
+ var STATUS_ARIA_LABELS = {
1485
+ pass: (count) => sprintf4(
1486
+ /* translators: %d: number of passed audits. */
1487
+ __19("%d passed audits, view all", "elementor"),
1488
+ count
1489
+ ),
1490
+ fail: (count) => sprintf4(
1491
+ /* translators: %d: number of failed audit violations. */
1492
+ __19("%d failed audits, view all", "elementor"),
1493
+ count
1494
+ ),
1495
+ skipped: (count) => sprintf4(
1496
+ /* translators: %d: number of skipped audits. */
1497
+ __19("%d skipped audits, view all", "elementor"),
1498
+ count
1499
+ )
1500
+ };
1501
+ var STATUS_GROUPS = ["fail", "pass", "skipped"];
1502
+ function OverviewPage({ onCategoryClick, onStatusClick, report }) {
1503
+ const populatedCategories = getPopulatedCategories(report.categories, ALL_CATEGORIES);
1504
+ const severityCounts = countSeverities(report);
1505
+ const statusCounts = auditStatusDisplayCounts(report);
1506
+ const overallScore = getScoreTier(report.overall);
1507
+ return /* @__PURE__ */ React20.createElement(Box17, { sx: { display: "flex", flexDirection: "column", gap: 4, p: 2 } }, /* @__PURE__ */ React20.createElement(Box17, { sx: { display: "flex", alignItems: "center", gap: 2 } }, /* @__PURE__ */ React20.createElement(ScoreCircle, { color: overallScore.color, score: report.overall }), /* @__PURE__ */ React20.createElement(Box17, { sx: { display: "flex", flexDirection: "column", gap: 0.5 } }, /* @__PURE__ */ React20.createElement(
1508
+ Chip3,
1509
+ {
1510
+ label: overallScore.label,
1511
+ color: overallScore.color,
1512
+ variant: "standard",
1513
+ size: "small",
1514
+ sx: { fontWeight: 600, alignSelf: "flex-start" }
1515
+ }
1516
+ ), /* @__PURE__ */ React20.createElement(Typography14, { variant: "body2", color: "text.secondary" }, __19("Overall score", "elementor")))), /* @__PURE__ */ React20.createElement(Box17, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, populatedCategories.map((category) => /* @__PURE__ */ React20.createElement(
1517
+ ScoreBar,
1518
+ {
1519
+ key: category,
1520
+ label: CATEGORY_LABELS[category],
1521
+ score: report.categories[category].score,
1522
+ onClick: () => onCategoryClick(category)
1523
+ }
1524
+ ))), /* @__PURE__ */ React20.createElement(Divider, null), /* @__PURE__ */ React20.createElement(Typography14, { variant: "subtitle1", fontWeight: "bold" }, __19("Audit statuses", "elementor")), /* @__PURE__ */ React20.createElement(Box17, { sx: { display: "flex", justifyContent: "space-around", gap: 2 } }, STATUS_GROUPS.map((status) => /* @__PURE__ */ React20.createElement(
1525
+ CountSummaryCircle,
1526
+ {
1527
+ key: status,
1528
+ ariaLabel: STATUS_ARIA_LABELS[status](statusCounts[status]),
1529
+ color: auditStatusColor(status),
1530
+ count: statusCounts[status],
1531
+ label: auditStatusLabel(status),
1532
+ onClick: () => onStatusClick(status)
1533
+ }
1534
+ ))), /* @__PURE__ */ React20.createElement(Divider, null), /* @__PURE__ */ React20.createElement(Typography14, { variant: "subtitle1", fontWeight: "bold" }, __19("Remaining issues", "elementor")), /* @__PURE__ */ React20.createElement(
1535
+ Box17,
1536
+ {
1537
+ component: "ul",
1538
+ sx: {
1539
+ display: "flex",
1540
+ flexDirection: "column",
1541
+ gap: 1,
1542
+ listStyle: "none",
1543
+ m: 0,
1544
+ p: 0
1545
+ }
1546
+ },
1547
+ ALL_SEVERITIES.map((severity) => {
1548
+ const count = severityCounts[severity];
1549
+ return /* @__PURE__ */ React20.createElement(
1550
+ Box17,
1551
+ {
1552
+ "aria-label": severityRemainingCountLabel(severity, count),
1553
+ component: "li",
1554
+ key: severity,
1555
+ sx: { alignItems: "center", display: "flex", gap: 0.5 }
1556
+ },
1557
+ /* @__PURE__ */ React20.createElement(SeverityIcon, { severity }),
1558
+ /* @__PURE__ */ React20.createElement(Typography14, { variant: "body2", fontWeight: "bold" }, count),
1559
+ /* @__PURE__ */ React20.createElement(Typography14, { variant: "body2" }, severityPluralLabel(severity))
1560
+ );
1561
+ })
1562
+ ));
1563
+ }
1564
+
1565
+ // src/components/report-shell.tsx
1566
+ function activeTab(page) {
1567
+ if (page === "overview") {
1568
+ return "overview";
1569
+ }
1570
+ if (page === "issues") {
1571
+ return "issues";
1572
+ }
1573
+ return page.backTo;
1574
+ }
1575
+ function isAllAuditsPage(page) {
1576
+ return typeof page === "object" && "page" in page && page.page === "all-audits";
1577
+ }
1578
+ function isCategoryPage(page) {
1579
+ return typeof page === "object" && "category" in page;
1580
+ }
1581
+ function ReportShell({ report }) {
1582
+ const [activePage, setActivePage] = useState4("overview");
1583
+ const currentTab = activeTab(activePage);
1584
+ const handleTabChange = (_, value) => {
1585
+ setActivePage(value);
1586
+ };
1587
+ const openCategory = (category, backTo) => {
1588
+ setActivePage({ category, backTo });
1589
+ };
1590
+ const openAllAudits = (expand, backTo = "issues") => {
1591
+ setActivePage({ page: "all-audits", expand, backTo });
1592
+ };
1593
+ const backFromSubPage = () => {
1594
+ if (isAllAuditsPage(activePage) || isCategoryPage(activePage)) {
1595
+ setActivePage(activePage.backTo);
1596
+ }
1597
+ };
1598
+ return /* @__PURE__ */ React21.createElement(Box18, null, /* @__PURE__ */ React21.createElement(
1599
+ Tabs,
1600
+ {
1601
+ "aria-label": __20("Audit navigation", "elementor"),
1602
+ value: currentTab,
1603
+ onChange: handleTabChange,
1604
+ textColor: "secondary",
1605
+ indicatorColor: "secondary",
1606
+ size: "small",
1607
+ centered: true,
1608
+ variant: "fullWidth"
1609
+ },
1610
+ /* @__PURE__ */ React21.createElement(Tab, { value: "overview", label: __20("Overview", "elementor") }),
1611
+ /* @__PURE__ */ React21.createElement(Tab, { value: "issues", label: __20("Issues", "elementor") })
1612
+ ), /* @__PURE__ */ React21.createElement(Divider2, null), activePage === "overview" && /* @__PURE__ */ React21.createElement(
1613
+ OverviewPage,
1614
+ {
1615
+ report,
1616
+ onCategoryClick: (category) => openCategory(category, "overview"),
1617
+ onStatusClick: (status) => openAllAudits(status, "overview")
1618
+ }
1619
+ ), activePage === "issues" && /* @__PURE__ */ React21.createElement(
1620
+ IssuesPage,
1621
+ {
1622
+ report,
1623
+ onCategoryClick: (category) => openCategory(category, "issues"),
1624
+ onAllAuditsClick: () => openAllAudits()
1625
+ }
1626
+ ), isAllAuditsPage(activePage) && /* @__PURE__ */ React21.createElement(
1627
+ AllAuditsPage,
1628
+ {
1629
+ report,
1630
+ initialExpandedStatus: activePage.expand,
1631
+ onBack: backFromSubPage
1632
+ }
1633
+ ), isCategoryPage(activePage) && /* @__PURE__ */ React21.createElement(CategoryPage, { category: activePage.category, report, onBack: backFromSubPage }));
1634
+ }
1635
+
1636
+ // src/components/audit-panel.tsx
1637
+ function AuditPanel() {
1638
+ const { status, report, error, run } = useAuditReport();
1639
+ const currentDocumentId = getCurrentDocumentId2() ?? 0;
1640
+ const onRun = () => run(currentDocumentId);
1641
+ const lastScanLabel = report ? new Date(report.runAt).toLocaleTimeString() : null;
1642
+ return /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(
1643
+ FloatingPanelHeader,
1644
+ {
1645
+ panelId: "audit-panel",
1646
+ title: __21("Page Audit", "elementor"),
1647
+ badge: __21("Beta", "elementor"),
1648
+ titleVariant: "subtitle2"
1649
+ }
1650
+ ), /* @__PURE__ */ React22.createElement(FloatingPanelBody, null, status === "idle" && /* @__PURE__ */ React22.createElement(WelcomePage, null), status === "loading" && /* @__PURE__ */ React22.createElement(LoadingPage, null), status === "error" && /* @__PURE__ */ React22.createElement(ErrorPage, { message: error ?? "", onRetry: onRun }), status === "ready" && report && /* @__PURE__ */ React22.createElement(ReportShell, { report })), /* @__PURE__ */ React22.createElement(FloatingPanelFooter, null, lastScanLabel ? /* @__PURE__ */ React22.createElement(Typography15, { variant: "caption", sx: { flex: 1 } }, __21("Last scan:", "elementor"), " ", lastScanLabel) : /* @__PURE__ */ React22.createElement(Box19, { sx: { flex: 1 } }), lastScanLabel && /* @__PURE__ */ React22.createElement(AuditFeedback, null), /* @__PURE__ */ React22.createElement(
1651
+ Button4,
1652
+ {
1653
+ variant: "contained",
1654
+ size: "small",
1655
+ onClick: onRun,
1656
+ disabled: status === "loading" || currentDocumentId === 0
1657
+ },
1658
+ lastScanLabel ? __21("Re-scan", "elementor") : __21("Run page audit", "elementor")
1659
+ )));
1660
+ }
1661
+
1662
+ // src/editor-panel.tsx
1663
+ var auditPanel = createFloatingPanel({
1664
+ id: AUDIT_PANEL_ID,
1665
+ title: "Audit",
1666
+ icon: () => null,
1667
+ component: AuditPanel,
1668
+ isDraggable: true,
1669
+ isResizable: true,
1670
+ defaults: {
1671
+ width: 360,
1672
+ height: 600,
1673
+ minWidth: 280,
1674
+ minHeight: 400
1675
+ }
1676
+ });
1677
+
1678
+ // src/hooks/use-audit-toggle-props.ts
1679
+ function useAuditToggleProps() {
1680
+ const { isOpen } = auditPanel.useFloatingPanelStatus();
1681
+ const { toggle } = auditPanel.useFloatingPanelActions();
1682
+ return {
1683
+ title: __22("Audit Page", "elementor"),
1684
+ icon: ShieldCheckIcon2,
1685
+ selected: isOpen,
1686
+ onClick: () => toggle()
1687
+ };
1688
+ }
1689
+
1690
+ // src/editor-app-bar.tsx
1691
+ var AUDIT_TOGGLE_PRIORITY = 24;
1692
+ function registerAppBarAuditsToggle() {
1693
+ utilitiesMenu.registerToggleAction({
1694
+ id: "toggle-audit-panel",
1695
+ priority: AUDIT_TOGGLE_PRIORITY,
1696
+ useProps: useAuditToggleProps
1697
+ });
1698
+ }
1699
+
1700
+ // src/audits/accessibility-policy.ts
1701
+ import { __ as __23 } from "@wordpress/i18n";
1702
+ var audit = {
1703
+ id: "audits/accessibility-policy",
1704
+ title: __23("Accessibility policy", "elementor"),
1705
+ description: __23(
1706
+ "An accessibility policy demonstrates your commitment to digital inclusion and may be legally required in some regions.",
1707
+ "elementor"
1708
+ ),
1709
+ fixHint: __23("Install the Ally plugin to add an accessibility policy to your website.", "elementor"),
1710
+ categories: ["compliance"],
1711
+ severity: "info",
1712
+ weight: 1,
1713
+ evaluate: (ctx) => {
1714
+ if (ctx.pageContext.ally_plugin_active) {
1715
+ return { status: "pass" };
1716
+ }
1717
+ return {
1718
+ status: "fail",
1719
+ violations: [
1720
+ {
1721
+ auditId: audit.id,
1722
+ label: __23("Ally plugin is not installed or active.", "elementor"),
1723
+ externalUrl: ctx.pageContext.ally_plugin_url
1724
+ }
1725
+ ]
1726
+ };
1727
+ }
1728
+ };
1729
+
1730
+ // src/audits/cookie-policy.ts
1731
+ import { __ as __24 } from "@wordpress/i18n";
1732
+ var audit2 = {
1733
+ id: "audits/cookie-policy",
1734
+ title: __24("Cookie policy", "elementor"),
1735
+ description: __24(
1736
+ "A cookie policy is required by privacy regulations such as GDPR (EU) or CCPA (US) to inform visitors about tracking and data collection.",
1737
+ "elementor"
1738
+ ),
1739
+ fixHint: __24("Install the Cookiez plugin to add a cookie policy to your website.", "elementor"),
1740
+ categories: ["compliance"],
1741
+ severity: "info",
1742
+ weight: 1,
1743
+ evaluate: (ctx) => {
1744
+ if (ctx.pageContext.cookiez_plugin_active) {
1745
+ return { status: "pass" };
1746
+ }
1747
+ return {
1748
+ status: "fail",
1749
+ violations: [
1750
+ {
1751
+ auditId: audit2.id,
1752
+ label: __24("Cookiez plugin is not installed or active.", "elementor"),
1753
+ externalUrl: ctx.pageContext.cookiez_plugin_url
1754
+ }
1755
+ ]
1756
+ };
1757
+ }
1758
+ };
1759
+
1760
+ // src/audits/deep-nesting.ts
1761
+ import { __ as __25 } from "@wordpress/i18n";
1762
+ var MAX_NESTING_DEPTH = 6;
1763
+ var audit3 = {
1764
+ id: "audits/deep-nesting",
1765
+ title: __25("Deep container nesting", "elementor"),
1766
+ description: __25(
1767
+ "Container nesting exceeds the recommended depth. Deep DOM trees hurt rendering performance and readability.",
1768
+ "elementor"
1769
+ ),
1770
+ fixHint: __25("Flatten the layout by removing unnecessary wrapper containers.", "elementor"),
1771
+ categories: ["performance", "best-practices"],
1772
+ severity: "warning",
1773
+ weight: 5,
1774
+ evaluate: (ctx) => {
1775
+ if (ctx.elements.tree.length === 0) {
1776
+ return { status: "skipped", reason: __25("No elements", "elementor") };
1777
+ }
1778
+ const violations = [];
1779
+ walkElements(ctx.elements.tree, (node, parents) => {
1780
+ if (node.elType !== "container") {
1781
+ return;
1782
+ }
1783
+ const depth = parents.length + 1;
1784
+ if (depth > MAX_NESTING_DEPTH) {
1785
+ violations.push({
1786
+ auditId: audit3.id,
1787
+ elementId: node.id,
1788
+ targetHint: "element-settings",
1789
+ label: __25("Container nesting is too deep.", "elementor")
1790
+ });
1791
+ }
1792
+ });
1793
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
1794
+ }
1795
+ };
1796
+
1797
+ // src/audits/default-design-system.ts
1798
+ import { __ as __26 } from "@wordpress/i18n";
1799
+ var audit4 = {
1800
+ id: "audits/default-design-system",
1801
+ title: __26("Default website kit", "elementor"),
1802
+ description: __26(
1803
+ "Your website is using the default design system colors and fonts. Custom branding makes the website feel uniquely yours.",
1804
+ "elementor"
1805
+ ),
1806
+ fixHint: __26("Open Site Settings and customize your kit (colors, fonts, layout).", "elementor"),
1807
+ categories: ["best-practices"],
1808
+ severity: "info",
1809
+ weight: 1,
1810
+ evaluate: (ctx) => {
1811
+ if (!ctx.pageContext.kit_is_default_unchanged) {
1812
+ return { status: "pass" };
1813
+ }
1814
+ return {
1815
+ status: "fail",
1816
+ violations: [
1817
+ {
1818
+ auditId: audit4.id,
1819
+ label: __26("Kit appears unchanged from default.", "elementor"),
1820
+ targetHint: "site-settings"
1821
+ }
1822
+ ]
1823
+ };
1824
+ }
1825
+ };
1826
+
1827
+ // src/audits/deprecated-widgets.ts
1828
+ import { getWidgetsCache } from "@elementor/editor-elements";
1829
+ import { __ as __27 } from "@wordpress/i18n";
1830
+ var DEPRECATION_CONTROL_NAME = "deprecation_message";
1831
+ var audit5 = {
1832
+ id: "audits/deprecated-widgets",
1833
+ title: __27("Deprecated widgets", "elementor"),
1834
+ description: __27(
1835
+ "Deprecated widgets should not be used. For better capabilities use the recommended replacement.",
1836
+ "elementor"
1837
+ ),
1838
+ fixHint: __27("Replace each deprecated widget with the recommended new widget shown in its panel.", "elementor"),
1839
+ categories: ["best-practices", "performance"],
1840
+ severity: "warning",
1841
+ weight: 7,
1842
+ evaluate: (ctx) => {
1843
+ const widgetsCache = getWidgetsCache();
1844
+ if (!widgetsCache) {
1845
+ return { status: "skipped", reason: "widgets-cache-unavailable" };
1846
+ }
1847
+ const violations = [];
1848
+ walkElements(ctx.elements.tree, (node) => {
1849
+ if (node.elType !== "widget") {
1850
+ return;
1851
+ }
1852
+ const config = widgetsCache[node.widgetType ?? ""];
1853
+ if (!config?.controls) {
1854
+ return;
1855
+ }
1856
+ const controls = config.controls;
1857
+ const deprecationControl = controls[DEPRECATION_CONTROL_NAME];
1858
+ if (!deprecationControl || typeof deprecationControl !== "object") {
1859
+ return;
1860
+ }
1861
+ violations.push({
1862
+ auditId: audit5.id,
1863
+ elementId: node.id,
1864
+ targetHint: "element-settings",
1865
+ label: __27("Using deprecated widget.", "elementor")
1866
+ });
1867
+ });
1868
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
1869
+ }
1870
+ };
1871
+
1872
+ // src/audits/heading-structure.ts
1873
+ import { __ as __28 } from "@wordpress/i18n";
1874
+ var HEADING_LEVELS = ["h1", "h2", "h3", "h4", "h5", "h6"];
1875
+ var HEADING_CONTROLS_REGISTRY = {
1876
+ // Core
1877
+ heading: [{ control: "header_size", requiresNonEmpty: "title" }],
1878
+ "e-heading": [{ control: "tag", requiresNonEmpty: "title" }],
1879
+ "icon-box": [{ control: "title_size", requiresNonEmpty: "title_text" }],
1880
+ "image-box": [{ control: "title_size", requiresNonEmpty: "title_text" }],
1881
+ counter: [{ control: "title_tag", requiresNonEmpty: "title" }],
1882
+ progress: [{ control: "title_tag", when: { title_display: "yes" }, requiresNonEmpty: "title" }],
1883
+ accordion: [{ control: "title_html_tag", repeat: { items: "tabs", textField: "tab_title" } }],
1884
+ toggle: [{ control: "title_html_tag", repeat: { items: "tabs", textField: "tab_title" } }],
1885
+ divider: [{ control: "html_tag", when: { look: "line_text" }, requiresNonEmpty: "text" }],
1886
+ "nested-accordion": [{ control: "title_tag", repeat: { items: "items", textField: "item_title" } }],
1887
+ "link-in-bio": [
1888
+ { control: "bio_heading_tag", requiresNonEmpty: "bio_heading" },
1889
+ { control: "bio_title_tag", requiresNonEmpty: "bio_title" },
1890
+ { control: "bio_about_tag", requiresNonEmpty: "bio_about" }
1891
+ ],
1892
+ // Pro — static / repeater
1893
+ "animated-headline": [
1894
+ {
1895
+ control: "tag",
1896
+ requiresAnyNonEmpty: ["before_text", "highlighted_text", "rotating_text", "after_text"]
1897
+ }
1898
+ ],
1899
+ "author-box": [{ control: "author_name_tag", requiresNonEmpty: "author_name" }],
1900
+ "call-to-action": [
1901
+ { control: "title_tag", requiresNonEmpty: "title" },
1902
+ { control: "description_tag", requiresNonEmpty: "description" }
1903
+ ],
1904
+ "flip-box": [
1905
+ { control: "title_tag", requiresNonEmpty: "title_text_a" },
1906
+ { control: "description_tag", requiresNonEmpty: "description_text_a" },
1907
+ { control: "title_tag", requiresNonEmpty: "title_text_b" },
1908
+ { control: "description_tag", requiresNonEmpty: "description_text_b" }
1909
+ ],
1910
+ "price-list": [
1911
+ { control: "title_tag", repeat: { items: "price_list", textField: "title" } },
1912
+ { control: "description_tag", repeat: { items: "price_list", textField: "item_description" } }
1913
+ ],
1914
+ "price-table": [{ control: "heading_tag", requiresNonEmpty: "heading" }],
1915
+ slides: [
1916
+ { control: "slides_title_tag", repeat: { items: "slides", textField: "heading" } },
1917
+ { control: "slides_description_tag", repeat: { items: "slides", textField: "description" } }
1918
+ ],
1919
+ "table-of-contents": [{ control: "html_tag", requiresNonEmpty: "title" }],
1920
+ "video-playlist": [
1921
+ { control: "playlist_title_tag", requiresNonEmpty: "playlist_title" },
1922
+ {
1923
+ control: "section_html_tag",
1924
+ repeat: { items: "tabs", tagSource: "item", itemWhen: { type: "section" }, textField: "title" }
1925
+ },
1926
+ {
1927
+ control: "video_html_tag",
1928
+ repeat: {
1929
+ items: "tabs",
1930
+ tagSource: "item",
1931
+ itemWhen: { type: ["youtube", "vimeo", "hosted"] },
1932
+ textField: "title"
1933
+ }
1934
+ }
1935
+ ],
1936
+ sitemap: [{ control: "sitemap_title_tag", repeat: { items: "sitemap_items", textField: "sitemap_title" } }],
1937
+ "loop-grid": [
1938
+ {
1939
+ control: "nothing_found_message_html_tag",
1940
+ when: { enable_nothing_found_message: "yes" },
1941
+ requiresNonEmpty: "nothing_found_message_text"
1942
+ }
1943
+ ],
1944
+ search: [
1945
+ {
1946
+ control: "nothing_found_message_html_tag",
1947
+ when: { enable_nothing_found_message: "yes" },
1948
+ requiresNonEmpty: "nothing_found_message_text"
1949
+ }
1950
+ ],
1951
+ // Pro — theme / Woo
1952
+ posts: [{ control: "title_tag", when: { show_title: "yes" } }],
1953
+ "archive-posts": [{ control: "title_tag", when: { show_title: "yes" } }],
1954
+ portfolio: [{ control: "title_tag", when: { show_title: "yes" } }],
1955
+ "theme-post-title": [{ control: "header_size" }],
1956
+ "theme-page-title": [{ control: "header_size" }],
1957
+ "theme-archive-title": [{ control: "header_size" }],
1958
+ "theme-site-title": [{ control: "header_size" }],
1959
+ "woocommerce-product-title": [{ control: "header_size" }]
1960
+ };
1961
+ function isNonEmptyString(value) {
1962
+ return typeof value === "string" && value.trim() !== "";
1963
+ }
1964
+ function matchesWhen(settings, when) {
1965
+ if (!when) {
1966
+ return true;
1967
+ }
1968
+ return Object.entries(when).every(([key, expected]) => {
1969
+ const actual = settings[key];
1970
+ if (Array.isArray(expected)) {
1971
+ return expected.includes(String(actual));
1972
+ }
1973
+ return String(actual) === expected;
1974
+ });
1975
+ }
1976
+ function hasRequiredText(settings, key) {
1977
+ if (!key) {
1978
+ return true;
1979
+ }
1980
+ return isNonEmptyString(settings[key]);
1981
+ }
1982
+ function hasAnyRequiredText(settings, keys) {
1983
+ if (!keys?.length) {
1984
+ return true;
1985
+ }
1986
+ return keys.some((key) => isNonEmptyString(settings[key]));
1987
+ }
1988
+ function parseHeadingLevel(raw) {
1989
+ if (typeof raw !== "string") {
1990
+ return null;
1991
+ }
1992
+ const idx = HEADING_LEVELS.indexOf(raw.toLowerCase());
1993
+ return idx >= 0 ? idx + 1 : null;
1994
+ }
1995
+ function getRepeaterRows(settings, key) {
1996
+ const rows = settings[key];
1997
+ return Array.isArray(rows) ? rows : [];
1998
+ }
1999
+ function pushHeading(headings, elementId, raw) {
2000
+ const level = parseHeadingLevel(raw);
2001
+ if (level !== null) {
2002
+ headings.push({ elementId, level });
2003
+ }
2004
+ }
2005
+ function extractHeadingsFromWidget(elementId, widgetType, settings) {
2006
+ const descriptors = HEADING_CONTROLS_REGISTRY[widgetType];
2007
+ if (!descriptors) {
2008
+ return [];
2009
+ }
2010
+ const headings = [];
2011
+ for (const descriptor of descriptors) {
2012
+ if (!matchesWhen(settings, descriptor.when)) {
2013
+ continue;
2014
+ }
2015
+ if (!hasRequiredText(settings, descriptor.requiresNonEmpty)) {
2016
+ continue;
2017
+ }
2018
+ if (!hasAnyRequiredText(settings, descriptor.requiresAnyNonEmpty)) {
2019
+ continue;
2020
+ }
2021
+ if (descriptor.repeat) {
2022
+ const { items, textField, itemWhen, tagSource = "widget" } = descriptor.repeat;
2023
+ for (const row of getRepeaterRows(settings, items)) {
2024
+ if (!matchesWhen(row, itemWhen)) {
2025
+ continue;
2026
+ }
2027
+ if (textField && !hasRequiredText(row, textField)) {
2028
+ continue;
2029
+ }
2030
+ const raw = tagSource === "item" ? row[descriptor.control] : settings[descriptor.control];
2031
+ pushHeading(headings, elementId, raw);
2032
+ }
2033
+ } else {
2034
+ pushHeading(headings, elementId, settings[descriptor.control]);
2035
+ }
2036
+ }
2037
+ return headings;
2038
+ }
2039
+ function extractPageHeadings(tree) {
2040
+ const headings = [];
2041
+ walkElements(tree, (node) => {
2042
+ if (node.elType !== "widget" || !node.widgetType) {
2043
+ return;
2044
+ }
2045
+ headings.push(...extractHeadingsFromWidget(node.id, node.widgetType, node.settings));
2046
+ });
2047
+ return headings;
2048
+ }
2049
+ var audit6 = {
2050
+ id: "audits/heading-structure",
2051
+ title: __28("Heading structure", "elementor"),
2052
+ description: __28("Pages should have exactly one H1 and a non-skipping heading order.", "elementor"),
2053
+ fixHint: __28("Ensure your page has one H1 and that heading levels do not skip (no H2 \u2192 H4).", "elementor"),
2054
+ categories: ["seo", "accessibility"],
2055
+ severity: "error",
2056
+ weight: 10,
2057
+ evaluate: (ctx) => {
2058
+ if (ctx.elements.tree.length === 0) {
2059
+ return { status: "skipped", reason: __28("No elements", "elementor") };
2060
+ }
2061
+ const headings = extractPageHeadings(ctx.elements.tree);
2062
+ const violations = [];
2063
+ if (headings.length === 0) {
2064
+ violations.push({
2065
+ auditId: audit6.id,
2066
+ label: __28("No headings found on the page.", "elementor")
2067
+ });
2068
+ } else {
2069
+ const h1Count = headings.filter((h) => h.level === 1).length;
2070
+ if (h1Count === 0) {
2071
+ violations.push({ auditId: audit6.id, label: __28("No H1 on the page.", "elementor") });
2072
+ }
2073
+ if (h1Count > 1) {
2074
+ headings.filter((h) => h.level === 1).slice(1).forEach(
2075
+ (h) => violations.push({
2076
+ auditId: audit6.id,
2077
+ elementId: h.elementId,
2078
+ targetHint: "element-settings",
2079
+ label: __28("More than one H1 on the page.", "elementor")
2080
+ })
2081
+ );
2082
+ }
2083
+ for (let i = 1; i < headings.length; i++) {
2084
+ if (headings[i].level - headings[i - 1].level > 1) {
2085
+ violations.push({
2086
+ auditId: audit6.id,
2087
+ elementId: headings[i].elementId,
2088
+ targetHint: "element-settings",
2089
+ label: __28("Heading level skipped.", "elementor")
2090
+ });
2091
+ }
2092
+ }
2093
+ }
2094
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
2095
+ }
2096
+ };
2097
+
2098
+ // src/audits/hidden-elements.ts
2099
+ import { __ as __29 } from "@wordpress/i18n";
2100
+ function isHiddenOnAllDevices(settings) {
2101
+ return Boolean(settings.hide_desktop) && Boolean(settings.hide_tablet) && Boolean(settings.hide_mobile);
2102
+ }
2103
+ var audit7 = {
2104
+ id: "audits/hidden-elements",
2105
+ title: __29("Hidden elements", "elementor"),
2106
+ description: __29(
2107
+ "Elements hidden on all devices simultaneously are effectively dead code in the page tree.",
2108
+ "elementor"
2109
+ ),
2110
+ fixHint: __29("Remove the element or make it visible on at least one device breakpoint.", "elementor"),
2111
+ categories: ["best-practices", "performance"],
2112
+ severity: "info",
2113
+ weight: 3,
2114
+ evaluate: (ctx) => {
2115
+ if (ctx.elements.tree.length === 0) {
2116
+ return { status: "skipped", reason: __29("No elements", "elementor") };
2117
+ }
2118
+ const violations = [];
2119
+ walkElements(ctx.elements.tree, (node) => {
2120
+ if (isHiddenOnAllDevices(node.settings)) {
2121
+ violations.push({
2122
+ auditId: audit7.id,
2123
+ elementId: node.id,
2124
+ targetHint: "element-settings",
2125
+ label: __29("Element is hidden on all devices.", "elementor")
2126
+ });
2127
+ }
2128
+ });
2129
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
2130
+ }
2131
+ };
2132
+
2133
+ // src/audits/images-alt-text.ts
2134
+ import { __ as __30 } from "@wordpress/i18n";
2135
+
2136
+ // src/utils/image-alt.ts
2137
+ function hasMeaningfulAlt(media, pageContext) {
2138
+ if (!media.id && !media.url) {
2139
+ return true;
2140
+ }
2141
+ if (media.id) {
2142
+ const alt = pageContext.image_sizes[media.id]?.alt ?? "";
2143
+ return alt.trim().length > 0;
2144
+ }
2145
+ return (media.alt ?? "").trim().length > 0;
2146
+ }
2147
+ function isImageSourcePresent(media) {
2148
+ return Boolean(media.id || media.url);
2149
+ }
2150
+
2151
+ // src/audits/images-alt-text.ts
2152
+ var audit8 = {
2153
+ id: "audits/images-alt-text",
2154
+ title: __30("Images alt text", "elementor"),
2155
+ description: __30("Every image needs a meaningful alt attribute for screen readers and image-search.", "elementor"),
2156
+ fixHint: __30("Open the image's settings and add an Alt Text describing the image.", "elementor"),
2157
+ categories: ["seo", "accessibility"],
2158
+ severity: "error",
2159
+ weight: 10,
2160
+ evaluate: (ctx) => {
2161
+ if (!hasPageImages(ctx.elements.tree)) {
2162
+ return { status: "skipped", reason: __30("No images", "elementor") };
2163
+ }
2164
+ const violations = [];
2165
+ const failedWidgetIds = /* @__PURE__ */ new Set();
2166
+ let missingAltImageCount = 0;
2167
+ walkImageLikeSources(ctx.elements.tree, ({ node, media }) => {
2168
+ if (!isImageSourcePresent(media) || hasMeaningfulAlt(media, ctx.pageContext)) {
2169
+ return;
2170
+ }
2171
+ missingAltImageCount++;
2172
+ if (!failedWidgetIds.has(node.id)) {
2173
+ failedWidgetIds.add(node.id);
2174
+ violations.push({
2175
+ auditId: audit8.id,
2176
+ elementId: node.id,
2177
+ targetHint: "element-settings",
2178
+ label: __30("Image is missing alt text.", "elementor"),
2179
+ externalUrl: ctx.pageContext.ally_plugin_url
2180
+ });
2181
+ }
2182
+ });
2183
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations, metadata: { missingAltImageCount } };
2184
+ }
2185
+ };
2186
+
2187
+ // src/audits/images-too-large.ts
2188
+ import { __ as __31, sprintf as sprintf5 } from "@wordpress/i18n";
2189
+ var SIZE_THRESHOLD_BYTES = 500 * 1024;
2190
+ var BYTES_PER_KB = 1024;
2191
+ var audit9 = {
2192
+ id: "audits/images-too-large",
2193
+ title: __31("Oversized images", "elementor"),
2194
+ description: __31("Large image files slow down the page.", "elementor"),
2195
+ fixHint: __31("Replace the image with a smaller version or enable image optimization.", "elementor"),
2196
+ categories: ["performance"],
2197
+ severity: "warning",
2198
+ weight: 7,
2199
+ evaluate: (ctx) => {
2200
+ if (!hasPageImages(ctx.elements.tree)) {
2201
+ return { status: "skipped", reason: __31("No images", "elementor") };
2202
+ }
2203
+ const widgetMaxKb = /* @__PURE__ */ new Map();
2204
+ let oversizedImageCount = 0;
2205
+ walkImageLikeSources(ctx.elements.tree, ({ node, media }) => {
2206
+ const id = media.id;
2207
+ if (!id) {
2208
+ return;
2209
+ }
2210
+ const size = ctx.pageContext.image_sizes[id];
2211
+ if (!size || size.filesize_bytes <= SIZE_THRESHOLD_BYTES) {
2212
+ return;
2213
+ }
2214
+ oversizedImageCount++;
2215
+ const kb = Math.round(size.filesize_bytes / BYTES_PER_KB);
2216
+ const currentMax = widgetMaxKb.get(node.id) ?? 0;
2217
+ widgetMaxKb.set(node.id, Math.max(currentMax, kb));
2218
+ });
2219
+ const violations = Array.from(widgetMaxKb.entries()).map(([elementId, kb]) => ({
2220
+ auditId: audit9.id,
2221
+ elementId,
2222
+ targetHint: "element-settings",
2223
+ label: sprintf5(
2224
+ /* translators: %d is the image file size in kilobytes. */
2225
+ __31("Image is %d KB (over 500 KB).", "elementor"),
2226
+ kb
2227
+ ),
2228
+ externalUrl: ctx.pageContext.image_optimization_plugin_url
2229
+ }));
2230
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations, metadata: { oversizedImageCount } };
2231
+ }
2232
+ };
2233
+
2234
+ // src/audits/nested-boxed-containers.ts
2235
+ import { __ as __32 } from "@wordpress/i18n";
2236
+ function isBoxed(settings) {
2237
+ return settings.content_width === "boxed";
2238
+ }
2239
+ var audit10 = {
2240
+ id: "audits/nested-boxed-containers",
2241
+ title: __32("Boxed container nested inside a boxed parent", "elementor"),
2242
+ description: __32("An inner container does not need to be boxed when its parent already is.", "elementor"),
2243
+ fixHint: __32("Change the inner container's content width to Full Width.", "elementor"),
2244
+ categories: ["best-practices", "performance"],
2245
+ severity: "info",
2246
+ weight: 5,
2247
+ evaluate: (ctx) => {
2248
+ if (ctx.elements.tree.length === 0) {
2249
+ return { status: "skipped", reason: __32("No elements", "elementor") };
2250
+ }
2251
+ const violations = [];
2252
+ walkElements(ctx.elements.tree, (node, parents) => {
2253
+ if (node.elType !== "container" || !isBoxed(node.settings)) {
2254
+ return;
2255
+ }
2256
+ const nearestContainerAncestor = [...parents].reverse().find((p) => p.elType === "container");
2257
+ if (nearestContainerAncestor && isBoxed(nearestContainerAncestor.settings)) {
2258
+ violations.push({
2259
+ auditId: audit10.id,
2260
+ elementId: node.id,
2261
+ targetHint: "element-settings",
2262
+ label: __32("Nested boxed container.", "elementor")
2263
+ });
2264
+ }
2265
+ });
2266
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
2267
+ }
2268
+ };
2269
+
2270
+ // src/audits/page-excerpt.ts
2271
+ import { __ as __33 } from "@wordpress/i18n";
2272
+ var audit11 = {
2273
+ id: "audits/page-excerpt",
2274
+ title: __33("Page excerpt", "elementor"),
2275
+ description: __33("A descriptive excerpt helps search engines and previews summarize the page.", "elementor"),
2276
+ fixHint: __33("Open Page Settings and write a short excerpt.", "elementor"),
2277
+ categories: ["seo"],
2278
+ severity: "warning",
2279
+ weight: 5,
2280
+ evaluate: (ctx) => {
2281
+ if (ctx.pageContext.post_excerpt) {
2282
+ return { status: "pass" };
2283
+ }
2284
+ return {
2285
+ status: "fail",
2286
+ violations: [
2287
+ {
2288
+ auditId: audit11.id,
2289
+ label: __33("Page has no excerpt.", "elementor"),
2290
+ targetHint: "page-settings",
2291
+ angieFix: true
2292
+ }
2293
+ ]
2294
+ };
2295
+ }
2296
+ };
2297
+
2298
+ // src/audits/page-featured-image.ts
2299
+ import { __ as __34 } from "@wordpress/i18n";
2300
+ var audit12 = {
2301
+ id: "audits/page-featured-image",
2302
+ title: __34("Page featured image", "elementor"),
2303
+ description: __34("Featured images are used by social shares and many themes for hero visuals.", "elementor"),
2304
+ fixHint: __34("Open Page Settings and set a featured image.", "elementor"),
2305
+ categories: ["seo"],
2306
+ severity: "info",
2307
+ weight: 5,
2308
+ evaluate: (ctx) => {
2309
+ if (ctx.pageContext.featured_image_id) {
2310
+ return { status: "pass" };
2311
+ }
2312
+ return {
2313
+ status: "fail",
2314
+ violations: [
2315
+ {
2316
+ auditId: audit12.id,
2317
+ label: __34("No featured image set.", "elementor"),
2318
+ targetHint: "page-settings"
2319
+ }
2320
+ ]
2321
+ };
2322
+ }
2323
+ };
2324
+
2325
+ // src/audits/page-title.ts
2326
+ import { __ as __35 } from "@wordpress/i18n";
2327
+ var MAX_TITLE_LENGTH = 60;
2328
+ var audit13 = {
2329
+ id: "audits/page-title",
2330
+ title: __35("Page title", "elementor"),
2331
+ description: __35("Pages need a clear title for SEO and screen-reader navigation.", "elementor"),
2332
+ fixHint: __35("Open Page Settings and add a title.", "elementor"),
2333
+ categories: ["seo"],
2334
+ severity: "error",
2335
+ weight: 10,
2336
+ evaluate: (ctx) => {
2337
+ const title = ctx.pageContext.post_title;
2338
+ if (!title) {
2339
+ return {
2340
+ status: "fail",
2341
+ violations: [
2342
+ {
2343
+ auditId: audit13.id,
2344
+ label: __35("Page has no title.", "elementor"),
2345
+ targetHint: "page-settings",
2346
+ angieFix: true
2347
+ }
2348
+ ]
2349
+ };
2350
+ }
2351
+ if (title.length > MAX_TITLE_LENGTH) {
2352
+ return {
2353
+ status: "fail",
2354
+ violations: [
2355
+ {
2356
+ auditId: audit13.id,
2357
+ label: __35("Page title is too long.", "elementor"),
2358
+ targetHint: "page-settings",
2359
+ angieFix: true
2360
+ }
2361
+ ]
2362
+ };
2363
+ }
2364
+ return { status: "pass" };
2365
+ }
2366
+ };
2367
+
2368
+ // src/audits/prefer-global-colors.ts
2369
+ import { __ as __36, sprintf as sprintf6 } from "@wordpress/i18n";
2370
+
2371
+ // src/utils/collect-hardcoded-colors.ts
2372
+ var RESERVED_SETTING_KEYS = /* @__PURE__ */ new Set(["__globals__", "__dynamic__"]);
2373
+ var REPEATER_ROW_ID_KEY = "_id";
2374
+ var HEX_COLOR_PATTERN = /^#[0-9a-f]{3,8}$/i;
2375
+ var RGB_COLOR_PATTERN = /^rgba?\(/i;
2376
+ function isGlobalColorReference(value) {
2377
+ return value.startsWith("globals/");
2378
+ }
2379
+ function isHardcodedColorCandidate(value) {
2380
+ const trimmed = value.trim();
2381
+ return HEX_COLOR_PATTERN.test(trimmed) || RGB_COLOR_PATTERN.test(trimmed);
2382
+ }
2383
+ function formatPath(prefix, key) {
2384
+ return prefix ? `${prefix}.${key}` : key;
2385
+ }
2386
+ function formatArrayPath(prefix, index) {
2387
+ return `${prefix}[${index}]`;
2388
+ }
2389
+ function collectHardcodedColors(settings, pathPrefix = "") {
2390
+ const linkedGlobals = settings.__globals__ ?? {};
2391
+ const results = [];
2392
+ for (const [key, value] of Object.entries(settings)) {
2393
+ if (RESERVED_SETTING_KEYS.has(key) || key === REPEATER_ROW_ID_KEY) {
2394
+ continue;
2395
+ }
2396
+ if (linkedGlobals[key]) {
2397
+ continue;
2398
+ }
2399
+ if (typeof value === "string") {
2400
+ if (!isGlobalColorReference(value) && isHardcodedColorCandidate(value)) {
2401
+ results.push({ path: formatPath(pathPrefix, key), value });
2402
+ }
2403
+ continue;
2404
+ }
2405
+ if (Array.isArray(value)) {
2406
+ const arrayPath = formatPath(pathPrefix, key);
2407
+ value.forEach((item, index) => {
2408
+ if (item && typeof item === "object" && !Array.isArray(item)) {
2409
+ results.push(
2410
+ ...collectHardcodedColors(
2411
+ item,
2412
+ formatArrayPath(arrayPath, index)
2413
+ )
2414
+ );
2415
+ }
2416
+ });
2417
+ continue;
2418
+ }
2419
+ if (value && typeof value === "object") {
2420
+ results.push(
2421
+ ...collectHardcodedColors(value, formatPath(pathPrefix, key))
2422
+ );
2423
+ }
2424
+ }
2425
+ return results;
2426
+ }
2427
+
2428
+ // src/utils/match-global-by-value.ts
2429
+ function findMatchingGlobalByValue(value, globals) {
2430
+ const trimmed = value.trim();
2431
+ return globals.find((global) => global.value === trimmed) ?? null;
2432
+ }
2433
+
2434
+ // src/audits/prefer-global-colors.ts
2435
+ var audit14 = {
2436
+ id: "audits/prefer-global-colors",
2437
+ title: __36("Prefer global colors", "elementor"),
2438
+ description: __36("Global colors make the design consistent and easy to update site-wide.", "elementor"),
2439
+ fixHint: __36("Replace the hard-coded value with the matching global color.", "elementor"),
2440
+ categories: ["best-practices"],
2441
+ severity: "info",
2442
+ weight: 3,
2443
+ evaluate: (ctx) => {
2444
+ if (ctx.elements.tree.length === 0) {
2445
+ return { status: "skipped", reason: __36("No elements", "elementor") };
2446
+ }
2447
+ if (ctx.kit.globals.colors.length === 0) {
2448
+ return { status: "skipped", reason: __36("No global colors", "elementor") };
2449
+ }
2450
+ const violations = [];
2451
+ walkElements(ctx.elements.tree, (node) => {
2452
+ for (const { value } of collectHardcodedColors(node.settings)) {
2453
+ const global = findMatchingGlobalByValue(value, ctx.kit.globals.colors);
2454
+ if (!global) {
2455
+ continue;
2456
+ }
2457
+ violations.push({
2458
+ auditId: audit14.id,
2459
+ elementId: node.id,
2460
+ targetHint: "element-settings",
2461
+ angieFix: true,
2462
+ label: sprintf6(
2463
+ /* translators: 1: hard-coded color value, 2: global color title. */
2464
+ __36('replace "%1$s" with "%2$s" global color', "elementor"),
2465
+ value,
2466
+ global.title
2467
+ )
2468
+ });
2469
+ }
2470
+ });
2471
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
2472
+ }
2473
+ };
2474
+
2475
+ // src/audits/prefer-global-fonts.ts
2476
+ import { __ as __37, sprintf as sprintf7 } from "@wordpress/i18n";
2477
+
2478
+ // src/utils/collect-hardcoded-fonts.ts
2479
+ var RESERVED_SETTING_KEYS2 = /* @__PURE__ */ new Set(["__globals__", "__dynamic__"]);
2480
+ var REPEATER_ROW_ID_KEY2 = "_id";
2481
+ var FONT_FAMILY_KEY_PATTERN = /font_family/;
2482
+ function isGlobalFontReference(value) {
2483
+ return value.startsWith("globals/");
2484
+ }
2485
+ function isHardcodedFontCandidate(key, value) {
2486
+ return FONT_FAMILY_KEY_PATTERN.test(key) && value.trim().length > 0;
2487
+ }
2488
+ function formatPath2(prefix, key) {
2489
+ return prefix ? `${prefix}.${key}` : key;
2490
+ }
2491
+ function formatArrayPath2(prefix, index) {
2492
+ return `${prefix}[${index}]`;
2493
+ }
2494
+ function collectHardcodedFonts(settings, pathPrefix = "") {
2495
+ const linkedGlobals = settings.__globals__ ?? {};
2496
+ const results = [];
2497
+ for (const [key, value] of Object.entries(settings)) {
2498
+ if (RESERVED_SETTING_KEYS2.has(key) || key === REPEATER_ROW_ID_KEY2) {
2499
+ continue;
2500
+ }
2501
+ if (linkedGlobals[key]) {
2502
+ continue;
2503
+ }
2504
+ if (typeof value === "string") {
2505
+ if (!isGlobalFontReference(value) && isHardcodedFontCandidate(key, value)) {
2506
+ results.push({ path: formatPath2(pathPrefix, key), value });
2507
+ }
2508
+ continue;
2509
+ }
2510
+ if (Array.isArray(value)) {
2511
+ const arrayPath = formatPath2(pathPrefix, key);
2512
+ value.forEach((item, index) => {
2513
+ if (item && typeof item === "object" && !Array.isArray(item)) {
2514
+ results.push(
2515
+ ...collectHardcodedFonts(
2516
+ item,
2517
+ formatArrayPath2(arrayPath, index)
2518
+ )
2519
+ );
2520
+ }
2521
+ });
2522
+ continue;
2523
+ }
2524
+ if (value && typeof value === "object") {
2525
+ results.push(
2526
+ ...collectHardcodedFonts(value, formatPath2(pathPrefix, key))
2527
+ );
2528
+ }
2529
+ }
2530
+ return results;
2531
+ }
2532
+
2533
+ // src/audits/prefer-global-fonts.ts
2534
+ var audit15 = {
2535
+ id: "audits/prefer-global-fonts",
2536
+ title: __37("Prefer global fonts", "elementor"),
2537
+ description: __37("Global fonts make the design consistent and easy to update site-wide.", "elementor"),
2538
+ fixHint: __37("Replace the hard-coded value with the matching global font.", "elementor"),
2539
+ categories: ["best-practices", "performance"],
2540
+ severity: "info",
2541
+ weight: 3,
2542
+ evaluate: (ctx) => {
2543
+ if (ctx.elements.tree.length === 0) {
2544
+ return { status: "skipped", reason: __37("No elements", "elementor") };
2545
+ }
2546
+ if (ctx.kit.globals.fonts.length === 0) {
2547
+ return { status: "skipped", reason: __37("No global fonts", "elementor") };
2548
+ }
2549
+ const violations = [];
2550
+ walkElements(ctx.elements.tree, (node) => {
2551
+ for (const { value } of collectHardcodedFonts(node.settings)) {
2552
+ const global = findMatchingGlobalByValue(value, ctx.kit.globals.fonts);
2553
+ if (!global) {
2554
+ continue;
2555
+ }
2556
+ violations.push({
2557
+ auditId: audit15.id,
2558
+ elementId: node.id,
2559
+ targetHint: "element-settings",
2560
+ label: sprintf7(
2561
+ /* translators: %s: global font title. */
2562
+ __37('replace hardcoded typography with "%s" global font', "elementor"),
2563
+ global.title
2564
+ )
2565
+ });
2566
+ }
2567
+ });
2568
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
2569
+ }
2570
+ };
2571
+
2572
+ // src/audits/privacy-policy.ts
2573
+ import { __ as __38 } from "@wordpress/i18n";
2574
+ var audit16 = {
2575
+ id: "audits/privacy-policy",
2576
+ title: __38("Privacy policy", "elementor"),
2577
+ description: __38(
2578
+ "A privacy policy page is required by privacy regulations such as GDPR (EU) and CCPA (US).",
2579
+ "elementor"
2580
+ ),
2581
+ fixHint: __38(
2582
+ "Go to Settings > Privacy in the WordPress admin and assign a published privacy policy page.",
2583
+ "elementor"
2584
+ ),
2585
+ categories: ["compliance"],
2586
+ severity: "info",
2587
+ weight: 1,
2588
+ evaluate: (ctx) => {
2589
+ if (ctx.pageContext.privacy_policy_url) {
2590
+ return { status: "pass" };
2591
+ }
2592
+ return {
2593
+ status: "fail",
2594
+ violations: [
2595
+ {
2596
+ auditId: audit16.id,
2597
+ label: __38("No privacy policy page is set.", "elementor"),
2598
+ externalUrl: ctx.pageContext.privacy_settings_url
2599
+ }
2600
+ ]
2601
+ };
2602
+ }
2603
+ };
2604
+
2605
+ // src/audits/robots-noindex.ts
2606
+ import { __ as __39 } from "@wordpress/i18n";
2607
+ var audit17 = {
2608
+ id: "audits/robots-noindex",
2609
+ title: __39("Search engine visibility", "elementor"),
2610
+ description: __39(
2611
+ "When search engine visibility is checked in WordPress Settings -> Reading -> Search Engine Visibility, search engines are discouraged from indexing the website.",
2612
+ "elementor"
2613
+ ),
2614
+ fixHint: __39(
2615
+ 'Go to Settings \u2192 Reading in the WordPress admin and uncheck "Discourage search engines from indexing this site" when the website should be indexed.',
2616
+ "elementor"
2617
+ ),
2618
+ categories: ["seo"],
2619
+ severity: "error",
2620
+ weight: 7,
2621
+ evaluate: (ctx) => {
2622
+ if (!ctx.pageContext.is_noindex) {
2623
+ return { status: "pass" };
2624
+ }
2625
+ return {
2626
+ status: "fail",
2627
+ violations: [
2628
+ {
2629
+ auditId: audit17.id,
2630
+ label: __39("Search engines are discouraged from indexing this website.", "elementor"),
2631
+ externalUrl: ctx.pageContext.reading_settings_url
2632
+ }
2633
+ ]
2634
+ };
2635
+ }
2636
+ };
2637
+
2638
+ // src/audits/sections-and-columns.ts
2639
+ import { __ as __40 } from "@wordpress/i18n";
2640
+ var audit18 = {
2641
+ id: "audits/sections-and-columns",
2642
+ title: __40("Sections and columns", "elementor"),
2643
+ description: __40(
2644
+ "Sections and columns are legacy elements. Containers render fewer DOM nodes and are more flexible.",
2645
+ "elementor"
2646
+ ),
2647
+ fixHint: __40("Use the Container Converter to replace each section/column with a container.", "elementor"),
2648
+ categories: ["best-practices", "performance"],
2649
+ severity: "warning",
2650
+ weight: 7,
2651
+ evaluate: (ctx) => {
2652
+ if (ctx.elements.tree.length === 0) {
2653
+ return { status: "skipped", reason: __40("No elements", "elementor") };
2654
+ }
2655
+ const violations = [];
2656
+ walkElements(ctx.elements.tree, (node) => {
2657
+ if (node.elType === "section" || node.elType === "column") {
2658
+ violations.push({
2659
+ auditId: audit18.id,
2660
+ elementId: node.id,
2661
+ label: node.elType === "section" ? __40("Section element", "elementor") : __40("Column element", "elementor")
2662
+ });
2663
+ }
2664
+ });
2665
+ return violations.length === 0 ? { status: "pass" } : { status: "fail", violations };
2666
+ }
2667
+ };
2668
+
2669
+ // src/audits/site-identity.ts
2670
+ import { __ as __41 } from "@wordpress/i18n";
2671
+ var audit19 = {
2672
+ id: "audits/site-identity",
2673
+ title: __41("Site identity", "elementor"),
2674
+ description: __41(
2675
+ "Site name, description, logo, and favicon establish your brand and appear in search results and browser tabs.",
2676
+ "elementor"
2677
+ ),
2678
+ fixHint: __41("Open Site Settings \u2192 Site Identity and complete all the missing fields.", "elementor"),
2679
+ categories: ["best-practices", "seo"],
2680
+ severity: "info",
2681
+ weight: 7,
2682
+ evaluate: (ctx) => {
2683
+ const { site_identity: identity } = ctx.pageContext;
2684
+ const violations = [];
2685
+ if (!identity.site_name_set) {
2686
+ violations.push({
2687
+ auditId: audit19.id,
2688
+ label: __41("Site name is missing or still uses the default.", "elementor"),
2689
+ targetHint: "site-identity-settings"
2690
+ });
2691
+ }
2692
+ if (!identity.site_description_set) {
2693
+ violations.push({
2694
+ auditId: audit19.id,
2695
+ label: __41("Site description is missing or still uses the default.", "elementor"),
2696
+ targetHint: "site-identity-settings",
2697
+ angieFix: true
2698
+ });
2699
+ }
2700
+ if (!identity.site_logo_set) {
2701
+ violations.push({
2702
+ auditId: audit19.id,
2703
+ label: __41("Site logo is not set.", "elementor"),
2704
+ targetHint: "site-identity-settings"
2705
+ });
2706
+ }
2707
+ if (!identity.site_favicon_set) {
2708
+ violations.push({
2709
+ auditId: audit19.id,
2710
+ label: __41("Site favicon is not set.", "elementor"),
2711
+ targetHint: "site-identity-settings"
2712
+ });
2713
+ }
2714
+ if (violations.length === 0) {
2715
+ return { status: "pass" };
2716
+ }
2717
+ return {
2718
+ status: "fail",
2719
+ violations
2720
+ };
2721
+ }
2722
+ };
2723
+
2724
+ // src/audits/too-many-widgets.ts
2725
+ import { __ as __42 } from "@wordpress/i18n";
2726
+ var WIDGET_COUNT_THRESHOLD = 100;
2727
+ var audit20 = {
2728
+ id: "audits/too-many-widgets",
2729
+ title: __42("Too many widgets", "elementor"),
2730
+ description: __42("Excessive DOM size caused by too many widgets degrades rendering performance.", "elementor"),
2731
+ fixHint: __42("Reduce the number of widgets on the page by removing or combining elements.", "elementor"),
2732
+ categories: ["best-practices", "performance"],
2733
+ severity: "warning",
2734
+ weight: 5,
2735
+ evaluate: (ctx) => {
2736
+ if (ctx.elements.tree.length === 0) {
2737
+ return { status: "skipped", reason: __42("No elements", "elementor") };
2738
+ }
2739
+ let widgetCount = 0;
2740
+ walkElements(ctx.elements.tree, (node) => {
2741
+ if (node.elType === "widget") {
2742
+ widgetCount++;
2743
+ }
2744
+ });
2745
+ if (widgetCount <= WIDGET_COUNT_THRESHOLD) {
2746
+ return { status: "pass" };
2747
+ }
2748
+ return {
2749
+ status: "fail",
2750
+ violations: [
2751
+ {
2752
+ auditId: audit20.id,
2753
+ label: __42("Page has too many widgets.", "elementor")
2754
+ }
2755
+ ]
2756
+ };
2757
+ }
2758
+ };
2759
+
2760
+ // src/register-audits.ts
2761
+ var AUDITS = [
2762
+ audit13,
2763
+ audit11,
2764
+ audit12,
2765
+ audit7,
2766
+ audit5,
2767
+ audit20,
2768
+ audit18,
2769
+ audit3,
2770
+ audit10,
2771
+ audit4,
2772
+ audit19,
2773
+ audit14,
2774
+ audit15,
2775
+ audit6,
2776
+ audit8,
2777
+ audit9,
2778
+ audit17,
2779
+ audit16,
2780
+ audit,
2781
+ audit2
2782
+ ];
2783
+ function registerAllAudits() {
2784
+ for (const audit21 of AUDITS) {
2785
+ registerAudit(audit21);
2786
+ }
2787
+ }
2788
+
2789
+ // src/init.ts
2790
+ function init() {
2791
+ registerSlice(slice);
2792
+ registerAllAudits();
2793
+ registerFloatingPanel(auditPanel.panel);
2794
+ registerAppBarAuditsToggle();
2795
+ }
2796
+ export {
2797
+ init,
2798
+ registerAudit,
2799
+ runPageAudit
2800
+ };
2801
+ //# sourceMappingURL=index.mjs.map