diff --git a/.claude/settings.json b/.claude/settings.json index 5266dfd..001ec9d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,18 @@ { "permissions": { - "allow": [ - "Bash(npx playwright:*)" + "allow": ["Bash(npx playwright:*)"] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/scripts/claude-guard.js\"" + } + ] + } ] } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8014e75..fc9709b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,12 @@ jobs: - name: Install dependencies run: npm ci + - name: Lint + run: npm run lint + + - name: Check formatting + run: npm run format:check + - name: Run unit tests run: npm test diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..f4e330c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +npx lint-staged +npm test +if git diff --cached --name-only | grep -qx "manifest.json"; then + node scripts/release-check.js --staged +fi diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..5de9c79 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +node scripts/pre-push.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..643bb5b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +lib/ +node_modules/ +playwright-report/ +test-results/ +coverage/ +*.zip +*.min.js +HTMLReport/TableFilter/ +package-lock.json +*.min.css diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..c93aaff --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": true, + "printWidth": 100, + "trailingComma": "es5", + "endOfLine": "auto" +} diff --git a/AGENTS.md b/AGENTS.md index 1aa98fd..bea3be1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,9 @@ Load the extension manually: `chrome://extensions` → Developer mode → **Load | `npm run test:e2e:ui` / `:debug` | Playwright UI mode / step debugger | | `npm run test:e2e:crop` | Crop-screenshot specs, headed | | `npm run test:e2e:report` | Open last HTML report | +| `npm run lint` / `npm run format` | ESLint / Prettier write (`format:check` only verifies) | +| `npm run check` | Lint + format check + unit tests | +| `npm run release:check` | Release gate: version bumped, `package.json` = `manifest.json`, zip allowlist covers the manifest, no high/critical `npm audit` findings | | `npm run test:all` | Unit + full E2E | | `.\genetareZip.ps1` | Build `ETExtension-.zip` for Web Store upload (PowerShell, runtime files only) | | `.\start_test_server.ps1` | `python -m http.server 8000` serving the repo; open `/test/e2e/test-pages/index.html` | @@ -160,9 +163,19 @@ If your agent supports these, use them: ## Definition of done -1. `npm test` passes. +1. `npm run check` passes (ESLint, Prettier check, Jest). Run `npm run format` to fix formatting. 2. Relevant E2E specs pass (`npm run test:e2e:all` for UI / message-flow changes). 3. New `chrome.*` calls are mocked in `jest.setup.js`; new specs are wired into `package.json`. 4. Extension reloaded and the change checked manually in Chrome when it affects UI. 5. The tutorial (`js/tutorial.js`, `tutorial.html`, images) and README features / screenshots updated if the change is user-visible. 6. This file updated if architecture, commands or conventions changed. +7. Release: bump `manifest.json` `version` (and `package.json` to match), then `npm run release:check` passes. + +### Git hooks (Husky) + +Installed by `npm install` (`prepare` script). They are the gate; do not bypass them. + +- **pre-commit**: lint-staged (ESLint + Prettier on staged files), full Jest suite, and `release:check` when the content of `manifest.json` changes. +- **pre-push**: headless E2E (`npm run test:e2e:ci`) when runtime files changed in the pushed range; skipped otherwise. CI runs the same suite. +- Never use `--no-verify` or `HUSKY=0`; a Claude Code hook (`scripts/claude-guard.js`) blocks them. If a hook fails, fix the cause. +- Why the checks are split this way: `docs/adr/0001-tiered-git-hooks.md`. diff --git a/HTMLReport/modules/reportData.js b/HTMLReport/modules/reportData.js index dc835be..9a30e11 100644 --- a/HTMLReport/modules/reportData.js +++ b/HTMLReport/modules/reportData.js @@ -6,32 +6,35 @@ import { Bug, Note, Idea, Question } from '../../src/Annotation.js'; * @returns {Promise} The reconstructed session or null if no data. */ export async function loadSessionData() { - const response = await chrome.runtime.sendMessage({ type: "getSessionData" }); - if (!response || response.annotationsCount === 0) { - return null; - } + const response = await chrome.runtime.sendMessage({ type: 'getSessionData' }); + if (!response || response.annotationsCount === 0) { + return null; + } - const sessionData = await chrome.runtime.sendMessage({ type: "getFullSession" }); - if (!sessionData) { - throw new Error('Could not get full session data'); - } + const sessionData = await chrome.runtime.sendMessage({ type: 'getFullSession' }); + if (!sessionData) { + throw new Error('Could not get full session data'); + } - const session = new Session(sessionData.startDateTime, sessionData.browserInfo); + const session = new Session(sessionData.startDateTime, sessionData.browserInfo); - const annotationConstructors = { Bug, Note, Idea, Question }; - const addMethods = { Bug: 'addBug', Note: 'addNote', Idea: 'addIdea', Question: 'addQuestion' }; + const annotationConstructors = { Bug, Note, Idea, Question }; + const addMethods = { Bug: 'addBug', Note: 'addNote', Idea: 'addIdea', Question: 'addQuestion' }; - sessionData.annotations.forEach(annotation => { - const Constructor = annotationConstructors[annotation.type]; - if (Constructor) { - const newAnnotation = new Constructor( - annotation.name, annotation.url, annotation.timestamp, annotation.imageURL - ); - session[addMethods[annotation.type]](newAnnotation); - } - }); + sessionData.annotations.forEach((annotation) => { + const Constructor = annotationConstructors[annotation.type]; + if (Constructor) { + const newAnnotation = new Constructor( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session[addMethods[annotation.type]](newAnnotation); + } + }); - return session; + return session; } /** @@ -40,12 +43,9 @@ export async function loadSessionData() { * @returns {Promise} The response from the background script. */ export function deleteAnnotation(annotationID) { - return new Promise((resolve) => { - chrome.runtime.sendMessage( - { type: "deleteAnnotation", annotationID }, - resolve - ); - }); + return new Promise((resolve) => { + chrome.runtime.sendMessage({ type: 'deleteAnnotation', annotationID }, resolve); + }); } /** @@ -54,15 +54,15 @@ export function deleteAnnotation(annotationID) { * @returns {object} */ export function serializeSession(session) { - return { - startDateTime: session.getStartDateTime(), - browserInfo: session.getBrowserInfo(), - annotations: session.getAnnotations().map(a => ({ - type: a.constructor.name, - name: a.name, - url: a.url, - timestamp: a.timestamp, - imageURL: a.imageURL - })) - }; + return { + startDateTime: session.getStartDateTime(), + browserInfo: session.getBrowserInfo(), + annotations: session.getAnnotations().map((a) => ({ + type: a.constructor.name, + name: a.name, + url: a.url, + timestamp: a.timestamp, + imageURL: a.imageURL, + })), + }; } diff --git a/HTMLReport/modules/reportDownload.js b/HTMLReport/modules/reportDownload.js index 5d3e276..3b4953f 100644 --- a/HTMLReport/modules/reportDownload.js +++ b/HTMLReport/modules/reportDownload.js @@ -4,196 +4,206 @@ import { serializeSession } from './reportData.js'; * Downloads all screenshots as a ZIP file. */ export async function downloadAllImages(session) { - const annotations = session.getAnnotations(); - const imagesWithScreenshots = annotations.filter(a => a.imageURL); + const annotations = session.getAnnotations(); + const imagesWithScreenshots = annotations.filter((a) => a.imageURL); - if (imagesWithScreenshots.length === 0) { - alert('No screenshots available to download.'); - return; - } + if (imagesWithScreenshots.length === 0) { + alert('No screenshots available to download.'); + return; + } - if (typeof JSZip === 'undefined') { - alert('JSZip library is not loaded. Cannot create ZIP file.'); - return; - } + if (typeof JSZip === 'undefined') { + alert('JSZip library is not loaded. Cannot create ZIP file.'); + return; + } - const zip = new JSZip(); - const imgFolder = zip.folder('screenshots'); + const zip = new JSZip(); + const imgFolder = zip.folder('screenshots'); - // Add README file - const readmeContent = `Exploratory Testing Screenshots + // Add README file + const readmeContent = `Exploratory Testing Screenshots Generated: ${new Date().toLocaleString()} Source: Exploratory Testing Chrome Extension Total screenshots: ${imagesWithScreenshots.length} This ZIP file is safe and contains screenshots captured during your testing session.`; - zip.file('README.txt', readmeContent); - - // Add all images to the ZIP - for (let i = 0; i < imagesWithScreenshots.length; i++) { - const annotation = imagesWithScreenshots[i]; - const type = annotation.constructor.name; - const timestamp = annotation.timestamp ? new Date(annotation.timestamp).toISOString().replace(/[:.]/g, '-') : `annotation-${i}`; - const fileName = `${i + 1}_${type}_${timestamp}.png`; - - // Convert base64 to binary - const base64Data = annotation.imageURL.split(',')[1]; - imgFolder.file(fileName, base64Data, { base64: true }); - } - - // Generate and download the ZIP - try { - const content = await zip.generateAsync({ type: 'blob' }); - const url = URL.createObjectURL(content); - const a = document.createElement('a'); - a.href = url; - a.download = `ExploratoryTesting_Screenshots_${new Date().toISOString().slice(0, 10)}.zip`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } catch (error) { - console.error('Error creating ZIP:', error); - alert('Error creating ZIP file. Please try again.'); - } + zip.file('README.txt', readmeContent); + + // Add all images to the ZIP + for (let i = 0; i < imagesWithScreenshots.length; i++) { + const annotation = imagesWithScreenshots[i]; + const type = annotation.constructor.name; + const timestamp = annotation.timestamp + ? new Date(annotation.timestamp).toISOString().replace(/[:.]/g, '-') + : `annotation-${i}`; + const fileName = `${i + 1}_${type}_${timestamp}.png`; + + // Convert base64 to binary + const base64Data = annotation.imageURL.split(',')[1]; + imgFolder.file(fileName, base64Data, { base64: true }); + } + + // Generate and download the ZIP + try { + const content = await zip.generateAsync({ type: 'blob' }); + const url = URL.createObjectURL(content); + const a = document.createElement('a'); + a.href = url; + a.download = `ExploratoryTesting_Screenshots_${new Date().toISOString().slice(0, 10)}.zip`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + console.error('Error creating ZIP:', error); + alert('Error creating ZIP file. Please try again.'); + } } const SVG_PATHS = { - Bug: '../images/bug.svg', - Note: '../images/note.svg', - Idea: '../images/light-bulb.svg', - Question: '../images/question.svg' + Bug: '../images/bug.svg', + Note: '../images/note.svg', + Idea: '../images/light-bulb.svg', + Question: '../images/question.svg', }; /** * Downloads a standalone HTML report with all resources embedded. */ export async function downloadCompleteReport(session) { - const reportContent = document.getElementById('report').cloneNode(true); + const reportContent = document.getElementById('report').cloneNode(true); - // Remove interactive-only elements from download - removeInteractiveElements(reportContent); + // Remove interactive-only elements from download + removeInteractiveElements(reportContent); - // Embed all images as base64 - await embedImages(reportContent); - const icons = await loadSvgIconsAsBase64(); - replaceSvgIcons(reportContent, icons); + // Embed all images as base64 + await embedImages(reportContent); + const icons = await loadSvgIconsAsBase64(); + replaceSvgIcons(reportContent, icons); - const styles = extractStyles(); - const sessionJSON = JSON.stringify(serializeSession(session)); + const styles = extractStyles(); + const sessionJSON = JSON.stringify(serializeSession(session)); - const html = buildStandaloneHtml(reportContent.outerHTML, styles, sessionJSON); + const html = buildStandaloneHtml(reportContent.outerHTML, styles, sessionJSON); - triggerDownload(html); + triggerDownload(html); } function removeInteractiveElements(container) { - // Convert chart canvas to static image - const chartCanvas = document.getElementById('annotationsChart'); - const chartContainer = container.querySelector('#chartContainer'); - if (chartCanvas && chartContainer) { - try { - // Get the canvas as base64 image - const chartImageData = chartCanvas.toDataURL('image/png'); - - // Replace canvas with img element - const canvas = chartContainer.querySelector('canvas'); - if (canvas) { - const img = document.createElement('img'); - img.src = chartImageData; - img.alt = 'Annotations Distribution Chart'; - img.style.maxWidth = '100%'; - img.style.height = 'auto'; - canvas.replaceWith(img); - } - } catch (error) { - console.error('Error converting chart to image:', error); - // If conversion fails, remove the chart - if (chartContainer) chartContainer.remove(); - } - } - - // Remove both download buttons - const downloadReportBtn = container.querySelector('#downloadReportBtn'); - if (downloadReportBtn) downloadReportBtn.remove(); - - const downloadImagesBtn = container.querySelector('#downloadImagesBtn'); - if (downloadImagesBtn) downloadImagesBtn.remove(); - - // Remove button group if it's now empty - const buttonGroup = container.querySelector('.button-group'); - if (buttonGroup && buttonGroup.children.length === 0) { - buttonGroup.remove(); - } - - // Remove delete column from header - const headerRow = container.querySelector('thead tr'); - if (headerRow && headerRow.lastElementChild) { - headerRow.lastElementChild.remove(); + // Convert chart canvas to static image + const chartCanvas = document.getElementById('annotationsChart'); + const chartContainer = container.querySelector('#chartContainer'); + if (chartCanvas && chartContainer) { + try { + // Get the canvas as base64 image + const chartImageData = chartCanvas.toDataURL('image/png'); + + // Replace canvas with img element + const canvas = chartContainer.querySelector('canvas'); + if (canvas) { + const img = document.createElement('img'); + img.src = chartImageData; + img.alt = 'Annotations Distribution Chart'; + img.style.maxWidth = '100%'; + img.style.height = 'auto'; + canvas.replaceWith(img); + } + } catch (error) { + console.error('Error converting chart to image:', error); + // If conversion fails, remove the chart + if (chartContainer) chartContainer.remove(); } - - // Remove delete column from each row - container.querySelectorAll('tbody tr').forEach(row => { - if (row.lastElementChild) row.lastElementChild.remove(); - }); + } + + // Remove both download buttons + const downloadReportBtn = container.querySelector('#downloadReportBtn'); + if (downloadReportBtn) downloadReportBtn.remove(); + + const downloadImagesBtn = container.querySelector('#downloadImagesBtn'); + if (downloadImagesBtn) downloadImagesBtn.remove(); + + // Remove button group if it's now empty + const buttonGroup = container.querySelector('.button-group'); + if (buttonGroup && buttonGroup.children.length === 0) { + buttonGroup.remove(); + } + + // Remove delete column from header + const headerRow = container.querySelector('thead tr'); + if (headerRow && headerRow.lastElementChild) { + headerRow.lastElementChild.remove(); + } + + // Remove delete column from each row + container.querySelectorAll('tbody tr').forEach((row) => { + if (row.lastElementChild) row.lastElementChild.remove(); + }); } async function embedImages(container) { - const images = container.querySelectorAll('.preview-image'); - await Promise.all(Array.from(images).map(img => { - return new Promise((resolve) => { - if (!img.src) return resolve(); - const tempImg = new Image(); - tempImg.crossOrigin = 'Anonymous'; - tempImg.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = tempImg.width; - canvas.height = tempImg.height; - canvas.getContext('2d').drawImage(tempImg, 0, 0); - img.src = canvas.toDataURL('image/png'); - resolve(); - }; - tempImg.onerror = () => resolve(); - tempImg.src = img.src; - }); - })); + const images = container.querySelectorAll('.preview-image'); + await Promise.all( + Array.from(images).map((img) => { + return new Promise((resolve) => { + if (!img.src) return resolve(); + const tempImg = new Image(); + tempImg.crossOrigin = 'Anonymous'; + tempImg.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = tempImg.width; + canvas.height = tempImg.height; + canvas.getContext('2d').drawImage(tempImg, 0, 0); + img.src = canvas.toDataURL('image/png'); + resolve(); + }; + tempImg.onerror = () => resolve(); + tempImg.src = img.src; + }); + }) + ); } async function loadSvgIconsAsBase64() { - const entries = Object.entries(SVG_PATHS); - const results = await Promise.all( - entries.map(([type, path]) => fetch(path).then(r => r.text()).then(svg => [type, svg])) - ); - const icons = {}; - results.forEach(([type, svg]) => { - icons[type] = `data:image/svg+xml;base64,${btoa(svg)}`; - }); - return icons; + const entries = Object.entries(SVG_PATHS); + const results = await Promise.all( + entries.map(([type, path]) => + fetch(path) + .then((r) => r.text()) + .then((svg) => [type, svg]) + ) + ); + const icons = {}; + results.forEach(([type, svg]) => { + icons[type] = `data:image/svg+xml;base64,${btoa(svg)}`; + }); + return icons; } function replaceSvgIcons(container, icons) { - container.querySelectorAll('.annotation-icon').forEach(icon => { - const type = icon.alt; - if (icons[type]) { - icon.src = icons[type]; - } - }); + container.querySelectorAll('.annotation-icon').forEach((icon) => { + const type = icon.alt; + if (icons[type]) { + icon.src = icons[type]; + } + }); } function extractStyles() { - return Array.from(document.styleSheets) - .map(sheet => { - try { - return Array.from(sheet.cssRules).map(rule => rule.cssText).join('\n'); - } catch { - return ''; - } - }) - .join('\n'); + return Array.from(document.styleSheets) + .map((sheet) => { + try { + return Array.from(sheet.cssRules) + .map((rule) => rule.cssText) + .join('\n'); + } catch { + return ''; + } + }) + .join('\n'); } function buildStandaloneHtml(reportHtml, styles, sessionJSON) { - return ` + return ` @@ -275,13 +285,13 @@ function buildStandaloneHtml(reportHtml, styles, sessionJSON) { } function triggerDownload(htmlContent) { - const blob = new Blob([htmlContent], { type: 'text/html' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `ExploratoryTestingReport_${new Date().toISOString().slice(0, 10)}.html`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `ExploratoryTestingReport_${new Date().toISOString().slice(0, 10)}.html`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); } diff --git a/HTMLReport/modules/reportEvents.js b/HTMLReport/modules/reportEvents.js index fef17a0..415ac2f 100644 --- a/HTMLReport/modules/reportEvents.js +++ b/HTMLReport/modules/reportEvents.js @@ -9,121 +9,121 @@ let currentFilter = 'all'; * Returns the current active filter type. */ export function getCurrentFilter() { - return currentFilter; + return currentFilter; } /** * Sets up all interactive event listeners for the report. */ export function setupAllListeners(session) { - setupFilterListeners(session); - setupDeleteListeners(); - setupImagePreviewListeners(); - setupDownloadListener(session); + setupFilterListeners(session); + setupDeleteListeners(); + setupImagePreviewListeners(); + setupDownloadListener(session); } /** * Re-binds row-level listeners after table re-render. */ export function rebindTableListeners() { - setupImagePreviewListeners(); + setupImagePreviewListeners(); } function setupFilterListeners(session) { - document.querySelectorAll('.filter-pill').forEach(button => { - button.addEventListener('click', function () { - document.querySelectorAll('.filter-pill').forEach(btn => btn.classList.remove('active')); - this.classList.add('active'); - currentFilter = this.dataset.type; - displayAnnotationsTable(session, currentFilter); - rebindTableListeners(); - }); + document.querySelectorAll('.filter-pill').forEach((button) => { + button.addEventListener('click', function () { + document.querySelectorAll('.filter-pill').forEach((btn) => btn.classList.remove('active')); + this.classList.add('active'); + currentFilter = this.dataset.type; + displayAnnotationsTable(session, currentFilter); + rebindTableListeners(); }); + }); } function setupDeleteListeners() { - // Delegate click on delete buttons - document.getElementById('annotationsTableBody').addEventListener('click', (e) => { - const btn = e.target.closest('.delete-btn'); - if (!btn) return; - annotationToDelete = parseInt(btn.dataset.index); - document.getElementById('divOverlay').style.display = 'block'; - }); - - document.getElementById('cancelDelete').addEventListener('click', () => { - document.getElementById('divOverlay').style.display = 'none'; - annotationToDelete = null; - }); - - document.getElementById('deleteYes').addEventListener('click', async () => { - if (annotationToDelete === null) return; - const response = await deleteAnnotation(annotationToDelete); - document.getElementById('divOverlay').style.display = 'none'; - annotationToDelete = null; - // Reload to resync regardless of response - location.reload(); - }); + // Delegate click on delete buttons + document.getElementById('annotationsTableBody').addEventListener('click', (e) => { + const btn = e.target.closest('.delete-btn'); + if (!btn) return; + annotationToDelete = parseInt(btn.dataset.index); + document.getElementById('divOverlay').style.display = 'block'; + }); + + document.getElementById('cancelDelete').addEventListener('click', () => { + document.getElementById('divOverlay').style.display = 'none'; + annotationToDelete = null; + }); + + document.getElementById('deleteYes').addEventListener('click', async () => { + if (annotationToDelete === null) return; + await deleteAnnotation(annotationToDelete); + document.getElementById('divOverlay').style.display = 'none'; + annotationToDelete = null; + // Reload to resync regardless of response + location.reload(); + }); } function setupImagePreviewListeners() { - document.querySelectorAll('.preview-image').forEach(img => { - img.addEventListener('click', () => showImagePreview(img.dataset.preview)); - img.addEventListener('mouseenter', (e) => showHoverPreview(img.dataset.preview, e)); - img.addEventListener('mousemove', updateHoverPosition); - img.addEventListener('mouseleave', hideHoverPreview); - }); + document.querySelectorAll('.preview-image').forEach((img) => { + img.addEventListener('click', () => showImagePreview(img.dataset.preview)); + img.addEventListener('mouseenter', (e) => showHoverPreview(img.dataset.preview, e)); + img.addEventListener('mousemove', updateHoverPosition); + img.addEventListener('mouseleave', hideHoverPreview); + }); } function setupDownloadListener(session) { - document.getElementById('downloadReportBtn').addEventListener('click', () => { - downloadCompleteReport(session); - }); + document.getElementById('downloadReportBtn').addEventListener('click', () => { + downloadCompleteReport(session); + }); - document.getElementById('downloadImagesBtn').addEventListener('click', () => { - downloadAllImages(session); - }); + document.getElementById('downloadImagesBtn').addEventListener('click', () => { + downloadAllImages(session); + }); } // --- Image Preview --- function showImagePreview(src) { - const preview = document.getElementById('imagePreview'); - const previewImg = preview.querySelector('img'); - previewImg.src = src; - preview.classList.add('active'); - - const closePreview = () => { - preview.classList.remove('active'); - preview.removeEventListener('click', closePreview); - }; - preview.addEventListener('click', closePreview); - previewImg.addEventListener('click', (e) => e.stopPropagation()); + const preview = document.getElementById('imagePreview'); + const previewImg = preview.querySelector('img'); + previewImg.src = src; + preview.classList.add('active'); + + const closePreview = () => { + preview.classList.remove('active'); + preview.removeEventListener('click', closePreview); + }; + preview.addEventListener('click', closePreview); + previewImg.addEventListener('click', (e) => e.stopPropagation()); } function showHoverPreview(src, event) { - const preview = document.getElementById('imageHoverPreview'); - preview.querySelector('img').src = src; - preview.classList.add('active'); - updateHoverPosition(event); + const preview = document.getElementById('imageHoverPreview'); + preview.querySelector('img').src = src; + preview.classList.add('active'); + updateHoverPosition(event); } function updateHoverPosition(event) { - const preview = document.getElementById('imageHoverPreview'); - if (!preview.classList.contains('active')) return; + const preview = document.getElementById('imageHoverPreview'); + if (!preview.classList.contains('active')) return; - const offset = 15; - const pw = preview.offsetWidth; - const ph = preview.offsetHeight; - let left = event.clientX + offset; - let top = event.clientY + offset; + const offset = 15; + const pw = preview.offsetWidth; + const ph = preview.offsetHeight; + let left = event.clientX + offset; + let top = event.clientY + offset; - if (left + pw > window.innerWidth) left = event.clientX - pw - offset; - if (top + ph > window.innerHeight) top = event.clientY - ph - offset; + if (left + pw > window.innerWidth) left = event.clientX - pw - offset; + if (top + ph > window.innerHeight) top = event.clientY - ph - offset; - preview.style.left = left + 'px'; - preview.style.top = top + 'px'; + preview.style.left = left + 'px'; + preview.style.top = top + 'px'; } function hideHoverPreview() { - document.getElementById('imageHoverPreview').classList.remove('active'); + document.getElementById('imageHoverPreview').classList.remove('active'); } diff --git a/HTMLReport/modules/reportUI.js b/HTMLReport/modules/reportUI.js index 0233bc8..4ddc9a8 100644 --- a/HTMLReport/modules/reportUI.js +++ b/HTMLReport/modules/reportUI.js @@ -1,26 +1,26 @@ const ANNOTATION_ICONS = { - Bug: '../images/bug.svg', - Note: '../images/note.svg', - Idea: '../images/light-bulb.svg', - Question: '../images/question.svg' + Bug: '../images/bug.svg', + Note: '../images/note.svg', + Idea: '../images/light-bulb.svg', + Question: '../images/question.svg', }; const ANNOTATION_COLORS = { - Bug: '#ef4444', - Note: '#22c55e', - Idea: '#f59e0b', - Question: '#3b82f6' + Bug: '#ef4444', + Note: '#22c55e', + Idea: '#f59e0b', + Question: '#3b82f6', }; /** * Renders session information in the header area. */ export function displaySessionInfo(session) { - const sessionInfo = document.getElementById('sessionInfo'); - const browserInfo = session.getBrowserInfo(); - const startDateTime = session.getStartDateTime(); + const sessionInfo = document.getElementById('sessionInfo'); + const browserInfo = session.getBrowserInfo(); + const startDateTime = session.getStartDateTime(); - sessionInfo.innerHTML = ` + sessionInfo.innerHTML = `
Start Date ${startDateTime.toLocaleString()} @@ -44,15 +44,22 @@ export function displaySessionInfo(session) { * Renders the summary stat cards. */ export function displayStats(session) { - const stats = [ - { type: 'Bug', label: 'Bugs', count: session.getBugs().length, icon: ANNOTATION_ICONS.Bug }, - { type: 'Note', label: 'Notes', count: session.getNotes().length, icon: ANNOTATION_ICONS.Note }, - { type: 'Idea', label: 'Ideas', count: session.getIdeas().length, icon: ANNOTATION_ICONS.Idea }, - { type: 'Question', label: 'Questions', count: session.getQuestions().length, icon: ANNOTATION_ICONS.Question } - ]; + const stats = [ + { type: 'Bug', label: 'Bugs', count: session.getBugs().length, icon: ANNOTATION_ICONS.Bug }, + { type: 'Note', label: 'Notes', count: session.getNotes().length, icon: ANNOTATION_ICONS.Note }, + { type: 'Idea', label: 'Ideas', count: session.getIdeas().length, icon: ANNOTATION_ICONS.Idea }, + { + type: 'Question', + label: 'Questions', + count: session.getQuestions().length, + icon: ANNOTATION_ICONS.Question, + }, + ]; - const statsContainer = document.getElementById('statsCards'); - statsContainer.innerHTML = stats.map(stat => ` + const statsContainer = document.getElementById('statsCards'); + statsContainer.innerHTML = stats + .map( + (stat) => `
${stat.type} @@ -62,78 +69,83 @@ export function displayStats(session) { ${stat.label}
- `).join(''); + ` + ) + .join(''); } /** * Creates the pie chart for annotation distribution. */ export function createAnnotationsChart(session) { - const data = [ - session.getBugs().length, - session.getNotes().length, - session.getIdeas().length, - session.getQuestions().length - ]; + const data = [ + session.getBugs().length, + session.getNotes().length, + session.getIdeas().length, + session.getQuestions().length, + ]; - // Don't render chart if no annotations - if (data.every(d => d === 0)) { - document.getElementById('chartContainer').style.display = 'none'; - return; - } + // Don't render chart if no annotations + if (data.every((d) => d === 0)) { + document.getElementById('chartContainer').style.display = 'none'; + return; + } - const ctx = document.getElementById('annotationsChart').getContext('2d'); - new Chart(ctx, { - type: 'doughnut', - data: { - labels: ['Bugs', 'Notes', 'Ideas', 'Questions'], - datasets: [{ - data, - backgroundColor: [ - ANNOTATION_COLORS.Bug, - ANNOTATION_COLORS.Note, - ANNOTATION_COLORS.Idea, - ANNOTATION_COLORS.Question - ], - borderWidth: 0, - hoverOffset: 4 - }] + const ctx = document.getElementById('annotationsChart').getContext('2d'); + new Chart(ctx, { + type: 'doughnut', + data: { + labels: ['Bugs', 'Notes', 'Ideas', 'Questions'], + datasets: [ + { + data, + backgroundColor: [ + ANNOTATION_COLORS.Bug, + ANNOTATION_COLORS.Note, + ANNOTATION_COLORS.Idea, + ANNOTATION_COLORS.Question, + ], + borderWidth: 0, + hoverOffset: 4, }, - options: { - responsive: true, - cutout: '60%', - plugins: { - legend: { - display: false - } - } - } - }); + ], + }, + options: { + responsive: true, + cutout: '60%', + plugins: { + legend: { + display: false, + }, + }, + }, + }); } /** * Renders the annotations table filtered by the current filter. */ export function displayAnnotationsTable(session, currentFilter) { - const tableBody = document.getElementById('annotationsTableBody'); - const annotations = session.getAnnotations(); - const filtered = annotations.filter( - a => currentFilter === 'all' || a.constructor.name === currentFilter - ); + const tableBody = document.getElementById('annotationsTableBody'); + const annotations = session.getAnnotations(); + const filtered = annotations.filter( + (a) => currentFilter === 'all' || a.constructor.name === currentFilter + ); - if (filtered.length === 0) { - tableBody.innerHTML = ` + if (filtered.length === 0) { + tableBody.innerHTML = ` No annotations found for this filter. `; - return; - } + return; + } - tableBody.innerHTML = filtered.map((annotation, index) => { - const type = annotation.constructor.name; - return ` + tableBody.innerHTML = filtered + .map((annotation, index) => { + const type = annotation.constructor.name; + return ` @@ -147,13 +159,15 @@ export function displayAnnotationsTable(session, currentFilter) { ${annotation.timestamp ? new Date(annotation.timestamp).toLocaleString() : 'N/A'} - ${annotation.imageURL - ? `Screenshot` - : '--'} + : '--' + } `; - }).join(''); + }) + .join(''); } function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } function truncateUrl(url) { - try { - const parsed = new URL(url); - const display = parsed.hostname + parsed.pathname; - return display.length > 50 ? display.substring(0, 47) + '...' : display; - } catch { - return url.length > 50 ? url.substring(0, 47) + '...' : url; - } + try { + const parsed = new URL(url); + const display = parsed.hostname + parsed.pathname; + return display.length > 50 ? display.substring(0, 47) + '...' : display; + } catch { + return url.length > 50 ? url.substring(0, 47) + '...' : url; + } } diff --git a/HTMLReport/preview.css b/HTMLReport/preview.css index 8f4513e..293ba47 100644 --- a/HTMLReport/preview.css +++ b/HTMLReport/preview.css @@ -2,37 +2,38 @@ CSS Custom Properties ============================================ */ :root { - --color-bg: #f0f2f5; - --color-surface: #ffffff; - --color-text: #1a1a2e; - --color-text-secondary: #64748b; - --color-border: #e2e8f0; - - --color-bug: #ef4444; - --color-bug-light: #fef2f2; - --color-note: #22c55e; - --color-note-light: #f0fdf4; - --color-idea: #f59e0b; - --color-idea-light: #fffbeb; - --color-question: #3b82f6; - --color-question-light: #eff6ff; - - --color-primary: #6366f1; - --color-primary-dark: #4f46e5; - --color-danger: #ef4444; - --color-danger-dark: #dc2626; - - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-xl: 20px; - - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); - --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12); - - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - --transition: 0.2s ease; + --color-bg: #f0f2f5; + --color-surface: #ffffff; + --color-text: #1a1a2e; + --color-text-secondary: #64748b; + --color-border: #e2e8f0; + + --color-bug: #ef4444; + --color-bug-light: #fef2f2; + --color-note: #22c55e; + --color-note-light: #f0fdf4; + --color-idea: #f59e0b; + --color-idea-light: #fffbeb; + --color-question: #3b82f6; + --color-question-light: #eff6ff; + + --color-primary: #6366f1; + --color-primary-dark: #4f46e5; + --color-danger: #ef4444; + --color-danger-dark: #dc2626; + + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 20px; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12); + + --font-sans: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --transition: 0.2s ease; } /* ============================================ @@ -41,781 +42,835 @@ *, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; + box-sizing: border-box; + margin: 0; + padding: 0; } body { - font-family: var(--font-sans); - background-color: var(--color-bg); - color: var(--color-text); - line-height: 1.6; - -webkit-font-smoothing: antialiased; + font-family: var(--font-sans); + background-color: var(--color-bg); + color: var(--color-text); + line-height: 1.6; + -webkit-font-smoothing: antialiased; } /* ============================================ Report Container ============================================ */ .report { - max-width: 1140px; - margin: 0 auto; - padding: 0 20px 40px; + max-width: 1140px; + margin: 0 auto; + padding: 0 20px 40px; } /* ============================================ Header ============================================ */ .report-header { - background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%); - margin: 0 -20px 20px; - padding: 32px 40px; - border-radius: 0 0 var(--radius-xl) var(--radius-xl); - position: relative; - overflow: hidden; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%); + margin: 0 -20px 20px; + padding: 32px 40px; + border-radius: 0 0 var(--radius-xl) var(--radius-xl); + position: relative; + overflow: hidden; } .report-header::before { - content: ''; - position: absolute; - top: -50%; - right: -20%; - width: 400px; - height: 400px; - background: rgba(255, 255, 255, 0.08); - border-radius: 50%; + content: ''; + position: absolute; + top: -50%; + right: -20%; + width: 400px; + height: 400px; + background: rgba(255, 255, 255, 0.08); + border-radius: 50%; } .report-header::after { - content: ''; - position: absolute; - bottom: -30%; - left: -10%; - width: 300px; - height: 300px; - background: rgba(255, 255, 255, 0.05); - border-radius: 50%; + content: ''; + position: absolute; + bottom: -30%; + left: -10%; + width: 300px; + height: 300px; + background: rgba(255, 255, 255, 0.05); + border-radius: 50%; } .report-header__content { - position: relative; - z-index: 1; + position: relative; + z-index: 1; } .report-header__title { - color: #ffffff; - font-size: 1.5rem; - font-weight: 700; - letter-spacing: -0.02em; - margin-bottom: 2px; + color: #ffffff; + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 2px; } .report-header__subtitle { - color: rgba(255, 255, 255, 0.8); - font-size: 0.9rem; - font-weight: 400; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; + font-weight: 400; } /* ============================================ Sections ============================================ */ .section { - margin-bottom: 16px; + margin-bottom: 16px; } .section--table { - margin-top: 0; + margin-top: 0; } .section--row { - display: flex; - gap: 24px; - align-items: flex-start; + display: flex; + gap: 24px; + align-items: flex-start; } .section--overview { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 20px; - align-items: stretch; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 20px; + align-items: stretch; } @media (max-width: 968px) { - .section--overview { - grid-template-columns: 1fr; - } + .section--overview { + grid-template-columns: 1fr; + } } @media (max-width: 768px) { - .section--row { - flex-direction: column; - } + .section--row { + flex-direction: column; + } } /* ============================================ Session Info ============================================ */ .info-column { - background: var(--color-surface); - padding: 18px 20px; - border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); - border: 1px solid var(--color-border); - display: flex; - flex-direction: column; - gap: 0; - justify-content: space-evenly; + background: var(--color-surface); + padding: 18px 20px; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: 0; + justify-content: space-evenly; } .info-item { - display: flex; - flex-direction: column; - gap: 3px; - padding: 6px 0; + display: flex; + flex-direction: column; + gap: 3px; + padding: 6px 0; } .info-label { - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-secondary); + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); } .info-value { - font-size: 0.85rem; - font-weight: 500; - color: var(--color-text); + font-size: 0.85rem; + font-weight: 500; + color: var(--color-text); } /* ============================================ Stats Cards ============================================ */ .stats-column { - display: flex; - flex-direction: column; - gap: 12px; - justify-content: space-evenly; + display: flex; + flex-direction: column; + gap: 12px; + justify-content: space-evenly; } @media (max-width: 968px) { - .stats-column { - display: grid; - grid-template-columns: repeat(2, 1fr); - } + .stats-column { + display: grid; + grid-template-columns: repeat(2, 1fr); + } } .stat-card { - background: var(--color-surface); - border-radius: var(--radius-md); - padding: 14px 16px; - display: flex; - align-items: center; - gap: 12px; - box-shadow: var(--shadow-sm); - border: 1px solid var(--color-border); - transition: transform var(--transition), box-shadow var(--transition); + background: var(--color-surface); + border-radius: var(--radius-md); + padding: 14px 16px; + display: flex; + align-items: center; + gap: 12px; + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); + transition: + transform var(--transition), + box-shadow var(--transition); } .stat-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); + transform: translateY(-2px); + box-shadow: var(--shadow-md); } .stat-card__icon { - width: 36px; - height: 36px; - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; } .stat-card__icon img { - width: 18px; - height: 18px; + width: 18px; + height: 18px; } -.stat-card--bug .stat-card__icon { background-color: var(--color-bug-light); } -.stat-card--note .stat-card__icon { background-color: var(--color-note-light); } -.stat-card--idea .stat-card__icon { background-color: var(--color-idea-light); } -.stat-card--question .stat-card__icon { background-color: var(--color-question-light); } +.stat-card--bug .stat-card__icon { + background-color: var(--color-bug-light); +} +.stat-card--note .stat-card__icon { + background-color: var(--color-note-light); +} +.stat-card--idea .stat-card__icon { + background-color: var(--color-idea-light); +} +.stat-card--question .stat-card__icon { + background-color: var(--color-question-light); +} .stat-card__content { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .stat-card__count { - font-size: 1.4rem; - font-weight: 700; - line-height: 1.2; + font-size: 1.4rem; + font-weight: 700; + line-height: 1.2; } -.stat-card--bug .stat-card__count { color: var(--color-bug); } -.stat-card--note .stat-card__count { color: var(--color-note); } -.stat-card--idea .stat-card__count { color: var(--color-idea); } -.stat-card--question .stat-card__count { color: var(--color-question); } +.stat-card--bug .stat-card__count { + color: var(--color-bug); +} +.stat-card--note .stat-card__count { + color: var(--color-note); +} +.stat-card--idea .stat-card__count { + color: var(--color-idea); +} +.stat-card--question .stat-card__count { + color: var(--color-question); +} .stat-card__label { - font-size: 0.78rem; - color: var(--color-text-secondary); - font-weight: 500; + font-size: 0.78rem; + color: var(--color-text-secondary); + font-weight: 500; } /* ============================================ Annotation Icons ============================================ */ .annotation-icon { - width: 20px; - height: 20px; - vertical-align: middle; + width: 20px; + height: 20px; + vertical-align: middle; } /* ============================================ Chart Card ============================================ */ .chart-column { - background: var(--color-surface); - border-radius: var(--radius-lg); - padding: 20px; - box-shadow: var(--shadow-sm); - border: 1px solid var(--color-border); - display: flex; - flex-direction: column; - justify-content: center; + background: var(--color-surface); + border-radius: var(--radius-lg); + padding: 20px; + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); + display: flex; + flex-direction: column; + justify-content: center; } .chart-column canvas { - width: 100% !important; - height: auto !important; - max-width: 220px; - max-height: 220px; - margin: 0 auto; + width: 100% !important; + height: auto !important; + max-width: 220px; + max-height: 220px; + margin: 0 auto; } .chart-card__title { - font-size: 0.85rem; - font-weight: 600; - color: var(--color-text); - margin-bottom: 12px; + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text); + margin-bottom: 12px; } /* ============================================ Toolbar Card (Filter + Download) ============================================ */ .toolbar-card { - background: var(--color-surface); - border-radius: var(--radius-lg) var(--radius-lg) 0 0; - padding: 16px 20px; - box-shadow: var(--shadow-sm); - border: 1px solid var(--color-border); - border-bottom: 2px solid var(--color-border); - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - gap: 20px; - flex-wrap: wrap; + background: var(--color-surface); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + padding: 16px 20px; + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); + border-bottom: 2px solid var(--color-border); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 20px; + flex-wrap: wrap; } @media (max-width: 768px) { - .toolbar-card { - flex-direction: column; - align-items: stretch; - } + .toolbar-card { + flex-direction: column; + align-items: stretch; + } } .filter-section { - flex: 1; + flex: 1; } .filter-section__label { - display: block; - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-secondary); - margin-bottom: 8px; + display: block; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); + margin-bottom: 8px; } .filter-pills { - display: flex; - gap: 8px; - flex-wrap: wrap; + display: flex; + gap: 8px; + flex-wrap: wrap; } .filter-pill { - padding: 6px 14px; - border: 1.5px solid var(--color-border); - border-radius: 50px; - background: var(--color-surface); - color: var(--color-text-secondary); - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - transition: all var(--transition); - font-family: var(--font-sans); + padding: 6px 14px; + border: 1.5px solid var(--color-border); + border-radius: 50px; + background: var(--color-surface); + color: var(--color-text-secondary); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition); + font-family: var(--font-sans); } .filter-pill:hover { - border-color: var(--color-primary); - color: var(--color-primary); - background: #f5f3ff; + border-color: var(--color-primary); + color: var(--color-primary); + background: #f5f3ff; } .filter-pill.active { - background: var(--color-primary); - color: #ffffff; - border-color: var(--color-primary); + background: var(--color-primary); + color: #ffffff; + border-color: var(--color-primary); } -.filter-pill--bug.active { background: var(--color-bug); border-color: var(--color-bug); } -.filter-pill--note.active { background: var(--color-note); border-color: var(--color-note); } -.filter-pill--idea.active { background: var(--color-idea); border-color: var(--color-idea); } -.filter-pill--question.active { background: var(--color-question); border-color: var(--color-question); } +.filter-pill--bug.active { + background: var(--color-bug); + border-color: var(--color-bug); +} +.filter-pill--note.active { + background: var(--color-note); + border-color: var(--color-note); +} +.filter-pill--idea.active { + background: var(--color-idea); + border-color: var(--color-idea); +} +.filter-pill--question.active { + background: var(--color-question); + border-color: var(--color-question); +} /* ============================================ Button Group & Download Button ============================================ */ .button-group { - display: flex; - gap: 10px; - flex-wrap: wrap; + display: flex; + gap: 10px; + flex-wrap: wrap; } .btn-download { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 8px 20px; - background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%); - color: #ffffff; - border: none; - border-radius: var(--radius-md); - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - transition: all var(--transition); - font-family: var(--font-sans); - flex-shrink: 0; - white-space: nowrap; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 20px; + background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%); + color: #ffffff; + border: none; + border-radius: var(--radius-md); + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + font-family: var(--font-sans); + flex-shrink: 0; + white-space: nowrap; } .btn-download:hover { - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); } .btn-download:active { - transform: translateY(0); + transform: translateY(0); } .btn-download--secondary { - background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); + background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); } .btn-download--secondary:hover { - box-shadow: 0 4px 12px rgba(75, 85, 99, 0.4); + box-shadow: 0 4px 12px rgba(75, 85, 99, 0.4); } /* ============================================ Table ============================================ */ .table-wrapper { - background: var(--color-surface); - border-radius: 0 0 var(--radius-lg) var(--radius-lg); - box-shadow: var(--shadow-sm); - border: 1px solid var(--color-border); - border-top: none; - overflow: hidden; + background: var(--color-surface); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); + border-top: none; + overflow: hidden; } .report-table { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } .report-table thead { - background: #f8fafc; + background: #f8fafc; } .report-table th { - padding: 14px 16px; - text-align: left; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-secondary); - border-bottom: 2px solid var(--color-border); + padding: 14px 16px; + text-align: left; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); + border-bottom: 2px solid var(--color-border); } .report-table td { - padding: 14px 16px; - border-bottom: 1px solid #f1f5f9; - font-size: 0.9rem; - vertical-align: middle; + padding: 14px 16px; + border-bottom: 1px solid #f1f5f9; + font-size: 0.9rem; + vertical-align: middle; } .report-table tbody tr:last-child td { - border-bottom: none; + border-bottom: none; } .annotation-row { - transition: background-color var(--transition); + transition: background-color var(--transition); } .annotation-row:hover { - background-color: #f8fafc; + background-color: #f8fafc; } -.annotation-row--bug td:first-child { border-left: 3px solid var(--color-bug); } -.annotation-row--note td:first-child { border-left: 3px solid var(--color-note); } -.annotation-row--idea td:first-child { border-left: 3px solid var(--color-idea); } -.annotation-row--question td:first-child { border-left: 3px solid var(--color-question); } +.annotation-row--bug td:first-child { + border-left: 3px solid var(--color-bug); +} +.annotation-row--note td:first-child { + border-left: 3px solid var(--color-note); +} +.annotation-row--idea td:first-child { + border-left: 3px solid var(--color-idea); +} +.annotation-row--question td:first-child { + border-left: 3px solid var(--color-question); +} .col-actions { - width: 70px; - text-align: center; + width: 70px; + text-align: center; } /* Type Badge */ .type-badge { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 12px; - border-radius: 50px; - font-size: 0.8rem; - font-weight: 600; - white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 50px; + font-size: 0.8rem; + font-weight: 600; + white-space: nowrap; } -.type-badge--bug { background: var(--color-bug-light); color: var(--color-bug); } -.type-badge--note { background: var(--color-note-light); color: var(--color-note); } -.type-badge--idea { background: var(--color-idea-light); color: var(--color-idea); } -.type-badge--question { background: var(--color-question-light); color: var(--color-question); } +.type-badge--bug { + background: var(--color-bug-light); + color: var(--color-bug); +} +.type-badge--note { + background: var(--color-note-light); + color: var(--color-note); +} +.type-badge--idea { + background: var(--color-idea-light); + color: var(--color-idea); +} +.type-badge--question { + background: var(--color-question-light); + color: var(--color-question); +} /* Table cell styles */ .annotation-description { - max-width: 300px; - white-space: pre-wrap; - word-break: break-word; - line-height: 1.5; + max-width: 300px; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.5; } .annotation-url { - max-width: 200px; - word-break: break-all; + max-width: 200px; + word-break: break-all; } .annotation-url a { - color: var(--color-primary); - text-decoration: none; - font-size: 0.85rem; + color: var(--color-primary); + text-decoration: none; + font-size: 0.85rem; } .annotation-url a:hover { - text-decoration: underline; + text-decoration: underline; } .annotation-time { - white-space: nowrap; - color: var(--color-text-secondary); - font-size: 0.85rem; + white-space: nowrap; + color: var(--color-text-secondary); + font-size: 0.85rem; } .text-muted { - color: var(--color-text-secondary); - font-size: 0.85rem; + color: var(--color-text-secondary); + font-size: 0.85rem; } .empty-state { - text-align: center; - padding: 40px 16px !important; - color: var(--color-text-secondary); - font-size: 0.95rem; + text-align: center; + padding: 40px 16px !important; + color: var(--color-text-secondary); + font-size: 0.95rem; } /* ============================================ Screenshot Thumbnails ============================================ */ .screenshot-cell { - width: 56px; - text-align: center; - position: relative; + width: 56px; + text-align: center; + position: relative; } .preview-image { - width: 36px; - height: 36px; - cursor: pointer; - border: 2px solid var(--color-border); - border-radius: var(--radius-sm); - transition: all var(--transition); - object-fit: cover; + width: 36px; + height: 36px; + cursor: pointer; + border: 2px solid var(--color-border); + border-radius: var(--radius-sm); + transition: all var(--transition); + object-fit: cover; } .preview-image:hover { - border-color: var(--color-primary); - transform: scale(1.1); - box-shadow: var(--shadow-md); + border-color: var(--color-primary); + transform: scale(1.1); + box-shadow: var(--shadow-md); } /* ============================================ Delete Button (in table) ============================================ */ .delete-btn { - background: none; - border: none; - color: var(--color-text-secondary); - cursor: pointer; - padding: 6px; - border-radius: var(--radius-sm); - transition: all var(--transition); - display: inline-flex; - align-items: center; - justify-content: center; + background: none; + border: none; + color: var(--color-text-secondary); + cursor: pointer; + padding: 6px; + border-radius: var(--radius-sm); + transition: all var(--transition); + display: inline-flex; + align-items: center; + justify-content: center; } .delete-btn:hover { - color: var(--color-danger); - background: var(--color-bug-light); + color: var(--color-danger); + background: var(--color-bug-light); } /* ============================================ Overlay & Dialog ============================================ */ .overlay { - display: none; - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - z-index: 1000; - animation: fadeIn 0.2s ease; + display: none; + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + z-index: 1000; + animation: fadeIn 0.2s ease; } .dialog { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: var(--color-surface); - padding: 32px; - border-radius: var(--radius-lg); - text-align: center; - box-shadow: var(--shadow-lg); - max-width: 400px; - width: 90%; - animation: slideUp 0.2s ease; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--color-surface); + padding: 32px; + border-radius: var(--radius-lg); + text-align: center; + box-shadow: var(--shadow-lg); + max-width: 400px; + width: 90%; + animation: slideUp 0.2s ease; } .dialog__icon { - margin-bottom: 16px; + margin-bottom: 16px; } .dialog__title { - font-size: 1.1rem; - font-weight: 600; - margin-bottom: 8px; - color: var(--color-text); + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 8px; + color: var(--color-text); } .dialog__message { - font-size: 0.9rem; - color: var(--color-text-secondary); - margin-bottom: 24px; - line-height: 1.5; + font-size: 0.9rem; + color: var(--color-text-secondary); + margin-bottom: 24px; + line-height: 1.5; } .dialog__actions { - display: flex; - gap: 12px; - justify-content: center; + display: flex; + gap: 12px; + justify-content: center; } .btn { - padding: 9px 20px; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: 0.9rem; - font-weight: 500; - transition: all var(--transition); - font-family: var(--font-sans); + padding: 9px 20px; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all var(--transition); + font-family: var(--font-sans); } .btn--secondary { - background-color: #f1f5f9; - color: var(--color-text); + background-color: #f1f5f9; + color: var(--color-text); } .btn--secondary:hover { - background-color: #e2e8f0; + background-color: #e2e8f0; } .btn--danger { - background-color: var(--color-danger); - color: #ffffff; + background-color: var(--color-danger); + color: #ffffff; } .btn--danger:hover { - background-color: var(--color-danger-dark); + background-color: var(--color-danger-dark); } /* ============================================ Image Preview (fullscreen) ============================================ */ .image-preview { - display: none; - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.92); - z-index: 2000; - cursor: pointer; + display: none; + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.92); + z-index: 2000; + cursor: pointer; } .image-preview.active { - display: flex; - justify-content: center; - align-items: center; - animation: fadeIn 0.2s ease; + display: flex; + justify-content: center; + align-items: center; + animation: fadeIn 0.2s ease; } .image-preview img { - max-width: 90%; - max-height: 90%; - object-fit: contain; - border-radius: var(--radius-md); - box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); + max-width: 90%; + max-height: 90%; + object-fit: contain; + border-radius: var(--radius-md); + box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); } .image-preview::after { - content: '\00d7'; - position: absolute; - top: 24px; - right: 24px; - color: #ffffff; - font-size: 28px; - font-weight: 300; - cursor: pointer; - width: 44px; - height: 44px; - line-height: 44px; - text-align: center; - background: rgba(255, 255, 255, 0.1); - border-radius: 50%; - transition: background var(--transition); + content: '\00d7'; + position: absolute; + top: 24px; + right: 24px; + color: #ffffff; + font-size: 28px; + font-weight: 300; + cursor: pointer; + width: 44px; + height: 44px; + line-height: 44px; + text-align: center; + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + transition: background var(--transition); } .image-preview::after:hover { - background: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.2); } /* ============================================ Hover Preview ============================================ */ .image-hover-preview { - display: none; - position: fixed; - width: 320px; - height: 320px; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); - z-index: 1000; - pointer-events: none; - overflow: hidden; + display: none; + position: fixed; + width: 320px; + height: 320px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + z-index: 1000; + pointer-events: none; + overflow: hidden; } .image-hover-preview img { - width: 100%; - height: 100%; - object-fit: contain; + width: 100%; + height: 100%; + object-fit: contain; } .image-hover-preview.active { - display: block; + display: block; } /* ============================================ Empty Report State ============================================ */ .empty-report { - text-align: center; - padding: 80px 20px; + text-align: center; + padding: 80px 20px; } .empty-report h2 { - font-size: 1.3rem; - color: var(--color-text); - margin-bottom: 8px; + font-size: 1.3rem; + color: var(--color-text); + margin-bottom: 8px; } .empty-report p { - color: var(--color-text-secondary); - font-size: 0.95rem; + color: var(--color-text-secondary); + font-size: 0.95rem; } /* ============================================ Animations ============================================ */ @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes slideUp { - from { - opacity: 0; - transform: translate(-50%, -45%); - } - to { - opacity: 1; - transform: translate(-50%, -50%); - } + from { + opacity: 0; + transform: translate(-50%, -45%); + } + to { + opacity: 1; + transform: translate(-50%, -50%); + } } /* ============================================ Responsive ============================================ */ @media (max-width: 640px) { - .report-header { - padding: 32px 24px; - } - - .report-header__title { - font-size: 1.5rem; - } - - .info-grid { - grid-template-columns: 1fr 1fr; - } - - .stats-grid { - grid-template-columns: 1fr 1fr; - } - - .chart-card { - width: 100%; - } - - .filter-pills { - gap: 6px; - } - - .filter-pill { - padding: 6px 12px; - font-size: 0.8rem; - } - - .table-wrapper { - overflow-x: auto; - } + .report-header { + padding: 32px 24px; + } + + .report-header__title { + font-size: 1.5rem; + } + + .info-grid { + grid-template-columns: 1fr 1fr; + } + + .stats-grid { + grid-template-columns: 1fr 1fr; + } + + .chart-card { + width: 100%; + } + + .filter-pills { + gap: 6px; + } + + .filter-pill { + padding: 6px 12px; + font-size: 0.8rem; + } + + .table-wrapper { + overflow-x: auto; + } } diff --git a/HTMLReport/preview.js b/HTMLReport/preview.js index 9817be9..492230d 100644 --- a/HTMLReport/preview.js +++ b/HTMLReport/preview.js @@ -1,34 +1,43 @@ import { loadSessionData } from './modules/reportData.js'; -import { displaySessionInfo, displayStats, createAnnotationsChart, displayAnnotationsTable } from './modules/reportUI.js'; -import { setupAllListeners, getCurrentFilter, rebindTableListeners } from './modules/reportEvents.js'; +import { + displaySessionInfo, + displayStats, + createAnnotationsChart, + displayAnnotationsTable, +} from './modules/reportUI.js'; +import { + setupAllListeners, + getCurrentFilter, + rebindTableListeners, +} from './modules/reportEvents.js'; async function initReport() { - try { - const session = await loadSessionData(); + try { + const session = await loadSessionData(); - if (!session) { - document.getElementById('report').innerHTML = ` + if (!session) { + document.getElementById('report').innerHTML = `

No session data available

Start a testing session and add annotations to generate a report.

`; - return; - } + return; + } - displaySessionInfo(session); - displayStats(session); - createAnnotationsChart(session); - displayAnnotationsTable(session, getCurrentFilter()); - setupAllListeners(session); - rebindTableListeners(); - } catch (error) { - console.error('Error loading report:', error); - document.getElementById('report').innerHTML = ` + displaySessionInfo(session); + displayStats(session); + createAnnotationsChart(session); + displayAnnotationsTable(session, getCurrentFilter()); + setupAllListeners(session); + rebindTableListeners(); + } catch (error) { + console.error('Error loading report:', error); + document.getElementById('report').innerHTML = `

Error loading data

${error.message}

`; - } + } } document.addEventListener('DOMContentLoaded', initReport); diff --git a/babel.config.js b/babel.config.js index efcf7ac..c74fb53 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,3 @@ module.exports = { - presets: [ - ['@babel/preset-env', {targets: {node: 'current'}}], - ], + presets: [['@babel/preset-env', { targets: { node: 'current' } }]], }; diff --git a/background.js b/background.js index 7cc7b40..4694bda 100644 --- a/background.js +++ b/background.js @@ -12,580 +12,676 @@ let session = new Session(); // Función para guardar la sesión en el storage async function saveSession() { - try { - await chrome.storage.local.set({ 'session': session }); - } catch (error) { - if (error.name === 'QUOTA_BYTES' || (chrome.runtime.lastError && chrome.runtime.lastError.message && chrome.runtime.lastError.message.includes('QUOTA_BYTES'))) { - console.error('Error saving session due to quota limit:', error); - - // Create a deep copy of the session object - const sessionCopy = JSON.parse(JSON.stringify(session)); - - // Find the oldest annotation with a non-null imageURL - let oldestAnnotationIndex = -1; - for (let i = 0; i < sessionCopy.annotations.length; i++) { - if (sessionCopy.annotations[i].imageURL) { - oldestAnnotationIndex = i; - break; - } - } - - if (oldestAnnotationIndex !== -1) { - // const originalImageURL = sessionCopy.annotations[oldestAnnotationIndex].imageURL; // Optional: for logging - sessionCopy.annotations[oldestAnnotationIndex].imageURL = "IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT"; - - try { - await chrome.storage.local.set({ 'session': sessionCopy }); - // Notify user about successful save after removing screenshot - const notifId = 'sessionSavedAfterQuota-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Session Saved with Adjustment', - message: 'The oldest screenshot was removed to save the session due to storage limits.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); - // Update the main session object to reflect the change if the save was successful. - session.annotations[oldestAnnotationIndex].imageURL = "IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT"; - - } catch (secondError) { - console.error('Error saving session even after removing screenshot:', secondError); - // Notify user about failed save even after removing screenshot - const notifId = 'sessionSaveFailedAfterQuota-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Session Save Failed', - message: 'Failed to save session. Insufficient storage even after removing the oldest screenshot.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); - } - } else { - // No annotation with imageURL found - console.error('Failed to save session. No screenshots to remove for quota.'); - const notifId = 'sessionSaveFailedNoScreenshot-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Session Save Failed', - message: 'Failed to save session. Insufficient storage and no screenshots to remove.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); - } - } else { - // Not a quota error, re-throw or handle as appropriate - console.error('Error saving session:', error); - throw error; + try { + await chrome.storage.local.set({ session: session }); + } catch (error) { + if ( + error.name === 'QUOTA_BYTES' || + (chrome.runtime.lastError && + chrome.runtime.lastError.message && + chrome.runtime.lastError.message.includes('QUOTA_BYTES')) + ) { + console.error('Error saving session due to quota limit:', error); + + // Create a deep copy of the session object + const sessionCopy = JSON.parse(JSON.stringify(session)); + + // Find the oldest annotation with a non-null imageURL + let oldestAnnotationIndex = -1; + for (let i = 0; i < sessionCopy.annotations.length; i++) { + if (sessionCopy.annotations[i].imageURL) { + oldestAnnotationIndex = i; + break; + } + } + + if (oldestAnnotationIndex !== -1) { + // const originalImageURL = sessionCopy.annotations[oldestAnnotationIndex].imageURL; // Optional: for logging + sessionCopy.annotations[oldestAnnotationIndex].imageURL = + 'IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT'; + + try { + await chrome.storage.local.set({ session: sessionCopy }); + // Notify user about successful save after removing screenshot + const notifId = 'sessionSavedAfterQuota-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Session Saved with Adjustment', + message: 'The oldest screenshot was removed to save the session due to storage limits.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); + // Update the main session object to reflect the change if the save was successful. + session.annotations[oldestAnnotationIndex].imageURL = + 'IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT'; + } catch (secondError) { + console.error('Error saving session even after removing screenshot:', secondError); + // Notify user about failed save even after removing screenshot + const notifId = 'sessionSaveFailedAfterQuota-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Session Save Failed', + message: + 'Failed to save session. Insufficient storage even after removing the oldest screenshot.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); } + } else { + // No annotation with imageURL found + console.error('Failed to save session. No screenshots to remove for quota.'); + const notifId = 'sessionSaveFailedNoScreenshot-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Session Save Failed', + message: 'Failed to save session. Insufficient storage and no screenshots to remove.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); + } + } else { + // Not a quota error, re-throw or handle as appropriate + console.error('Error saving session:', error); + throw error; } + } } // Función para cargar la sesión desde el storage async function loadSession() { - const data = await chrome.storage.local.get('session'); - if (data.session) { - // Reconstruir el objeto Session con sus métodos - const loadedSession = data.session; - session = new Session(loadedSession.startDateTime, loadedSession.browserInfo); - - // Reconstruir las anotaciones - loadedSession.annotations.forEach(annotation => { - let newAnnotation; - switch (annotation.type) { - case "Bug": - newAnnotation = new Bug(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addBug(newAnnotation); - break; - case "Note": - newAnnotation = new Note(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addNote(newAnnotation); - break; - case "Idea": - newAnnotation = new Idea(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addIdea(newAnnotation); - break; - case "Question": - newAnnotation = new Question(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addQuestion(newAnnotation); - break; - } - }); - } + const data = await chrome.storage.local.get('session'); + if (data.session) { + // Reconstruir el objeto Session con sus métodos + const loadedSession = data.session; + session = new Session(loadedSession.startDateTime, loadedSession.browserInfo); + + // Reconstruir las anotaciones + loadedSession.annotations.forEach((annotation) => { + let newAnnotation; + switch (annotation.type) { + case 'Bug': + newAnnotation = new Bug( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addBug(newAnnotation); + break; + case 'Note': + newAnnotation = new Note( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addNote(newAnnotation); + break; + case 'Idea': + newAnnotation = new Idea( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addIdea(newAnnotation); + break; + case 'Question': + newAnnotation = new Question( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addQuestion(newAnnotation); + break; + } + }); + } } // Cargar la sesión al iniciar loadSession(); // Helper function for notifications of processing errors (before addAnnotation is called) -function notifyProcessingError(annotationType, descriptionName, errorMessage = "") { - const typeStr = annotationType || "Annotation"; - const nameStr = descriptionName || ""; - const notifId = 'annotationProcessingError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: `${typeStr} Processing Failed`, - message: `Could not process ${typeStr.toLowerCase()} "${nameStr}" for screenshot. Error: ${errorMessage}` - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); +// eslint-disable-next-line no-unused-vars +function notifyProcessingError(annotationType, descriptionName, errorMessage = '') { + const typeStr = annotationType || 'Annotation'; + const nameStr = descriptionName || ''; + const notifId = 'annotationProcessingError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: `${typeStr} Processing Failed`, + message: `Could not process ${typeStr.toLowerCase()} "${nameStr}" for screenshot. Error: ${errorMessage}`, + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); } - // Escuchar mensajes del popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - // Keep 'return true' if any path in this listener might call sendResponse asynchronously. - // For "csToBgCropData", we are not sending a response back to content script currently. - // For other existing cases, they do use sendResponse. - let isAsync = false; - - switch (request.type) { - case "initiateCropSelection": - // Forward the crop selection request to the content script - chrome.tabs.query({ active: true, currentWindow: true }, async function(tabs) { - if (tabs && tabs[0] && tabs[0].id != null) { - const tab = tabs[0]; - - // Check if the page allows content scripts - if (tab.url.startsWith('chrome://') || tab.url.startsWith('edge://') || - tab.url.startsWith('chrome-extension://') || tab.url.startsWith('about:')) { - const notifId = 'cropSelectionError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Selection Not Available', - message: 'Screen selection cannot be used on this type of page. Please try on a regular webpage.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 5000); - return; - } - - try { - // Try to inject content script if it's not already loaded - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ['js/content_script.js'] - }).catch(() => { - // Content script might already be loaded, ignore error - console.log("Background: Content script might already be loaded"); - }); - - // Small delay to ensure content script is ready - setTimeout(() => { - chrome.tabs.sendMessage(tab.id, { - type: "startSelection", - annotationType: request.annotationType, - description: request.description - }, function(response) { - if (chrome.runtime.lastError) { - console.error("Background: Error sending startSelection to content script:", chrome.runtime.lastError.message); - const notifId = 'cropSelectionError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Selection Failed', - message: 'Could not start screen selection. Please try again.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 5000); - } else { - console.log("Background: Successfully initiated crop selection on content script"); - } - }); - }, 100); - - } catch (error) { - console.error("Background: Error injecting content script:", error); - const notifId = 'cropSelectionError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Selection Failed', - message: 'Could not start screen selection. Error: ' + error.message - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 5000); - } - } else { - console.error("Background: No active tab found for crop selection"); - } + // Keep 'return true' if any path in this listener might call sendResponse asynchronously. + // For "csToBgCropData", we are not sending a response back to content script currently. + // For other existing cases, they do use sendResponse. + let isAsync = false; + + switch (request.type) { + case 'initiateCropSelection': + // Forward the crop selection request to the content script + chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) { + if (tabs && tabs[0] && tabs[0].id != null) { + const tab = tabs[0]; + + // Check if the page allows content scripts + if ( + tab.url.startsWith('chrome://') || + tab.url.startsWith('edge://') || + tab.url.startsWith('chrome-extension://') || + tab.url.startsWith('about:') + ) { + const notifId = 'cropSelectionError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Selection Not Available', + message: + 'Screen selection cannot be used on this type of page. Please try on a regular webpage.', }); - break; - case "addBug": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Bug", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "addIdea": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Idea", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "addNote": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Note", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "addQuestion": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Question", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "requestCropScreenshot": - console.log("Background: Received requestCropScreenshot", request); - handleCropScreenshotRequest(request) - .then((croppedImageData) => { - sendResponse({ croppedImageData: croppedImageData }); - }) - .catch((error) => { - console.error("Background: Failed to capture crop screenshot:", error); - sendResponse({ croppedImageData: null }); - }); - isAsync = true; - break; - case "csToBgCropData": - console.log("Background: Received csToBgCropData", request); - handleProcessAnnotatedCrop(request) - .then(() => { - console.log("Background: Annotated crop processed successfully"); - }) - .catch((error) => { - console.error("Background: Failed to process annotated crop:", error); - }); - break; - case "updateAnnotationName": - var AnnotationID = request.annotationID; - var newName = request.newName; - var annotations = session.getAnnotations(); - var annotation = annotations[AnnotationID]; - annotation.setName(newName); - saveSession().then(() => sendResponse({ status: "ok" })); - break; - case "deleteAnnotation": - session.deleteAnnotation(request.annotationID); - saveSession().then(() => sendResponse({ status: "ok" })); - break; - case "exportSessionCSV": - if (!exportSessionCSV()) { - sendResponse({ status: "nothing to export" }); - } else { - sendResponse({ status: "ok" }); - } - break; - case "exportSessionJSon": - if (!exportSessionJSon()) { - sendResponse({ status: "nothing to export" }); - } else { - sendResponse({ status: "ok" }); - } - break; - case "importSessionJSon": - var fileData = request.jSonSession; - if (!importSessionJSon(fileData)) { - sendResponse({ status: "nothing to import" }); - } else { - sendResponse({ status: "ok" }); - } - break; - case "clearSession": - clearSession().then(() => sendResponse({ status: "ok" })); - break; - case "getSessionData": - sendResponse({ - bugs: session.getBugs().length, - notes: session.getNotes().length, - ideas: session.getIdeas().length, - questions: session.getQuestions().length, - annotationsCount: session.getAnnotations().length - }); - break; - case "getFullSession": - if (!session) { - sendResponse(null); - return true; - } - sendResponse({ - startDateTime: session.StartDateTime, - browserInfo: { - browser: session.BrowserInfo.browser || "Chrome", - browserVersion: session.BrowserInfo.browserVersion || chrome.runtime.getManifest().version, - os: session.BrowserInfo.os || navigator.platform, - osVersion: session.BrowserInfo.osVersion || navigator.userAgent, - cookies: session.BrowserInfo.cookies || navigator.cookieEnabled, - flashVersion: session.BrowserInfo.flashVersion || "N/A" + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); + return; + } + + try { + // Try to inject content script if it's not already loaded + await chrome.scripting + .executeScript({ + target: { tabId: tab.id }, + files: ['js/content_script.js'], + }) + .catch(() => { + // Content script might already be loaded, ignore error + console.log('Background: Content script might already be loaded'); + }); + + // Small delay to ensure content script is ready + setTimeout(() => { + chrome.tabs.sendMessage( + tab.id, + { + type: 'startSelection', + annotationType: request.annotationType, + description: request.description, }, - annotations: session.annotations.map(annotation => ({ - type: annotation.constructor.name, - name: annotation.name, - url: annotation.url, - timestamp: annotation.timestamp, - imageURL: annotation.imageURL - })) + function (response) { + if (chrome.runtime.lastError) { + console.error( + 'Background: Error sending startSelection to content script:', + chrome.runtime.lastError.message + ); + const notifId = 'cropSelectionError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Selection Failed', + message: 'Could not start screen selection. Please try again.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); + } else { + console.log( + 'Background: Successfully initiated crop selection on content script' + ); + } + } + ); + }, 100); + } catch (error) { + console.error('Background: Error injecting content script:', error); + const notifId = 'cropSelectionError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Selection Failed', + message: 'Could not start screen selection. Error: ' + error.message, }); - break; - } - return isAsync; // Return true only if sendResponse is used asynchronously in any of the handled cases. + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); + } + } else { + console.error('Background: No active tab found for crop selection'); + } + }); + break; + case 'addBug': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Bug', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'addIdea': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Idea', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'addNote': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Note', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'addQuestion': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Question', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'requestCropScreenshot': + console.log('Background: Received requestCropScreenshot', request); + handleCropScreenshotRequest(request) + .then((croppedImageData) => { + sendResponse({ croppedImageData: croppedImageData }); + }) + .catch((error) => { + console.error('Background: Failed to capture crop screenshot:', error); + sendResponse({ croppedImageData: null }); + }); + isAsync = true; + break; + case 'csToBgCropData': + console.log('Background: Received csToBgCropData', request); + handleProcessAnnotatedCrop(request) + .then(() => { + console.log('Background: Annotated crop processed successfully'); + }) + .catch((error) => { + console.error('Background: Failed to process annotated crop:', error); + }); + break; + case 'updateAnnotationName': + var AnnotationID = request.annotationID; + var newName = request.newName; + var annotations = session.getAnnotations(); + var annotation = annotations[AnnotationID]; + annotation.setName(newName); + saveSession().then(() => sendResponse({ status: 'ok' })); + break; + case 'deleteAnnotation': + session.deleteAnnotation(request.annotationID); + saveSession().then(() => sendResponse({ status: 'ok' })); + break; + case 'exportSessionCSV': + if (!exportSessionCSV()) { + sendResponse({ status: 'nothing to export' }); + } else { + sendResponse({ status: 'ok' }); + } + break; + case 'exportSessionJSon': + if (!exportSessionJSon()) { + sendResponse({ status: 'nothing to export' }); + } else { + sendResponse({ status: 'ok' }); + } + break; + case 'importSessionJSon': + var fileData = request.jSonSession; + if (!importSessionJSon(fileData)) { + sendResponse({ status: 'nothing to import' }); + } else { + sendResponse({ status: 'ok' }); + } + break; + case 'clearSession': + clearSession().then(() => sendResponse({ status: 'ok' })); + break; + case 'getSessionData': + sendResponse({ + bugs: session.getBugs().length, + notes: session.getNotes().length, + ideas: session.getIdeas().length, + questions: session.getQuestions().length, + annotationsCount: session.getAnnotations().length, + }); + break; + case 'getFullSession': + if (!session) { + sendResponse(null); + return true; + } + sendResponse({ + startDateTime: session.StartDateTime, + browserInfo: { + browser: session.BrowserInfo.browser || 'Chrome', + browserVersion: + session.BrowserInfo.browserVersion || chrome.runtime.getManifest().version, + os: session.BrowserInfo.os || navigator.platform, + osVersion: session.BrowserInfo.osVersion || navigator.userAgent, + cookies: session.BrowserInfo.cookies || navigator.cookieEnabled, + flashVersion: session.BrowserInfo.flashVersion || 'N/A', + }, + annotations: session.annotations.map((annotation) => ({ + type: annotation.constructor.name, + name: annotation.name, + url: annotation.url, + timestamp: annotation.timestamp, + imageURL: annotation.imageURL, + })), + }); + break; + } + return isAsync; // Return true only if sendResponse is used asynchronously in any of the handled cases. }); - // Handle crop screenshot request - capture and crop, then return to content script async function handleCropScreenshotRequest(request) { - try { - console.log("Background: Processing crop screenshot request", request); - - // Capture the visible tab - const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: "png" }); - if (!dataUrl) { - throw new Error("Failed to capture screenshot"); - } - - // Convert dataUrl to Blob - const response = await fetch(dataUrl); - const blob = await response.blob(); - - // Create bitmap from image - const bitmap = await createImageBitmap(blob); + try { + console.log('Background: Processing crop screenshot request', request); - // Create offscreen canvas with crop dimensions - const canvas = new OffscreenCanvas( - request.coordinates.width, - request.coordinates.height - ); - const ctx = canvas.getContext('2d'); - - // Draw the selected portion - ctx.drawImage( - bitmap, - request.coordinates.x, - request.coordinates.y, - request.coordinates.width, - request.coordinates.height, - 0, - 0, - request.coordinates.width, - request.coordinates.height - ); - - // Convert canvas to blob - const croppedBlob = await canvas.convertToBlob({ type: 'image/png' }); + // Capture the visible tab + const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'png' }); + if (!dataUrl) { + throw new Error('Failed to capture screenshot'); + } - // Convert blob to dataUrl - const croppedDataUrl = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.readAsDataURL(croppedBlob); - }); + // Convert dataUrl to Blob + const response = await fetch(dataUrl); + const blob = await response.blob(); + + // Create bitmap from image + const bitmap = await createImageBitmap(blob); + + // Create offscreen canvas with crop dimensions + const canvas = new OffscreenCanvas(request.coordinates.width, request.coordinates.height); + const ctx = canvas.getContext('2d'); + + // Draw the selected portion + ctx.drawImage( + bitmap, + request.coordinates.x, + request.coordinates.y, + request.coordinates.width, + request.coordinates.height, + 0, + 0, + request.coordinates.width, + request.coordinates.height + ); + + // Convert canvas to blob + const croppedBlob = await canvas.convertToBlob({ type: 'image/png' }); + + // Convert blob to dataUrl + const croppedDataUrl = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(croppedBlob); + }); - console.log("Background: Successfully created cropped screenshot"); - return croppedDataUrl; - } catch (error) { - console.error("Background: Error in handleCropScreenshotRequest:", error); - throw error; - } + console.log('Background: Successfully created cropped screenshot'); + return croppedDataUrl; + } catch (error) { + console.error('Background: Error in handleCropScreenshotRequest:', error); + throw error; + } } // Handle annotated crop data - save the final annotated image async function handleProcessAnnotatedCrop(request) { - try { - console.log("Background: Processing annotated crop", request); - - // Create annotation with the annotated image - await addAnnotation( - request.annotationType.charAt(0).toUpperCase() + request.annotationType.slice(1), - request.description, - request.annotatedImageData - ); - - console.log("Background: Successfully processed annotated crop"); - } catch (error) { - console.error("Background: Error in handleProcessAnnotatedCrop:", error); - throw error; - } + try { + console.log('Background: Processing annotated crop', request); + + // Create annotation with the annotated image + await addAnnotation( + request.annotationType.charAt(0).toUpperCase() + request.annotationType.slice(1), + request.description, + request.annotatedImageData + ); + + console.log('Background: Successfully processed annotated crop'); + } catch (error) { + console.error('Background: Error in handleProcessAnnotatedCrop:', error); + throw error; + } } - async function addAnnotation(type, name, imageURL) { - console.log("Background: addAnnotation called. Type:", type, ". Name:", name, ". Image URL (first 100 chars):", imageURL ? imageURL.substring(0, 100) : "No image"); - if (session.getAnnotations().length == 0) { - await startSession(); - } - - return new Promise((resolve, reject) => { - chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { - try { - const currentUrl = tabs[0] ? tabs[0].url : "N/A"; // Handle missing tab - const now = new Date().getTime(); - - let newAnnotation; - let annotationSimpleType = ""; // For user-friendly notification - - switch (type) { - case "Bug": - newAnnotation = new Bug(name, currentUrl, now, imageURL); - session.addBug(newAnnotation); - annotationSimpleType = "Bug"; - break; - case "Note": - newAnnotation = new Note(name, currentUrl, now, imageURL); - session.addNote(newAnnotation); - annotationSimpleType = "Note"; - break; - case "Idea": - newAnnotation = new Idea(name, currentUrl, now, imageURL); - session.addIdea(newAnnotation); - annotationSimpleType = "Idea"; - break; - case "Question": - newAnnotation = new Question(name, currentUrl, now, imageURL); - session.addQuestion(newAnnotation); - annotationSimpleType = "Question"; - break; - default: // Should not happen - return reject(new Error("Unknown annotation type")); - } + console.log( + 'Background: addAnnotation called. Type:', + type, + '. Name:', + name, + '. Image URL (first 100 chars):', + imageURL ? imageURL.substring(0, 100) : 'No image' + ); + if (session.getAnnotations().length == 0) { + await startSession(); + } + + return new Promise((resolve, reject) => { + chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { + try { + const currentUrl = tabs[0] ? tabs[0].url : 'N/A'; // Handle missing tab + const now = new Date().getTime(); + + let newAnnotation; + let annotationSimpleType = ''; // For user-friendly notification + + switch (type) { + case 'Bug': + newAnnotation = new Bug(name, currentUrl, now, imageURL); + session.addBug(newAnnotation); + annotationSimpleType = 'Bug'; + break; + case 'Note': + newAnnotation = new Note(name, currentUrl, now, imageURL); + session.addNote(newAnnotation); + annotationSimpleType = 'Note'; + break; + case 'Idea': + newAnnotation = new Idea(name, currentUrl, now, imageURL); + session.addIdea(newAnnotation); + annotationSimpleType = 'Idea'; + break; + case 'Question': + newAnnotation = new Question(name, currentUrl, now, imageURL); + session.addQuestion(newAnnotation); + annotationSimpleType = 'Question'; + break; + default: // Should not happen + return reject(new Error('Unknown annotation type')); + } - console.log("Background: Attempting to save session for annotation Type:", type, "Name:", name); - saveSession().then(() => { - // --- Create Notification --- - const notifId = 'annotationSaved-' + Date.now(); - const notifOptions = { - type: 'basic', - iconUrl: 'icons/iconbig.png', // Ensure this icon path is correct - title: `${annotationSimpleType} Saved!`, - message: `Your ${annotationSimpleType.toLowerCase()} "${name}" has been successfully saved.` - }; - chrome.notifications.create(notifId, notifOptions); - // Optional: Clear notification after a few seconds - setTimeout(() => { - chrome.notifications.clear(notifId); - }, 5000); // Clear after 5 seconds - // --- End Notification --- - - resolve(); // Resolve the main promise - }).catch(error => { - console.error("Background: Error during saveSession for", type, name, ":", error); - // Optionally, show an error notification here too - const errorNotifId = 'annotationError-' + Date.now(); - chrome.notifications.create(errorNotifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: `${annotationSimpleType || type} Save Failed`, - message: `Could not save ${annotationSimpleType.toLowerCase() || type.toLowerCase()} "${name}". Error: ${error.message}` - }); - setTimeout(() => { - chrome.notifications.clear(errorNotifId); - }, 7000); // Keep error notifications slightly longer - - reject(error); - }); - - } catch (error) { // Catch synchronous errors in the promise executor - console.error("Background: Error in addAnnotation sync part:", error); - // Send a notification for this synchronous error as well - const syncErrorNotifId = 'annotationSyncError-' + Date.now(); - chrome.notifications.create(syncErrorNotifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: `${type || 'Annotation'} Setup Failed`, - message: `Failed to initiate saving for "${name}". Error: ${error.message}` - }); - setTimeout(() => { - chrome.notifications.clear(syncErrorNotifId); - }, 7000); - reject(error); - } + console.log( + 'Background: Attempting to save session for annotation Type:', + type, + 'Name:', + name + ); + saveSession() + .then(() => { + // --- Create Notification --- + const notifId = 'annotationSaved-' + Date.now(); + const notifOptions = { + type: 'basic', + iconUrl: 'icons/iconbig.png', // Ensure this icon path is correct + title: `${annotationSimpleType} Saved!`, + message: `Your ${annotationSimpleType.toLowerCase()} "${name}" has been successfully saved.`, + }; + chrome.notifications.create(notifId, notifOptions); + // Optional: Clear notification after a few seconds + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); // Clear after 5 seconds + // --- End Notification --- + + resolve(); // Resolve the main promise + }) + .catch((error) => { + console.error('Background: Error during saveSession for', type, name, ':', error); + // Optionally, show an error notification here too + const errorNotifId = 'annotationError-' + Date.now(); + chrome.notifications.create(errorNotifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: `${annotationSimpleType || type} Save Failed`, + message: `Could not save ${annotationSimpleType.toLowerCase() || type.toLowerCase()} "${name}". Error: ${error.message}`, + }); + setTimeout(() => { + chrome.notifications.clear(errorNotifId); + }, 7000); // Keep error notifications slightly longer + + reject(error); + }); + } catch (error) { + // Catch synchronous errors in the promise executor + console.error('Background: Error in addAnnotation sync part:', error); + // Send a notification for this synchronous error as well + const syncErrorNotifId = 'annotationSyncError-' + Date.now(); + chrome.notifications.create(syncErrorNotifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: `${type || 'Annotation'} Setup Failed`, + message: `Failed to initiate saving for "${name}". Error: ${error.message}`, }); + setTimeout(() => { + chrome.notifications.clear(syncErrorNotifId); + }, 7000); + reject(error); + } }); + }); } async function startSession() { - var systemInfo = getSystemInfo(); - session = new Session(Date.now(), systemInfo); - await saveSession(); + var systemInfo = getSystemInfo(); + session = new Session(Date.now(), systemInfo); + await saveSession(); } async function clearSession() { - session.clearAnnotations(); - await saveSession(); + session.clearAnnotations(); + await saveSession(); } function exportSessionCSV() { - if (session.getAnnotations().length == 0) return false; + if (session.getAnnotations().length == 0) return false; - var exportService = new ExportSessionCSV(session); - var csvData = exportService.getCSVData(); + var exportService = new ExportSessionCSV(session); + var csvData = exportService.getCSVData(); - var browserInfo = session.getBrowserInfo(); - var browserInfoString = browserInfo.browser + "_" + browserInfo.browserVersion; + var browserInfo = session.getBrowserInfo(); + var browserInfoString = browserInfo.browser + '_' + browserInfo.browserVersion; - // Formatear la fecha correctamente - const date = new Date(session.getStartDateTime()); - const startDateTime = date.getFullYear() + - ('0' + (date.getMonth() + 1)).slice(-2) + - ('0' + date.getDate()).slice(-2) + '_' + - ('0' + date.getHours()).slice(-2) + - ('0' + date.getMinutes()).slice(-2); + // Formatear la fecha correctamente + const date = new Date(session.getStartDateTime()); + const startDateTime = + date.getFullYear() + + ('0' + (date.getMonth() + 1)).slice(-2) + + ('0' + date.getDate()).slice(-2) + + '_' + + ('0' + date.getHours()).slice(-2) + + ('0' + date.getMinutes()).slice(-2); - var fileName = "ExploratorySession_" + browserInfoString + "_" + startDateTime + ".csv"; + var fileName = 'ExploratorySession_' + browserInfoString + '_' + startDateTime + '.csv'; - // Crear data URL - const dataUrl = 'data:text/csv;charset=utf-8;base64,' + btoa(csvData); + // Crear data URL + const dataUrl = 'data:text/csv;charset=utf-8;base64,' + btoa(csvData); - chrome.downloads.download({ - url: dataUrl, - filename: fileName, - saveAs: true - }); + chrome.downloads.download({ + url: dataUrl, + filename: fileName, + saveAs: true, + }); - return true; + return true; } function exportSessionJSon() { - if (session.getAnnotations().length == 0) return false; + if (session.getAnnotations().length == 0) return false; - var exportJSonService = new JSonSessionService(); - var jsonData = exportJSonService.getJSon(session); + var exportJSonService = new JSonSessionService(); + var jsonData = exportJSonService.getJSon(session); - var browserInfo = session.getBrowserInfo(); - var browserInfoString = browserInfo.browser + "_" + browserInfo.browserVersion; + var browserInfo = session.getBrowserInfo(); + var browserInfoString = browserInfo.browser + '_' + browserInfo.browserVersion; - // Formatear la fecha correctamente - const date = new Date(session.getStartDateTime()); - const startDateTime = date.getFullYear() + - ('0' + (date.getMonth() + 1)).slice(-2) + - ('0' + date.getDate()).slice(-2) + '_' + - ('0' + date.getHours()).slice(-2) + - ('0' + date.getMinutes()).slice(-2); + // Formatear la fecha correctamente + const date = new Date(session.getStartDateTime()); + const startDateTime = + date.getFullYear() + + ('0' + (date.getMonth() + 1)).slice(-2) + + ('0' + date.getDate()).slice(-2) + + '_' + + ('0' + date.getHours()).slice(-2) + + ('0' + date.getMinutes()).slice(-2); - var fileName = "ExploratorySession_" + browserInfoString + "_" + startDateTime + ".json"; + var fileName = 'ExploratorySession_' + browserInfoString + '_' + startDateTime + '.json'; - // Crear data URL - const dataUrl = 'data:application/json;base64,' + btoa(jsonData); + // Crear data URL + const dataUrl = 'data:application/json;base64,' + btoa(jsonData); - chrome.downloads.download({ - url: dataUrl, - filename: fileName, - saveAs: true - }); + chrome.downloads.download({ + url: dataUrl, + filename: fileName, + saveAs: true, + }); - return true; + return true; } function importSessionJSon(JSonSessionData) { - debugger; - var exportJSonService = new JSonSessionService(); - var importedSession = exportJSonService.getSession(JSonSessionData); + var exportJSonService = new JSonSessionService(); + var importedSession = exportJSonService.getSession(JSonSessionData); - if (importedSession == null) - return false; + if (importedSession == null) return false; - clearSession(); - session = importedSession; + clearSession(); + session = importedSession; - return true; -} \ No newline at end of file + return true; +} diff --git a/css/popUp.css b/css/popUp.css index a3ce129..42f5a7e 100644 --- a/css/popUp.css +++ b/css/popUp.css @@ -2,574 +2,577 @@ CSS Custom Properties (matching report theme) ============================================ */ :root { - --color-bg: #f0f2f5; - --color-surface: #ffffff; - --color-text: #1a1a2e; - --color-text-secondary: #64748b; - --color-border: #e2e8f0; + --color-bg: #f0f2f5; + --color-surface: #ffffff; + --color-text: #1a1a2e; + --color-text-secondary: #64748b; + --color-border: #e2e8f0; - --color-bug: #ef4444; - --color-bug-light: #fef2f2; - --color-note: #22c55e; - --color-note-light: #f0fdf4; - --color-idea: #f59e0b; - --color-idea-light: #fffbeb; - --color-question: #3b82f6; - --color-question-light: #eff6ff; + --color-bug: #ef4444; + --color-bug-light: #fef2f2; + --color-note: #22c55e; + --color-note-light: #f0fdf4; + --color-idea: #f59e0b; + --color-idea-light: #fffbeb; + --color-question: #3b82f6; + --color-question-light: #eff6ff; - --color-primary: #6366f1; - --color-primary-dark: #4f46e5; + --color-primary: #6366f1; + --color-primary-dark: #4f46e5; - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - --transition: 0.2s ease; + --font-sans: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --transition: 0.2s ease; } body { - min-width: 300px; - max-width: 400px; - background-color: #ffffff; - font-family: var(--font-sans); - color: var(--color-text); - -webkit-font-smoothing: antialiased; + min-width: 300px; + max-width: 400px; + background-color: #ffffff; + font-family: var(--font-sans); + color: var(--color-text); + -webkit-font-smoothing: antialiased; } .form-control-sm { - width: 100%; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - font-family: var(--font-sans); - transition: border-color var(--transition), box-shadow var(--transition); + width: 100%; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-family: var(--font-sans); + transition: + border-color var(--transition), + box-shadow var(--transition); } .form-control-sm:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } /* ============================================ Annotation Action Buttons (Modern Style) ============================================ */ .annotation-group { - gap: 6px !important; + gap: 6px !important; } .add, .screenshot, .crop-screenshot { - background-repeat: no-repeat; - background-size: 18px 18px; - background-position: center; - min-width: 38px; - min-height: 38px; - border-radius: var(--radius-md) !important; - border-width: 1.5px !important; - transition: all var(--transition); - position: relative; - overflow: hidden; + background-repeat: no-repeat; + background-size: 18px 18px; + background-position: center; + min-width: 38px; + min-height: 38px; + border-radius: var(--radius-md) !important; + border-width: 1.5px !important; + transition: all var(--transition); + position: relative; + overflow: hidden; } .add::before, .screenshot::before, .crop-screenshot::before { - content: ''; - position: absolute; - inset: 0; - background: currentColor; - opacity: 0; - transition: opacity var(--transition); + content: ''; + position: absolute; + inset: 0; + background: currentColor; + opacity: 0; + transition: opacity var(--transition); } .add:hover, .screenshot:hover, .crop-screenshot:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .add:hover::before, .screenshot:hover::before, .crop-screenshot:hover::before { - opacity: 0.1; + opacity: 0.1; } .add:active, .screenshot:active, .crop-screenshot:active { - transform: translateY(0); + transform: translateY(0); } .add { - background-image: url("../images/diff-added.svg"); + background-image: url('../images/diff-added.svg'); } .screenshot { - background-image: url("../images/device-camera.svg"); + background-image: url('../images/device-camera.svg'); } .crop-screenshot { - background-image: url("../images/crop.png"); + background-image: url('../images/crop.png'); } .cancel { - border-radius: var(--radius-md) !important; - border-width: 1.5px !important; - min-height: 38px; - padding: 8px 16px; - font-weight: 500; - transition: all var(--transition); + border-radius: var(--radius-md) !important; + border-width: 1.5px !important; + min-height: 38px; + padding: 8px 16px; + font-weight: 500; + transition: all var(--transition); } .cancel:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .cancel:active { - transform: translateY(0); + transform: translateY(0); } /* ============================================ Cards ============================================ */ .card { - background: var(--color-surface); - border: 1.5px solid var(--color-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-sm); - margin-bottom: 10px; - transition: all var(--transition); + background: var(--color-surface); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + margin-bottom: 10px; + transition: all var(--transition); } .card:hover { - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-md); } .card-header { - padding: 4px 0 0 7px; - background: transparent; - border-bottom: none; + padding: 4px 0 0 7px; + background: transparent; + border-bottom: none; } .header-with-help { - display: flex; - align-items: center; - justify-content: space-between; - padding-right: 8px; + display: flex; + align-items: center; + justify-content: space-between; + padding-right: 8px; } .help-btn { - width: 26px; - height: 26px; - padding: 0; - border: 1.5px solid var(--color-primary); - border-radius: 50%; - background: var(--color-surface); - color: var(--color-primary); - font-weight: 700; - line-height: 1; - cursor: pointer; - transition: all var(--transition); + width: 26px; + height: 26px; + padding: 0; + border: 1.5px solid var(--color-primary); + border-radius: 50%; + background: var(--color-surface); + color: var(--color-primary); + font-weight: 700; + line-height: 1; + cursor: pointer; + transition: all var(--transition); } .help-btn:hover { - background: var(--color-primary); - color: #ffffff; + background: var(--color-primary); + color: #ffffff; } .card-header h5 { - font-size: 1rem; - font-weight: 600; - color: var(--color-text); - margin: 8px 0; + font-size: 1rem; + font-weight: 600; + color: var(--color-text); + margin: 8px 0; } .card, .card-body, -.card>.card>.card-header, -.card-body>.card-footer { - padding: 2px 3px; - border: none; +.card > .card > .card-header, +.card-body > .card-footer { + padding: 2px 3px; + border: none; } -.card>.card { - box-shadow: none; - margin-bottom: 0; +.card > .card { + box-shadow: none; + margin-bottom: 0; } .cancel { - float: right; + float: right; } -.list-group-item>.card>.card-header, -.list-group-item>.card>.card-footer, -.list-group-item>.card>.card-body { - padding: 2px; +.list-group-item > .card > .card-header, +.list-group-item > .card > .card-footer, +.list-group-item > .card > .card-body { + padding: 2px; } .reportAnnotation { - display: none; - margin-top: 2px; + display: none; + margin-top: 2px; } #resetConfirmation { - margin-top: 20px; + margin-top: 20px; } /* ============================================ Main Header ============================================ */ .card:first-child { - background: var(--color-surface); - border: 1px solid var(--color-border); - box-shadow: var(--shadow-sm); + background: var(--color-surface); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); } .card:first-child .card-header h5 { - color: var(--color-text); - font-weight: 700; + color: var(--color-text); + font-weight: 700; } /* ============================================ Buttons - Matching Report Colors ============================================ */ .btn { - border-radius: var(--radius-sm); - font-family: var(--font-sans); - font-weight: 500; - transition: all var(--transition); + border-radius: var(--radius-sm); + font-family: var(--font-sans); + font-weight: 500; + transition: all var(--transition); } /* Bug Button */ .BugButton.btn-danger { - background-color: var(--color-bug); - border-color: var(--color-bug); - color: #ffffff; + background-color: var(--color-bug); + border-color: var(--color-bug); + color: #ffffff; } .BugButton.btn-danger:hover { - background-color: #dc2626; - border-color: #dc2626; - transform: translateY(-1px); - box-shadow: var(--shadow-md); + background-color: #dc2626; + border-color: #dc2626; + transform: translateY(-1px); + box-shadow: var(--shadow-md); } .btn-outline-danger { - color: var(--color-bug); - border-color: var(--color-bug); + color: var(--color-bug); + border-color: var(--color-bug); } .btn-outline-danger:hover { - background-color: var(--color-bug-light); - border-color: var(--color-bug); - color: var(--color-bug); + background-color: var(--color-bug-light); + border-color: var(--color-bug); + color: var(--color-bug); } /* Note Button */ .NoteButton.btn-warning { - background-color: var(--color-note); - border-color: var(--color-note); - color: #ffffff; + background-color: var(--color-note); + border-color: var(--color-note); + color: #ffffff; } .NoteButton.btn-warning:hover { - background-color: #16a34a; - border-color: #16a34a; - transform: translateY(-1px); - box-shadow: var(--shadow-md); + background-color: #16a34a; + border-color: #16a34a; + transform: translateY(-1px); + box-shadow: var(--shadow-md); } .btn-outline-warning { - color: var(--color-note); - border-color: var(--color-note); + color: var(--color-note); + border-color: var(--color-note); } .btn-outline-warning:hover { - background-color: var(--color-note-light); - border-color: var(--color-note); - color: var(--color-note); + background-color: var(--color-note-light); + border-color: var(--color-note); + color: var(--color-note); } /* Question Button */ .QuestionButton.btn-success { - background-color: var(--color-question); - border-color: var(--color-question); - color: #ffffff; + background-color: var(--color-question); + border-color: var(--color-question); + color: #ffffff; } .QuestionButton.btn-success:hover { - background-color: #2563eb; - border-color: #2563eb; - transform: translateY(-1px); - box-shadow: var(--shadow-md); + background-color: #2563eb; + border-color: #2563eb; + transform: translateY(-1px); + box-shadow: var(--shadow-md); } .btn-outline-success { - color: var(--color-question); - border-color: var(--color-question); + color: var(--color-question); + border-color: var(--color-question); } .btn-outline-success:hover { - background-color: var(--color-question-light); - border-color: var(--color-question); - color: var(--color-question); + background-color: var(--color-question-light); + border-color: var(--color-question); + color: var(--color-question); } /* Idea Button */ .IdeaButton.btn-info { - background-color: var(--color-idea); - border-color: var(--color-idea); - color: #ffffff; + background-color: var(--color-idea); + border-color: var(--color-idea); + color: #ffffff; } .IdeaButton.btn-info:hover { - background-color: #d97706; - border-color: #d97706; - transform: translateY(-1px); - box-shadow: var(--shadow-md); + background-color: #d97706; + border-color: #d97706; + transform: translateY(-1px); + box-shadow: var(--shadow-md); } .btn-outline-info { - color: var(--color-idea); - border-color: var(--color-idea); + color: var(--color-idea); + border-color: var(--color-idea); } .btn-outline-info:hover { - background-color: var(--color-idea-light); - border-color: var(--color-idea); - color: var(--color-idea); + background-color: var(--color-idea-light); + border-color: var(--color-idea); + color: var(--color-idea); } /* Secondary Buttons */ .btn-secondary { - background-color: #f1f5f9; - border-color: #e2e8f0; - color: var(--color-text-secondary); + background-color: #f1f5f9; + border-color: #e2e8f0; + color: var(--color-text-secondary); } .btn-secondary:hover { - background-color: #e2e8f0; - border-color: #cbd5e1; + background-color: #e2e8f0; + border-color: #cbd5e1; } .btn-outline-secondary { - color: var(--color-text-secondary); - border-color: var(--color-border); + color: var(--color-text-secondary); + border-color: var(--color-border); } .btn-outline-secondary:hover { - background-color: #f1f5f9; - border-color: var(--color-border); + background-color: #f1f5f9; + border-color: var(--color-border); } /* Badges (Modern Style) */ .badge-light { - background-color: rgba(255, 255, 255, 0.25); - color: #ffffff; - border-radius: 12px; - padding: 3px 10px; - font-weight: 700; - font-size: 0.8rem; - min-width: 24px; - text-align: center; - display: inline-flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.25); + color: #ffffff; + border-radius: 12px; + padding: 3px 10px; + font-weight: 700; + font-size: 0.8rem; + min-width: 24px; + text-align: center; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); } /* ============================================ Footer & Actions ============================================ */ .footer { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-sm); - padding: 8px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + padding: 8px; } #resetBtn { - max-height: 38px; - margin-right: 100px; - border-radius: var(--radius-md); - padding: 8px 16px; - font-weight: 500; - border: 1.5px solid var(--color-bug); - color: var(--color-bug); - background-color: var(--color-surface); - transition: all var(--transition); + max-height: 38px; + margin-right: 100px; + border-radius: var(--radius-md); + padding: 8px 16px; + font-weight: 500; + border: 1.5px solid var(--color-bug); + color: var(--color-bug); + background-color: var(--color-surface); + transition: all var(--transition); } #resetBtn:hover { - background-color: var(--color-bug); - color: #ffffff; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2); + background-color: var(--color-bug); + color: #ffffff; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2); } #resetBtn:active { - transform: translateY(0); + transform: translateY(0); } /* ============================================ Export/Import Buttons (Modern Style) ============================================ */ button input.file-input { - display: none; + display: none; } .session-group { - gap: 6px; + gap: 6px; } #exportCSVBtn, #exportJsonBtn, #importJsonBtn, #previewBtn { - background-repeat: no-repeat; - background-size: 18px 18px; - background-position: center; - min-width: 38px; - min-height: 38px; - background-color: var(--color-surface); - border: 1.5px solid var(--color-border); - border-radius: var(--radius-md); - transition: all var(--transition); - position: relative; - overflow: hidden; - cursor: pointer; + background-repeat: no-repeat; + background-size: 18px 18px; + background-position: center; + min-width: 38px; + min-height: 38px; + background-color: var(--color-surface); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-md); + transition: all var(--transition); + position: relative; + overflow: hidden; + cursor: pointer; } #exportCSVBtn::before, #exportJsonBtn::before, #importJsonBtn::before, #previewBtn::before { - content: ''; - position: absolute; - inset: 0; - background: var(--color-primary); - opacity: 0; - transition: opacity var(--transition); + content: ''; + position: absolute; + inset: 0; + background: var(--color-primary); + opacity: 0; + transition: opacity var(--transition); } #exportCSVBtn:hover, #exportJsonBtn:hover, #importJsonBtn:hover, #previewBtn:hover { - border-color: var(--color-primary); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); + border-color: var(--color-primary); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); } #exportCSVBtn:hover::before, #exportJsonBtn:hover::before, #importJsonBtn:hover::before, #previewBtn:hover::before { - opacity: 0.08; + opacity: 0.08; } #exportCSVBtn:active, #exportJsonBtn:active, #importJsonBtn:active, #previewBtn:active { - transform: translateY(0); + transform: translateY(0); } #exportCSVBtn { - background-image: url("../images/003-csv.svg"); + background-image: url('../images/003-csv.svg'); } #exportJsonBtn { - background-image: url("../images/002-download.svg"); - margin-left: 0; + background-image: url('../images/002-download.svg'); + margin-left: 0; } #importJsonBtn { - background-image: url("../images/004-up-arrow.svg"); - margin-left: 0; + background-image: url('../images/004-up-arrow.svg'); + margin-left: 0; } #previewBtn { - background-image: url("../images/001-graphic.svg"); - margin-left: 0; + background-image: url('../images/001-graphic.svg'); + margin-left: 0; } /* ============================================ Reset Confirmation (Modern Style) ============================================ */ #resetConfirmation { - border-radius: var(--radius-lg); - overflow: hidden; - box-shadow: var(--shadow-md); + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow-md); } #resetConfirmation .card-header { - background-color: var(--color-bug); - color: #ffffff; - border-radius: 0; - padding: 14px 16px; - font-weight: 600; - font-size: 0.95rem; + background-color: var(--color-bug); + color: #ffffff; + border-radius: 0; + padding: 14px 16px; + font-weight: 600; + font-size: 0.95rem; } #resetConfirmation .card-body { - padding: 20px 16px; + padding: 20px 16px; } #resetConfirmation .text-danger { - color: var(--color-bug); - font-weight: 500; - margin-bottom: 16px; + color: var(--color-bug); + font-weight: 500; + margin-bottom: 16px; } #resetYes.actionButton, #resetNo.cancelButton { - border-radius: var(--radius-md); - padding: 8px 20px; - font-weight: 500; - border-width: 1.5px; - transition: all var(--transition); + border-radius: var(--radius-md); + padding: 8px 20px; + font-weight: 500; + border-width: 1.5px; + transition: all var(--transition); } #resetYes.actionButton { - background-color: var(--color-bug); - border-color: var(--color-bug); - color: #ffffff; + background-color: var(--color-bug); + border-color: var(--color-bug); + color: #ffffff; } #resetYes.actionButton:hover { - background-color: #dc2626; - border-color: #dc2626; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2); + background-color: #dc2626; + border-color: #dc2626; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2); } #resetYes.actionButton:active { - transform: translateY(0); + transform: translateY(0); } #resetNo.cancelButton { - background-color: var(--color-note); - border-color: var(--color-note); - color: #ffffff; + background-color: var(--color-note); + border-color: var(--color-note); + color: #ffffff; } #resetNo.cancelButton:hover { - background-color: #16a34a; - border-color: #16a34a; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2); + background-color: #16a34a; + border-color: #16a34a; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2); } #resetNo.cancelButton:active { - transform: translateY(0); -} \ No newline at end of file + transform: translateY(0); +} diff --git a/css/tutorial.css b/css/tutorial.css index 553dd1d..9881cac 100644 --- a/css/tutorial.css +++ b/css/tutorial.css @@ -2,129 +2,129 @@ /* popUp.css limits the body to popup width; the tutorial page needs the full window. */ body.tutorial { - min-width: 0; - max-width: none; - background-color: var(--color-bg); + min-width: 0; + max-width: none; + background-color: var(--color-bg); } .tutorial-header { - text-align: center; - padding: 40px 16px 24px; - background: linear-gradient(135deg, var(--color-primary), #8b5cf6); - color: #ffffff; + text-align: center; + padding: 40px 16px 24px; + background: linear-gradient(135deg, var(--color-primary), #8b5cf6); + color: #ffffff; } .tutorial-header h1 { - font-size: 1.8rem; - font-weight: 700; - margin-bottom: 8px; + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 8px; } .tutorial-header p { - margin-bottom: 16px; - opacity: 0.9; + margin-bottom: 16px; + opacity: 0.9; } .tutorial-header .btn-primary { - background-color: #ffffff; - border-color: #ffffff; - color: var(--color-primary-dark); - border-radius: var(--radius-md); - padding: 8px 20px; - font-weight: 600; + background-color: #ffffff; + border-color: #ffffff; + color: var(--color-primary-dark); + border-radius: var(--radius-md); + padding: 8px 20px; + font-weight: 600; } .tutorial-main { - max-width: 1000px; - margin: 0 auto; - padding: 24px 16px 64px; + max-width: 1000px; + margin: 0 auto; + padding: 24px 16px 64px; } .scene { - margin-bottom: 48px; + margin-bottom: 48px; } .scene h2 { - font-size: 1.3rem; - font-weight: 700; - margin-bottom: 4px; + font-size: 1.3rem; + font-weight: 700; + margin-bottom: 4px; } .scene-caption { - color: var(--color-text-secondary); - margin-bottom: 16px; + color: var(--color-text-secondary); + margin-bottom: 16px; } /* Popup mock: same width as the real popup, inert. */ .popup-mock { - width: 400px; - max-width: 100%; - margin: 0 auto; - padding: 8px; - background: #ffffff; - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-md); - pointer-events: none; - user-select: none; + width: 400px; + max-width: 100%; + margin: 0 auto; + padding: 8px; + background: #ffffff; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + pointer-events: none; + user-select: none; } .popup-mock #resetBtn { - margin-right: 0; + margin-right: 0; } .popup-mock .session-buttons { - display: inline-flex; - gap: 6px; + display: inline-flex; + gap: 6px; } /* Screenshots with transparent hotspots that Driver.js can highlight. */ .shot { - position: relative; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-md); - overflow: hidden; - background: #ffffff; + position: relative; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + overflow: hidden; + background: #ffffff; } .shot img { - display: block; - width: 100%; - height: auto; + display: block; + width: 100%; + height: auto; } .hotspot { - position: absolute; - display: block; - pointer-events: none; + position: absolute; + display: block; + pointer-events: none; } /* Driver.js popover tweaks to match the extension look. */ .driver-popover.et-tour { - font-family: var(--font-sans); - border-radius: var(--radius-md); - max-width: 340px; + font-family: var(--font-sans); + border-radius: var(--radius-md); + max-width: 340px; } .driver-popover.et-tour .driver-popover-title { - font-size: 1.05rem; - font-weight: 700; + font-size: 1.05rem; + font-weight: 700; } .driver-popover.et-tour .driver-popover-description { - line-height: 1.45; + line-height: 1.45; } .driver-popover.et-tour .driver-popover-description kbd { - background: #f1f5f9; - border: 1px solid var(--color-border); - border-radius: 4px; - color: var(--color-text); - padding: 0 4px; + background: #f1f5f9; + border: 1px solid var(--color-border); + border-radius: 4px; + color: var(--color-text); + padding: 0 4px; } .driver-popover.et-tour .driver-popover-next-btn, .driver-popover.et-tour .driver-popover-prev-btn { - border-radius: var(--radius-sm); + border-radius: var(--radius-sm); } diff --git a/docs/adr/0001-tiered-git-hooks.md b/docs/adr/0001-tiered-git-hooks.md new file mode 100644 index 0000000..6d4a280 --- /dev/null +++ b/docs/adr/0001-tiered-git-hooks.md @@ -0,0 +1,23 @@ +# Tiered git hooks: fast checks on commit, E2E on push + +## Status + +Accepted + +## Context + +Agents and humans both commit to this repo, and the Web Store release needs a clean build. The full E2E suite takes about 4 minutes and launches a real browser with the extension loaded, so running everything on every commit would push people toward `--no-verify`, which defeats the gate. + +## Decision + +- **pre-commit** runs only fast checks: lint-staged (ESLint and Prettier on staged files) and the Jest suite, plus `release:check` when the content of `manifest.json` changes. +- **pre-push** runs the headless Playwright suite, and only when runtime files changed in the pushed range. +- CI repeats the same checks and is the final authority. +- A Claude Code `PreToolUse` hook rejects `--no-verify`, `HUSKY=0` and `core.hooksPath` changes, since a git hook alone cannot stop an agent from skipping it. + +## Consequences + +- Commits stay under about 30 seconds, so the hooks are not worth bypassing. +- A commit can reach the local branch with a broken E2E test; it is caught at push, not commit. +- Docs-only or test-only pushes skip E2E unless they touch `test/e2e/`. +- Hook logic lives in `scripts/` and `.husky/`, and has to be kept in step with `package.json` scripts and the CI workflow. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d1b6b72 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,69 @@ +const js = require('@eslint/js'); +const globals = require('globals'); +const prettier = require('eslint-config-prettier'); + +module.exports = [ + { + ignores: [ + 'lib/**', + 'node_modules/**', + 'playwright-report/**', + 'test-results/**', + 'coverage/**', + '**/*.min.js', + 'HTMLReport/TableFilter/**', + ], + }, + js.configs.recommended, + { + // Extension runtime: ES modules running in the browser or service worker + files: ['src/**/*.js', 'js/**/*.js', 'background.js', 'HTMLReport/**/*.js'], + languageOptions: { + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.serviceworker, + chrome: 'readonly', + $: 'readonly', + jQuery: 'readonly', + // vendored libraries loaded via