@design-guard/core 0.3.1
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.
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/research/business-researcher.d.ts +42 -0
- package/dist/research/business-researcher.d.ts.map +1 -0
- package/dist/research/business-researcher.js +888 -0
- package/dist/research/business-researcher.js.map +1 -0
- package/dist/research/design-synthesizer.d.ts +47 -0
- package/dist/research/design-synthesizer.d.ts.map +1 -0
- package/dist/research/design-synthesizer.js +628 -0
- package/dist/research/design-synthesizer.js.map +1 -0
- package/dist/research/research-cache.d.ts +7 -0
- package/dist/research/research-cache.d.ts.map +1 -0
- package/dist/research/research-cache.js +62 -0
- package/dist/research/research-cache.js.map +1 -0
- package/dist/research/types.d.ts +99 -0
- package/dist/research/types.d.ts.map +1 -0
- package/dist/research/types.js +6 -0
- package/dist/research/types.js.map +1 -0
- package/dist/templates/design-md.d.ts +53 -0
- package/dist/templates/design-md.d.ts.map +1 -0
- package/dist/templates/design-md.js +315 -0
- package/dist/templates/design-md.js.map +1 -0
- package/dist/templates/prompts.d.ts +32 -0
- package/dist/templates/prompts.d.ts.map +1 -0
- package/dist/templates/prompts.js +39 -0
- package/dist/templates/prompts.js.map +1 -0
- package/dist/tokens/dtcg-converter.d.ts +62 -0
- package/dist/tokens/dtcg-converter.d.ts.map +1 -0
- package/dist/tokens/dtcg-converter.js +385 -0
- package/dist/tokens/dtcg-converter.js.map +1 -0
- package/dist/utils/prompt-enhancer.d.ts +22 -0
- package/dist/utils/prompt-enhancer.d.ts.map +1 -0
- package/dist/utils/prompt-enhancer.js +104 -0
- package/dist/utils/prompt-enhancer.js.map +1 -0
- package/dist/utils/validators.d.ts +126 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +110 -0
- package/dist/utils/validators.js.map +1 -0
- package/dist/validation/design-validator.d.ts +45 -0
- package/dist/validation/design-validator.d.ts.map +1 -0
- package/dist/validation/design-validator.js +396 -0
- package/dist/validation/design-validator.js.map +1 -0
- package/dist/validation/output-validator.d.ts +46 -0
- package/dist/validation/output-validator.d.ts.map +1 -0
- package/dist/validation/output-validator.js +146 -0
- package/dist/validation/output-validator.js.map +1 -0
- package/dist/validation/rules/alt-text.d.ts +8 -0
- package/dist/validation/rules/alt-text.d.ts.map +1 -0
- package/dist/validation/rules/alt-text.js +35 -0
- package/dist/validation/rules/alt-text.js.map +1 -0
- package/dist/validation/rules/business-alignment.d.ts +7 -0
- package/dist/validation/rules/business-alignment.d.ts.map +1 -0
- package/dist/validation/rules/business-alignment.js +98 -0
- package/dist/validation/rules/business-alignment.js.map +1 -0
- package/dist/validation/rules/color-adherence.d.ts +8 -0
- package/dist/validation/rules/color-adherence.d.ts.map +1 -0
- package/dist/validation/rules/color-adherence.js +92 -0
- package/dist/validation/rules/color-adherence.js.map +1 -0
- package/dist/validation/rules/empty-body.d.ts +6 -0
- package/dist/validation/rules/empty-body.d.ts.map +1 -0
- package/dist/validation/rules/empty-body.js +24 -0
- package/dist/validation/rules/empty-body.js.map +1 -0
- package/dist/validation/rules/heading-hierarchy.d.ts +8 -0
- package/dist/validation/rules/heading-hierarchy.d.ts.map +1 -0
- package/dist/validation/rules/heading-hierarchy.js +48 -0
- package/dist/validation/rules/heading-hierarchy.js.map +1 -0
- package/dist/validation/rules/index.d.ts +21 -0
- package/dist/validation/rules/index.d.ts.map +1 -0
- package/dist/validation/rules/index.js +66 -0
- package/dist/validation/rules/index.js.map +1 -0
- package/dist/validation/rules/no-centered-everything.d.ts +6 -0
- package/dist/validation/rules/no-centered-everything.d.ts.map +1 -0
- package/dist/validation/rules/no-centered-everything.js +59 -0
- package/dist/validation/rules/no-centered-everything.js.map +1 -0
- package/dist/validation/rules/no-default-fonts.d.ts +8 -0
- package/dist/validation/rules/no-default-fonts.d.ts.map +1 -0
- package/dist/validation/rules/no-default-fonts.js +111 -0
- package/dist/validation/rules/no-default-fonts.js.map +1 -0
- package/dist/validation/rules/no-div-soup.d.ts +6 -0
- package/dist/validation/rules/no-div-soup.d.ts.map +1 -0
- package/dist/validation/rules/no-div-soup.js +45 -0
- package/dist/validation/rules/no-div-soup.js.map +1 -0
- package/dist/validation/rules/no-duplicate-ctas.d.ts +6 -0
- package/dist/validation/rules/no-duplicate-ctas.d.ts.map +1 -0
- package/dist/validation/rules/no-duplicate-ctas.js +32 -0
- package/dist/validation/rules/no-duplicate-ctas.js.map +1 -0
- package/dist/validation/rules/no-generic-hero.d.ts +7 -0
- package/dist/validation/rules/no-generic-hero.d.ts.map +1 -0
- package/dist/validation/rules/no-generic-hero.js +46 -0
- package/dist/validation/rules/no-generic-hero.js.map +1 -0
- package/dist/validation/rules/no-icon-grid.d.ts +8 -0
- package/dist/validation/rules/no-icon-grid.d.ts.map +1 -0
- package/dist/validation/rules/no-icon-grid.js +43 -0
- package/dist/validation/rules/no-icon-grid.js.map +1 -0
- package/dist/validation/rules/no-lorem-ipsum.d.ts +6 -0
- package/dist/validation/rules/no-lorem-ipsum.d.ts.map +1 -0
- package/dist/validation/rules/no-lorem-ipsum.js +66 -0
- package/dist/validation/rules/no-lorem-ipsum.js.map +1 -0
- package/dist/validation/rules/no-missing-meta.d.ts +6 -0
- package/dist/validation/rules/no-missing-meta.d.ts.map +1 -0
- package/dist/validation/rules/no-missing-meta.js +49 -0
- package/dist/validation/rules/no-missing-meta.js.map +1 -0
- package/dist/validation/rules/no-missing-responsive.d.ts +6 -0
- package/dist/validation/rules/no-missing-responsive.d.ts.map +1 -0
- package/dist/validation/rules/no-missing-responsive.js +30 -0
- package/dist/validation/rules/no-missing-responsive.js.map +1 -0
- package/dist/validation/rules/no-placeholder-images.d.ts +6 -0
- package/dist/validation/rules/no-placeholder-images.d.ts.map +1 -0
- package/dist/validation/rules/no-placeholder-images.js +73 -0
- package/dist/validation/rules/no-placeholder-images.js.map +1 -0
- package/dist/validation/rules/no-saas-speak.d.ts +6 -0
- package/dist/validation/rules/no-saas-speak.d.ts.map +1 -0
- package/dist/validation/rules/no-saas-speak.js +74 -0
- package/dist/validation/rules/no-saas-speak.js.map +1 -0
- package/dist/validation/rules/no-slop-gradients.d.ts +6 -0
- package/dist/validation/rules/no-slop-gradients.d.ts.map +1 -0
- package/dist/validation/rules/no-slop-gradients.js +24 -0
- package/dist/validation/rules/no-slop-gradients.js.map +1 -0
- package/dist/validation/rules/no-uniform-spacing.d.ts +6 -0
- package/dist/validation/rules/no-uniform-spacing.d.ts.map +1 -0
- package/dist/validation/rules/no-uniform-spacing.js +64 -0
- package/dist/validation/rules/no-uniform-spacing.js.map +1 -0
- package/dist/validation/rules/types.d.ts +36 -0
- package/dist/validation/rules/types.d.ts.map +1 -0
- package/dist/validation/rules/types.js +7 -0
- package/dist/validation/rules/types.js.map +1 -0
- package/dist/validation/tailwind-parser.d.ts +36 -0
- package/dist/validation/tailwind-parser.d.ts.map +1 -0
- package/dist/validation/tailwind-parser.js +214 -0
- package/dist/validation/tailwind-parser.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Common AI default fonts to flag */
|
|
2
|
+
const DEFAULT_FONTS = [
|
|
3
|
+
'Inter', 'Poppins', 'Roboto', 'Open Sans', 'Lato',
|
|
4
|
+
'Montserrat', 'Nunito', 'Raleway',
|
|
5
|
+
];
|
|
6
|
+
/** System font stacks that shouldn't be sole font-family */
|
|
7
|
+
const SYSTEM_ONLY_FONTS = ['system-ui', '-apple-system', 'BlinkMacSystemFont'];
|
|
8
|
+
/**
|
|
9
|
+
* Check if a font is intentionally specified in DESIGN.md Section 3 (Typography).
|
|
10
|
+
* Only considers fonts listed as Heading, Body, or Mono fonts -- NOT fonts
|
|
11
|
+
* mentioned in "Don't" lists or other contexts.
|
|
12
|
+
*/
|
|
13
|
+
function isIntentionalFont(fontName, designMdContent) {
|
|
14
|
+
if (!designMdContent)
|
|
15
|
+
return false;
|
|
16
|
+
// Extract Section 3 (Typography) content between ## 3. and the next ## heading
|
|
17
|
+
const section3Match = designMdContent.match(/##\s*3\..*?Typography[\s\S]*?(?=##\s*\d+\.|$)/i);
|
|
18
|
+
if (!section3Match)
|
|
19
|
+
return false;
|
|
20
|
+
const section3 = section3Match[0];
|
|
21
|
+
// Look for font-family declarations like: **Heading**: "Space Grotesk", sans-serif
|
|
22
|
+
const fontDeclarations = section3.match(/(?:\*\*(?:Heading|Body|Mono|Display|Caption|Code)\*\*\s*:\s*)(["']?)([^,"'\n]+)\1/gi) || [];
|
|
23
|
+
for (const decl of fontDeclarations) {
|
|
24
|
+
const nameMatch = decl.match(/:\s*["']?([^,"'\n]+)["']?/);
|
|
25
|
+
if (nameMatch) {
|
|
26
|
+
const declaredFont = nameMatch[1].trim();
|
|
27
|
+
if (declaredFont.toLowerCase() === fontName.toLowerCase()) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Flags Inter, Poppins, and other common AI-default fonts.
|
|
36
|
+
* Bug Fix (Frente A): expanded font list, Google Fonts link detection,
|
|
37
|
+
* system-ui sole font-family detection, Section 3 parsing for intentional fonts.
|
|
38
|
+
*/
|
|
39
|
+
export const noDefaultFonts = {
|
|
40
|
+
id: 'no-default-fonts',
|
|
41
|
+
name: 'No Default Fonts',
|
|
42
|
+
description: 'Flags common AI-default fonts (Inter, Poppins, Roboto, etc.) unless explicitly specified in DESIGN.md.',
|
|
43
|
+
severity: 'warning',
|
|
44
|
+
category: 'slop',
|
|
45
|
+
check(context) {
|
|
46
|
+
const issues = [];
|
|
47
|
+
const { allStyles, allClasses, designMdContent, $ } = context;
|
|
48
|
+
// 1a. Check each default font in CSS font-family declarations
|
|
49
|
+
for (const font of DEFAULT_FONTS) {
|
|
50
|
+
const escapedFont = font.replace(/\s+/g, '\\s+');
|
|
51
|
+
const fontRegex = new RegExp(`font-family[^;]*\\b${escapedFont}\\b`, 'i');
|
|
52
|
+
if (fontRegex.test(allStyles) && !isIntentionalFont(font, designMdContent)) {
|
|
53
|
+
issues.push({
|
|
54
|
+
type: 'warning',
|
|
55
|
+
category: 'slop',
|
|
56
|
+
message: `Detected "${font}" font — common AI default. Consider a more distinctive typeface.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 1b. Check Google Fonts <link> tags for flagged fonts
|
|
61
|
+
const linkHrefs = $('link[href*="fonts.googleapis.com"]').map((_i, el) => $(el).attr('href') || '').get();
|
|
62
|
+
for (const href of linkHrefs) {
|
|
63
|
+
for (const font of DEFAULT_FONTS) {
|
|
64
|
+
const encodedFont = font.replace(/\s+/g, '+');
|
|
65
|
+
if (href.includes(encodedFont) && !isIntentionalFont(font, designMdContent)) {
|
|
66
|
+
issues.push({
|
|
67
|
+
type: 'warning',
|
|
68
|
+
category: 'slop',
|
|
69
|
+
message: `Google Fonts link loads "${font}" — common AI default. Consider a more distinctive typeface.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 1c. Check for system-ui/-apple-system/BlinkMacSystemFont as sole font-family
|
|
75
|
+
const fontFamilyDecls = allStyles.match(/font-family\s*:\s*([^;}"]+)/gi) || [];
|
|
76
|
+
for (const decl of fontFamilyDecls) {
|
|
77
|
+
const value = decl.replace(/font-family\s*:\s*/i, '').trim();
|
|
78
|
+
const fonts = value.split(',').map(f => f.trim().replace(/['"]/g, ''));
|
|
79
|
+
// If ONLY system fonts (no named font after them)
|
|
80
|
+
const namedFonts = fonts.filter(f => !SYSTEM_ONLY_FONTS.includes(f) && f !== 'sans-serif' && f !== 'serif' && f !== 'monospace');
|
|
81
|
+
if (namedFonts.length === 0 && fonts.length > 0) {
|
|
82
|
+
const hasSystemFont = fonts.some(f => SYSTEM_ONLY_FONTS.includes(f));
|
|
83
|
+
const isSansSerifAlone = fonts.length === 1 && fonts[0] === 'sans-serif';
|
|
84
|
+
if (hasSystemFont) {
|
|
85
|
+
issues.push({
|
|
86
|
+
type: 'warning',
|
|
87
|
+
category: 'slop',
|
|
88
|
+
message: `Font-family uses only system fonts (${fonts.join(', ')}). Specify a custom typeface for brand identity.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (isSansSerifAlone) {
|
|
92
|
+
issues.push({
|
|
93
|
+
type: 'warning',
|
|
94
|
+
category: 'slop',
|
|
95
|
+
message: 'Font-family is bare "sans-serif" — specify a named font for brand consistency.',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 1d. Check Tailwind font classes (font-sans resolves to Inter/system-ui)
|
|
101
|
+
if (/\bfont-sans\b/.test(allClasses) && !isIntentionalFont('Inter', designMdContent)) {
|
|
102
|
+
issues.push({
|
|
103
|
+
type: 'info',
|
|
104
|
+
category: 'slop',
|
|
105
|
+
message: 'Tailwind "font-sans" class detected — resolves to system sans-serif. Consider specifying a custom font.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return issues;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=no-default-fonts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-default-fonts.js","sourceRoot":"","sources":["../../../src/validation/rules/no-default-fonts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-div-soup.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-div-soup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAKxD;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,QA2CvB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const SEMANTIC_ELEMENTS = ['section', 'article', 'nav', 'header', 'footer', 'aside', 'main'];
|
|
2
|
+
/**
|
|
3
|
+
* Detects excessive div usage without semantic HTML.
|
|
4
|
+
*/
|
|
5
|
+
export const noDivSoup = {
|
|
6
|
+
id: 'no-div-soup',
|
|
7
|
+
name: 'No Div Soup',
|
|
8
|
+
description: 'Flags excessive div usage relative to semantic HTML elements.',
|
|
9
|
+
severity: 'warning',
|
|
10
|
+
category: 'structure',
|
|
11
|
+
check(context) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const { $ } = context;
|
|
14
|
+
const divCount = $('div').length;
|
|
15
|
+
let semanticCount = 0;
|
|
16
|
+
for (const tag of SEMANTIC_ELEMENTS) {
|
|
17
|
+
semanticCount += $(tag).length;
|
|
18
|
+
}
|
|
19
|
+
// Flag if div to semantic ratio exceeds 5:1
|
|
20
|
+
if (semanticCount > 0 && divCount / semanticCount > 5) {
|
|
21
|
+
issues.push({
|
|
22
|
+
type: 'warning',
|
|
23
|
+
category: 'structure',
|
|
24
|
+
message: `Div-to-semantic element ratio is ${divCount}:${semanticCount} (>${5}:1) — use semantic HTML elements.`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else if (semanticCount === 0 && divCount > 5) {
|
|
28
|
+
issues.push({
|
|
29
|
+
type: 'warning',
|
|
30
|
+
category: 'structure',
|
|
31
|
+
message: `${divCount} divs with zero semantic elements — use <section>, <nav>, <header>, <footer>, <main>.`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Flag missing <main> element
|
|
35
|
+
if ($('main').length === 0 && $('body').children().length > 0) {
|
|
36
|
+
issues.push({
|
|
37
|
+
type: 'warning',
|
|
38
|
+
category: 'structure',
|
|
39
|
+
message: 'Missing <main> element — page should have a <main> landmark.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=no-div-soup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-div-soup.js","sourceRoot":"","sources":["../../../src/validation/rules/no-div-soup.ts"],"names":[],"mappings":"AAGA,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAE7F;;GAEG;AACH,MAAM,CAAC,MAAM,SAAS,GAAa;IACjC,EAAE,EAAE,aAAa;IACjB,IAAI,EAAE,aAAa;IACnB,WAAW,EAAE,+DAA+D;IAC5E,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,WAAW;IAErB,KAAK,CAAC,OAAoB;QACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC;QAEtB,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;QACjC,IAAI,aAAa,GAAG,CAAC,CAAC;QACtB,KAAK,MAAM,GAAG,IAAI,iBAAiB,EAAE,CAAC;YACpC,aAAa,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;QACjC,CAAC;QAED,4CAA4C;QAC5C,IAAI,aAAa,GAAG,CAAC,IAAI,QAAQ,GAAG,aAAa,GAAG,CAAC,EAAE,CAAC;YACtD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,oCAAoC,QAAQ,IAAI,aAAa,MAAM,CAAC,mCAAmC;aACjH,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,aAAa,KAAK,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,GAAG,QAAQ,uFAAuF;aAC5G,CAAC,CAAC;QACL,CAAC;QAED,8BAA8B;QAC9B,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,8DAA8D;aACxE,CAAC,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-duplicate-ctas.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-duplicate-ctas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAGxD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,QAgC7B,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects when the same CTA text appears 3+ times.
|
|
3
|
+
*/
|
|
4
|
+
export const noDuplicateCtas = {
|
|
5
|
+
id: 'no-duplicate-ctas',
|
|
6
|
+
name: 'No Duplicate CTAs',
|
|
7
|
+
description: 'Flags repeated call-to-action text appearing 3 or more times.',
|
|
8
|
+
severity: 'warning',
|
|
9
|
+
category: 'content',
|
|
10
|
+
check(context) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const { $ } = context;
|
|
13
|
+
const ctaCounts = {};
|
|
14
|
+
$('a, button').each((_i, el) => {
|
|
15
|
+
const text = $(el).text().trim().toLowerCase();
|
|
16
|
+
if (text) {
|
|
17
|
+
ctaCounts[text] = (ctaCounts[text] || 0) + 1;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
for (const [text, count] of Object.entries(ctaCounts)) {
|
|
21
|
+
if (count >= 3) {
|
|
22
|
+
issues.push({
|
|
23
|
+
type: 'warning',
|
|
24
|
+
category: 'content',
|
|
25
|
+
message: `CTA "${text}" appears ${count} times — consider varying your call-to-action text.`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return issues;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=no-duplicate-ctas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-duplicate-ctas.js","sourceRoot":"","sources":["../../../src/validation/rules/no-duplicate-ctas.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,EAAE,EAAE,mBAAmB;IACvB,IAAI,EAAE,mBAAmB;IACzB,WAAW,EAAE,+DAA+D;IAC5E,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,SAAS;IAEnB,KAAK,CAAC,OAAoB;QACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC;QAEtB,MAAM,SAAS,GAA2B,EAAE,CAAC;QAE7C,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;YAC7B,MAAM,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAC/C,IAAI,IAAI,EAAE,CAAC;gBACT,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YACtD,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,SAAS;oBACf,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,QAAQ,IAAI,aAAa,KAAK,qDAAqD;iBAC7F,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-generic-hero.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-generic-hero.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAGxD;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,QAiD3B,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects the classic AI hero pattern: centered text, gradient background,
|
|
3
|
+
* heading + paragraph + button.
|
|
4
|
+
*/
|
|
5
|
+
export const noGenericHero = {
|
|
6
|
+
id: 'no-generic-hero',
|
|
7
|
+
name: 'No Generic Hero',
|
|
8
|
+
description: 'Detects the classic AI-generated hero pattern (centered text, gradient, heading + paragraph + button).',
|
|
9
|
+
severity: 'info',
|
|
10
|
+
category: 'slop',
|
|
11
|
+
check(context) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const { $ } = context;
|
|
14
|
+
// Look at first section or header as the hero candidate
|
|
15
|
+
const heroCandidate = $('body > section, body > header, body > div > section, body > div > header').first();
|
|
16
|
+
if (heroCandidate.length === 0)
|
|
17
|
+
return issues;
|
|
18
|
+
// Check for centered text
|
|
19
|
+
const heroClasses = heroCandidate.attr('class') || '';
|
|
20
|
+
const heroStyle = heroCandidate.attr('style') || '';
|
|
21
|
+
const heroHtml = heroCandidate.html() || '';
|
|
22
|
+
const isCentered = /\btext-center\b/.test(heroClasses) ||
|
|
23
|
+
/text-align\s*:\s*center/i.test(heroStyle) ||
|
|
24
|
+
/\btext-center\b/.test(heroHtml) ||
|
|
25
|
+
/text-align\s*:\s*center/i.test(heroHtml);
|
|
26
|
+
// Check for gradient background
|
|
27
|
+
const hasGradient = /gradient/i.test(heroStyle) ||
|
|
28
|
+
/\bbg-gradient\b/.test(heroClasses) ||
|
|
29
|
+
/gradient/i.test(heroHtml);
|
|
30
|
+
// Check for heading + paragraph + button (the Holy Trinity)
|
|
31
|
+
const hasHeading = heroCandidate.find('h1, h2').length > 0;
|
|
32
|
+
const hasParagraph = heroCandidate.find('p').length > 0;
|
|
33
|
+
const hasButton = heroCandidate.find('a, button').length > 0;
|
|
34
|
+
const hasHolyTrinity = hasHeading && hasParagraph && hasButton;
|
|
35
|
+
// Flag if all three signals are present
|
|
36
|
+
if (isCentered && hasGradient && hasHolyTrinity) {
|
|
37
|
+
issues.push({
|
|
38
|
+
type: 'info',
|
|
39
|
+
category: 'slop',
|
|
40
|
+
message: 'Generic AI hero detected: centered text + gradient + heading/paragraph/button pattern.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return issues;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=no-generic-hero.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-generic-hero.js","sourceRoot":"","sources":["../../../src/validation/rules/no-generic-hero.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAa;IACrC,EAAE,EAAE,iBAAiB;IACrB,IAAI,EAAE,iBAAiB;IACvB,WAAW,EAAE,wGAAwG;IACrH,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,MAAM;IAEhB,KAAK,CAAC,OAAoB;QACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC;QAEtB,wDAAwD;QACxD,MAAM,aAAa,GAAG,CAAC,CAAC,0EAA0E,CAAC,CAAC,KAAK,EAAE,CAAC;QAC5G,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,MAAM,CAAC;QAE9C,0BAA0B;QAC1B,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACtD,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACpD,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QAE5C,MAAM,UAAU,GACd,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC;YACnC,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC;YAC1C,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC;YAChC,0BAA0B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE5C,gCAAgC;QAChC,MAAM,WAAW,GACf,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC;YAC3B,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC;YACnC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE7B,4DAA4D;QAC5D,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QAC3D,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QAC7D,MAAM,cAAc,GAAG,UAAU,IAAI,YAAY,IAAI,SAAS,CAAC;QAE/D,wCAAwC;QACxC,IAAI,UAAU,IAAI,WAAW,IAAI,cAAc,EAAE,CAAC;YAChD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,wFAAwF;aAClG,CAAC,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LintRule } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detects the classic icon grid pattern in any section (not just second).
|
|
4
|
+
* Bug Fix (Frente A): 3-6 items, more icon types (img, span, material-symbols, FA),
|
|
5
|
+
* all sections, warning severity instead of info.
|
|
6
|
+
*/
|
|
7
|
+
export declare const noIconGrid: LintRule;
|
|
8
|
+
//# sourceMappingURL=no-icon-grid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-icon-grid.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-icon-grid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAGxD;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,QA0CxB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects the classic icon grid pattern in any section (not just second).
|
|
3
|
+
* Bug Fix (Frente A): 3-6 items, more icon types (img, span, material-symbols, FA),
|
|
4
|
+
* all sections, warning severity instead of info.
|
|
5
|
+
*/
|
|
6
|
+
export const noIconGrid = {
|
|
7
|
+
id: 'no-icon-grid',
|
|
8
|
+
name: 'No Icon Grid',
|
|
9
|
+
description: 'Detects uniform icon grid patterns (3-6 icons) typically used as filler sections.',
|
|
10
|
+
severity: 'warning',
|
|
11
|
+
category: 'slop',
|
|
12
|
+
check(context) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
const { $ } = context;
|
|
15
|
+
const sections = $('section, [class*="section"], main > div').toArray();
|
|
16
|
+
for (let sIdx = 0; sIdx < sections.length; sIdx++) {
|
|
17
|
+
const section = $(sections[sIdx]);
|
|
18
|
+
// Count all icon-like elements: svg, <i> with class, <img> with small/icon-like attrs, <span> with icon classes
|
|
19
|
+
const svgIcons = section.find('svg').length;
|
|
20
|
+
const iIcons = section.find('i[class]').length;
|
|
21
|
+
const imgIcons = section.find('img[width]').filter((_i, el) => {
|
|
22
|
+
const w = parseInt($(el).attr('width') || '0', 10);
|
|
23
|
+
return w > 0 && w <= 64;
|
|
24
|
+
}).length + section.find('img[class*="icon"]').length;
|
|
25
|
+
const spanIcons = section.find('span[class*="material-symbols"], span[class*="material-icons"], ' +
|
|
26
|
+
'span[class*="fa-"], span[class*="lucide-"], span[class*="icon"]').length;
|
|
27
|
+
const totalIcons = svgIcons + iIcons + imgIcons + spanIcons;
|
|
28
|
+
const columns = section.find('[class*="col"], [class*="grid"]');
|
|
29
|
+
if (totalIcons >= 3 && totalIcons <= 6 && columns.length > 0) {
|
|
30
|
+
const isSecondSection = sIdx === 1;
|
|
31
|
+
issues.push({
|
|
32
|
+
type: 'warning',
|
|
33
|
+
category: 'slop',
|
|
34
|
+
message: isSecondSection
|
|
35
|
+
? `Second section is a ${totalIcons}-icon grid — classic AI layout pattern. Consider a more distinctive layout.`
|
|
36
|
+
: `Section ${sIdx + 1} contains a uniform ${totalIcons}-icon grid — common AI layout pattern.`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return issues;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=no-icon-grid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-icon-grid.js","sourceRoot":"","sources":["../../../src/validation/rules/no-icon-grid.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAa;IAClC,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,cAAc;IACpB,WAAW,EAAE,mFAAmF;IAChG,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,MAAM;IAEhB,KAAK,CAAC,OAAoB;QACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC;QAEtB,MAAM,QAAQ,GAAG,CAAC,CAAC,yCAAyC,CAAC,CAAC,OAAO,EAAE,CAAC;QACxE,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;YAClD,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAClC,gHAAgH;YAChH,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;YAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;YAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;gBAC5D,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;gBACnD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC1B,CAAC,CAAC,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,MAAM,CAAC;YACtD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAC5B,kEAAkE;gBAClE,iEAAiE,CAClE,CAAC,MAAM,CAAC;YACT,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;YAE5D,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;YAChE,IAAI,UAAU,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7D,MAAM,eAAe,GAAG,IAAI,KAAK,CAAC,CAAC;gBACnC,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,SAAS;oBACf,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,eAAe;wBACtB,CAAC,CAAC,uBAAuB,UAAU,6EAA6E;wBAChH,CAAC,CAAC,WAAW,IAAI,GAAG,CAAC,uBAAuB,UAAU,wCAAwC;iBACjG,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-lorem-ipsum.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-lorem-ipsum.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAYxD;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,QA2D1B,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const PLACEHOLDER_PATTERNS = [
|
|
2
|
+
/lorem\s+ipsum/i,
|
|
3
|
+
/dolor\s+sit\s+amet/i,
|
|
4
|
+
/consectetur\s+adipiscing/i,
|
|
5
|
+
/placeholder\s+text/i,
|
|
6
|
+
/sample\s+text/i,
|
|
7
|
+
/dummy\s+text/i,
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Detects placeholder text that was never replaced with real content.
|
|
11
|
+
*/
|
|
12
|
+
export const noLoremIpsum = {
|
|
13
|
+
id: 'no-lorem-ipsum',
|
|
14
|
+
name: 'No Lorem Ipsum',
|
|
15
|
+
description: 'Detects placeholder text (lorem ipsum, dummy text) indicating unfinished content.',
|
|
16
|
+
severity: 'error',
|
|
17
|
+
category: 'content',
|
|
18
|
+
check(context) {
|
|
19
|
+
const issues = [];
|
|
20
|
+
const { $, html } = context;
|
|
21
|
+
// Check for standard placeholder phrases
|
|
22
|
+
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
23
|
+
if (pattern.test(html)) {
|
|
24
|
+
issues.push({
|
|
25
|
+
type: 'error',
|
|
26
|
+
category: 'content',
|
|
27
|
+
message: `Placeholder text detected: "${html.match(pattern)?.[0]}".`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Check for repeated filler strings like "Text" or "Content" used as placeholders
|
|
32
|
+
const textNodes = [];
|
|
33
|
+
$('body *')
|
|
34
|
+
.not('script, style, code, pre')
|
|
35
|
+
.each((_i, el) => {
|
|
36
|
+
const directText = $(el)
|
|
37
|
+
.contents()
|
|
38
|
+
.filter(function () {
|
|
39
|
+
return this.type === 'text';
|
|
40
|
+
})
|
|
41
|
+
.text()
|
|
42
|
+
.trim();
|
|
43
|
+
if (directText) {
|
|
44
|
+
textNodes.push(directText);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const fillerCounts = {};
|
|
48
|
+
for (const text of textNodes) {
|
|
49
|
+
const normalized = text.toLowerCase().trim();
|
|
50
|
+
if (normalized === 'text' || normalized === 'content') {
|
|
51
|
+
fillerCounts[normalized] = (fillerCounts[normalized] || 0) + 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const [filler, count] of Object.entries(fillerCounts)) {
|
|
55
|
+
if (count >= 3) {
|
|
56
|
+
issues.push({
|
|
57
|
+
type: 'error',
|
|
58
|
+
category: 'content',
|
|
59
|
+
message: `Repeated filler "${filler}" appears ${count} times — likely placeholder content.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return issues;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=no-lorem-ipsum.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-lorem-ipsum.js","sourceRoot":"","sources":["../../../src/validation/rules/no-lorem-ipsum.ts"],"names":[],"mappings":"AAGA,MAAM,oBAAoB,GAAa;IACrC,gBAAgB;IAChB,qBAAqB;IACrB,2BAA2B;IAC3B,qBAAqB;IACrB,gBAAgB;IAChB,eAAe;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAa;IACpC,EAAE,EAAE,gBAAgB;IACpB,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,mFAAmF;IAChG,QAAQ,EAAE,OAAO;IACjB,QAAQ,EAAE,SAAS;IAEnB,KAAK,CAAC,OAAoB;QACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;QAE5B,yCAAyC;QACzC,KAAK,MAAM,OAAO,IAAI,oBAAoB,EAAE,CAAC;YAC3C,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvB,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,OAAO;oBACb,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,+BAA+B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI;iBACrE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,kFAAkF;QAClF,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,CAAC,CAAC,QAAQ,CAAC;aACR,GAAG,CAAC,0BAA0B,CAAC;aAC/B,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;YACf,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC;iBACrB,QAAQ,EAAE;iBACV,MAAM,CAAC;gBACN,OAAO,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;YAC9B,CAAC,CAAC;iBACD,IAAI,EAAE;iBACN,IAAI,EAAE,CAAC;YACV,IAAI,UAAU,EAAE,CAAC;gBACf,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC,CAAC,CAAC;QAEL,MAAM,YAAY,GAA2B,EAAE,CAAC;QAChD,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;YAC7C,IAAI,UAAU,KAAK,MAAM,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;gBACtD,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;QAED,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YAC3D,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,OAAO;oBACb,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,oBAAoB,MAAM,aAAa,KAAK,sCAAsC;iBAC5F,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-missing-meta.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-missing-meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAGxD;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,QAkD3B,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects missing essential meta tags and attributes.
|
|
3
|
+
*/
|
|
4
|
+
export const noMissingMeta = {
|
|
5
|
+
id: 'no-missing-meta',
|
|
6
|
+
name: 'No Missing Meta',
|
|
7
|
+
description: 'Checks for essential meta tags: lang attribute, viewport, description, and title.',
|
|
8
|
+
severity: 'warning',
|
|
9
|
+
category: 'structure',
|
|
10
|
+
check(context) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const { $ } = context;
|
|
13
|
+
// Check for <html lang="...">
|
|
14
|
+
const htmlLang = $('html').attr('lang');
|
|
15
|
+
if (!htmlLang) {
|
|
16
|
+
issues.push({
|
|
17
|
+
type: 'warning',
|
|
18
|
+
category: 'structure',
|
|
19
|
+
message: 'Missing lang attribute on <html> element.',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Check for <meta name="viewport">
|
|
23
|
+
if ($('meta[name="viewport"]').length === 0) {
|
|
24
|
+
issues.push({
|
|
25
|
+
type: 'warning',
|
|
26
|
+
category: 'structure',
|
|
27
|
+
message: 'Missing <meta name="viewport"> tag.',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Check for <meta name="description">
|
|
31
|
+
if ($('meta[name="description"]').length === 0) {
|
|
32
|
+
issues.push({
|
|
33
|
+
type: 'warning',
|
|
34
|
+
category: 'structure',
|
|
35
|
+
message: 'Missing <meta name="description"> tag.',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Check for <title>
|
|
39
|
+
if ($('title').length === 0 || !$('title').text().trim()) {
|
|
40
|
+
issues.push({
|
|
41
|
+
type: 'warning',
|
|
42
|
+
category: 'structure',
|
|
43
|
+
message: 'Missing or empty <title> element.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return issues;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=no-missing-meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-missing-meta.js","sourceRoot":"","sources":["../../../src/validation/rules/no-missing-meta.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAa;IACrC,EAAE,EAAE,iBAAiB;IACrB,IAAI,EAAE,iBAAiB;IACvB,WAAW,EAAE,mFAAmF;IAChG,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,WAAW;IAErB,KAAK,CAAC,OAAoB;QACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC;QAEtB,8BAA8B;QAC9B,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,2CAA2C;aACrD,CAAC,CAAC;QACL,CAAC;QAED,mCAAmC;QACnC,IAAI,CAAC,CAAC,uBAAuB,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,qCAAqC;aAC/C,CAAC,CAAC;QACL,CAAC;QAED,sCAAsC;QACtC,IAAI,CAAC,CAAC,0BAA0B,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,wCAAwC;aAClD,CAAC,CAAC;QACL,CAAC;QAED,oBAAoB;QACpB,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;YACzD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,mCAAmC;aAC7C,CAAC,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-missing-responsive.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-missing-responsive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAGxD;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,QA6BjC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects pages with zero responsive design indicators.
|
|
3
|
+
*/
|
|
4
|
+
export const noMissingResponsive = {
|
|
5
|
+
id: 'no-missing-responsive',
|
|
6
|
+
name: 'No Missing Responsive',
|
|
7
|
+
description: 'Flags pages over 50 lines with no responsive design (media queries, viewport meta, or Tailwind responsive prefixes).',
|
|
8
|
+
severity: 'warning',
|
|
9
|
+
category: 'layout',
|
|
10
|
+
check(context) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const { html, allStyles, allClasses, $ } = context;
|
|
13
|
+
// Only check pages over 50 lines
|
|
14
|
+
const lineCount = html.split('\n').length;
|
|
15
|
+
if (lineCount <= 50)
|
|
16
|
+
return issues;
|
|
17
|
+
const hasMediaQueries = /@media\b/.test(allStyles);
|
|
18
|
+
const hasTailwindResponsive = /\b(sm|md|lg|xl|2xl):/.test(allClasses);
|
|
19
|
+
const hasViewportMeta = $('meta[name="viewport"]').length > 0;
|
|
20
|
+
if (!hasMediaQueries && !hasTailwindResponsive && !hasViewportMeta) {
|
|
21
|
+
issues.push({
|
|
22
|
+
type: 'warning',
|
|
23
|
+
category: 'layout',
|
|
24
|
+
message: 'No responsive design indicators found (no @media queries, no viewport meta, no responsive Tailwind prefixes).',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return issues;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=no-missing-responsive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-missing-responsive.js","sourceRoot":"","sources":["../../../src/validation/rules/no-missing-responsive.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,EAAE,EAAE,uBAAuB;IAC3B,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,sHAAsH;IACnI,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,QAAQ;IAElB,KAAK,CAAC,OAAoB;QACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC;QAEnD,iCAAiC;QACjC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;QAC1C,IAAI,SAAS,IAAI,EAAE;YAAE,OAAO,MAAM,CAAC;QAEnC,MAAM,eAAe,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnD,MAAM,qBAAqB,GAAG,sBAAsB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACtE,MAAM,eAAe,GAAG,CAAC,CAAC,uBAAuB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QAE9D,IAAI,CAAC,eAAe,IAAI,CAAC,qBAAqB,IAAI,CAAC,eAAe,EAAE,CAAC;YACnE,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,QAAQ;gBAClB,OAAO,EAAE,+GAA+G;aACzH,CAAC,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-placeholder-images.d.ts","sourceRoot":"","sources":["../../../src/validation/rules/no-placeholder-images.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAoBxD;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,QA4DjC,CAAC"}
|