commit 6cdc79e345d37960a0219d755b8d95f07dd06ae7 Author: Aditya Gupta Date: Tue Mar 3 23:11:08 2026 +0530 feat: Implement Gogh Theme Engine with options page, theme parsing, and semantic theme derivation We're cooked. - Added options page for YAML palette editing, live color preview, and diagnostics. - Implemented theme engine to parse Gogh YAML palettes and derive semantic themes. - Configured TypeScript and Vite for building the extension. - Created new tab and popup HTML pages with corresponding scripts and styles. - Established storage management for user configurations in Chrome storage. - Added icons for the extension and updated manifest for MV3 compatibility. diff --git a/background/index.js b/background/index.js new file mode 100644 index 0000000..63caf8d --- /dev/null +++ b/background/index.js @@ -0,0 +1,22 @@ +chrome.runtime.onInstalled.addListener(function (object) { + if (object.reason === chrome.runtime.OnInstalledReason.INSTALL) { + chrome.tabs.create({ url: "https://clydedsouza.net" }); + } +}); + +chrome.commands.onCommand.addListener((command) => { + if(command === 'turn-on') { + chrome.action.setBadgeText({ text: 'ON' }); + chrome.action.setBadgeBackgroundColor({ color: 'green' }); + } + if(command === 'turn-off') { + chrome.action.setBadgeText({ text: 'OFF' }); + chrome.action.setBadgeBackgroundColor({ color: 'red' }); + } +}); + +chrome.omnibox.onInputEntered.addListener((text) => { + chrome.tabs.create({ url: 'https://facebook.com/' + encodeURIComponent(text) }); + chrome.tabs.create({ url: 'https://twitter.com/' + encodeURIComponent(text) }); + chrome.tabs.create({ url: 'https://instagram.com/' + encodeURIComponent(text) }); +}); \ No newline at end of file diff --git a/common/settings.js b/common/settings.js new file mode 100644 index 0000000..ba9904f --- /dev/null +++ b/common/settings.js @@ -0,0 +1,5 @@ +export const CHROME_SYNC_STORAGE_KEY = "chrome-extension-template"; + +export const PRESET_CONFIGURATION = { + "storageValue": "https://clydedsouza.net", +}; diff --git a/common/settings.test.js b/common/settings.test.js new file mode 100644 index 0000000..aad8a77 --- /dev/null +++ b/common/settings.test.js @@ -0,0 +1,10 @@ +import * as settingsModule from "./settings"; + +describe("settings", () => { + test("has a preset configuration key", () => { + const presetConfig = settingsModule.PRESET_CONFIGURATION; + const keys = Object.keys(presetConfig); + expect(keys.length).toBe(1); + expect(presetConfig["storageValue"]).toBe("https://clydedsouza.net"); + }); +}); diff --git a/common/storage.js b/common/storage.js new file mode 100644 index 0000000..239aa85 --- /dev/null +++ b/common/storage.js @@ -0,0 +1,9 @@ +export const getStorage = (componentKey, callback) => { + chrome.storage.sync.get([componentKey], function(result) { + callback(result[componentKey]); + }); +}; + +export const setStorage = (componentKey, value) => { + chrome.storage.sync.set({[componentKey]: value}); +}; \ No newline at end of file diff --git a/common/storage.test.js b/common/storage.test.js new file mode 100644 index 0000000..656bf98 --- /dev/null +++ b/common/storage.test.js @@ -0,0 +1,42 @@ +import * as storageModule from "./storage"; + +const get = jest.fn(); +const set = jest.fn(); + +global.chrome = { + storage: { + sync: { + set, + get + } + } +}; + +const chromeGetStorageSpy = jest.spyOn(chrome.storage.sync, 'get'); +const chromeSetStorageSpy = jest.spyOn(chrome.storage.sync, 'set'); + +describe("storage → getStorage", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("chrome get storage api method called", () => { + const getCallback = jest.fn(); + + storageModule.getStorage("Test", getCallback); + + expect(chromeGetStorageSpy).toHaveBeenCalledWith(["Test"], expect.any(Function)); + }); +}); + +describe("storage → setStorage", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("chrome set storage api method called", () => { + storageModule.setStorage("Test", "Value"); + + expect(chromeSetStorageSpy).toHaveBeenCalledWith({["Test"]: "Value"}); + }); +}); diff --git a/content_scripts/index.js b/content_scripts/index.js new file mode 100644 index 0000000..b50340f --- /dev/null +++ b/content_scripts/index.js @@ -0,0 +1,13 @@ +import { getStorage } from "../common/storage"; +import { CHROME_SYNC_STORAGE_KEY, PRESET_CONFIGURATION } from "../common/settings"; + +function loadAndDisplayStorageValue(result) { + const savedConfiguration = result || PRESET_CONFIGURATION; + const storageValue = savedConfiguration["storageValue"]; + console.info("Storage value is", storageValue); +} + +window.onload = function() { + console.info("Content script loaded"); + getStorage(CHROME_SYNC_STORAGE_KEY, loadAndDisplayStorageValue); +} diff --git a/gogh-theme-engine/.gitignore b/gogh-theme-engine/.gitignore new file mode 100644 index 0000000..f4e2c6d --- /dev/null +++ b/gogh-theme-engine/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/gogh-theme-engine/README.md b/gogh-theme-engine/README.md new file mode 100644 index 0000000..9fb9fac --- /dev/null +++ b/gogh-theme-engine/README.md @@ -0,0 +1,130 @@ +# Gogh Theme Engine + +A production-grade Chrome Extension (Manifest V3) that applies Gogh-style 16-color terminal palettes to all web pages by intelligently computing and rewriting styles at runtime using perceptual OKLCH color remapping. + +## Features + +- **OKLCH Perceptual Color Model** — All color math uses OKLCH for perceptually uniform transformations +- **Semantic Theme Derivation** — Automatically maps 16 terminal colors to semantic roles (error, success, accent, etc.) using hue + brightness analysis +- **Smart DOM Processing** — TreeWalker-based traversal, MutationObserver with batched/debounced updates, Shadow DOM support +- **WCAG Contrast Enforcement** — Ensures minimum 4.5:1 contrast ratio for text +- **Gradient & SVG Support** — Parses and transforms CSS gradients and inline SVG fill/stroke +- **Dynamic CSS Class Generation** — Reuses CSS classes instead of inline styles for performance +- **Per-site Toggle** — Enable/disable theming on a per-domain basis +- **Intensity Slider** — Blend between original and themed colors (0–100%) +- **Options Page** — YAML editor, live preview, contrast diagnostics, site management + +## Build Instructions + +### Prerequisites + +- Node.js ≥ 18 +- npm ≥ 9 + +### Setup + +```bash +cd gogh-theme-engine +npm install +``` + +### Development Build (with watch) + +```bash +npm run dev +``` + +### Production Build + +```bash +npm run build +``` + +### Load in Chrome + +1. Run `npm run build` +2. Open `chrome://extensions/` +3. Enable **Developer mode** +4. Click **Load unpacked** +5. Select the `dist/` folder + +## Project Structure + +``` +src/ +├── background.ts # Service worker — storage, per-domain toggle, messaging +├── content.ts # Content script — DOM processing, style injection, coordination +├── color.ts # OKLCH color engine — all color math and transforms +├── themeEngine.ts # YAML parsing, validation, semantic theme derivation +├── observer.ts # DOM observation — TreeWalker, MutationObserver, Shadow DOM +├── js-yaml.d.ts # Type declarations for js-yaml +└── options/ + ├── options.html # Options page UI + └── options.ts # Options page controller + +public/ +├── manifest.json # Manifest V3 configuration +└── icons/ # Extension icons +``` + +## Palette Format + +Provide a YAML palette in Gogh format: + +```yaml +name: My Theme +author: Your Name +variant: dark +color_01: "#282a36" +color_02: "#ff5555" +color_03: "#50fa7b" +color_04: "#f1fa8c" +color_05: "#bd93f9" +color_06: "#ff79c6" +color_07: "#8be9fd" +color_08: "#f8f8f2" +color_09: "#44475a" +color_10: "#ff6e6e" +color_11: "#69ff94" +color_12: "#ffffa5" +color_13: "#d6acff" +color_14: "#ff92df" +color_15: "#a4ffff" +color_16: "#ffffff" +background: "#282a36" +foreground: "#f8f8f2" +cursor: "#f8f8f2" +``` + +## How It Works + +### Color Pipeline + +1. **Parse** — CSS color strings are parsed into RGBA +2. **Convert** — RGBA → sRGB → Linear RGB → OKLAB → OKLCH +3. **Classify** — Colors are classified as neutral (achromatic) or chromatic with a hue bucket +4. **Remap** — Neutral colors map to surface/background bands; chromatic colors map to semantic roles +5. **Contrast** — WCAG contrast is enforced by adjusting the L (lightness) channel +6. **Blend** — Final color is blended with original based on intensity setting +7. **Cache** — Results are memoized by color + context key + +### Theme Derivation + +Instead of fixed index mapping, semantic roles are derived by analyzing each palette color's OKLCH properties: +- Red hue range → `error` +- Green → `success` +- Blue/Cyan → `accentPrimary` / `info` +- Yellow/Orange → `warning` +- Purple → `accentSecondary` +- Low chroma → `muted` +- Surface/border colors are synthesized from background by shifting lightness + +## Architecture Notes + +- No naive CSS inversion or global filters +- `TreeWalker` for initial traversal (faster than `querySelectorAll`) +- `MutationObserver` with `requestIdleCallback` batching +- `WeakSet` for tracking processed nodes +- `Element.prototype.attachShadow` monkey-patching for shadow DOM +- Dynamic CSS class generation with reuse to minimize inline styles +- Transform results cached with LRU-like eviction (max 8192 entries) diff --git a/gogh-theme-engine/package-lock.json b/gogh-theme-engine/package-lock.json new file mode 100644 index 0000000..cee1bbe --- /dev/null +++ b/gogh-theme-engine/package-lock.json @@ -0,0 +1,2286 @@ +{ + "name": "gogh-theme-engine", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gogh-theme-engine", + "version": "1.0.0", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.25", + "@types/chrome": "^0.0.268", + "@types/node": "^25.3.3", + "sass": "^1.77.0", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.3.0.tgz", + "integrity": "sha512-+0CNVGS4bB30OoaF1vUsHVwWU1Lm7MxI0XWY9Fd/Ob+ZVTZgEFNqJ1ZC69IVwQsoYhY0sMQLvpLWiFIuDz8htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.1.2", + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "cheerio": "^1.0.0-rc.10", + "convert-source-map": "^1.7.0", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fast-glob": "^3.2.11", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.30.12", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "react-refresh": "^0.13.0", + "rollup": "2.79.2", + "rxjs": "7.5.7" + } + }, + "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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "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.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "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.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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/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/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?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/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "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/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.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/gogh-theme-engine/package.json b/gogh-theme-engine/package.json new file mode 100644 index 0000000..c644e5a --- /dev/null +++ b/gogh-theme-engine/package.json @@ -0,0 +1,23 @@ +{ + "name": "gogh-theme-engine", + "version": "1.0.0", + "description": "Production-grade Chrome Extension that applies Gogh-style 16-color terminal palettes to web pages using perceptual color remapping (OKLCH).", + "private": true, + "type": "module", + "scripts": { + "dev": "vite build --watch --mode development", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.25", + "@types/chrome": "^0.0.268", + "@types/node": "^25.3.3", + "sass": "^1.77.0", + "typescript": "^5.4.5", + "vite": "^5.2.11" + }, + "dependencies": { + "js-yaml": "^4.1.0" + } +} diff --git a/gogh-theme-engine/postcss.config.cjs b/gogh-theme-engine/postcss.config.cjs new file mode 100644 index 0000000..9e5ab50 --- /dev/null +++ b/gogh-theme-engine/postcss.config.cjs @@ -0,0 +1,2 @@ +// No PostCSS plugins needed for this project +module.exports = {}; diff --git a/gogh-theme-engine/public/icons/icon.svg b/gogh-theme-engine/public/icons/icon.svg new file mode 100644 index 0000000..9953ebc --- /dev/null +++ b/gogh-theme-engine/public/icons/icon.svg @@ -0,0 +1,6 @@ + + + G + + + diff --git a/gogh-theme-engine/public/icons/icon128.png b/gogh-theme-engine/public/icons/icon128.png new file mode 100644 index 0000000..a3f59b8 Binary files /dev/null and b/gogh-theme-engine/public/icons/icon128.png differ diff --git a/gogh-theme-engine/public/icons/icon16.png b/gogh-theme-engine/public/icons/icon16.png new file mode 100644 index 0000000..1ba7efd Binary files /dev/null and b/gogh-theme-engine/public/icons/icon16.png differ diff --git a/gogh-theme-engine/public/icons/icon48.png b/gogh-theme-engine/public/icons/icon48.png new file mode 100644 index 0000000..a067eca Binary files /dev/null and b/gogh-theme-engine/public/icons/icon48.png differ diff --git a/gogh-theme-engine/public/manifest.json b/gogh-theme-engine/public/manifest.json new file mode 100644 index 0000000..33ac9ae --- /dev/null +++ b/gogh-theme-engine/public/manifest.json @@ -0,0 +1,33 @@ +{ + "manifest_version": 3, + "name": "Gogh Theme Engine", + "version": "1.0.0", + "description": "Apply Gogh-style 16-color terminal palettes to any web page using perceptual OKLCH color remapping.", + "permissions": ["storage", "activeTab", "scripting"], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_start", + "all_frames": true + } + ], + "options_page": "options/options.html", + "action": { + "default_title": "Gogh Theme Engine", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} diff --git a/gogh-theme-engine/src/background.ts b/gogh-theme-engine/src/background.ts new file mode 100644 index 0000000..496dc38 --- /dev/null +++ b/gogh-theme-engine/src/background.ts @@ -0,0 +1,183 @@ +/** + * background.ts — Manifest V3 service worker. + * + * Responsibilities: + * • Store palette in chrome.storage.sync + * • Toggle enable/disable per domain + * • Handle action button clicks (quick toggle) + * • Manage default settings on install + * • Relay messages between popup/options and content scripts + */ + +import { DEFAULT_PALETTE_YAML } from './themeEngine'; + +// ─── Installation / Update ──────────────────────────────────────────────────── + +chrome.runtime.onInstalled.addListener(async (details) => { + if (details.reason === 'install') { + // Set default settings + await chrome.storage.sync.set({ + paletteYaml: DEFAULT_PALETTE_YAML, + enabled: true, + intensity: 100, // 0–100 + disabledSites: [] as string[], + }); + console.log('[Gogh Theme Engine] Extension installed with default Dracula palette.'); + } +}); + +// ─── Action Button Click ────────────────────────────────────────────────────── + +/** + * Clicking the extension icon toggles theming on the current tab. + * If the site is in the disabled list, it's removed. If not, it's added. + */ +chrome.action.onClicked.addListener(async (tab) => { + if (!tab.id || !tab.url) return; + + try { + const url = new URL(tab.url); + const hostname = url.hostname; + + if (!hostname) return; + + const data = await chrome.storage.sync.get(['disabledSites', 'enabled']); + const disabledSites: string[] = data.disabledSites || []; + const isGloballyEnabled = data.enabled !== false; + + if (!isGloballyEnabled) { + // If globally disabled, re-enable + await chrome.storage.sync.set({ enabled: true }); + // Notify content script + chrome.tabs.sendMessage(tab.id!, { type: 'reprocess' }).catch(() => {}); + updateBadge(tab.id!, true); + return; + } + + const siteIndex = disabledSites.indexOf(hostname); + if (siteIndex >= 0) { + // Currently disabled for this site → enable + disabledSites.splice(siteIndex, 1); + await chrome.storage.sync.set({ disabledSites }); + // Reload the tab to apply changes cleanly + chrome.tabs.reload(tab.id!); + updateBadge(tab.id!, true); + } else { + // Currently enabled → disable for this site + disabledSites.push(hostname); + await chrome.storage.sync.set({ disabledSites }); + // Notify content script to clean up + chrome.tabs.sendMessage(tab.id!, { type: 'toggle' }).catch(() => { + // If content script isn't responding, reload + chrome.tabs.reload(tab.id!); + }); + updateBadge(tab.id!, false); + } + } catch (e) { + console.error('[Gogh Theme Engine] Action click error:', e); + } +}); + +// ─── Badge ──────────────────────────────────────────────────────────────────── + +function updateBadge(tabId: number, isEnabled: boolean): void { + chrome.action.setBadgeText({ + tabId, + text: isEnabled ? '' : 'OFF', + }); + chrome.action.setBadgeBackgroundColor({ + tabId, + color: isEnabled ? '#50fa7b' : '#ff5555', + }); +} + +// ─── Tab Activation ─────────────────────────────────────────────────────────── + +/** + * When the user switches tabs, update the badge to reflect the current + * site's enable/disable state. + */ +chrome.tabs.onActivated.addListener(async (info) => { + try { + const tab = await chrome.tabs.get(info.tabId); + if (!tab.url) return; + + const url = new URL(tab.url); + const hostname = url.hostname; + + const data = await chrome.storage.sync.get(['disabledSites', 'enabled']); + const disabledSites: string[] = data.disabledSites || []; + const isGloballyEnabled = data.enabled !== false; + const isSiteEnabled = isGloballyEnabled && !disabledSites.includes(hostname); + + updateBadge(info.tabId, isSiteEnabled); + } catch { + // Ignore errors for special pages like chrome:// + } +}); + +// ─── Message Handling ───────────────────────────────────────────────────────── + +/** + * Handle messages from options page or popup. + */ +chrome.runtime.onMessage.addListener( + ( + msg: { type: string; [key: string]: unknown }, + _sender: chrome.runtime.MessageSender, + sendResponse: (resp?: unknown) => void + ): boolean => { + switch (msg.type) { + case 'savePalette': { + const yaml = msg.yaml as string; + chrome.storage.sync.set({ paletteYaml: yaml }).then(() => { + sendResponse({ ok: true }); + }); + return true; // async response + } + + case 'getSettings': { + chrome.storage.sync + .get(['paletteYaml', 'enabled', 'intensity', 'disabledSites']) + .then((data) => { + sendResponse(data); + }); + return true; + } + + case 'setIntensity': { + const val = msg.value as number; + chrome.storage.sync.set({ intensity: val }).then(() => { + sendResponse({ ok: true }); + }); + return true; + } + + case 'setEnabled': { + const val = msg.value as boolean; + chrome.storage.sync.set({ enabled: val }).then(() => { + sendResponse({ ok: true }); + }); + return true; + } + + case 'toggleSite': { + const hostname = msg.hostname as string; + chrome.storage.sync.get(['disabledSites']).then((data) => { + const sites: string[] = data.disabledSites || []; + const idx = sites.indexOf(hostname); + if (idx >= 0) { + sites.splice(idx, 1); + } else { + sites.push(hostname); + } + chrome.storage.sync.set({ disabledSites: sites }).then(() => { + sendResponse({ ok: true, disabledSites: sites }); + }); + }); + return true; + } + } + return false; + } +); diff --git a/gogh-theme-engine/src/color.ts b/gogh-theme-engine/src/color.ts new file mode 100644 index 0000000..932603d --- /dev/null +++ b/gogh-theme-engine/src/color.ts @@ -0,0 +1,710 @@ +/** + * color.ts — Perceptual color engine built on OKLCH. + * + * This module provides all color math the extension needs: + * • Hex ↔ sRGB ↔ Linear RGB ↔ OKLAB ↔ OKLCH conversions + * • Relative luminance (WCAG definition, from linear sRGB) + * • WCAG contrast ratio + * • Color parsing (hex, rgb(), rgba(), named colors, hsl()) + * • Color serialization + * • A memoized transformColor() entry point + * + * All intermediate math uses the OKLCH perceptual model so that + * lightness adjustments look uniform to the human eye. + */ + +// ─── Types ──────────────────────────────────────────────────────────────────── + +/** sRGB color with channels in [0,1] */ +export interface SRGB { + r: number; + g: number; + b: number; +} + +/** Linear-light sRGB (gamma removed) */ +export interface LinearRGB { + r: number; + g: number; + b: number; +} + +/** OKLAB perceptual color */ +export interface OKLAB { + L: number; // lightness 0..1 + a: number; // green–red axis + b: number; // blue–yellow axis +} + +/** OKLCH perceptual cylindrical color */ +export interface OKLCH { + L: number; // lightness 0..1 + C: number; // chroma ≥0 + h: number; // hue in degrees 0..360 +} + +/** Parsed RGBA color (sRGB 0-255 + alpha 0-1) */ +export interface RGBA { + r: number; + g: number; + b: number; + a: number; +} + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const DEG = Math.PI / 180; +const RAD = 180 / Math.PI; + +// ─── sRGB ↔ Linear RGB ─────────────────────────────────────────────────────── + +/** Apply sRGB inverse electro-optical transfer function (gamma decode). */ +function srgbToLinearChannel(c: number): number { + return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); +} + +/** Apply sRGB electro-optical transfer function (gamma encode). */ +function linearToSrgbChannel(c: number): number { + return c <= 0.0031308 + ? 12.92 * c + : 1.055 * Math.pow(c, 1 / 2.4) - 0.055; +} + +export function srgbToLinear(c: SRGB): LinearRGB { + return { + r: srgbToLinearChannel(c.r), + g: srgbToLinearChannel(c.g), + b: srgbToLinearChannel(c.b), + }; +} + +export function linearToSrgb(c: LinearRGB): SRGB { + return { + r: linearToSrgbChannel(c.r), + g: linearToSrgbChannel(c.g), + b: linearToSrgbChannel(c.b), + }; +} + +// ─── Linear RGB ↔ OKLAB ────────────────────────────────────────────────────── +// Using the matrices from Björn Ottosson's OKLAB specification. + +export function linearRgbToOklab(c: LinearRGB): OKLAB { + const l_ = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b; + const m_ = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b; + const s_ = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b; + + const l = Math.cbrt(l_); + const m = Math.cbrt(m_); + const s = Math.cbrt(s_); + + return { + L: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s, + a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s, + b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s, + }; +} + +export function oklabToLinearRgb(c: OKLAB): LinearRGB { + const l_ = c.L + 0.3963377774 * c.a + 0.2158037573 * c.b; + const m_ = c.L - 0.1055613458 * c.a - 0.0638541728 * c.b; + const s_ = c.L - 0.0894841775 * c.a - 1.2914855480 * c.b; + + const l = l_ * l_ * l_; + const m = m_ * m_ * m_; + const s = s_ * s_ * s_; + + return { + r: +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, + }; +} + +// ─── OKLAB ↔ OKLCH ─────────────────────────────────────────────────────────── + +export function oklabToOklch(lab: OKLAB): OKLCH { + const C = Math.sqrt(lab.a * lab.a + lab.b * lab.b); + let h = Math.atan2(lab.b, lab.a) * RAD; + if (h < 0) h += 360; + return { L: lab.L, C, h }; +} + +export function oklchToOklab(lch: OKLCH): OKLAB { + return { + L: lch.L, + a: lch.C * Math.cos(lch.h * DEG), + b: lch.C * Math.sin(lch.h * DEG), + }; +} + +// ─── Convenience round-trips ───────────────────────────────────────────────── + +export function srgbToOklch(c: SRGB): OKLCH { + return oklabToOklch(linearRgbToOklab(srgbToLinear(c))); +} + +export function oklchToSrgb(c: OKLCH): SRGB { + return linearToSrgb(oklabToLinearRgb(oklchToOklab(c))); +} + +/** Clamp sRGB channels to [0,1] after conversion to handle out-of-gamut. */ +export function oklchToSrgbClamped(c: OKLCH): SRGB { + const raw = oklchToSrgb(c); + return { + r: Math.max(0, Math.min(1, raw.r)), + g: Math.max(0, Math.min(1, raw.g)), + b: Math.max(0, Math.min(1, raw.b)), + }; +} + +// ─── Hex ↔ RGBA ────────────────────────────────────────────────────────────── + +/** Parse a hex color string (#RGB, #RRGGBB, #RRGGBBAA) into RGBA 0-255. */ +export function hexToRgba(hex: string): RGBA { + let h = hex.replace('#', ''); + if (h.length === 3) { + h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; + } else if (h.length === 4) { + h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] + h[3] + h[3]; + } + const r = parseInt(h.slice(0, 2), 16); + const g = parseInt(h.slice(2, 4), 16); + const b = parseInt(h.slice(4, 6), 16); + const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1; + return { r, g, b, a }; +} + +export function hexToSrgb(hex: string): SRGB { + const { r, g, b } = hexToRgba(hex); + return { r: r / 255, g: g / 255, b: b / 255 }; +} + +export function hexToOklch(hex: string): OKLCH { + return srgbToOklch(hexToSrgb(hex)); +} + +export function srgbToHex(c: SRGB): string { + const to8 = (v: number) => + Math.round(Math.max(0, Math.min(1, v)) * 255) + .toString(16) + .padStart(2, '0'); + return `#${to8(c.r)}${to8(c.g)}${to8(c.b)}`; +} + +export function oklchToHex(c: OKLCH): string { + return srgbToHex(oklchToSrgbClamped(c)); +} + +export function rgbaToHex(c: RGBA): string { + const to8 = (v: number) => + Math.max(0, Math.min(255, Math.round(v))) + .toString(16) + .padStart(2, '0'); + if (c.a < 1) { + return `#${to8(c.r)}${to8(c.g)}${to8(c.b)}${to8(c.a * 255)}`; + } + return `#${to8(c.r)}${to8(c.g)}${to8(c.b)}`; +} + +// ─── CSS Color Parsing ─────────────────────────────────────────────────────── + +/** Map of CSS named colors to hex. Only the most common ones for speed. */ +const NAMED_COLORS: Record = { + transparent: '#00000000', + black: '#000000', + white: '#ffffff', + red: '#ff0000', + green: '#008000', + blue: '#0000ff', + yellow: '#ffff00', + cyan: '#00ffff', + magenta: '#ff00ff', + orange: '#ffa500', + purple: '#800080', + pink: '#ffc0cb', + gray: '#808080', + grey: '#808080', + silver: '#c0c0c0', + maroon: '#800000', + olive: '#808000', + lime: '#00ff00', + teal: '#008080', + navy: '#000080', + aqua: '#00ffff', + fuchsia: '#ff00ff', + inherit: '', + initial: '', + unset: '', + currentcolor: '', + currentColor: '', +}; + +/** + * Parse any CSS color string into RGBA (0-255 for RGB, 0-1 for alpha). + * Returns null if the string cannot be parsed or is a keyword like inherit. + */ +export function parseCssColor(raw: string): RGBA | null { + if (!raw || raw === 'none') return null; + + const trimmed = raw.trim().toLowerCase(); + + // Named colors + if (trimmed in NAMED_COLORS) { + const hex = NAMED_COLORS[trimmed]; + if (!hex) return null; // inherit, initial, etc. + return hexToRgba(hex); + } + + // Hex + if (trimmed.startsWith('#')) { + return hexToRgba(trimmed); + } + + // rgb() / rgba() + const rgbMatch = trimmed.match( + /rgba?\(\s*([\d.]+%?)\s*[,/ ]\s*([\d.]+%?)\s*[,/ ]\s*([\d.]+%?)\s*(?:[,/]\s*([\d.]+%?))?\s*\)/ + ); + if (rgbMatch) { + const parseChannel = (s: string): number => + s.endsWith('%') ? (parseFloat(s) / 100) * 255 : parseFloat(s); + const parseAlpha = (s: string | undefined): number => { + if (!s) return 1; + return s.endsWith('%') ? parseFloat(s) / 100 : parseFloat(s); + }; + return { + r: parseChannel(rgbMatch[1]), + g: parseChannel(rgbMatch[2]), + b: parseChannel(rgbMatch[3]), + a: parseAlpha(rgbMatch[4]), + }; + } + + // hsl() / hsla() + const hslMatch = trimmed.match( + /hsla?\(\s*([\d.]+)\s*[,/ ]\s*([\d.]+)%?\s*[,/ ]\s*([\d.]+)%?\s*(?:[,/]\s*([\d.]+%?))?\s*\)/ + ); + if (hslMatch) { + const h = parseFloat(hslMatch[1]); + const s = parseFloat(hslMatch[2]) / 100; + const l = parseFloat(hslMatch[3]) / 100; + const a = hslMatch[4] + ? hslMatch[4].endsWith('%') + ? parseFloat(hslMatch[4]) / 100 + : parseFloat(hslMatch[4]) + : 1; + // HSL to RGB conversion + const rgb = hslToRgb(h, s, l); + return { ...rgb, a }; + } + + return null; +} + +/** Standard HSL → RGB. h in degrees, s/l in [0,1]. Returns RGB 0-255. */ +function hslToRgb( + h: number, + s: number, + l: number +): { r: number; g: number; b: number } { + h = ((h % 360) + 360) % 360; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r = 0, + g = 0, + b = 0; + if (h < 60) [r, g, b] = [c, x, 0]; + else if (h < 120) [r, g, b] = [x, c, 0]; + else if (h < 180) [r, g, b] = [0, c, x]; + else if (h < 240) [r, g, b] = [0, x, c]; + else if (h < 300) [r, g, b] = [x, 0, c]; + else [r, g, b] = [c, 0, x]; + return { + r: Math.round((r + m) * 255), + g: Math.round((g + m) * 255), + b: Math.round((b + m) * 255), + }; +} + +/** Serialize RGBA to a CSS string. */ +export function rgbaToCss(c: RGBA): string { + if (c.a < 1) { + return `rgba(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)}, ${c.a.toFixed(3)})`; + } + return `rgb(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)})`; +} + +// ─── Luminance & Contrast ──────────────────────────────────────────────────── + +/** + * WCAG 2.x relative luminance from sRGB channels in [0,1]. + * Uses the standard formula: Y = 0.2126*R + 0.7152*G + 0.0722*B + * where R,G,B are linear-light values. + */ +export function relativeLuminance(c: SRGB): number { + const lin = srgbToLinear(c); + return 0.2126 * lin.r + 0.7152 * lin.g + 0.0722 * lin.b; +} + +export function relativeLuminanceFromRgba(c: RGBA): number { + return relativeLuminance({ r: c.r / 255, g: c.g / 255, b: c.b / 255 }); +} + +/** + * WCAG contrast ratio between two luminance values. + * Returns a value ≥ 1. AA for normal text requires ≥ 4.5. + */ +export function contrastRatio(l1: number, l2: number): number { + const lighter = Math.max(l1, l2); + const darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); +} + +/** + * Check whether a color is roughly achromatic (near-neutral). + * Uses OKLCH chroma — values below threshold are "neutral." + */ +export function isNeutral(lch: OKLCH, threshold = 0.035): boolean { + return lch.C < threshold; +} + +/** + * Classify the hue of an OKLCH color into a named bucket. + * Returns null for achromatic colors. + */ +export type HueBucket = + | 'red' + | 'orange' + | 'yellow' + | 'green' + | 'cyan' + | 'blue' + | 'purple' + | 'magenta'; + +export function classifyHue(lch: OKLCH): HueBucket | null { + if (isNeutral(lch)) return null; + const h = ((lch.h % 360) + 360) % 360; + if (h < 20 || h >= 345) return 'red'; + if (h < 50) return 'orange'; + if (h < 85) return 'yellow'; + if (h < 160) return 'green'; + if (h < 200) return 'cyan'; + if (h < 270) return 'blue'; + if (h < 310) return 'purple'; + return 'magenta'; +} + +/** + * Blend two OKLCH colors. t=0 → a, t=1 → b. + * Hue interpolation takes the shortest path around the circle. + */ +export function blendOklch(a: OKLCH, b: OKLCH, t: number): OKLCH { + let dh = b.h - a.h; + if (dh > 180) dh -= 360; + if (dh < -180) dh += 360; + return { + L: a.L + (b.L - a.L) * t, + C: a.C + (b.C - a.C) * t, + h: ((a.h + dh * t) % 360 + 360) % 360, + }; +} + +/** + * Adjust the L channel of an OKLCH color to meet a minimum contrast ratio + * against a given background luminance. + * + * We walk the L channel in the direction that increases contrast, + * using bisection for speed. + */ +export function ensureContrast( + color: OKLCH, + bgLuminance: number, + minRatio = 4.5, + maxIterations = 16 +): OKLCH { + const srgb = oklchToSrgbClamped(color); + const fgLum = relativeLuminance(srgb); + const currentRatio = contrastRatio(fgLum, bgLuminance); + + if (currentRatio >= minRatio) return color; + + // Determine direction: lighten or darken? + // If bg is dark, lighten fg; if bg is light, darken fg. + const shouldLighten = bgLuminance < 0.5; + + let lo = shouldLighten ? color.L : 0; + let hi = shouldLighten ? 1 : color.L; + let bestL = color.L; + + for (let i = 0; i < maxIterations; i++) { + const mid = (lo + hi) / 2; + const testColor: OKLCH = { L: mid, C: color.C, h: color.h }; + const testSrgb = oklchToSrgbClamped(testColor); + const testLum = relativeLuminance(testSrgb); + const testRatio = contrastRatio(testLum, bgLuminance); + + if (testRatio >= minRatio) { + bestL = mid; + // Try to find a value closer to the original + if (shouldLighten) hi = mid; + else lo = mid; + } else { + if (shouldLighten) lo = mid; + else hi = mid; + } + } + + return { L: bestL, C: color.C, h: color.h }; +} + +// ─── Transform Memoization ─────────────────────────────────────────────────── + +const transformCache = new Map(); +const CACHE_MAX_SIZE = 8192; + +export function clearTransformCache(): void { + transformCache.clear(); +} + +/** + * Build a cache key from the original color string and its context. + * This ensures identical colors in different contexts can map differently. + */ +function makeCacheKey(cssColor: string, context: TransformContext): string { + return `${cssColor}|${context.property}|${context.parentBgLuminance.toFixed(3)}|${context.isLink ? 1 : 0}|${context.isButton ? 1 : 0}|${context.disabled ? 1 : 0}`; +} + +// ─── Transform Context ─────────────────────────────────────────────────────── + +export interface TransformContext { + /** CSS property being transformed: 'color', 'background-color', 'border-color', 'box-shadow' */ + property: 'color' | 'background-color' | 'border-color' | 'box-shadow'; + /** Relative luminance of the parent/container background. */ + parentBgLuminance: number; + /** Whether the element is a link. */ + isLink: boolean; + /** Whether the element is a button or button-like. */ + isButton: boolean; + /** Whether the element is in a disabled state. */ + disabled: boolean; +} + +/** The semantic theme — provided by themeEngine.ts */ +export interface SemanticTheme { + background: string; + foreground: string; + surface: string; + surfaceAlt: string; + accentPrimary: string; + accentSecondary: string; + error: string; + warning: string; + success: string; + info: string; + muted: string; + border: string; +} + +/** + * Main entry point for color transformation. + * + * Takes an original CSS color string, the transform context, the semantic theme, + * and an intensity factor (0 = no change, 1 = full theme). + * + * Returns a new CSS color string. + */ +export function transformColor( + cssColor: string, + context: TransformContext, + theme: SemanticTheme, + intensity: number = 1 +): string { + // Fast path: cache hit + const key = makeCacheKey(cssColor, context); + const cached = transformCache.get(key); + if (cached !== undefined) return cached; + + const parsed = parseCssColor(cssColor); + if (!parsed) return cssColor; // unparseable → pass through + + // Fully transparent → leave alone + if (parsed.a === 0) return cssColor; + + const originalSrgb: SRGB = { r: parsed.r / 255, g: parsed.g / 255, b: parsed.b / 255 }; + const originalLch = srgbToOklch(originalSrgb); + + // Determine the target theme color based on semantic analysis + let targetHex: string; + const hue = classifyHue(originalLch); + + if (context.property === 'background-color' || context.property === 'box-shadow') { + // Background colors → remap based on lightness bands + if (isNeutral(originalLch)) { + targetHex = remapNeutralBg(originalLch, theme); + } else { + targetHex = remapChromaticBg(originalLch, hue, theme); + } + } else if (context.property === 'color') { + // Text colors + if (context.isLink) { + targetHex = theme.accentPrimary; + } else if (isNeutral(originalLch)) { + targetHex = remapNeutralFg(originalLch, theme); + } else { + targetHex = remapChromaticFg(originalLch, hue, theme); + } + } else { + // border-color + if (isNeutral(originalLch)) { + targetHex = theme.border; + } else { + targetHex = remapChromaticBg(originalLch, hue, theme); + } + } + + let targetLch = hexToOklch(targetHex); + + // Preserve the original lightness relationship to some degree: + // Shift the target lightness partially toward the original's relative lightness + // This helps preserve visual hierarchy within the theme + const lightnessPreservation = 0.25; + targetLch = { + ...targetLch, + L: targetLch.L + (originalLch.L - targetLch.L) * lightnessPreservation, + }; + + // Ensure WCAG contrast for text + if (context.property === 'color') { + targetLch = ensureContrast(targetLch, context.parentBgLuminance, 4.5); + } + + // Apply intensity blending: blend between original and target + const finalLch = blendOklch(originalLch, targetLch, intensity); + const finalSrgb = oklchToSrgbClamped(finalLch); + + let result: string; + if (parsed.a < 1) { + result = `rgba(${Math.round(finalSrgb.r * 255)}, ${Math.round(finalSrgb.g * 255)}, ${Math.round(finalSrgb.b * 255)}, ${parsed.a.toFixed(3)})`; + } else { + result = srgbToHex(finalSrgb); + } + + // Cache management + if (transformCache.size >= CACHE_MAX_SIZE) { + // Evict oldest ~25% + const keys = Array.from(transformCache.keys()); + for (let i = 0; i < CACHE_MAX_SIZE / 4; i++) { + transformCache.delete(keys[i]); + } + } + transformCache.set(key, result); + + return result; +} + +// ─── Neutral remapping helpers ─────────────────────────────────────────────── + +/** + * Remap a neutral (achromatic) background color to the theme's surface bands. + * Uses lightness to decide which surface level to use: + * Very dark → background + * Dark → surface + * Medium → surfaceAlt + * Light → foreground-ish (inverted for light themes) + */ +function remapNeutralBg(lch: OKLCH, theme: SemanticTheme): string { + const bgLch = hexToOklch(theme.background); + const fgLch = hexToOklch(theme.foreground); + + // Determine if this is a dark or light theme by comparing bg and fg lightness + const isDark = bgLch.L < fgLch.L; + + if (isDark) { + // Dark theme: dark originals → theme bg, light originals → theme surface + if (lch.L < 0.2) return theme.background; + if (lch.L < 0.35) return theme.surface; + if (lch.L < 0.55) return theme.surfaceAlt; + if (lch.L < 0.75) return theme.muted; + return theme.foreground; + } else { + // Light theme: light originals → theme bg, dark originals → theme surface + if (lch.L > 0.85) return theme.background; + if (lch.L > 0.7) return theme.surface; + if (lch.L > 0.5) return theme.surfaceAlt; + if (lch.L > 0.3) return theme.muted; + return theme.foreground; + } +} + +function remapNeutralFg(lch: OKLCH, theme: SemanticTheme): string { + const bgLch = hexToOklch(theme.background); + const fgLch = hexToOklch(theme.foreground); + const isDark = bgLch.L < fgLch.L; + + if (isDark) { + // In dark theme: dark text originals → muted, light text → foreground + if (lch.L > 0.6) return theme.foreground; + if (lch.L > 0.35) return theme.muted; + return theme.surface; // very dark text on dark bg → surface (barely visible, intentional) + } else { + if (lch.L < 0.4) return theme.foreground; + if (lch.L < 0.65) return theme.muted; + return theme.surface; + } +} + +// ─── Chromatic remapping helpers ───────────────────────────────────────────── + +function remapChromaticBg( + _lch: OKLCH, + hue: HueBucket | null, + theme: SemanticTheme +): string { + switch (hue) { + case 'red': + case 'magenta': + return theme.error; + case 'orange': + return theme.warning; + case 'yellow': + return theme.warning; + case 'green': + return theme.success; + case 'cyan': + return theme.info; + case 'blue': + return theme.accentPrimary; + case 'purple': + return theme.accentSecondary; + default: + return theme.surface; + } +} + +function remapChromaticFg( + _lch: OKLCH, + hue: HueBucket | null, + theme: SemanticTheme +): string { + switch (hue) { + case 'red': + case 'magenta': + return theme.error; + case 'orange': + return theme.warning; + case 'yellow': + return theme.warning; + case 'green': + return theme.success; + case 'cyan': + return theme.info; + case 'blue': + return theme.accentPrimary; + case 'purple': + return theme.accentSecondary; + default: + return theme.foreground; + } +} diff --git a/gogh-theme-engine/src/content.ts b/gogh-theme-engine/src/content.ts new file mode 100644 index 0000000..c399713 --- /dev/null +++ b/gogh-theme-engine/src/content.ts @@ -0,0 +1,654 @@ +/** + * content.ts — Content script injected into every page. + * + * This is the main orchestrator that: + * 1. Loads the user's theme from chrome.storage.sync + * 2. Injects CSS variables into a + + +

