From df4d26c049c0552c6a6aeedc2ff299377f1d45a4 Mon Sep 17 00:00:00 2001 From: Bun Bun Date: Tue, 16 Jun 2026 18:27:31 +0000 Subject: [PATCH] Initial build: SimpleScan Solo v0.1.0 - Freelancer receipt capture Chrome extension --- .gitignore | 7 + dist-test/test/simplescan.test.js | 63 + dist-test/types.js | 5 + dist-test/utils/categories.js | 68 ++ dist-test/utils/export.js | 32 + dist-test/utils/storage.js | 33 + options.html | 153 +++ package-lock.json | 1848 +++++++++++++++++++++++++++++ package.json | 24 + public/icons/icon128.png | Bin 0 -> 307 bytes public/icons/icon16.png | Bin 0 -> 79 bytes public/icons/icon48.png | Bin 0 -> 124 bytes scripts/icons/icon128.png | Bin 0 -> 307 bytes scripts/icons/icon16.png | Bin 0 -> 79 bytes scripts/icons/icon48.png | Bin 0 -> 124 bytes scripts/make-icons.cjs | 65 + scripts/make-icons2.cjs | 65 + scripts/secret-scan.cjs | 40 + src/background.ts | 21 + src/content/email-scanner.ts | 157 +++ src/manifest.json | 43 + src/options.ts | 202 ++++ src/popup/popup.css | 421 +++++++ src/popup/popup.html | 107 ++ src/popup/popup.ts | 286 +++++ src/test/simplescan.test.ts | 71 ++ src/types.ts | 53 + src/utils/categories.ts | 72 ++ src/utils/export.ts | 37 + src/utils/storage.ts | 40 + tsconfig.json | 22 + tsconfig.test.json | 13 + vite.config.ts | 14 + 33 files changed, 3962 insertions(+) create mode 100644 .gitignore create mode 100644 dist-test/test/simplescan.test.js create mode 100644 dist-test/types.js create mode 100644 dist-test/utils/categories.js create mode 100644 dist-test/utils/export.js create mode 100644 dist-test/utils/storage.js create mode 100644 options.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/icons/icon128.png create mode 100644 public/icons/icon16.png create mode 100644 public/icons/icon48.png create mode 100644 scripts/icons/icon128.png create mode 100644 scripts/icons/icon16.png create mode 100644 scripts/icons/icon48.png create mode 100644 scripts/make-icons.cjs create mode 100644 scripts/make-icons2.cjs create mode 100644 scripts/secret-scan.cjs create mode 100644 src/background.ts create mode 100644 src/content/email-scanner.ts create mode 100644 src/manifest.json create mode 100644 src/options.ts create mode 100644 src/popup/popup.css create mode 100644 src/popup/popup.html create mode 100644 src/popup/popup.ts create mode 100644 src/test/simplescan.test.ts create mode 100644 src/types.ts create mode 100644 src/utils/categories.ts create mode 100644 src/utils/export.ts create mode 100644 src/utils/storage.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..513ad09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +*.log +.venv/ +.verdict +.DS_Store +*.local diff --git a/dist-test/test/simplescan.test.js b/dist-test/test/simplescan.test.js new file mode 100644 index 0000000..6b1154b --- /dev/null +++ b/dist-test/test/simplescan.test.js @@ -0,0 +1,63 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +// Test categories +import { TAX_CATEGORIES, getCategoryLabel, getCategoryIcon } from '../utils/categories.js'; +describe('categories', () => { + it('has 10 tax categories', () => { + assert.strictEqual(TAX_CATEGORIES.length, 10); + }); + it('gets correct label', () => { + assert.strictEqual(getCategoryLabel('software'), 'Software'); + assert.strictEqual(getCategoryLabel('equipment'), 'Equipment'); + assert.strictEqual(getCategoryLabel('nonexistent'), 'Other'); + }); + it('gets correct icon', () => { + assert.strictEqual(getCategoryIcon('software'), 'โš™๏ธ'); + assert.strictEqual(getCategoryIcon('travel'), 'โœˆ๏ธ'); + assert.strictEqual(getCategoryIcon('nonexistent'), '๐Ÿ“ฆ'); + }); +}); +// Test export +import { exportToCsv } from '../utils/export.js'; +describe('export', () => { + it('exports receipts to CSV', () => { + const receipts = [ + { + id: '1', + title: 'Adobe CC', + amount: 59.99, + currency: 'USD', + category: 'software', + date: '2026-06-01', + vendor: 'Adobe', + notes: 'Monthly', + source: 'manual', + createdAt: '2026-06-01T00:00:00Z', + }, + ]; + const csv = exportToCsv(receipts); + console.log('CSV output:', csv); + assert.ok(csv.includes('Date'), 'CSV should have Date header'); + assert.ok(csv.includes('Adobe CC'), 'CSV should have title'); + assert.ok(csv.includes('59.99'), 'CSV should have amount'); + assert.ok(csv.includes('Software'), 'CSV should have category label'); + }); + it('escapes quotes in CSV', () => { + const receipts = [ + { + id: '1', + title: 'My "Special" Item', + amount: 10, + currency: 'USD', + category: 'other', + date: '2026-06-01', + vendor: 'Vendor', + notes: '', + source: 'manual', + createdAt: '2026-06-01T00:00:00Z', + }, + ]; + const csv = exportToCsv(receipts); + assert.ok(csv.includes('""'), 'CSV should escape quotes'); + }); +}); diff --git a/dist-test/types.js b/dist-test/types.js new file mode 100644 index 0000000..eca3916 --- /dev/null +++ b/dist-test/types.js @@ -0,0 +1,5 @@ +export const DEFAULT_SETTINGS = { + defaultCurrency: 'USD', + taxYear: new Date().getFullYear().toString(), + ownerName: '', +}; diff --git a/dist-test/utils/categories.js b/dist-test/utils/categories.js new file mode 100644 index 0000000..876fecd --- /dev/null +++ b/dist-test/utils/categories.js @@ -0,0 +1,68 @@ +export const TAX_CATEGORIES = [ + { + id: 'equipment', + label: 'Equipment', + description: 'Computers, cameras, tools, hardware', + icon: '๐Ÿ’ป', + }, + { + id: 'home_office', + label: 'Home Office', + description: 'Desk, chair, lighting, office supplies', + icon: '๐Ÿ ', + }, + { + id: 'software', + label: 'Software', + description: 'Apps, subscriptions, SaaS tools', + icon: 'โš™๏ธ', + }, + { + id: 'internet_phone', + label: 'Internet & Phone', + description: 'ISP, cell plan, domain hosting', + icon: '๐Ÿ“ก', + }, + { + id: 'travel', + label: 'Travel', + description: 'Flights, hotels, rideshare, mileage', + icon: 'โœˆ๏ธ', + }, + { + id: 'meals', + label: 'Meals', + description: 'Business meals (50% deductible)', + icon: '๐Ÿฝ๏ธ', + }, + { + id: 'professional_services', + label: 'Professional Services', + description: 'Lawyer, accountant, contractor payments', + icon: '๐Ÿ“‹', + }, + { + id: 'marketing', + label: 'Marketing', + description: 'Ads, design, copywriting, PR', + icon: '๐Ÿ“ข', + }, + { + id: 'education', + label: 'Education', + description: 'Courses, books, conferences', + icon: '๐Ÿ“š', + }, + { + id: 'other', + label: 'Other', + description: 'Miscellaneous business expenses', + icon: '๐Ÿ“ฆ', + }, +]; +export function getCategoryLabel(id) { + return TAX_CATEGORIES.find((c) => c.id === id)?.label || 'Other'; +} +export function getCategoryIcon(id) { + return TAX_CATEGORIES.find((c) => c.id === id)?.icon || '๐Ÿ“ฆ'; +} diff --git a/dist-test/utils/export.js b/dist-test/utils/export.js new file mode 100644 index 0000000..ee5b025 --- /dev/null +++ b/dist-test/utils/export.js @@ -0,0 +1,32 @@ +import { getCategoryLabel } from './categories.js'; +export function exportToCsv(receipts) { + const headers = ['Date', 'Vendor', 'Title', 'Category', 'Amount', 'Currency', 'Notes', 'Source']; + const rows = receipts.map((r) => [ + r.date, + r.vendor, + r.title, + getCategoryLabel(r.category), + r.amount.toFixed(2), + r.currency, + r.notes, + r.source, + ]); + const csv = [headers, ...rows] + .map((row) => row + .map((cell) => { + const str = String(cell).replace(/"/g, '""'); + return `"${str}"`; + }) + .join(',')) + .join('\n'); + return csv; +} +export function downloadBlob(content, filename, mimeType) { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} diff --git a/dist-test/utils/storage.js b/dist-test/utils/storage.js new file mode 100644 index 0000000..73a7c1f --- /dev/null +++ b/dist-test/utils/storage.js @@ -0,0 +1,33 @@ +import { DEFAULT_SETTINGS } from '../types.js'; +const RECEIPTS_KEY = 'simplescan_receipts'; +const SETTINGS_KEY = 'simplescan_settings'; +export async function getReceipts() { + const result = await chrome.storage.local.get(RECEIPTS_KEY); + return result[RECEIPTS_KEY] || []; +} +export async function saveReceipt(receipt) { + const receipts = await getReceipts(); + const existingIndex = receipts.findIndex((r) => r.id === receipt.id); + if (existingIndex >= 0) { + receipts[existingIndex] = receipt; + } + else { + receipts.unshift(receipt); + } + await chrome.storage.local.set({ [RECEIPTS_KEY]: receipts }); +} +export async function deleteReceipt(id) { + const receipts = await getReceipts(); + const filtered = receipts.filter((r) => r.id !== id); + await chrome.storage.local.set({ [RECEIPTS_KEY]: filtered }); +} +export async function getSettings() { + const result = await chrome.storage.local.get(SETTINGS_KEY); + return result[SETTINGS_KEY] || { ...DEFAULT_SETTINGS }; +} +export async function saveSettings(settings) { + await chrome.storage.local.set({ [SETTINGS_KEY]: settings }); +} +export async function clearAllReceipts() { + await chrome.storage.local.remove(RECEIPTS_KEY); +} diff --git a/options.html b/options.html new file mode 100644 index 0000000..db8b841 --- /dev/null +++ b/options.html @@ -0,0 +1,153 @@ + + + + + + SimpleScan Solo - Settings & Export + + + +

