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})();