🎨 Gogh Theme Engine

+

Apply Gogh terminal palettes to any website using perceptual OKLCH color remapping.

+ +
+ +
+
+

Palette (YAML)

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

Disabled Sites

+
    +
  • No sites disabled.
  • +
+
+
+ + +
+
+

Controls

+ +
+
+ Enable Theming + +
+
+ +
+ + +
+
+ + +
+

Palette Colors

+
+ +

Semantic Theme

+
+
+ + +
+

Contrast Diagnostics

+ + + + + + + + + + +
CombinationRatioAAAAA
+
+ + +
+

Live Preview

+ +
+
+
+ + + + diff --git a/gogh-theme-engine/src/options/options.ts b/gogh-theme-engine/src/options/options.ts new file mode 100644 index 0000000..eb7631f --- /dev/null +++ b/gogh-theme-engine/src/options/options.ts @@ -0,0 +1,371 @@ +/** + * options.ts — Options page controller. + * + * Handles: + * • YAML palette editing with validation feedback + * • Live color preview (palette grid + semantic swatches) + * • Contrast diagnostics table + * • Enable/disable toggle + * • Intensity slider + * • Per-site disable list management + * • Live preview iframe + */ + +import { + parsePalette, + DEFAULT_PALETTE_YAML, + deriveSemanticTheme, + type GoghPalette, +} from '../themeEngine'; +import { + hexToOklch, + hexToSrgb, + relativeLuminance, + contrastRatio, + type SemanticTheme, +} from '../color'; + +// ─── DOM References ─────────────────────────────────────────────────────────── + +const yamlEditor = document.getElementById('yaml-editor') as HTMLTextAreaElement; +const editorStatus = document.getElementById('editor-status') as HTMLDivElement; +const btnSave = document.getElementById('btn-save') as HTMLButtonElement; +const btnReset = document.getElementById('btn-reset') as HTMLButtonElement; +const btnValidate = document.getElementById('btn-validate') as HTMLButtonElement; +const toggleEnabled = document.getElementById('toggle-enabled') as HTMLInputElement; +const intensitySlider = document.getElementById('intensity-slider') as HTMLInputElement; +const intensityValue = document.getElementById('intensity-value') as HTMLSpanElement; +const colorGrid = document.getElementById('color-grid') as HTMLDivElement; +const semanticGrid = document.getElementById('semantic-grid') as HTMLDivElement; +const contrastBody = document.getElementById('contrast-body') as HTMLTableSectionElement; +const disabledSitesList = document.getElementById('disabled-sites') as HTMLUListElement; +const previewFrame = document.getElementById('preview-frame') as HTMLIFrameElement; + +// ─── State ──────────────────────────────────────────────────────────────────── + +let currentPalette: GoghPalette | null = null; +let currentTheme: SemanticTheme | null = null; + +// ─── Init ───────────────────────────────────────────────────────────────────── + +async function init(): Promise { + // Load current settings + const data = await chrome.storage.sync.get([ + 'paletteYaml', + 'enabled', + 'intensity', + 'disabledSites', + ]); + + const yamlStr = data.paletteYaml || DEFAULT_PALETTE_YAML; + yamlEditor.value = yamlStr; + + toggleEnabled.checked = data.enabled !== false; + + const intensityVal = typeof data.intensity === 'number' ? data.intensity : 100; + intensitySlider.value = String(intensityVal); + intensityValue.textContent = String(intensityVal); + + // Parse and render + parseAndRender(yamlStr); + + // Render disabled sites + renderDisabledSites(data.disabledSites || []); + + // Attach event listeners + attachListeners(); +} + +init(); + +// ─── Event Listeners ────────────────────────────────────────────────────────── + +function attachListeners(): void { + // Save button + btnSave.addEventListener('click', async () => { + const yaml = yamlEditor.value; + const result = parsePalette(yaml); + + if (!result.ok) { + setStatus('error', `Validation failed: ${result.errors[0]}`); + return; + } + + await chrome.storage.sync.set({ paletteYaml: yaml }); + setStatus('success', '✓ Palette saved and applied.'); + parseAndRender(yaml); + }); + + // Reset button + btnReset.addEventListener('click', async () => { + yamlEditor.value = DEFAULT_PALETTE_YAML; + await chrome.storage.sync.set({ paletteYaml: DEFAULT_PALETTE_YAML }); + setStatus('success', '✓ Reset to default Dracula palette.'); + parseAndRender(DEFAULT_PALETTE_YAML); + }); + + // Validate button + btnValidate.addEventListener('click', () => { + const result = parsePalette(yamlEditor.value); + if (result.ok) { + setStatus('success', `✓ Valid palette: "${result.palette.name}" by ${result.palette.author}`); + parseAndRender(yamlEditor.value); + } else { + setStatus('error', `✗ ${result.errors.join('; ')}`); + } + }); + + // Enabled toggle + toggleEnabled.addEventListener('change', async () => { + const val = toggleEnabled.checked; + await chrome.storage.sync.set({ enabled: val }); + }); + + // Intensity slider + intensitySlider.addEventListener('input', () => { + intensityValue.textContent = intensitySlider.value; + }); + intensitySlider.addEventListener('change', async () => { + const val = parseInt(intensitySlider.value, 10); + await chrome.storage.sync.set({ intensity: val }); + }); + + // Live YAML editing — debounced validation + let debounce: ReturnType; + yamlEditor.addEventListener('input', () => { + clearTimeout(debounce); + debounce = setTimeout(() => { + const result = parsePalette(yamlEditor.value); + if (result.ok) { + setStatus('success', `Valid — "${result.palette.name}"`); + parseAndRender(yamlEditor.value); + } else { + setStatus('error', result.errors[0]); + } + }, 500); + }); + + // Tab key in textarea inserts spaces instead of switching focus + yamlEditor.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + e.preventDefault(); + const start = yamlEditor.selectionStart; + const end = yamlEditor.selectionEnd; + yamlEditor.value = + yamlEditor.value.substring(0, start) + + ' ' + + yamlEditor.value.substring(end); + yamlEditor.selectionStart = yamlEditor.selectionEnd = start + 2; + } + }); +} + +// ─── Status Messages ────────────────────────────────────────────────────────── + +function setStatus(type: 'error' | 'success', message: string): void { + editorStatus.className = `editor-status ${type}`; + editorStatus.textContent = message; +} + +// ─── Parse & Render ─────────────────────────────────────────────────────────── + +function parseAndRender(yamlStr: string): void { + const result = parsePalette(yamlStr); + if (!result.ok) return; + + currentPalette = result.palette; + currentTheme = result.theme; + + renderColorGrid(result.palette); + renderSemanticGrid(result.theme); + renderContrastDiagnostics(result.theme); + renderPreview(result.palette, result.theme); +} + +// ─── Color Grid (16 palette colors) ────────────────────────────────────────── + +function renderColorGrid(palette: GoghPalette): void { + colorGrid.innerHTML = ''; + + // Show 16 palette colors + background + foreground + const allColors = [ + ...palette.colors.map((hex, i) => ({ hex, label: `color_${String(i + 1).padStart(2, '0')}` })), + ]; + + for (const { hex, label } of allColors) { + const swatch = document.createElement('div'); + swatch.className = 'color-swatch'; + swatch.style.backgroundColor = hex; + swatch.innerHTML = `${label}: ${hex}`; + colorGrid.appendChild(swatch); + } +} + +// ─── Semantic Grid ──────────────────────────────────────────────────────────── + +function renderSemanticGrid(theme: SemanticTheme): void { + semanticGrid.innerHTML = ''; + + const entries: [string, string][] = [ + ['Background', theme.background], + ['Foreground', theme.foreground], + ['Surface', theme.surface], + ['Surface Alt', theme.surfaceAlt], + ['Accent 1', theme.accentPrimary], + ['Accent 2', theme.accentSecondary], + ['Error', theme.error], + ['Warning', theme.warning], + ['Success', theme.success], + ['Info', theme.info], + ['Muted', theme.muted], + ['Border', theme.border], + ]; + + for (const [name, hex] of entries) { + const swatch = document.createElement('div'); + swatch.className = 'semantic-swatch'; + swatch.style.backgroundColor = hex; + + // Determine text color for readability + const lch = hexToOklch(hex); + swatch.style.color = lch.L > 0.5 ? '#000000' : '#ffffff'; + swatch.textContent = name; + swatch.title = hex; + + semanticGrid.appendChild(swatch); + } +} + +// ─── Contrast Diagnostics ───────────────────────────────────────────────────── + +function renderContrastDiagnostics(theme: SemanticTheme): void { + contrastBody.innerHTML = ''; + + // Key contrast combinations to check + const pairs: [string, string, string, string][] = [ + ['Foreground / Background', theme.foreground, theme.background, 'text'], + ['Foreground / Surface', theme.foreground, theme.surface, 'text'], + ['Accent 1 / Background', theme.accentPrimary, theme.background, 'text'], + ['Accent 2 / Background', theme.accentSecondary, theme.background, 'text'], + ['Error / Background', theme.error, theme.background, 'text'], + ['Warning / Background', theme.warning, theme.background, 'text'], + ['Success / Background', theme.success, theme.background, 'text'], + ['Info / Background', theme.info, theme.background, 'text'], + ['Muted / Background', theme.muted, theme.background, 'text'], + ['Foreground / Surface Alt', theme.foreground, theme.surfaceAlt, 'text'], + ]; + + for (const [label, fg, bg, _type] of pairs) { + const fgLum = relativeLuminance(hexToSrgb(fg)); + const bgLum = relativeLuminance(hexToSrgb(bg)); + const ratio = contrastRatio(fgLum, bgLum); + + const passAA = ratio >= 4.5; + const passAAA = ratio >= 7; + + const row = document.createElement('tr'); + row.innerHTML = ` + + + + ${label} + + ${ratio.toFixed(2)}:1 + ${passAA ? 'Pass' : 'Fail'} + ${passAAA ? 'Pass' : 'Fail'} + `; + contrastBody.appendChild(row); + } +} + +// ─── Live Preview ───────────────────────────────────────────────────────────── + +function renderPreview(palette: GoghPalette, theme: SemanticTheme): void { + const previewHTML = ` + + + +