๐Ÿ“ธ SimpleScan Solo

+

Settings & Export Center

+ +
+

โš™๏ธ Your Settings

+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+

๐Ÿ“Š Receipt Summary

+
+
Total Receipts0
+
Total Amount$0.00
+
+
+ +
+

๐Ÿ“„ Export Receipts

+
+ + +
+
+ +
+

๐Ÿ“‹ All Receipts

+
+

No receipts yet. Add some from the extension popup.

+
+
+ +
+

โš ๏ธ Danger Zone

+

This will permanently delete all your saved receipts. This cannot be undone.

+ +
+ + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8c3b6ce --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1848 @@ +{ + "name": "simplescan-solo", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simplescan-solo", + "version": "0.1.0", + "dependencies": { + "@types/node": "^25.9.3", + "jspdf": "^2.5.1", + "jspdf-autotable": "^3.8.2", + "tesseract.js": "^5.0.5" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.26", + "@types/chrome": "^0.0.268", + "typescript": "^5.4.5", + "vite": "^5.2.12" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.6.1.tgz", + "integrity": "sha512-0RmYlUtQGvHXCz3B/FW7P5j+RBZUc4mk3GIRPXCx5a52N8AQ0e35GvE+Pcap/TKuI09PJgcRTdXeBOavtLVyYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "convert-source-map": "^1.7.0", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.30.12", + "node-html-parser": "^7.0.2", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "react-refresh": "^0.13.0", + "rollup": "2.80.0", + "rxjs": "7.5.7", + "tinyglobby": "^0.2.15" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chrome": { + "version": "0.0.268", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.268.tgz", + "integrity": "sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz", + "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", + "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.5.tgz", + "integrity": "sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q==", + "license": "Apache-2.0" + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.8.4.tgz", + "integrity": "sha512-rSffGoBsJYX83iTRv8Ft7FhqfgEL2nLpGAIiqruEQQ3e4r0qdLFbPUB7N9HAle0I3XgpisvyW751VHCqKUVOgQ==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2.5.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tesseract.js": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.1.1.tgz", + "integrity": "sha512-lzVl/Ar3P3zhpUT31NjqeCo1f+D5+YfpZ5J62eo2S14QNVOmHBTtbchHm/YAbOOOzCegFnKf4B3Qih9LuldcYQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-electron": "^2.2.2", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^5.1.1", + "wasm-feature-detect": "^1.2.11", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-5.1.1.tgz", + "integrity": "sha512-KX3bYSU5iGcO1XJa+QGPbi+Zjo2qq6eBhNjSGR5E5q0JtzkoipJKOUQD7ph8kFyteCEfEQ0maWLu8MCXtvX5uQ==", + "license": "Apache-2.0" + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..082c15b --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "simplescan-solo", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "tsc -p tsconfig.test.json && node --test dist-test/**/*.test.js" + }, + "dependencies": { + "@types/node": "^25.9.3", + "jspdf": "^2.5.1", + "jspdf-autotable": "^3.8.2", + "tesseract.js": "^5.0.5" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.26", + "@types/chrome": "^0.0.268", + "typescript": "^5.4.5", + "vite": "^5.2.12" + } +} diff --git a/public/icons/icon128.png b/public/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6533f4984035c89dcbf8e25bbc179bd956b397 GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRdwrzdT(WLn`LHz39lxpultBfaL+6 zrVs0xB&)wn;>sv0es?)(bDaP`Lqiz@!xmBD*^ahZNK(F>8kYf?WoPM9zuz*ay=NN5$i6A*lL@bK_hA;-bV$$6DoQE}mMy<0nX V`>6J<-3K&|!PC{xWt~$(69D&_B}M=M literal 0 HcmV?d00001 diff --git a/scripts/icons/icon128.png b/scripts/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6533f4984035c89dcbf8e25bbc179bd956b397 GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRdwrzdT(WLn`LHz39lxpultBfaL+6 zrVs0xB&)wn;>sv0es?)(bDaP`Lqiz@!xmBD*^ahZNK(F>8kYf?WoPM9zuz*ay=NN5$i6A*lL@bK_hA;-bV$$6DoQE}mMy<0nX V`>6J<-3K&|!PC{xWt~$(69D&_B}M=M literal 0 HcmV?d00001 diff --git a/scripts/make-icons.cjs b/scripts/make-icons.cjs new file mode 100644 index 0000000..46158a8 --- /dev/null +++ b/scripts/make-icons.cjs @@ -0,0 +1,65 @@ +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); + +const iconsDir = path.join(__dirname, 'icons'); +fs.mkdirSync(iconsDir, { recursive: true }); + +function crc32(buf) { + const table = []; + for (let i = 0; i < 256; i++) { + let c = i; + for (let k = 0; k < 8; k++) { + c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); + } + table[i] = c; + } + let crc = -1; + for (const byte of buf) { + crc = table[(crc ^ byte) & 0xFF] ^ (crc >>> 8); + } + return (crc ^ -1) >>> 0; +} + +function makeChunk(typeStr, data) { + const typeBuf = Buffer.from(typeStr, 'ascii'); + const c = crc32(Buffer.concat([typeBuf, data])); + const crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(c, 0); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(data.length, 0); + return Buffer.concat([lenBuf, typeBuf, data, crcBuf]); +} + +function makePng(size, r, g, b) { + const sig = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + const ihdrData = Buffer.alloc(13); + ihdrData.writeUInt32BE(size, 0); + ihdrData.writeUInt32BE(size, 4); + ihdrData.writeUInt8(8, 8); + ihdrData.writeUInt8(2, 9); + ihdrData.writeUInt8(0, 10); + ihdrData.writeUInt8(0, 11); + ihdrData.writeUInt8(0, 12); + + const rawData = []; + for (let y = 0; y < size; y++) { + rawData.push(0); + for (let x = 0; x < size; x++) { + rawData.push(r, g, b); + } + } + const imageData = zlib.deflateSync(Buffer.from(rawData)); + + const ihdr = makeChunk('IHDR', ihdrData); + const idat = makeChunk('IDAT', imageData); + const iend = makeChunk('IEND', Buffer.alloc(0)); + + return Buffer.concat([sig, ihdr, idat, iend]); +} + +const r = 15, g = 52, b = 96; +fs.writeFileSync(path.join(iconsDir, 'icon16.png'), makePng(16, r, g, b)); +fs.writeFileSync(path.join(iconsDir, 'icon48.png'), makePng(48, r, g, b)); +fs.writeFileSync(path.join(iconsDir, 'icon128.png'), makePng(128, r, g, b)); +console.log('Icons created'); diff --git a/scripts/make-icons2.cjs b/scripts/make-icons2.cjs new file mode 100644 index 0000000..74ed5f2 --- /dev/null +++ b/scripts/make-icons2.cjs @@ -0,0 +1,65 @@ +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); + +const iconsDir = path.join(__dirname, '..', 'public', 'icons'); +fs.mkdirSync(iconsDir, { recursive: true }); + +function crc32(buf) { + const table = []; + for (let i = 0; i < 256; i++) { + let c = i; + for (let k = 0; k < 8; k++) { + c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); + } + table[i] = c; + } + let crc = -1; + for (const byte of buf) { + crc = table[(crc ^ byte) & 0xFF] ^ (crc >>> 8); + } + return (crc ^ -1) >>> 0; +} + +function makeChunk(typeStr, data) { + const typeBuf = Buffer.from(typeStr, 'ascii'); + const c = crc32(Buffer.concat([typeBuf, data])); + const crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(c, 0); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(data.length, 0); + return Buffer.concat([lenBuf, typeBuf, data, crcBuf]); +} + +function makePng(size, r, g, b) { + const sig = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + const ihdrData = Buffer.alloc(13); + ihdrData.writeUInt32BE(size, 0); + ihdrData.writeUInt32BE(size, 4); + ihdrData.writeUInt8(8, 8); + ihdrData.writeUInt8(2, 9); + ihdrData.writeUInt8(0, 10); + ihdrData.writeUInt8(0, 11); + ihdrData.writeUInt8(0, 12); + + const rawData = []; + for (let y = 0; y < size; y++) { + rawData.push(0); + for (let x = 0; x < size; x++) { + rawData.push(r, g, b); + } + } + const imageData = zlib.deflateSync(Buffer.from(rawData)); + + const ihdr = makeChunk('IHDR', ihdrData); + const idat = makeChunk('IDAT', imageData); + const iend = makeChunk('IEND', Buffer.alloc(0)); + + return Buffer.concat([sig, ihdr, idat, iend]); +} + +const r = 15, g = 52, b = 96; +fs.writeFileSync(path.join(iconsDir, 'icon16.png'), makePng(16, r, g, b)); +fs.writeFileSync(path.join(iconsDir, 'icon48.png'), makePng(48, r, g, b)); +fs.writeFileSync(path.join(iconsDir, 'icon128.png'), makePng(128, r, g, b)); +console.log('Icons created in public/icons'); diff --git a/scripts/secret-scan.cjs b/scripts/secret-scan.cjs new file mode 100644 index 0000000..10960bd --- /dev/null +++ b/scripts/secret-scan.cjs @@ -0,0 +1,40 @@ +const fs = require('fs'); +const path = require('path'); + +const secretPatterns = [ + /sk-[a-zA-Z0-9]{20,}/g, + /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*/g, + /api[_-]?key\s*[:=]\s*['"][a-zA-Z0-9]{16,}['"]/gi, + /token\s*[:=]\s*['"][a-zA-Z0-9]{16,}['"]/gi, +]; + +const filesToCheck = [ + 'src/types.ts', 'src/background.ts', 'src/options.ts', + 'src/popup/popup.ts', 'src/popup/popup.html', 'src/popup/popup.css', + 'src/content/email-scanner.ts', + 'src/utils/storage.ts', 'src/utils/categories.ts', 'src/utils/export.ts', + 'options.html', 'package.json', 'vite.config.ts', + 'tsconfig.json', 'tsconfig.test.json', '.gitignore' +]; + +let found = false; +for (const file of filesToCheck) { + const fp = path.join(__dirname, '..', file); + if (!fs.existsSync(fp)) continue; + const content = fs.readFileSync(fp, 'utf8'); + for (const pattern of secretPatterns) { + const matches = content.match(pattern); + if (matches) { + console.log(`SECRET FOUND in ${file}: ${matches[0].substring(0, 30)}...`); + found = true; + } + } +} + +if (found) { + console.log('SECRETS DETECTED - aborting'); + process.exit(1); +} else { + console.log('No secrets found'); + process.exit(0); +} diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..a4767d9 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,21 @@ +// Background service worker for SimpleScan Solo + +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === 'install') { + // Set default settings on install + chrome.storage.local.set({ + simplescan_settings: { + defaultCurrency: 'USD', + taxYear: new Date().getFullYear().toString(), + ownerName: '', + }, + simplescan_receipts: [], + }); + } +}); + +// Keep alive for messaging +chrome.runtime.onMessage.addListener((_request, _sender, sendResponse) => { + sendResponse({ ok: true }); + return true; +}); diff --git a/src/content/email-scanner.ts b/src/content/email-scanner.ts new file mode 100644 index 0000000..47f79f2 --- /dev/null +++ b/src/content/email-scanner.ts @@ -0,0 +1,157 @@ +// Content script: scan Gmail / Outlook web for receipt emails + +interface EmailHint { + subject: string; + sender: string; + date: string; + amount?: number; + currency?: string; + vendor?: string; +} + +function detectHost(): 'gmail' | 'outlook' | null { + const host = location.hostname; + if (host.includes('mail.google.com')) return 'gmail'; + if (host.includes('outlook.')) return 'outlook'; + return null; +} + +function parseAmount(text: string): { amount?: number; currency?: string } { + // Match patterns like $29.99, USD 29.99, 29.99 USD, โ‚ฌ19.50 + const patterns = [ + /\$\s?([\d,]+\.?\d{0,2})/, + /โ‚ฌ\s?([\d,]+\.?\d{0,2})/, + /ยฃ\s?([\d,]+\.?\d{0,2})/, + /([\d,]+\.?\d{0,2})\s?USD/i, + /([\d,]+\.?\d{0,2})\s?EUR/i, + /total[\s:]*\$?\s?([\d,]+\.?\d{0,2})/i, + /amount[\s:]*\$?\s?([\d,]+\.?\d{0,2})/i, + /charged[\s:]*\$?\s?([\d,]+\.?\d{0,2})/i, + /payment[\s:]*\$?\s?([\d,]+\.?\d{0,2})/i, + /price[\s:]*\$?\s?([\d,]+\.?\d{0,2})/i, + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const amount = parseFloat(match[1].replace(/,/g, '')); + const currency = text.includes('โ‚ฌ') + ? 'EUR' + : text.includes('ยฃ') + ? 'GBP' + : text.includes('EUR') + ? 'EUR' + : 'USD'; + if (!isNaN(amount) && amount > 0) { + return { amount, currency }; + } + } + } + return {}; +} + +function isReceiptEmail(subject: string, body: string): boolean { + const receiptKeywords = [ + 'receipt', 'invoice', 'payment confirmation', 'order confirmation', + 'your receipt', 'purchase confirmation', 'billing receipt', + 'subscription receipt', 'payment receipt', 'order summary', + 'thank you for your purchase', 'payment successful', + 'charge confirmation', 'transaction confirmation', + ]; + const lowerSub = subject.toLowerCase(); + const lowerBody = body.toLowerCase().slice(0, 2000); + return receiptKeywords.some((k) => lowerSub.includes(k) || lowerBody.includes(k)); +} + +function scanGmail(): EmailHint[] { + const hints: EmailHint[] = []; + // Try to find email rows in conversation view or list view + const subjects = document.querySelectorAll('h2[data-thread-perm-id]'); + if (subjects.length > 0) { + // Conversation view - single email thread + for (const subjEl of subjects) { + const subject = subjEl.textContent?.trim() || ''; + const senderEl = document.querySelector('span[email]'); + const sender = senderEl?.getAttribute('email') || senderEl?.textContent?.trim() || ''; + const dateEl = document.querySelector('td[colspan] span[title]'); + const date = dateEl?.getAttribute('title') || dateEl?.textContent?.trim() || ''; + const bodyEl = document.querySelector('.ii.gt'); + const body = bodyEl?.textContent?.trim() || ''; + if (isReceiptEmail(subject, body)) { + const { amount, currency } = parseAmount(subject + ' ' + body); + hints.push({ subject, sender, date, amount, currency, vendor: sender.split('@')[1] || sender }); + } + } + } else { + // List view - multiple emails + const rows = document.querySelectorAll('tr.zA'); + for (const row of rows) { + const subjectEl = row.querySelector('.y6'); + const subject = subjectEl?.textContent?.trim() || ''; + const senderEl = row.querySelector('.yW span[email]'); + const sender = senderEl?.getAttribute('email') || senderEl?.textContent?.trim() || ''; + const dateEl = row.querySelector('.xY span'); + const date = dateEl?.textContent?.trim() || ''; + const snippetEl = row.querySelector('.y2'); + const snippet = snippetEl?.textContent?.trim() || ''; + if (isReceiptEmail(subject, snippet)) { + const { amount, currency } = parseAmount(subject + ' ' + snippet); + hints.push({ subject, sender, date, amount, currency, vendor: sender.split('@')[1] || sender }); + } + } + } + return hints; +} + +function scanOutlook(): EmailHint[] { + const hints: EmailHint[] = []; + // Outlook web - reading pane or list view + const readingPaneSubject = document.querySelector('[role="region"] div[title]'); + if (readingPaneSubject) { + // Reading pane mode + const subject = readingPaneSubject.getAttribute('title')?.trim() || ''; + const senderEl = document.querySelector('[role="region"] span[data-lpc-disable-compression]'); + const sender = senderEl?.textContent?.trim() || ''; + const dateEl = document.querySelector('[role="region"] [data-testid="messageHeaderDate"]'); + const date = dateEl?.textContent?.trim() || ''; + const bodyEl = document.querySelector('[role="region"] #UniqueMessageBody, [role="region"] .x_text-container'); + const body = bodyEl?.textContent?.trim() || ''; + if (isReceiptEmail(subject, body)) { + const { amount, currency } = parseAmount(subject + ' ' + body); + hints.push({ subject, sender, date, amount, currency, vendor: sender.split('@')[1] || sender }); + } + } else { + // List view + const items = document.querySelectorAll('[data-testid="mailListItem"], .lvHighlightAllClass'); + for (const item of items) { + const subjectEl = item.querySelector('[data-testid="threadItemSubject"]'); + const subject = subjectEl?.textContent?.trim() || ''; + const senderEl = item.querySelector('[data-testid="threadItemFrom"]'); + const sender = senderEl?.textContent?.trim() || ''; + const dateEl = item.querySelector('[data-testid="threadItemDate"]'); + const date = dateEl?.textContent?.trim() || ''; + const snippetEl = item.querySelector('[data-testid="threadItemPreview"]'); + const snippet = snippetEl?.textContent?.trim() || ''; + if (isReceiptEmail(subject, snippet)) { + const { amount, currency } = parseAmount(subject + ' ' + snippet); + hints.push({ subject, sender, date, amount, currency, vendor: sender.split('@')[1] || sender }); + } + } + } + return hints; +} + +// Listen for messages from popup +chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { + if (request.action === 'scanEmails') { + const host = detectHost(); + let receipts: EmailHint[] = []; + if (host === 'gmail') { + receipts = scanGmail(); + } else if (host === 'outlook') { + receipts = scanOutlook(); + } + sendResponse({ receipts }); + } + return true; // async response +}); diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..506e13e --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,43 @@ +{ + "manifest_version": 3, + "name": "SimpleScan Solo - Freelancer Receipts", + "version": "0.1.0", + "description": "Dead-simple receipt capture for freelancers. No team bloat, just capture, categorize, export.", + "permissions": ["storage", "activeTab", "scripting"], + "host_permissions": [ + "https://mail.google.com/*", + "https://outlook.live.com/*", + "https://outlook.office.com/*", + "https://outlook.office365.com/*" + ], + "action": { + "default_popup": "src/popup/popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "background": { + "service_worker": "src/background.ts", + "type": "module" + }, + "content_scripts": [ + { + "matches": [ + "https://mail.google.com/*", + "https://outlook.live.com/*", + "https://outlook.office.com/*", + "https://outlook.office365.com/*" + ], + "js": ["src/content/email-scanner.ts"], + "run_at": "document_idle" + } + ], + "options_page": "options.html", + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..abed01e --- /dev/null +++ b/src/options.ts @@ -0,0 +1,202 @@ +import { getReceipts, getSettings, saveSettings, clearAllReceipts } from './utils/storage.js'; +import { getCategoryLabel } from './utils/categories.js'; +import { exportToCsv, downloadBlob } from './utils/export.js'; +import type { Receipt, AppSettings } from './types.js'; + +const nameInput = document.getElementById('s-name') as HTMLInputElement; +const currencySelect = document.getElementById('s-currency') as HTMLSelectElement; +const yearInput = document.getElementById('s-year') as HTMLInputElement; +const saveBtn = document.getElementById('save-settings') as HTMLButtonElement; +const sumCount = document.getElementById('sum-count') as HTMLSpanElement; +const sumTotal = document.getElementById('sum-total') as HTMLSpanElement; +const tableWrap = document.getElementById('receipts-table-wrap') as HTMLDivElement; +const toast = document.getElementById('toast') as HTMLDivElement; + +let currentReceipts: Receipt[] = []; + +async function init() { + const settings = await getSettings(); + nameInput.value = settings.ownerName || ''; + currencySelect.value = settings.defaultCurrency || 'USD'; + yearInput.value = settings.taxYear || new Date().getFullYear().toString(); + + await loadData(); + + saveBtn.addEventListener('click', async () => { + const settings: AppSettings = { + ownerName: nameInput.value.trim(), + defaultCurrency: currencySelect.value, + taxYear: yearInput.value.trim(), + }; + await saveSettings(settings); + showToast('Settings saved'); + }); + + document.getElementById('export-csv')!.addEventListener('click', async () => { + if (currentReceipts.length === 0) { + showToast('No receipts to export'); + return; + } + const csv = exportToCsv(currentReceipts); + const year = yearInput.value || new Date().getFullYear(); + downloadBlob(csv, `simplescan-receipts-${year}.csv`, 'text/csv'); + showToast('CSV downloaded'); + }); + + document.getElementById('export-pdf')!.addEventListener('click', async () => { + if (currentReceipts.length === 0) { + showToast('No receipts to export'); + return; + } + exportPdf(currentReceipts); + }); + + document.getElementById('clear-all')!.addEventListener('click', async () => { + if (!confirm('Are you sure? This will delete ALL receipts forever.')) return; + await clearAllReceipts(); + await loadData(); + showToast('All receipts deleted'); + }); +} + +async function loadData() { + currentReceipts = await getReceipts(); + sumCount.textContent = String(currentReceipts.length); + + const total = currentReceipts.reduce((s, r) => s + r.amount, 0); + const currency = currentReceipts[0]?.currency || 'USD'; + sumTotal.textContent = formatMoney(total, currency); + + if (currentReceipts.length === 0) { + tableWrap.innerHTML = '

No receipts yet. Add some from the extension popup.

'; + return; + } + + tableWrap.innerHTML = ` + + + + + + + + + + + + ${currentReceipts + .map( + (r) => ` + + + + + + + + ` + ) + .join('')} + +
DateVendorDescriptionCategoryAmount
${escapeHtml(r.date)}${escapeHtml(r.vendor)}${escapeHtml(r.title)}${getCategoryLabel(r.category)}${formatMoney(r.amount, r.currency)}
+ `; +} + +function exportPdf(receipts: Receipt[]) { + // Simple PDF generation using browser print to PDF approach + const currency = receipts[0]?.currency || 'USD'; + const total = receipts.reduce((s, r) => s + r.amount, 0); + const year = yearInput.value || new Date().getFullYear(); + const owner = nameInput.value.trim() || 'Freelancer'; + + const html = ` + + + + + Receipt Report ${year} + + + +

Receipt Report โ€” ${year}

+
${escapeHtml(owner)} ยท ${receipts.length} receipts ยท ${formatMoney(total, currency)} total
+ + + + + + + + + + + + + ${receipts + .map( + (r) => ` + + + + + + + + + ` + ) + .join('')} + + + + + + +
DateVendorDescriptionCategoryAmountNotes
${escapeHtml(r.date)}${escapeHtml(r.vendor)}${escapeHtml(r.title)}${getCategoryLabel(r.category)}${formatMoney(r.amount, r.currency)}${escapeHtml(r.notes)}
Total${formatMoney(total, currency)}
+ + + + `; + + const printWindow = window.open('', '_blank'); + if (!printWindow) { + showToast('Popup blocked. Allow popups for PDF export.'); + return; + } + printWindow.document.write(html); + printWindow.document.close(); + printWindow.focus(); + setTimeout(() => { + printWindow.print(); + }, 300); + showToast('PDF print dialog opened'); +} + +function formatMoney(amount: number, currency: string): string { + const map: Record = { USD: '$', EUR: 'โ‚ฌ', GBP: 'ยฃ', CAD: 'C$', AUD: 'A$' }; + const symbol = map[currency] || currency + ' '; + return symbol + amount.toFixed(2); +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function showToast(message: string) { + toast.textContent = message; + toast.classList.remove('hidden'); + setTimeout(() => toast.classList.add('hidden'), 2500); +} + +init(); diff --git a/src/popup/popup.css b/src/popup/popup.css new file mode 100644 index 0000000..632bb71 --- /dev/null +++ b/src/popup/popup.css @@ -0,0 +1,421 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + font-size: 14px; + line-height: 1.4; + color: #1a1a2e; + background: #f8f9fa; + width: 380px; + min-height: 520px; +} + +#app { + padding: 12px; +} + +.header { + text-align: center; + margin-bottom: 12px; +} + +.header h1 { + font-size: 18px; + font-weight: 700; + color: #16213e; +} + +.subtitle { + font-size: 12px; + color: #6c757d; +} + +/* Tabs */ +.tabs { + display: flex; + gap: 4px; + margin-bottom: 12px; + background: #e9ecef; + border-radius: 8px; + padding: 3px; +} + +.tab-btn { + flex: 1; + padding: 8px 4px; + border: none; + background: transparent; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + color: #495057; + cursor: pointer; + transition: all 0.15s; +} + +.tab-btn:hover { + background: rgba(255,255,255,0.5); +} + +.tab-btn.active { + background: #fff; + color: #0f3460; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); +} + +/* Panels */ +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; +} + +/* Form */ +.form-group { + margin-bottom: 10px; +} + +.form-row { + display: flex; + gap: 8px; +} + +.form-row .form-group { + flex: 1; +} + +label { + display: block; + font-size: 12px; + font-weight: 600; + color: #343a40; + margin-bottom: 4px; +} + +input, select { + width: 100%; + padding: 8px 10px; + border: 1px solid #ced4da; + border-radius: 6px; + font-size: 13px; + background: #fff; + color: #212529; +} + +input:focus, select:focus { + outline: none; + border-color: #0f3460; + box-shadow: 0 0 0 2px rgba(15,52,96,0.1); +} + +.upload-area { + border: 2px dashed #ced4da; + border-radius: 8px; + padding: 16px; + text-align: center; + position: relative; + cursor: pointer; + transition: border-color 0.15s; +} + +.upload-area:hover { + border-color: #0f3460; +} + +.upload-area input[type="file"] { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.upload-hint { + font-size: 12px; + color: #6c757d; +} + +.image-preview { + max-width: 100%; + max-height: 120px; + margin-top: 8px; + border-radius: 6px; + display: block; +} + +.image-preview.hidden { + display: none; +} + +/* Buttons */ +.btn-primary { + width: 100%; + padding: 10px; + border: none; + border-radius: 8px; + background: #0f3460; + color: #fff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} + +.btn-primary:hover { + background: #16213e; +} + +.btn-secondary { + padding: 8px 14px; + border: 1px solid #ced4da; + border-radius: 6px; + background: #fff; + color: #343a40; + font-size: 12px; + font-weight: 500; + cursor: pointer; +} + +.btn-secondary:hover { + background: #f8f9fa; +} + +.btn-ghost { + padding: 8px 14px; + border: none; + border-radius: 6px; + background: transparent; + color: #6c757d; + font-size: 12px; + font-weight: 500; + cursor: pointer; +} + +.btn-ghost:hover { + color: #343a40; +} + +.btn-delete { + padding: 4px 8px; + border: none; + background: transparent; + cursor: pointer; + font-size: 14px; + opacity: 0.6; +} + +.btn-delete:hover { + opacity: 1; +} + +.btn-import { + margin-top: 6px; + padding: 6px 12px; + border: 1px solid #0f3460; + border-radius: 6px; + background: #fff; + color: #0f3460; + font-size: 12px; + font-weight: 500; + cursor: pointer; +} + +.btn-import:hover { + background: #0f3460; + color: #fff; +} + +/* Receipts list */ +.toolbar { + margin-bottom: 10px; +} + +.search-box input { + padding: 8px 10px; + font-size: 13px; +} + +.summary { + font-size: 12px; + color: #6c757d; + margin-top: 6px; + text-align: right; +} + +.receipts-list { + max-height: 300px; + overflow-y: auto; +} + +.empty-state { + text-align: center; + padding: 32px 16px; + color: #6c757d; +} + +.empty-icon { + font-size: 36px; + margin-bottom: 8px; +} + +.empty-hint { + font-size: 12px; + margin-top: 4px; +} + +.receipt-card { + background: #fff; + border-radius: 8px; + padding: 10px; + margin-bottom: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.receipt-main { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.receipt-icon { + font-size: 20px; + flex-shrink: 0; +} + +.receipt-info { + flex: 1; + min-width: 0; +} + +.receipt-title { + font-weight: 600; + font-size: 13px; + color: #212529; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.receipt-meta { + font-size: 11px; + color: #6c757d; + margin-top: 1px; +} + +.receipt-notes { + font-size: 11px; + color: #868e96; + margin-top: 2px; + font-style: italic; +} + +.receipt-amount { + font-weight: 700; + font-size: 14px; + color: #0f3460; + flex-shrink: 0; +} + +.receipt-thumb { + max-width: 100%; + max-height: 80px; + margin-top: 6px; + border-radius: 4px; +} + +.receipt-actions { + display: flex; + justify-content: flex-end; + margin-top: 4px; +} + +/* Email tab */ +.email-intro { + font-size: 13px; + color: #495057; + margin-bottom: 12px; + padding: 10px; + background: #e7f3ff; + border-radius: 8px; +} + +.email-results { + margin-top: 12px; +} + +.email-item { + background: #fff; + border-radius: 8px; + padding: 10px; + margin-bottom: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.email-subject { + font-weight: 600; + font-size: 13px; + color: #212529; +} + +.email-meta { + font-size: 11px; + color: #6c757d; + margin-top: 2px; +} + +.email-amount { + font-weight: 700; + font-size: 14px; + color: #0f3460; + margin-top: 4px; +} + +.email-empty { + text-align: center; + padding: 20px; + color: #6c757d; + font-size: 13px; +} + +.loading { + text-align: center; + padding: 20px; + color: #0f3460; + font-size: 13px; +} + +/* Export bar */ +.export-bar { + display: flex; + gap: 6px; + justify-content: center; + margin-top: 12px; + padding-top: 10px; + border-top: 1px solid #e9ecef; +} + +/* Toast */ +.toast { + position: fixed; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + background: #16213e; + color: #fff; + padding: 8px 16px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + z-index: 100; + transition: opacity 0.3s; +} + +.toast.hidden { + opacity: 0; + pointer-events: none; +} diff --git a/src/popup/popup.html b/src/popup/popup.html new file mode 100644 index 0000000..021ab12 --- /dev/null +++ b/src/popup/popup.html @@ -0,0 +1,107 @@ + + + + + + SimpleScan Solo + + + +
+
+

๐Ÿ“ธ SimpleScan Solo

+

Freelancer receipts, simplified

+
+ + + + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ +
๐Ÿ“Ž Click to upload or drag image
+ +
+
+ +
+
+ + +
+
+ +
+ 0 receipts ยท $0.00 +
+
+
+
+ + + +
+
+ + +
+ + + +
+ + +
+ + + + diff --git a/src/popup/popup.ts b/src/popup/popup.ts new file mode 100644 index 0000000..4fb7b7a --- /dev/null +++ b/src/popup/popup.ts @@ -0,0 +1,286 @@ +import { getReceipts, saveReceipt, deleteReceipt } from '../utils/storage.js'; +import { TAX_CATEGORIES, getCategoryLabel, getCategoryIcon } from '../utils/categories.js'; +import { exportToCsv, downloadBlob } from '../utils/export.js'; +import type { Receipt } from '../types.js'; + +// --- DOM refs --- +const tabButtons = document.querySelectorAll('.tab-btn'); +const panels = document.querySelectorAll('.tab-panel'); +const form = document.getElementById('receipt-form') as HTMLFormElement; +const categorySelect = document.getElementById('r-category') as HTMLSelectElement; +const imageInput = document.getElementById('r-image') as HTMLInputElement; +const imagePreview = document.getElementById('image-preview') as HTMLImageElement; +const receiptsList = document.getElementById('receipts-list') as HTMLDivElement; +const searchInput = document.getElementById('search-input') as HTMLInputElement; +const receiptCount = document.getElementById('receipt-count') as HTMLSpanElement; +const totalAmount = document.getElementById('total-amount') as HTMLSpanElement; +const scanEmailBtn = document.getElementById('scan-email') as HTMLButtonElement; +const emailResults = document.getElementById('email-results') as HTMLDivElement; +const toast = document.getElementById('toast') as HTMLDivElement; + +let allReceipts: Receipt[] = []; +let imageDataUrl = ''; + +// --- Init --- +function init() { + // Populate categories + categorySelect.innerHTML = TAX_CATEGORIES.map( + (c) => `` + ).join(''); + + // Default date to today + (document.getElementById('r-date') as HTMLInputElement).valueAsDate = new Date(); + + loadReceipts(); + setupTabs(); + setupForm(); + setupSearch(); + setupExport(); + setupEmailScan(); + setupImageUpload(); +} + +// --- Tabs --- +function setupTabs() { + tabButtons.forEach((tab) => { + tab.addEventListener('click', () => { + const target = tab.dataset.tab!; + tabButtons.forEach((t) => t.classList.toggle('active', t.dataset.tab === target)); + panels.forEach((p) => p.classList.toggle('active', p.id === `tab-${target}`)); + if (target === 'list') loadReceipts(); + }); + }); +} + +// --- Form --- +function setupForm() { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const receipt: Receipt = { + id: crypto.randomUUID(), + title: (document.getElementById('r-title') as HTMLInputElement).value.trim(), + amount: parseFloat((document.getElementById('r-amount') as HTMLInputElement).value), + currency: (document.getElementById('r-currency') as HTMLSelectElement).value, + category: (document.getElementById('r-category') as HTMLSelectElement).value as Receipt['category'], + date: (document.getElementById('r-date') as HTMLInputElement).value, + vendor: (document.getElementById('r-vendor') as HTMLInputElement).value.trim(), + notes: (document.getElementById('r-notes') as HTMLInputElement).value.trim(), + imageDataUrl: imageDataUrl || undefined, + source: imageDataUrl ? 'upload' : 'manual', + createdAt: new Date().toISOString(), + }; + await saveReceipt(receipt); + showToast('Receipt saved!'); + form.reset(); + imageDataUrl = ''; + imagePreview.classList.add('hidden'); + imagePreview.src = ''; + (document.getElementById('r-date') as HTMLInputElement).valueAsDate = new Date(); + // Switch to list tab + tabButtons[1].click(); + }); +} + +// --- Image upload --- +function setupImageUpload() { + imageInput.addEventListener('change', () => { + const file = imageInput.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + imageDataUrl = reader.result as string; + imagePreview.src = imageDataUrl; + imagePreview.classList.remove('hidden'); + }; + reader.readAsDataURL(file); + }); +} + +// --- Receipts list --- +async function loadReceipts() { + allReceipts = await getReceipts(); + renderReceipts(allReceipts); +} + +function renderReceipts(receipts: Receipt[]) { + if (receipts.length === 0) { + receiptsList.innerHTML = ` +
+
๐Ÿ“ญ
+

No receipts yet.

+

Add your first receipt in the "Add" tab.

+
+ `; + receiptCount.textContent = '0'; + totalAmount.textContent = '$0.00'; + return; + } + + const total = receipts.reduce((sum, r) => sum + r.amount, 0); + const currency = receipts[0]?.currency || 'USD'; + receiptCount.textContent = String(receipts.length); + totalAmount.textContent = formatMoney(total, currency); + + receiptsList.innerHTML = receipts + .map( + (r) => ` +
+
+
${getCategoryIcon(r.category)}
+
+
${escapeHtml(r.title)}
+
+ ${formatDate(r.date)} ยท ${escapeHtml(r.vendor)} ยท ${getCategoryLabel(r.category)} +
+ ${r.notes ? `
${escapeHtml(r.notes)}
` : ''} +
+
${formatMoney(r.amount, r.currency)}
+
+ ${r.imageDataUrl ? `Receipt` : ''} +
+ +
+
+ ` + ) + .join(''); + + receiptsList.querySelectorAll('.btn-delete').forEach((btn) => { + btn.addEventListener('click', async (e) => { + const id = (e.currentTarget as HTMLButtonElement).dataset.id!; + await deleteReceipt(id); + await loadReceipts(); + showToast('Receipt deleted'); + }); + }); +} + +function setupSearch() { + searchInput.addEventListener('input', () => { + const q = searchInput.value.toLowerCase().trim(); + if (!q) { + renderReceipts(allReceipts); + return; + } + const filtered = allReceipts.filter( + (r) => + r.title.toLowerCase().includes(q) || + r.vendor.toLowerCase().includes(q) || + r.notes.toLowerCase().includes(q) || + getCategoryLabel(r.category).toLowerCase().includes(q) + ); + renderReceipts(filtered); + }); +} + +// --- Export --- +function setupExport() { + document.getElementById('export-csv')!.addEventListener('click', async () => { + const receipts = await getReceipts(); + if (receipts.length === 0) { + showToast('No receipts to export'); + return; + } + const csv = exportToCsv(receipts); + const year = new Date().getFullYear(); + downloadBlob(csv, `simplescan-receipts-${year}.csv`, 'text/csv'); + showToast('CSV downloaded'); + }); + + document.getElementById('export-pdf')!.addEventListener('click', async () => { + const receipts = await getReceipts(); + if (receipts.length === 0) { + showToast('No receipts to export'); + return; + } + // PDF export via background service worker (offscreen or simple approach) + // For popup, we'll open options page with receipts pre-loaded + chrome.runtime.openOptionsPage(); + showToast('Open Settings tab for PDF export'); + }); + + document.getElementById('open-options')!.addEventListener('click', () => { + chrome.runtime.openOptionsPage(); + }); +} + +// --- Email scan --- +function setupEmailScan() { + scanEmailBtn.addEventListener('click', async () => { + emailResults.innerHTML = '
๐Ÿ” Scanning open email tabs...
'; + try { + const tabs = await chrome.tabs.query({ url: ['*://mail.google.com/*', '*://outlook.live.com/*', '*://outlook.office.com/*', '*://outlook.office365.com/*'] }); + if (tabs.length === 0) { + emailResults.innerHTML = ''; + return; + } + const results: { receipts: { subject: string; sender: string; amount?: number; vendor?: string; date: string }[] } = await chrome.tabs.sendMessage(tabs[0].id!, { action: 'scanEmails' }); + if (!results || !results.receipts || results.receipts.length === 0) { + emailResults.innerHTML = ''; + return; + } + emailResults.innerHTML = results.receipts + .map( + (r, i) => ` + + ` + ) + .join(''); + + emailResults.querySelectorAll('.btn-import').forEach((btn) => { + btn.addEventListener('click', async (e) => { + const idx = parseInt((e.currentTarget as HTMLButtonElement).dataset.index!, 10); + const er = results.receipts[idx]; + const receipt: Receipt = { + id: crypto.randomUUID(), + title: er.subject, + amount: er.amount || 0, + currency: 'USD', + category: 'software', + date: new Date().toISOString().split('T')[0], + vendor: er.vendor || er.sender, + notes: `Imported from email: ${er.subject}`, + source: 'email', + createdAt: new Date().toISOString(), + }; + await saveReceipt(receipt); + showToast('Receipt imported from email'); + tabButtons[1].click(); + }); + }); + } catch (err) { + emailResults.innerHTML = ``; + } + }); +} + +// --- Helpers --- +function formatMoney(amount: number, currency: string): string { + const map: Record = { USD: '$', EUR: 'โ‚ฌ', GBP: 'ยฃ', CAD: 'C$', AUD: 'A$' }; + const symbol = map[currency] || currency + ' '; + return symbol + amount.toFixed(2); +} + +function formatDate(iso: string): string { + const d = new Date(iso + 'T00:00:00'); + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function showToast(message: string) { + toast.textContent = message; + toast.classList.remove('hidden'); + setTimeout(() => toast.classList.add('hidden'), 2500); +} + +init(); diff --git a/src/test/simplescan.test.ts b/src/test/simplescan.test.ts new file mode 100644 index 0000000..05815a8 --- /dev/null +++ b/src/test/simplescan.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; + +// Test categories +import { TAX_CATEGORIES, getCategoryLabel, getCategoryIcon } from '../utils/categories.js'; + +describe('categories', () => { + it('has 10 tax categories', () => { + assert.strictEqual(TAX_CATEGORIES.length, 10); + }); + + it('gets correct label', () => { + assert.strictEqual(getCategoryLabel('software'), 'Software'); + assert.strictEqual(getCategoryLabel('equipment'), 'Equipment'); + assert.strictEqual(getCategoryLabel('nonexistent'), 'Other'); + }); + + it('gets correct icon', () => { + assert.strictEqual(getCategoryIcon('software'), 'โš™๏ธ'); + assert.strictEqual(getCategoryIcon('travel'), 'โœˆ๏ธ'); + assert.strictEqual(getCategoryIcon('nonexistent'), '๐Ÿ“ฆ'); + }); +}); + +// Test export +import { exportToCsv } from '../utils/export.js'; +import type { Receipt } from '../types.js'; + +describe('export', () => { + it('exports receipts to CSV', () => { + const receipts: Receipt[] = [ + { + id: '1', + title: 'Adobe CC', + amount: 59.99, + currency: 'USD', + category: 'software', + date: '2026-06-01', + vendor: 'Adobe', + notes: 'Monthly', + source: 'manual', + createdAt: '2026-06-01T00:00:00Z', + }, + ]; + const csv = exportToCsv(receipts); + console.log('CSV output:', csv); + assert.ok(csv.includes('Date'), 'CSV should have Date header'); + assert.ok(csv.includes('Adobe CC'), 'CSV should have title'); + assert.ok(csv.includes('59.99'), 'CSV should have amount'); + assert.ok(csv.includes('Software'), 'CSV should have category label'); + }); + + it('escapes quotes in CSV', () => { + const receipts: Receipt[] = [ + { + id: '1', + title: 'My "Special" Item', + amount: 10, + currency: 'USD', + category: 'other', + date: '2026-06-01', + vendor: 'Vendor', + notes: '', + source: 'manual', + createdAt: '2026-06-01T00:00:00Z', + }, + ]; + const csv = exportToCsv(receipts); + assert.ok(csv.includes('""'), 'CSV should escape quotes'); + }); +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..29225d2 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,53 @@ +export interface Receipt { + id: string; + title: string; + amount: number; + currency: string; + category: TaxCategory; + date: string; // ISO date + vendor: string; + notes: string; + imageDataUrl?: string; + source: 'manual' | 'email' | 'upload' | 'camera'; + createdAt: string; +} + +export type TaxCategory = + | 'equipment' + | 'home_office' + | 'software' + | 'internet_phone' + | 'travel' + | 'meals' + | 'professional_services' + | 'marketing' + | 'education' + | 'other'; + +export interface CategoryInfo { + id: TaxCategory; + label: string; + description: string; + icon: string; +} + +export interface EmailReceiptHint { + subject: string; + sender: string; + date: string; + amount?: number; + currency?: string; + vendor?: string; +} + +export interface AppSettings { + defaultCurrency: string; + taxYear: string; + ownerName: string; +} + +export const DEFAULT_SETTINGS: AppSettings = { + defaultCurrency: 'USD', + taxYear: new Date().getFullYear().toString(), + ownerName: '', +}; diff --git a/src/utils/categories.ts b/src/utils/categories.ts new file mode 100644 index 0000000..f090542 --- /dev/null +++ b/src/utils/categories.ts @@ -0,0 +1,72 @@ +import type { CategoryInfo } from '../types.js'; + +export const TAX_CATEGORIES: CategoryInfo[] = [ + { + id: 'equipment', + label: 'Equipment', + description: 'Computers, cameras, tools, hardware', + icon: '๐Ÿ’ป', + }, + { + id: 'home_office', + label: 'Home Office', + description: 'Desk, chair, lighting, office supplies', + icon: '๐Ÿ ', + }, + { + id: 'software', + label: 'Software', + description: 'Apps, subscriptions, SaaS tools', + icon: 'โš™๏ธ', + }, + { + id: 'internet_phone', + label: 'Internet & Phone', + description: 'ISP, cell plan, domain hosting', + icon: '๐Ÿ“ก', + }, + { + id: 'travel', + label: 'Travel', + description: 'Flights, hotels, rideshare, mileage', + icon: 'โœˆ๏ธ', + }, + { + id: 'meals', + label: 'Meals', + description: 'Business meals (50% deductible)', + icon: '๐Ÿฝ๏ธ', + }, + { + id: 'professional_services', + label: 'Professional Services', + description: 'Lawyer, accountant, contractor payments', + icon: '๐Ÿ“‹', + }, + { + id: 'marketing', + label: 'Marketing', + description: 'Ads, design, copywriting, PR', + icon: '๐Ÿ“ข', + }, + { + id: 'education', + label: 'Education', + description: 'Courses, books, conferences', + icon: '๐Ÿ“š', + }, + { + id: 'other', + label: 'Other', + description: 'Miscellaneous business expenses', + icon: '๐Ÿ“ฆ', + }, +]; + +export function getCategoryLabel(id: string): string { + return TAX_CATEGORIES.find((c) => c.id === id)?.label || 'Other'; +} + +export function getCategoryIcon(id: string): string { + return TAX_CATEGORIES.find((c) => c.id === id)?.icon || '๐Ÿ“ฆ'; +} diff --git a/src/utils/export.ts b/src/utils/export.ts new file mode 100644 index 0000000..2e2993b --- /dev/null +++ b/src/utils/export.ts @@ -0,0 +1,37 @@ +import type { Receipt } from '../types.js'; +import { getCategoryLabel } from './categories.js'; + +export function exportToCsv(receipts: Receipt[]): string { + const headers = ['Date', 'Vendor', 'Title', 'Category', 'Amount', 'Currency', 'Notes', 'Source']; + const rows = receipts.map((r) => [ + r.date, + r.vendor, + r.title, + getCategoryLabel(r.category), + r.amount.toFixed(2), + r.currency, + r.notes, + r.source, + ]); + const csv = [headers, ...rows] + .map((row) => + row + .map((cell) => { + const str = String(cell).replace(/"/g, '""'); + return `"${str}"`; + }) + .join(',') + ) + .join('\n'); + return csv; +} + +export function downloadBlob(content: BlobPart, filename: string, mimeType: string): void { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..4a90aec --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,40 @@ +import type { Receipt, AppSettings } from '../types.js'; +import { DEFAULT_SETTINGS } from '../types.js'; + +const RECEIPTS_KEY = 'simplescan_receipts'; +const SETTINGS_KEY = 'simplescan_settings'; + +export async function getReceipts(): Promise { + const result = await chrome.storage.local.get(RECEIPTS_KEY); + return (result[RECEIPTS_KEY] as Receipt[]) || []; +} + +export async function saveReceipt(receipt: Receipt): Promise { + const receipts = await getReceipts(); + const existingIndex = receipts.findIndex((r) => r.id === receipt.id); + if (existingIndex >= 0) { + receipts[existingIndex] = receipt; + } else { + receipts.unshift(receipt); + } + await chrome.storage.local.set({ [RECEIPTS_KEY]: receipts }); +} + +export async function deleteReceipt(id: string): Promise { + const receipts = await getReceipts(); + const filtered = receipts.filter((r) => r.id !== id); + await chrome.storage.local.set({ [RECEIPTS_KEY]: filtered }); +} + +export async function getSettings(): Promise { + const result = await chrome.storage.local.get(SETTINGS_KEY); + return (result[SETTINGS_KEY] as AppSettings) || { ...DEFAULT_SETTINGS }; +} + +export async function saveSettings(settings: AppSettings): Promise { + await chrome.storage.local.set({ [SETTINGS_KEY]: settings }); +} + +export async function clearAllReceipts(): Promise { + await chrome.storage.local.remove(RECEIPTS_KEY); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..11bf0f0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["chrome"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/test"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..5ff0f2a --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist-test" + }, + "include": ["src/test/**/*.ts", "src/utils/**/*.ts", "src/types.ts"], + "exclude": ["src/popup", "src/content", "src/background.ts", "src/options.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6dddc52 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import { crx } from '@crxjs/vite-plugin' +import manifest from './src/manifest.json' with { type: 'json' } + +export default defineConfig({ + build: { + rollupOptions: { + input: { + options: 'options.html', + }, + }, + }, + plugins: [crx({ manifest })], +})