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