Site Quality Scanner - Typos, Copy & Usability Analyzer

Scans your entire site for typos, copy improvements, and usability issues with AI-powered analysis

Size

39.6 KB

Version

1.1.1

Created

Nov 3, 2025

Updated

about 1 month ago

1// ==UserScript==
2// @name		Site Quality Scanner - Typos, Copy & Usability Analyzer
3// @description		Scans your entire site for typos, copy improvements, and usability issues with AI-powered analysis
4// @version		1.1.1
5// @match		https://*.secure.getcarefull.com/*
6// @icon		https://secure.getcarefull.com/favicon.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    // State management
12    let scanResults = [];
13    let isScanning = false;
14    let scannedPages = new Set();
15
16    // Debounce utility
17    function debounce(func, wait) {
18        let timeout;
19        return function executedFunction(...args) {
20            const later = () => {
21                clearTimeout(timeout);
22                func(...args);
23            };
24            clearTimeout(timeout);
25            timeout = setTimeout(later, wait);
26        };
27    }
28
29    // Create floating scanner panel
30    function createScannerPanel() {
31        const panel = document.createElement('div');
32        panel.id = 'quality-scanner-panel';
33        panel.innerHTML = `
34            <div class="scanner-header">
35                <h3>🔍 Site Quality Scanner</h3>
36                <button id="scanner-close" class="scanner-btn-icon"></button>
37            </div>
38            <div class="scanner-body">
39                <div class="scanner-controls">
40                    <button id="start-scan-btn" class="scanner-btn scanner-btn-primary">
41                        Start Full Site Scan
42                    </button>
43                    <button id="scan-current-btn" class="scanner-btn scanner-btn-secondary">
44                        Scan Current Page Only
45                    </button>
46                </div>
47                <div id="scan-progress" class="scan-progress" style="display: none;">
48                    <div class="progress-bar">
49                        <div id="progress-fill" class="progress-fill"></div>
50                    </div>
51                    <p id="progress-text">Scanning...</p>
52                </div>
53                <div id="scan-results" class="scan-results"></div>
54            </div>
55        `;
56
57        document.body.appendChild(panel);
58        attachPanelListeners();
59    }
60
61    // Create toggle button
62    function createToggleButton() {
63        const button = document.createElement('button');
64        button.id = 'quality-scanner-toggle';
65        button.innerHTML = '🔍';
66        button.title = 'Open Quality Scanner';
67        document.body.appendChild(button);
68
69        button.addEventListener('click', () => {
70            const panel = document.getElementById('quality-scanner-panel');
71            if (panel) {
72                panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
73            }
74        });
75    }
76
77    // Attach event listeners to panel
78    function attachPanelListeners() {
79        document.getElementById('scanner-close').addEventListener('click', () => {
80            document.getElementById('quality-scanner-panel').style.display = 'none';
81        });
82
83        document.getElementById('start-scan-btn').addEventListener('click', startFullSiteScan);
84        document.getElementById('scan-current-btn').addEventListener('click', scanCurrentPage);
85    }
86
87    // Extract all links from current domain
88    function extractSiteLinks() {
89        const links = new Set();
90        const currentDomain = window.location.hostname;
91        
92        document.querySelectorAll('a[href]').forEach(link => {
93            try {
94                const url = new URL(link.href, window.location.origin);
95                if (url.hostname === currentDomain && !url.hash) {
96                    links.add(url.href);
97                }
98            } catch (e) {
99                console.error('Invalid URL:', link.href);
100            }
101        });
102
103        // Add current page
104        links.add(window.location.href);
105        
106        return Array.from(links);
107    }
108
109    // Extract page content for analysis
110    function extractPageContent() {
111        const content = {
112            url: window.location.href,
113            title: document.title,
114            headings: [],
115            paragraphs: [],
116            buttons: [],
117            links: [],
118            forms: [],
119            images: [],
120            metadata: {}
121        };
122
123        // Extract headings
124        document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
125            if (heading.textContent.trim()) {
126                content.headings.push({
127                    level: heading.tagName,
128                    text: heading.textContent.trim()
129                });
130            }
131        });
132
133        // Extract paragraphs
134        document.querySelectorAll('p').forEach(p => {
135            const text = p.textContent.trim();
136            if (text && text.length > 10) {
137                content.paragraphs.push(text);
138            }
139        });
140
141        // Extract buttons
142        document.querySelectorAll('button, a.btn, [role="button"]').forEach(btn => {
143            const text = btn.textContent.trim();
144            if (text) {
145                content.buttons.push({
146                    text: text,
147                    type: btn.tagName,
148                    ariaLabel: btn.getAttribute('aria-label')
149                });
150            }
151        });
152
153        // Extract links
154        document.querySelectorAll('a[href]').forEach(link => {
155            const text = link.textContent.trim();
156            if (text) {
157                content.links.push({
158                    text: text,
159                    href: link.href,
160                    ariaLabel: link.getAttribute('aria-label')
161                });
162            }
163        });
164
165        // Extract form information
166        document.querySelectorAll('form').forEach(form => {
167            const formData = {
168                action: form.action,
169                inputs: []
170            };
171            
172            form.querySelectorAll('input, textarea, select').forEach(input => {
173                formData.inputs.push({
174                    type: input.type || input.tagName,
175                    name: input.name,
176                    placeholder: input.placeholder,
177                    label: input.labels?.[0]?.textContent?.trim() || '',
178                    required: input.required
179                });
180            });
181            
182            if (formData.inputs.length > 0) {
183                content.forms.push(formData);
184            }
185        });
186
187        // Extract images
188        document.querySelectorAll('img').forEach(img => {
189            content.images.push({
190                src: img.src,
191                alt: img.alt,
192                hasAlt: !!img.alt
193            });
194        });
195
196        // Extract metadata
197        content.metadata = {
198            description: document.querySelector('meta[name="description"]')?.content || '',
199            viewport: document.querySelector('meta[name="viewport"]')?.content || '',
200            lang: document.documentElement.lang || 'not set'
201        };
202
203        return content;
204    }
205
206    // Analyze content with AI
207    async function analyzeContent(pageContent) {
208        console.log('Analyzing page:', pageContent.url);
209        
210        const prompt = `You are a UX/UI expert and copywriter. Analyze this webpage content and provide detailed feedback.
211
212Page URL: ${pageContent.url}
213Page Title: ${pageContent.title}
214
215Headings: ${JSON.stringify(pageContent.headings)}
216Paragraphs: ${pageContent.paragraphs.slice(0, 10).join(' | ')}
217Buttons: ${JSON.stringify(pageContent.buttons)}
218Links: ${JSON.stringify(pageContent.links.slice(0, 20))}
219Forms: ${JSON.stringify(pageContent.forms)}
220Images: ${pageContent.images.length} images (${pageContent.images.filter(img => !img.hasAlt).length} missing alt text)
221
222Analyze and provide:
2231. Typos and spelling errors
2242. Grammar and punctuation issues
2253. Copy improvements (clarity, tone, engagement)
2264. Usability issues (navigation, accessibility, UX)
2275. Design recommendations (layout, hierarchy, visual design)`;
228
229        try {
230            const analysis = await RM.aiCall(prompt, {
231                type: 'json_schema',
232                json_schema: {
233                    name: 'quality_analysis',
234                    schema: {
235                        type: 'object',
236                        properties: {
237                            typos: {
238                                type: 'array',
239                                items: {
240                                    type: 'object',
241                                    properties: {
242                                        text: { type: 'string' },
243                                        correction: { type: 'string' },
244                                        location: { type: 'string' },
245                                        severity: { type: 'string', enum: ['high', 'medium', 'low'] }
246                                    },
247                                    required: ['text', 'correction', 'location', 'severity']
248                                }
249                            },
250                            copyImprovements: {
251                                type: 'array',
252                                items: {
253                                    type: 'object',
254                                    properties: {
255                                        original: { type: 'string' },
256                                        improved: { type: 'string' },
257                                        reason: { type: 'string' },
258                                        location: { type: 'string' },
259                                        priority: { type: 'string', enum: ['high', 'medium', 'low'] }
260                                    },
261                                    required: ['original', 'improved', 'reason', 'location', 'priority']
262                                }
263                            },
264                            usabilityIssues: {
265                                type: 'array',
266                                items: {
267                                    type: 'object',
268                                    properties: {
269                                        issue: { type: 'string' },
270                                        recommendation: { type: 'string' },
271                                        impact: { type: 'string', enum: ['high', 'medium', 'low'] },
272                                        category: { type: 'string', enum: ['navigation', 'accessibility', 'forms', 'content', 'visual', 'performance'] }
273                                    },
274                                    required: ['issue', 'recommendation', 'impact', 'category']
275                                }
276                            },
277                            designRecommendations: {
278                                type: 'array',
279                                items: {
280                                    type: 'object',
281                                    properties: {
282                                        recommendation: { type: 'string' },
283                                        benefit: { type: 'string' },
284                                        priority: { type: 'string', enum: ['high', 'medium', 'low'] }
285                                    },
286                                    required: ['recommendation', 'benefit', 'priority']
287                                }
288                            },
289                            overallScore: {
290                                type: 'object',
291                                properties: {
292                                    content: { type: 'number', minimum: 0, maximum: 10 },
293                                    usability: { type: 'number', minimum: 0, maximum: 10 },
294                                    accessibility: { type: 'number', minimum: 0, maximum: 10 }
295                                },
296                                required: ['content', 'usability', 'accessibility']
297                            }
298                        },
299                        required: ['typos', 'copyImprovements', 'usabilityIssues', 'designRecommendations', 'overallScore']
300                    }
301                }
302            });
303
304            return {
305                url: pageContent.url,
306                title: pageContent.title,
307                analysis: analysis
308            };
309        } catch (error) {
310            console.error('AI analysis failed:', error);
311            return {
312                url: pageContent.url,
313                title: pageContent.title,
314                error: 'Analysis failed: ' + error.message
315            };
316        }
317    }
318
319    // Scan current page
320    async function scanCurrentPage() {
321        if (isScanning) return;
322        
323        isScanning = true;
324        showProgress('Scanning current page...');
325        
326        try {
327            const content = extractPageContent();
328            const result = await analyzeContent(content);
329            scanResults = [result];
330            displayResults();
331        } catch (error) {
332            console.error('Scan failed:', error);
333            showError('Scan failed: ' + error.message);
334        } finally {
335            isScanning = false;
336            hideProgress();
337        }
338    }
339
340    // Start full site scan
341    async function startFullSiteScan() {
342        if (isScanning) return;
343        
344        isScanning = true;
345        scanResults = [];
346        scannedPages.clear();
347        
348        showProgress('Discovering pages...');
349        
350        try {
351            const links = extractSiteLinks();
352            console.log(`Found ${links.length} pages to scan`);
353            
354            // Limit to first 10 pages to avoid overwhelming the system
355            const pagesToScan = links.slice(0, 10);
356            
357            for (let i = 0; i < pagesToScan.length; i++) {
358                const url = pagesToScan[i];
359                
360                if (scannedPages.has(url)) continue;
361                scannedPages.add(url);
362                
363                updateProgress(`Scanning page ${i + 1} of ${pagesToScan.length}...`, (i / pagesToScan.length) * 100);
364                
365                try {
366                    // Fetch page content
367                    const response = await GM.xmlhttpRequest({
368                        method: 'GET',
369                        url: url
370                    });
371                    
372                    // Parse HTML
373                    const parser = new DOMParser();
374                    const doc = parser.parseFromString(response.responseText, 'text/html');
375                    
376                    // Extract content from parsed document
377                    const content = extractContentFromDocument(doc, url);
378                    
379                    // Analyze
380                    const result = await analyzeContent(content);
381                    scanResults.push(result);
382                    
383                } catch (error) {
384                    console.error(`Failed to scan ${url}:`, error);
385                    scanResults.push({
386                        url: url,
387                        error: 'Failed to fetch or analyze page'
388                    });
389                }
390            }
391            
392            displayResults();
393            
394        } catch (error) {
395            console.error('Full site scan failed:', error);
396            showError('Scan failed: ' + error.message);
397        } finally {
398            isScanning = false;
399            hideProgress();
400        }
401    }
402
403    // Extract content from a document object
404    function extractContentFromDocument(doc, url) {
405        const content = {
406            url: url,
407            title: doc.title,
408            headings: [],
409            paragraphs: [],
410            buttons: [],
411            links: [],
412            forms: [],
413            images: [],
414            metadata: {}
415        };
416
417        // Extract headings
418        doc.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
419            if (heading.textContent.trim()) {
420                content.headings.push({
421                    level: heading.tagName,
422                    text: heading.textContent.trim()
423                });
424            }
425        });
426
427        // Extract paragraphs
428        doc.querySelectorAll('p').forEach(p => {
429            const text = p.textContent.trim();
430            if (text && text.length > 10) {
431                content.paragraphs.push(text);
432            }
433        });
434
435        // Extract buttons
436        doc.querySelectorAll('button, a.btn, [role="button"]').forEach(btn => {
437            const text = btn.textContent.trim();
438            if (text) {
439                content.buttons.push({
440                    text: text,
441                    type: btn.tagName
442                });
443            }
444        });
445
446        // Extract images
447        doc.querySelectorAll('img').forEach(img => {
448            content.images.push({
449                src: img.src,
450                alt: img.alt,
451                hasAlt: !!img.alt
452            });
453        });
454
455        return content;
456    }
457
458    // Display results
459    function displayResults() {
460        const resultsContainer = document.getElementById('scan-results');
461        
462        if (scanResults.length === 0) {
463            resultsContainer.innerHTML = '<p class="no-results">No results yet. Start a scan!</p>';
464            return;
465        }
466
467        let html = `<div class="results-summary">
468            <h4>Scan Complete - ${scanResults.length} page(s) analyzed</h4>
469        </div>`;
470
471        scanResults.forEach((result) => {
472            if (result.error) {
473                html += `<div class="result-page">
474                    <h5>${result.title || result.url}</h5>
475                    <p class="error-message">❌ ${result.error}</p>
476                </div>`;
477                return;
478            }
479
480            const analysis = result.analysis;
481            const totalIssues = analysis.typos.length + analysis.copyImprovements.length + analysis.usabilityIssues.length;
482            const isCurrentPage = result.url === window.location.href;
483
484            html += `
485                <div class="result-page">
486                    <div class="result-header">
487                        <h5>${result.title}</h5>
488                        <a href="${result.url}" target="_blank" class="result-url">🔗 View Page</a>
489                    </div>
490                    
491                    <div class="score-cards">
492                        <div class="score-card">
493                            <div class="score-value">${analysis.overallScore.content}/10</div>
494                            <div class="score-label">Content</div>
495                        </div>
496                        <div class="score-card">
497                            <div class="score-value">${analysis.overallScore.usability}/10</div>
498                            <div class="score-label">Usability</div>
499                        </div>
500                        <div class="score-card">
501                            <div class="score-value">${analysis.overallScore.accessibility}/10</div>
502                            <div class="score-label">Accessibility</div>
503                        </div>
504                    </div>
505
506                    <div class="issues-summary">
507                        <span class="issue-count">📝 ${totalIssues} total issues found</span>
508                    </div>
509            `;
510
511            // Typos
512            if (analysis.typos.length > 0) {
513                html += `<div class="issue-section">
514                    <h6>🔤 Typos & Spelling (${analysis.typos.length})</h6>
515                    <div class="issue-list">`;
516                
517                analysis.typos.forEach((typo, index) => {
518                    const severityClass = `severity-${typo.severity}`;
519                    html += `<div class="issue-item ${severityClass}">
520                        <div class="issue-badge">${typo.severity}</div>
521                        <div class="issue-content">
522                            <div class="issue-text"><strong>"${typo.text}"</strong> → "${typo.correction}"</div>
523                            <div class="issue-location">📍 ${typo.location}</div>
524                        </div>
525                        ${isCurrentPage ? `<button class="find-btn" data-text="${escapeHtml(typo.text)}" data-url="${result.url}">📍 Find</button>` : ''}
526                    </div>`;
527                });
528                
529                html += '</div></div>';
530            }
531
532            // Copy Improvements
533            if (analysis.copyImprovements.length > 0) {
534                html += `<div class="issue-section">
535                    <h6>✍️ Copy Improvements (${analysis.copyImprovements.length})</h6>
536                    <div class="issue-list">`;
537                
538                analysis.copyImprovements.forEach((improvement, index) => {
539                    const priorityClass = `priority-${improvement.priority}`;
540                    html += `<div class="issue-item ${priorityClass}">
541                        <div class="issue-badge">${improvement.priority}</div>
542                        <div class="issue-content">
543                            <div class="issue-text"><strong>Original:</strong> "${improvement.original}"</div>
544                            <div class="issue-text"><strong>Improved:</strong> "${improvement.improved}"</div>
545                            <div class="issue-reason">💡 ${improvement.reason}</div>
546                            <div class="issue-location">📍 ${improvement.location}</div>
547                        </div>
548                        ${isCurrentPage ? `<button class="find-btn" data-text="${escapeHtml(improvement.original)}" data-url="${result.url}">📍 Find</button>` : ''}
549                    </div>`;
550                });
551                
552                html += '</div></div>';
553            }
554
555            // Usability Issues
556            if (analysis.usabilityIssues.length > 0) {
557                html += `<div class="issue-section">
558                    <h6>🎯 Usability Issues (${analysis.usabilityIssues.length})</h6>
559                    <div class="issue-list">`;
560                
561                analysis.usabilityIssues.forEach(issue => {
562                    const impactClass = `impact-${issue.impact}`;
563                    const categoryIcon = getCategoryIcon(issue.category);
564                    html += `<div class="issue-item ${impactClass}">
565                        <div class="issue-badge">${issue.impact}</div>
566                        <div class="issue-content">
567                            <div class="issue-category">${categoryIcon} ${issue.category}</div>
568                            <div class="issue-text"><strong>Issue:</strong> ${issue.issue}</div>
569                            <div class="issue-recommendation">✅ <strong>Fix:</strong> ${issue.recommendation}</div>
570                        </div>
571                    </div>`;
572                });
573                
574                html += '</div></div>';
575            }
576
577            // Design Recommendations
578            if (analysis.designRecommendations.length > 0) {
579                html += `<div class="issue-section">
580                    <h6>🎨 Design Recommendations (${analysis.designRecommendations.length})</h6>
581                    <div class="issue-list">`;
582                
583                analysis.designRecommendations.forEach(rec => {
584                    const priorityClass = `priority-${rec.priority}`;
585                    html += `<div class="issue-item ${priorityClass}">
586                        <div class="issue-badge">${rec.priority}</div>
587                        <div class="issue-content">
588                            <div class="issue-text"><strong>${rec.recommendation}</strong></div>
589                            <div class="issue-benefit">💎 ${rec.benefit}</div>
590                        </div>
591                    </div>`;
592                });
593                
594                html += '</div></div>';
595            }
596
597            html += '</div>';
598        });
599
600        resultsContainer.innerHTML = html;
601        
602        // Attach event listeners to find buttons
603        attachFindButtonListeners();
604    }
605
606    // Escape HTML for data attributes
607    function escapeHtml(text) {
608        const div = document.createElement('div');
609        div.textContent = text;
610        return div.innerHTML;
611    }
612
613    // Attach event listeners to find buttons
614    function attachFindButtonListeners() {
615        document.querySelectorAll('.find-btn').forEach(button => {
616            button.addEventListener('click', function() {
617                const textToFind = this.getAttribute('data-text');
618                const url = this.getAttribute('data-url');
619                
620                if (url === window.location.href) {
621                    findAndHighlightText(textToFind);
622                } else {
623                    // Open the page in a new tab
624                    window.open(url, '_blank');
625                }
626            });
627        });
628    }
629
630    // Find and highlight text on the page
631    function findAndHighlightText(searchText) {
632        // Remove previous highlights
633        document.querySelectorAll('.scanner-highlight').forEach(el => {
634            const parent = el.parentNode;
635            parent.replaceChild(document.createTextNode(el.textContent), el);
636            parent.normalize();
637        });
638
639        // Find all text nodes
640        const walker = document.createTreeWalker(
641            document.body,
642            NodeFilter.SHOW_TEXT,
643            {
644                acceptNode: function(node) {
645                    // Skip script, style, and scanner panel
646                    const parent = node.parentElement;
647                    if (!parent) return NodeFilter.FILTER_REJECT;
648                    if (parent.closest('#quality-scanner-panel, #quality-scanner-toggle, script, style, noscript')) {
649                        return NodeFilter.FILTER_REJECT;
650                    }
651                    return NodeFilter.FILTER_ACCEPT;
652                }
653            }
654        );
655
656        const textNodes = [];
657        let node;
658        while (node = walker.nextNode()) {
659            textNodes.push(node);
660        }
661
662        // Search for the text
663        let found = false;
664        const searchLower = searchText.toLowerCase().trim();
665        
666        for (const textNode of textNodes) {
667            const text = textNode.textContent;
668            const textLower = text.toLowerCase();
669            const index = textLower.indexOf(searchLower);
670            
671            if (index !== -1) {
672                found = true;
673                
674                // Split the text node and wrap the match in a highlight span
675                const beforeText = text.substring(0, index);
676                const matchText = text.substring(index, index + searchText.length);
677                const afterText = text.substring(index + searchText.length);
678                
679                const highlight = document.createElement('span');
680                highlight.className = 'scanner-highlight';
681                highlight.textContent = matchText;
682                
683                const parent = textNode.parentNode;
684                const beforeNode = document.createTextNode(beforeText);
685                const afterNode = document.createTextNode(afterText);
686                
687                parent.insertBefore(beforeNode, textNode);
688                parent.insertBefore(highlight, textNode);
689                parent.insertBefore(afterNode, textNode);
690                parent.removeChild(textNode);
691                
692                // Scroll to the highlighted element
693                highlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
694                
695                // Remove highlight after 5 seconds
696                setTimeout(() => {
697                    if (highlight.parentNode) {
698                        const parent = highlight.parentNode;
699                        parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
700                        parent.normalize();
701                    }
702                }, 5000);
703                
704                break;
705            }
706        }
707        
708        if (!found) {
709            alert('Text not found on this page. It may have been changed or is on a different page.');
710        }
711    }
712
713    // Get category icon
714    function getCategoryIcon(category) {
715        const icons = {
716            navigation: '🧭',
717            accessibility: '♿',
718            forms: '📋',
719            content: '📝',
720            visual: '👁️',
721            performance: '⚡'
722        };
723        return icons[category] || '📌';
724    }
725
726    // Progress functions
727    function showProgress(text) {
728        const progressDiv = document.getElementById('scan-progress');
729        const progressText = document.getElementById('progress-text');
730        progressDiv.style.display = 'block';
731        progressText.textContent = text;
732        updateProgress(text, 0);
733    }
734
735    function updateProgress(text, percent) {
736        const progressText = document.getElementById('progress-text');
737        const progressFill = document.getElementById('progress-fill');
738        progressText.textContent = text;
739        progressFill.style.width = percent + '%';
740    }
741
742    function hideProgress() {
743        const progressDiv = document.getElementById('scan-progress');
744        progressDiv.style.display = 'none';
745    }
746
747    function showError(message) {
748        const resultsContainer = document.getElementById('scan-results');
749        resultsContainer.innerHTML = `<div class="error-message">${message}</div>`;
750    }
751
752    // Add styles
753    function addStyles() {
754        const styles = `
755            #quality-scanner-toggle {
756                position: fixed;
757                bottom: 20px;
758                right: 20px;
759                width: 60px;
760                height: 60px;
761                border-radius: 50%;
762                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
763                color: white;
764                border: none;
765                font-size: 28px;
766                cursor: pointer;
767                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
768                z-index: 9999;
769                transition: transform 0.2s, box-shadow 0.2s;
770            }
771
772            #quality-scanner-toggle:hover {
773                transform: scale(1.1);
774                box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
775            }
776
777            #quality-scanner-panel {
778                position: fixed;
779                top: 50%;
780                left: 50%;
781                transform: translate(-50%, -50%);
782                width: 90%;
783                max-width: 900px;
784                max-height: 85vh;
785                background: white;
786                border-radius: 12px;
787                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
788                z-index: 10000;
789                display: flex;
790                flex-direction: column;
791                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
792            }
793
794            .scanner-header {
795                display: flex;
796                justify-content: space-between;
797                align-items: center;
798                padding: 20px 24px;
799                border-bottom: 1px solid #e5e7eb;
800                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
801                color: white;
802                border-radius: 12px 12px 0 0;
803            }
804
805            .scanner-header h3 {
806                margin: 0;
807                font-size: 20px;
808                font-weight: 600;
809            }
810
811            .scanner-btn-icon {
812                background: rgba(255, 255, 255, 0.2);
813                border: none;
814                color: white;
815                width: 32px;
816                height: 32px;
817                border-radius: 6px;
818                cursor: pointer;
819                font-size: 18px;
820                display: flex;
821                align-items: center;
822                justify-content: center;
823                transition: background 0.2s;
824            }
825
826            .scanner-btn-icon:hover {
827                background: rgba(255, 255, 255, 0.3);
828            }
829
830            .scanner-body {
831                padding: 24px;
832                overflow-y: auto;
833                flex: 1;
834            }
835
836            .scanner-controls {
837                display: flex;
838                gap: 12px;
839                margin-bottom: 20px;
840            }
841
842            .scanner-btn {
843                padding: 12px 24px;
844                border: none;
845                border-radius: 8px;
846                font-size: 14px;
847                font-weight: 600;
848                cursor: pointer;
849                transition: all 0.2s;
850                flex: 1;
851            }
852
853            .scanner-btn-primary {
854                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
855                color: white;
856            }
857
858            .scanner-btn-primary:hover {
859                transform: translateY(-2px);
860                box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
861            }
862
863            .scanner-btn-secondary {
864                background: #f3f4f6;
865                color: #374151;
866            }
867
868            .scanner-btn-secondary:hover {
869                background: #e5e7eb;
870            }
871
872            .scan-progress {
873                margin-bottom: 20px;
874                padding: 20px;
875                background: #f9fafb;
876                border-radius: 8px;
877            }
878
879            .progress-bar {
880                width: 100%;
881                height: 8px;
882                background: #e5e7eb;
883                border-radius: 4px;
884                overflow: hidden;
885                margin-bottom: 12px;
886            }
887
888            .progress-fill {
889                height: 100%;
890                background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
891                transition: width 0.3s;
892                border-radius: 4px;
893            }
894
895            #progress-text {
896                margin: 0;
897                color: #6b7280;
898                font-size: 14px;
899                text-align: center;
900            }
901
902            .scan-results {
903                display: flex;
904                flex-direction: column;
905                gap: 20px;
906            }
907
908            .results-summary {
909                padding: 16px;
910                background: #f0fdf4;
911                border-left: 4px solid #10b981;
912                border-radius: 8px;
913            }
914
915            .results-summary h4 {
916                margin: 0;
917                color: #065f46;
918                font-size: 16px;
919            }
920
921            .result-page {
922                border: 1px solid #e5e7eb;
923                border-radius: 8px;
924                padding: 20px;
925                background: white;
926            }
927
928            .result-header {
929                display: flex;
930                justify-content: space-between;
931                align-items: center;
932                margin-bottom: 16px;
933                padding-bottom: 16px;
934                border-bottom: 2px solid #f3f4f6;
935            }
936
937            .result-header h5 {
938                margin: 0;
939                font-size: 18px;
940                color: #111827;
941            }
942
943            .result-url {
944                color: #667eea;
945                text-decoration: none;
946                font-size: 14px;
947                font-weight: 500;
948            }
949
950            .result-url:hover {
951                text-decoration: underline;
952            }
953
954            .score-cards {
955                display: grid;
956                grid-template-columns: repeat(3, 1fr);
957                gap: 12px;
958                margin-bottom: 16px;
959            }
960
961            .score-card {
962                background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
963                padding: 16px;
964                border-radius: 8px;
965                text-align: center;
966            }
967
968            .score-value {
969                font-size: 28px;
970                font-weight: 700;
971                color: #111827;
972                margin-bottom: 4px;
973            }
974
975            .score-label {
976                font-size: 12px;
977                color: #6b7280;
978                text-transform: uppercase;
979                font-weight: 600;
980            }
981
982            .issues-summary {
983                margin-bottom: 20px;
984                padding: 12px;
985                background: #fef3c7;
986                border-radius: 6px;
987                text-align: center;
988            }
989
990            .issue-count {
991                color: #92400e;
992                font-weight: 600;
993                font-size: 14px;
994            }
995
996            .issue-section {
997                margin-bottom: 20px;
998            }
999
1000            .issue-section h6 {
1001                margin: 0 0 12px 0;
1002                font-size: 16px;
1003                color: #111827;
1004                font-weight: 600;
1005            }
1006
1007            .issue-list {
1008                display: flex;
1009                flex-direction: column;
1010                gap: 12px;
1011            }
1012
1013            .issue-item {
1014                display: flex;
1015                gap: 12px;
1016                padding: 16px;
1017                border-radius: 8px;
1018                border-left: 4px solid #d1d5db;
1019                background: #f9fafb;
1020            }
1021
1022            .issue-item.severity-high,
1023            .issue-item.impact-high,
1024            .issue-item.priority-high {
1025                border-left-color: #ef4444;
1026                background: #fef2f2;
1027            }
1028
1029            .issue-item.severity-medium,
1030            .issue-item.impact-medium,
1031            .issue-item.priority-medium {
1032                border-left-color: #f59e0b;
1033                background: #fffbeb;
1034            }
1035
1036            .issue-item.severity-low,
1037            .issue-item.impact-low,
1038            .issue-item.priority-low {
1039                border-left-color: #3b82f6;
1040                background: #eff6ff;
1041            }
1042
1043            .issue-badge {
1044                background: #374151;
1045                color: white;
1046                padding: 4px 8px;
1047                border-radius: 4px;
1048                font-size: 11px;
1049                font-weight: 600;
1050                text-transform: uppercase;
1051                height: fit-content;
1052            }
1053
1054            .issue-content {
1055                flex: 1;
1056            }
1057
1058            .issue-text {
1059                margin-bottom: 8px;
1060                color: #374151;
1061                font-size: 14px;
1062                line-height: 1.5;
1063            }
1064
1065            .issue-location,
1066            .issue-reason,
1067            .issue-recommendation,
1068            .issue-benefit,
1069            .issue-category {
1070                margin-top: 8px;
1071                color: #6b7280;
1072                font-size: 13px;
1073                line-height: 1.5;
1074            }
1075
1076            .error-message {
1077                padding: 16px;
1078                background: #fef2f2;
1079                border-left: 4px solid #ef4444;
1080                border-radius: 8px;
1081                color: #991b1b;
1082            }
1083
1084            .no-results {
1085                text-align: center;
1086                color: #6b7280;
1087                padding: 40px;
1088                font-size: 16px;
1089            }
1090
1091            .find-btn {
1092                background: #667eea;
1093                color: white;
1094                border: none;
1095                padding: 8px 16px;
1096                border-radius: 6px;
1097                font-size: 12px;
1098                font-weight: 600;
1099                cursor: pointer;
1100                transition: all 0.2s;
1101                white-space: nowrap;
1102                height: fit-content;
1103            }
1104
1105            .find-btn:hover {
1106                background: #5568d3;
1107                transform: translateY(-1px);
1108                box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
1109            }
1110
1111            .scanner-highlight {
1112                background: #fef08a;
1113                padding: 2px 4px;
1114                border-radius: 3px;
1115                animation: pulse-highlight 1s ease-in-out;
1116                box-shadow: 0 0 0 3px rgba(250, 204, 21, 0.3);
1117            }
1118
1119            @keyframes pulse-highlight {
1120                0%, 100% {
1121                    background: #fef08a;
1122                }
1123                50% {
1124                    background: #fde047;
1125                }
1126            }
1127        `;
1128
1129        TM_addStyle(styles);
1130    }
1131
1132    // Initialize
1133    function init() {
1134        console.log('Quality Scanner initialized');
1135        addStyles();
1136        createToggleButton();
1137        createScannerPanel();
1138    }
1139
1140    // Run when DOM is ready
1141    if (document.readyState === 'loading') {
1142        document.addEventListener('DOMContentLoaded', init);
1143    } else {
1144        init();
1145    }
1146
1147})();
Site Quality Scanner - Typos, Copy & Usability Analyzer | Robomonkey