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