Site Quality Scanner - Typos, Copy & Usability Analyzer

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

Size

40.5 KB

Version

1.1.3

Created

Nov 3, 2025

Updated

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