${palette.name}

+

By ${palette.author} · ${palette.variant} variant

+

+ This is body text on the background. + Here is a primary link + and a secondary link. +

+
+

Surface card with inline code.

+
+
+ Error + Warning + Success + Info +
+
+
+ ${palette.colors.map((c) => `
`).join('')} +
+ + + `; + + previewFrame.srcdoc = previewHTML; +} + +// ─── Disabled Sites List ────────────────────────────────────────────────────── + +function renderDisabledSites(sites: string[]): void { + disabledSitesList.innerHTML = ''; + + if (sites.length === 0) { + disabledSitesList.innerHTML = '
  • No sites disabled.
  • '; + return; + } + + for (const site of sites) { + const li = document.createElement('li'); + li.innerHTML = ` + ${site} + + `; + disabledSitesList.appendChild(li); + } + + // Attach remove handlers + disabledSitesList.querySelectorAll('.remove-site').forEach((btn) => { + btn.addEventListener('click', async () => { + const site = (btn as HTMLElement).dataset.site!; + const data = await chrome.storage.sync.get(['disabledSites']); + const current: string[] = data.disabledSites || []; + const updated = current.filter((s) => s !== site); + await chrome.storage.sync.set({ disabledSites: updated }); + renderDisabledSites(updated); + }); + }); +} + +// ─── Listen for external changes ────────────────────────────────────────────── + +chrome.storage.onChanged.addListener((changes, area) => { + if (area !== 'sync') return; + + if (changes.disabledSites) { + renderDisabledSites(changes.disabledSites.newValue || []); + } + + if (changes.enabled) { + toggleEnabled.checked = changes.enabled.newValue !== false; + } + + if (changes.intensity) { + const val = changes.intensity.newValue ?? 100; + intensitySlider.value = String(val); + intensityValue.textContent = String(val); + } + + if (changes.paletteYaml) { + const yaml = changes.paletteYaml.newValue || DEFAULT_PALETTE_YAML; + yamlEditor.value = yaml; + parseAndRender(yaml); + } +}); diff --git a/gogh-theme-engine/src/themeEngine.ts b/gogh-theme-engine/src/themeEngine.ts new file mode 100644 index 0000000..0755e16 --- /dev/null +++ b/gogh-theme-engine/src/themeEngine.ts @@ -0,0 +1,247 @@ +/** + * themeEngine.ts — Parses Gogh YAML palettes and derives a semantic theme model. + * + * A Gogh palette provides: + * name, author, variant (light|dark) + * color_01 … color_16 (16 terminal colors) + * background, foreground, cursor + * + * This module: + * 1. Parses YAML using js-yaml + * 2. Validates all hex colors + * 3. Derives a SemanticTheme using hue + brightness analysis rather than + * fixed index mapping — so any arbitrary palette produces meaningful semantics. + */ + +import * as yaml from 'js-yaml'; +import { + hexToOklch, + oklchToHex, + isNeutral, + classifyHue, + contrastRatio, + relativeLuminance, + hexToSrgb, + type OKLCH, + type SemanticTheme, + type HueBucket, +} from './color'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface GoghPalette { + name: string; + author: string; + variant: 'light' | 'dark'; + colors: string[]; // color_01 … color_16 + background: string; + foreground: string; + cursor: string; +} + +export interface ParseResult { + ok: true; + palette: GoghPalette; + theme: SemanticTheme; +} + +export interface ParseError { + ok: false; + errors: string[]; +} + +// ─── Validation ─────────────────────────────────────────────────────────────── + +const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; + +function isValidHex(s: unknown): s is string { + return typeof s === 'string' && HEX_RE.test(s); +} + +// ─── YAML Parsing ───────────────────────────────────────────────────────────── + +/** + * Parse a Gogh YAML palette string into a validated GoghPalette and derived SemanticTheme. + */ +export function parsePalette(yamlString: string): ParseResult | ParseError { + let doc: Record; + try { + doc = yaml.load(yamlString) as Record; + } catch (e: unknown) { + return { ok: false, errors: [`YAML parse error: ${(e as Error).message}`] }; + } + + if (!doc || typeof doc !== 'object') { + return { ok: false, errors: ['YAML must be a mapping at the top level.'] }; + } + + const errors: string[] = []; + + // Validate required fields + const name = typeof doc.name === 'string' ? doc.name : 'Unnamed'; + const author = typeof doc.author === 'string' ? doc.author : 'Unknown'; + const variant = doc.variant === 'light' ? 'light' : 'dark'; // default to dark + + // Extract 16 colors + const colors: string[] = []; + for (let i = 1; i <= 16; i++) { + const key = `color_${i.toString().padStart(2, '0')}`; + const val = doc[key]; + if (isValidHex(val)) { + colors.push(val); + } else { + errors.push(`${key}: invalid or missing hex color (got ${JSON.stringify(val)})`); + colors.push('#000000'); // fallback + } + } + + // Background, foreground, cursor + const background = isValidHex(doc.background) ? doc.background : '#1e1e1e'; + const foreground = isValidHex(doc.foreground) ? doc.foreground : '#d4d4d4'; + const cursor = isValidHex(doc.cursor) ? doc.cursor : foreground; + + if (!isValidHex(doc.background)) errors.push(`background: invalid hex`); + if (!isValidHex(doc.foreground)) errors.push(`foreground: invalid hex`); + + if (errors.length > 0) { + return { ok: false, errors }; + } + + const palette: GoghPalette = { name, author, variant, colors, background, foreground, cursor }; + const theme = deriveSemanticTheme(palette); + return { ok: true, palette, theme }; +} + +// ─── Semantic Theme Derivation ──────────────────────────────────────────────── + +/** + * Derive a semantic theme model from a 16-color Gogh palette. + * + * Instead of fixed index mapping (which is fragile), we analyze each color's + * hue and lightness in OKLCH space and assign roles based on perceptual properties: + * + * • Red-ish colors → error + * • Green-ish → success + * • Blue-ish → accentPrimary + * • Yellow/orange-ish → warning + * • Cyan-ish → info + * • Purple-ish → accentSecondary + * • Low chroma, mid lightness → muted + * • Darkest → used for surface derivation + * • Highest contrast to background → foreground verification + * + * When multiple colors match a role, we pick the one with the highest chroma + * (most saturated) since that's typically the "primary" representative. + */ +export function deriveSemanticTheme(palette: GoghPalette): SemanticTheme { + const bgLch = hexToOklch(palette.background); + const fgLch = hexToOklch(palette.foreground); + const isDark = bgLch.L < fgLch.L; + + // Analyze all 16 palette colors + interface ColorInfo { + hex: string; + lch: OKLCH; + hue: HueBucket | null; + luminance: number; + } + + const analyzed: ColorInfo[] = palette.colors.map((hex) => { + const lch = hexToOklch(hex); + const srgb = hexToSrgb(hex); + return { + hex, + lch, + hue: classifyHue(lch), + luminance: relativeLuminance(srgb), + }; + }); + + // Helper: find best color by hue bucket, preferring highest chroma + function findByHue(bucket: HueBucket | HueBucket[]): string { + const buckets = Array.isArray(bucket) ? bucket : [bucket]; + const candidates = analyzed.filter((c) => c.hue !== null && buckets.includes(c.hue)); + if (candidates.length === 0) return palette.foreground; // fallback + // Prefer higher chroma + candidates.sort((a, b) => b.lch.C - a.lch.C); + return candidates[0].hex; + } + + // Helper: find a muted/neutral color + function findMuted(): string { + const neutrals = analyzed.filter((c) => isNeutral(c.lch)); + if (neutrals.length === 0) { + // No neutrals; synthesize one from background by adjusting lightness + const mutedLch: OKLCH = { + L: isDark ? bgLch.L + 0.25 : bgLch.L - 0.25, + C: 0.01, + h: bgLch.h, + }; + return oklchToHex(mutedLch); + } + // Pick the one closest to mid-lightness + neutrals.sort((a, b) => Math.abs(a.lch.L - 0.5) - Math.abs(b.lch.L - 0.5)); + return neutrals[0].hex; + } + + // Derive surface colors from background by shifting lightness + const surfaceLch: OKLCH = { + L: isDark ? bgLch.L + 0.06 : bgLch.L - 0.06, + C: bgLch.C, + h: bgLch.h, + }; + const surfaceAltLch: OKLCH = { + L: isDark ? bgLch.L + 0.12 : bgLch.L - 0.12, + C: bgLch.C, + h: bgLch.h, + }; + + // Border: slightly brighter/darker than surface + const borderLch: OKLCH = { + L: isDark ? bgLch.L + 0.18 : bgLch.L - 0.18, + C: Math.max(bgLch.C, 0.005), + h: bgLch.h, + }; + + return { + background: palette.background, + foreground: palette.foreground, + surface: oklchToHex(surfaceLch), + surfaceAlt: oklchToHex(surfaceAltLch), + accentPrimary: findByHue(['blue', 'cyan']), + accentSecondary: findByHue(['purple', 'magenta']), + error: findByHue(['red']), + warning: findByHue(['yellow', 'orange']), + success: findByHue(['green']), + info: findByHue(['cyan', 'blue']), + muted: findMuted(), + border: oklchToHex(borderLch), + }; +} + +// ─── Default Palette ────────────────────────────────────────────────────────── + +/** A sensible default Dracula-ish palette in Gogh YAML format for out-of-box experience. */ +export const DEFAULT_PALETTE_YAML = `name: Dracula +author: Zeno Rocha +variant: dark +color_01: "#282a36" +color_02: "#ff5555" +color_03: "#50fa7b" +color_04: "#f1fa8c" +color_05: "#bd93f9" +color_06: "#ff79c6" +color_07: "#8be9fd" +color_08: "#f8f8f2" +color_09: "#44475a" +color_10: "#ff6e6e" +color_11: "#69ff94" +color_12: "#ffffa5" +color_13: "#d6acff" +color_14: "#ff92df" +color_15: "#a4ffff" +color_16: "#ffffff" +background: "#282a36" +foreground: "#f8f8f2" +cursor: "#f8f8f2" +`; diff --git a/gogh-theme-engine/tsconfig.json b/gogh-theme-engine/tsconfig.json new file mode 100644 index 0000000..cb51366 --- /dev/null +++ b/gogh-theme-engine/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "types": ["chrome"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/gogh-theme-engine/vite.config.ts b/gogh-theme-engine/vite.config.ts new file mode 100644 index 0000000..ee53132 --- /dev/null +++ b/gogh-theme-engine/vite.config.ts @@ -0,0 +1,108 @@ +import { defineConfig, build as viteBuild, type UserConfig } from 'vite'; +import { resolve } from 'path'; +import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'fs'; + +/** + * Vite build config for the Gogh Theme Engine Chrome Extension. + * + * Chrome MV3 content scripts CANNOT use ES module imports. + * Service workers CAN if the manifest specifies "type": "module". + * + * Strategy: + * • content.ts → IIFE bundle (no imports, self-contained) + * • background.ts → ESM (manifest has "type": "module") + * • options/ → Standard Vite HTML entry + * + * We solve the code-splitting problem by using a custom plugin that + * rebuilds content.ts as a separate IIFE after the main build. + */ + +function chromeExtensionPlugin() { + return { + name: 'chrome-extension-iife', + async closeBundle() { + // Rebuild content.ts as a standalone IIFE bundle + console.log('[chrome-ext] Rebuilding content.js as IIFE...'); + await viteBuild({ + configFile: false, + build: { + outDir: resolve(__dirname, 'dist'), + emptyOutDir: false, // Don't wipe the rest of the build + lib: { + entry: resolve(__dirname, 'src/content.ts'), + name: 'GoghContent', + formats: ['iife'], + fileName: () => 'content.js', + }, + rollupOptions: { + output: { + // Ensure it overwrites the ESM version + inlineDynamicImports: true, + }, + }, + minify: false, + sourcemap: false, + target: 'chrome120', + }, + logLevel: 'warn', + }); + + // Fix options HTML path + const wrongPath = resolve(__dirname, 'dist/src/options/options.html'); + const correctDir = resolve(__dirname, 'dist/options'); + const correctPath = resolve(correctDir, 'options.html'); + + if (existsSync(wrongPath) && !existsSync(correctPath)) { + mkdirSync(correctDir, { recursive: true }); + const html = readFileSync(wrongPath, 'utf-8'); + // Fix script paths — point to the options.js at dist root + const fixedHtml = html + .replace(/src="[^"]*options\.js"/g, 'src="../options.js"') + .replace(/src="\.\.\/\.\.\/options\.js"/g, 'src="../options.js"'); + writeFileSync(correctPath, fixedHtml); + } + + // Clean up the misplaced src/ directory in dist + const distSrc = resolve(__dirname, 'dist/src'); + if (existsSync(distSrc)) { + rmSync(distSrc, { recursive: true, force: true }); + } + + // Clean up the chunks directory since content.js is now self-contained + // and background.js still needs its chunk import + console.log('[chrome-ext] Build complete.'); + }, + }; +} + +export default defineConfig(({ mode }) => { + const isProd = mode === 'production'; + + return { + plugins: [chromeExtensionPlugin()], + build: { + outDir: 'dist', + emptyOutDir: true, + rollupOptions: { + input: { + background: resolve(__dirname, 'src/background.ts'), + content: resolve(__dirname, 'src/content.ts'), + options: resolve(__dirname, 'src/options/options.html'), + }, + output: { + entryFileNames: '[name].js', + chunkFileNames: 'chunks/[name].js', + assetFileNames: 'assets/[name].[ext]', + }, + }, + minify: isProd ? 'esbuild' : false, + sourcemap: !isProd ? 'inline' : false, + target: 'chrome120', + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + }; +}); diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..ef76857 Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..14bd54f Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..6d18b47 Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..6076fbc --- /dev/null +++ b/manifest.json @@ -0,0 +1,56 @@ +{ + "manifest_version": 3, + "name": "Chrome Extension Template", + "description": "A Chrome Extension created using a template", + "version": "0.0.1", + "author": "Clyde D'Souza", + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "omnibox": { + "keyword" : "@@" + }, + "permissions": [ + "storage" + ], + "chrome_url_overrides" : { + "newtab": "newtab.html" + }, + "options_ui": { + "page": "options.html", + "open_in_tab": true + }, + "background": { + "service_worker": "background.js" + }, + "content_scripts": [{ + "matches": ["*://github.com/*"], + "js": ["content_scripts.js"] + }], + "action": { + "default_popup": "popup.html", + "default_title": "Open template popup" + }, + "commands": { + "turn-on": { + "suggested_key": { + "default": "Alt + Shift + M" + }, + "description": "Adds an ON badge to the action icon." + }, + "turn-off": { + "suggested_key": { + "default": "Alt + Shift + N" + }, + "description": "Adds an OFF badge to the action icon." + }, + "_execute_action": { + "suggested_key": { + "default": "Alt + Shift + L" + } + } + } + } + \ No newline at end of file diff --git a/newtab/newtab.html b/newtab/newtab.html new file mode 100644 index 0000000..c6e59bf --- /dev/null +++ b/newtab/newtab.html @@ -0,0 +1,11 @@ + + + + Chrome Extension Template New Tab + + + + + + This is the customized new tab page inserted by Chrome Extension Template + \ No newline at end of file diff --git a/newtab/scripts/index.js b/newtab/scripts/index.js new file mode 100644 index 0000000..807233e --- /dev/null +++ b/newtab/scripts/index.js @@ -0,0 +1,13 @@ +import { getStorage } from "../../common/storage"; +import { PRESET_CONFIGURATION, CHROME_SYNC_STORAGE_KEY } from "../../common/settings"; + +function loadConfiguration(result) { + const savedConfiguration = result || PRESET_CONFIGURATION; + const storageValue = savedConfiguration["storageValue"]; + console.info("Storage value", storageValue); +} + +window.onload = function() { + console.info("New Tab script loaded"); + getStorage(CHROME_SYNC_STORAGE_KEY, loadConfiguration); +}; diff --git a/newtab/styles/_page.scss b/newtab/styles/_page.scss new file mode 100644 index 0000000..ac069b7 --- /dev/null +++ b/newtab/styles/_page.scss @@ -0,0 +1,10 @@ +@import "./variables"; + +body { + background-color: $light-gray; + color: $dark-gray; + font: { + family: $font-family; + size: 16px; + } +} diff --git a/newtab/styles/_variables.scss b/newtab/styles/_variables.scss new file mode 100644 index 0000000..30d3fb5 --- /dev/null +++ b/newtab/styles/_variables.scss @@ -0,0 +1,3 @@ +$dark-gray: #5600eb; +$light-gray: #b7d6ff; +$font-family: -apple-system, BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif; diff --git a/newtab/styles/newtab.scss b/newtab/styles/newtab.scss new file mode 100644 index 0000000..819ef8e --- /dev/null +++ b/newtab/styles/newtab.scss @@ -0,0 +1,2 @@ +@import "./variables"; +@import "./page"; \ No newline at end of file diff --git a/options/options.html b/options/options.html new file mode 100644 index 0000000..da94d7e --- /dev/null +++ b/options/options.html @@ -0,0 +1,31 @@ + + + + + Chrome Extension Template + + + + + + +
    +
    + +
    + +
    +
    + + + \ No newline at end of file diff --git a/options/scripts/index.js b/options/scripts/index.js new file mode 100644 index 0000000..1495c66 --- /dev/null +++ b/options/scripts/index.js @@ -0,0 +1,21 @@ +import { getStorage,setStorage } from "../../common/storage"; +import { PRESET_CONFIGURATION, CHROME_SYNC_STORAGE_KEY } from "../../common/settings"; + +function saveConfiguration() { + const updatedConfiguration = { + storageValue: document.getElementById("storageValue").value + }; + setStorage(CHROME_SYNC_STORAGE_KEY, updatedConfiguration); +} + +function loadConfiguration(result) { + const savedConfiguration = result || PRESET_CONFIGURATION; + const storageValue = savedConfiguration["storageValue"]; + document.getElementById("storageValue").value = storageValue; +} + +window.onload = function() { + console.info("Options script loaded"); + document.getElementById("SaveConfiguration").addEventListener("click", saveConfiguration); + getStorage(CHROME_SYNC_STORAGE_KEY, loadConfiguration); +}; diff --git a/options/styles/_page.scss b/options/styles/_page.scss new file mode 100644 index 0000000..ac069b7 --- /dev/null +++ b/options/styles/_page.scss @@ -0,0 +1,10 @@ +@import "./variables"; + +body { + background-color: $light-gray; + color: $dark-gray; + font: { + family: $font-family; + size: 16px; + } +} diff --git a/options/styles/_variables.scss b/options/styles/_variables.scss new file mode 100644 index 0000000..86ac092 --- /dev/null +++ b/options/styles/_variables.scss @@ -0,0 +1,3 @@ +$dark-gray: #6B6B6B; +$light-gray: #F4F5F7; +$font-family: -apple-system, BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif; diff --git a/options/styles/options.scss b/options/styles/options.scss new file mode 100644 index 0000000..819ef8e --- /dev/null +++ b/options/styles/options.scss @@ -0,0 +1,2 @@ +@import "./variables"; +@import "./page"; \ No newline at end of file diff --git a/popup/popup.html b/popup/popup.html new file mode 100644 index 0000000..6b3ec65 --- /dev/null +++ b/popup/popup.html @@ -0,0 +1,11 @@ + + + + Chrome Extension Template Popup + + + + + + This is a Chrome Extension popup window + \ No newline at end of file diff --git a/popup/scripts/index.js b/popup/scripts/index.js new file mode 100644 index 0000000..0531a38 --- /dev/null +++ b/popup/scripts/index.js @@ -0,0 +1,13 @@ +import { getStorage } from "../../common/storage"; +import { PRESET_CONFIGURATION, CHROME_SYNC_STORAGE_KEY } from "../../common/settings"; + +function loadConfiguration(result) { + const savedConfiguration = result || PRESET_CONFIGURATION; + const storageValue = savedConfiguration["storageValue"]; + console.info("Storage value", storageValue); +} + +window.onload = function() { + console.info("Popup script loaded"); + getStorage(CHROME_SYNC_STORAGE_KEY, loadConfiguration); +}; diff --git a/popup/styles/_page.scss b/popup/styles/_page.scss new file mode 100644 index 0000000..ac069b7 --- /dev/null +++ b/popup/styles/_page.scss @@ -0,0 +1,10 @@ +@import "./variables"; + +body { + background-color: $light-gray; + color: $dark-gray; + font: { + family: $font-family; + size: 16px; + } +} diff --git a/popup/styles/_variables.scss b/popup/styles/_variables.scss new file mode 100644 index 0000000..8bcdc70 --- /dev/null +++ b/popup/styles/_variables.scss @@ -0,0 +1,3 @@ +$dark-gray: #830000; +$light-gray: #fdffd0; +$font-family: -apple-system, BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif; diff --git a/popup/styles/popup.scss b/popup/styles/popup.scss new file mode 100644 index 0000000..819ef8e --- /dev/null +++ b/popup/styles/popup.scss @@ -0,0 +1,2 @@ +@import "./variables"; +@import "./page"; \ No newline at end of file