diff --git a/.gitignore b/.gitignore index e24ea199..47ffcf73 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ betterseqtaplus-safari/ .parcel-cache .env .env.submit -dependency-graph.svg \ No newline at end of file +dependency-graph.svg + diff --git a/bun.lock b/bun.lock index 1231b0fe..d02ee205 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "betterseqtaplus", "dependencies": { - "@bedframe/core": "^0.0.46", + "@bedframe/core": "^0.1.0", "@codemirror/autocomplete": "^6.18.6", "@codemirror/commands": "^6.8.0", "@codemirror/lang-css": "^6.3.1", @@ -13,7 +13,7 @@ "@codemirror/search": "^6.5.10", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.36.4", - "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/forms": "^0.5.10", "@tsconfig/svelte": "^5.0.4", "@types/chrome": "^0.1.4", @@ -52,17 +52,17 @@ "react-dom": "17", "rss-parser": "^3.13.0", "sortablejs": "^1.15.6", - "svelte": "^5.22.6", + "svelte": "^5.46.4", "typescript": "^5.8.2", "uuid": "^11.1.0", - "vite": "^6.2.1", + "vite": "^8.0.5", "webextension-polyfill": "^0.12.0", }, "devDependencies": { "@babel/plugin-transform-runtime": "^7.26.9", "@babel/runtime": "^7.26.9", - "@bedframe/cli": "^0.0.95", - "@crxjs/vite-plugin": "^2.2.0", + "@bedframe/cli": "^0.1.2", + "@crxjs/vite-plugin": "^2.4.0", "@types/mime-types": "^3.0.1", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", @@ -127,9 +127,9 @@ "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "7.27.1", "@babel/helper-validator-identifier": "7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], - "@bedframe/cli": ["@bedframe/cli@0.0.95", "", { "dependencies": { "@bedframe/core": "0.0.46", "commander": "14.0.0", "execa": "9.6.0", "kolorist": "1.8.0", "listr": "0.14.3", "nanospinner": "1.2.2", "node-fetch": "3.3.2", "pkg-install": "1.0.0", "prompts": "2.4.2", "vite": "6.3.5" }, "peerDependencies": { "concurrently": "8.2.2" }, "bin": { "bedframe": "dist/bedframe.js", "create-bedframe": "dist/create-bedframe.js" } }, "sha512-WSb0HhHCfH7/tS5dDC/HL/VgKrIFGLmI0OesmcwQntrJdHVirtjrDVjcTFG3lC3LB5Ax85P1CY50Gy5aDNc0sQ=="], + "@bedframe/cli": ["@bedframe/cli@0.1.2", "", { "dependencies": { "@bedframe/core": "0.1.0", "commander": "^14.0.2", "execa": "^9.6.1", "kolorist": "^1.8.0", "listr": "^0.14.3", "nanospinner": "^1.2.2", "node-fetch": "^3.3.2", "pkg-install": "^1.0.0", "prompts": "^2.4.2", "vite": "^6.4.1" }, "peerDependencies": { "concurrently": "^8.2.1" }, "bin": { "bedframe": "dist/bedframe.js", "create-bedframe": "dist/create-bedframe.js" } }, "sha512-nu0VSfGLhY9f62w+fDRQi2YnfoY9c6u28ZlJ8rH6f57ItLo5TNrZetfw37fYNnh8yK2RSAWU7+6KCkdVm0Fokg=="], - "@bedframe/core": ["@bedframe/core@0.0.46", "", { "dependencies": { "@crxjs/vite-plugin": "2.0.2" }, "peerDependencies": { "vite-plugin-dts": "3.9.1", "vite-plugin-externalize-deps": "0.7.0", "vitest": "0.34.6" } }, "sha512-cOshFUrBksWnVQ08chunlvAetwhuytkX7NdH6blNNylYzsgCaLGBbCJ2EZ0d18kimFVNZoODrc+812if5dio/w=="], + "@bedframe/core": ["@bedframe/core@0.1.0", "", { "dependencies": { "@crxjs/vite-plugin": "2.3.0" }, "peerDependencies": { "vite-plugin-dts": "^3.7.0", "vite-plugin-externalize-deps": "^0.7.0", "vitest": "^0.34.6" } }, "sha512-bM9vuYG67m9lVTui966AmkoxPPdEHEDOKKjzAWV/Ymgur818fRhMMpblx3+PLs8kTCek1m79fjYKoE8PhqJ22g=="], "@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.6", "", { "dependencies": { "@codemirror/language": "6.11.3", "@codemirror/state": "6.5.2", "@codemirror/view": "6.38.1", "@lezer/common": "1.2.3" } }, "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg=="], @@ -147,7 +147,13 @@ "@codemirror/view": ["@codemirror/view@6.38.1", "", { "dependencies": { "@codemirror/state": "6.5.2", "crelt": "1.0.6", "style-mod": "4.1.2", "w3c-keyname": "2.2.8" } }, "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ=="], - "@crxjs/vite-plugin": ["@crxjs/vite-plugin@2.2.0", "", { "dependencies": { "@rollup/pluginutils": "4.2.1", "@webcomponents/custom-elements": "1.6.0", "acorn-walk": "8.3.4", "cheerio": "1.1.2", "convert-source-map": "1.9.0", "debug": "4.4.1", "es-module-lexer": "0.10.5", "fast-glob": "3.3.3", "fs-extra": "10.1.0", "jsesc": "3.1.0", "magic-string": "0.30.18", "pathe": "2.0.3", "picocolors": "1.1.1", "react-refresh": "0.13.0", "rollup": "2.79.2", "rxjs": "7.5.7" } }, "sha512-HpT1GLbUQy42nlpN4sGzFgulacBraMM778s8Q+oPo4cb26DwO9tTwdndlvAS8fe6vEProFXvbdt37objp/0IQA=="], + "@crxjs/vite-plugin": ["@crxjs/vite-plugin@2.4.0", "", { "dependencies": { "@rollup/pluginutils": "^4.1.2", "@webcomponents/custom-elements": "^1.5.0", "acorn-walk": "^8.2.0", "convert-source-map": "^1.7.0", "debug": "^4.3.3", "es-module-lexer": "^0.10.0", "fast-glob": "^3.2.11", "fs-extra": "^10.0.1", "jsesc": "^3.0.2", "magic-string": "^0.30.12", "node-html-parser": "^7.0.2", "pathe": "^2.0.1", "picocolors": "^1.1.1", "react-refresh": "^0.13.0", "rollup": "2.79.2", "rxjs": "7.5.7" }, "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-bDLdq0W2V1SkMQDJjrcYyjK9/uKtdl4joT7GRImcootCjZdKRiRYt+cv9z8tJoU/tK3o1lX48LTqN7JMsk5AQg=="], + + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], @@ -293,12 +299,16 @@ "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.89", "", { "os": "win32", "cpu": "x64" }, "sha512-WMej0LZrIqIncQcx0JHaMXlnAG7sncwJh7obs/GBgp0xF9qABjwoRwIooMWCZkSansapKGNUHhamY6qEnFN7gA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "1.19.1" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="], + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "1.0.3", "is-glob": "4.0.3", "micromatch": "4.0.8", "node-addon-api": "7.1.1" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], @@ -349,6 +359,38 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + "@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "2.0.2", "picomatch": "2.3.1" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA=="], @@ -409,14 +451,14 @@ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "8.15.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], - "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "4.0.1", "debug": "4.4.1", "deepmerge": "4.3.1", "kleur": "4.1.5", "magic-string": "0.30.18", "vitefu": "1.1.1" }, "peerDependencies": { "svelte": "5.38.6", "vite": "6.3.5" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="], - - "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "5.1.1", "svelte": "5.38.6", "vite": "6.3.5" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.0.0", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g=="], "@tailwindcss/forms": ["@tailwindcss/forms@0.5.10", "", { "dependencies": { "mini-svg-data-uri": "1.4.4" }, "peerDependencies": { "tailwindcss": "3.4.17" } }, "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw=="], "@tsconfig/svelte": ["@tsconfig/svelte@5.0.5", "", {}, "sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], "@types/chai": ["@types/chai@4.3.20", "", {}, "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ=="], @@ -527,7 +569,7 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], "assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], @@ -631,7 +673,7 @@ "colors-named-hex": ["colors-named-hex@1.0.2", "", {}, "sha512-k6kq1e1pUCQvSVwIaGFq2l0LrkAPQZWyeuZn1Z8nOiYSEZiKoFj4qx690h2Kd34DFl9Me0gKS6MUwAMBJj8nuA=="], - "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "complex.js": ["complex.js@2.4.2", "", {}, "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g=="], @@ -693,6 +735,8 @@ "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + "devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="], + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], @@ -773,7 +817,7 @@ "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "5.3.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], - "esrap": ["esrap@2.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA=="], + "esrap": ["esrap@2.2.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig=="], "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "5.3.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], @@ -787,7 +831,7 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "execa": ["execa@9.6.0", "", { "dependencies": { "@sindresorhus/merge-streams": "4.0.0", "cross-spawn": "7.0.6", "figures": "6.1.0", "get-stream": "9.0.1", "human-signals": "8.0.1", "is-plain-obj": "4.1.0", "is-stream": "4.0.1", "npm-run-path": "6.0.0", "pretty-ms": "9.2.0", "signal-exit": "4.1.0", "strip-final-newline": "4.0.0", "yoctocolors": "2.1.2" } }, "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], @@ -995,6 +1039,30 @@ "lie": ["lie@3.1.1", "", { "dependencies": { "immediate": "3.0.6" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -1125,6 +1193,8 @@ "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1143,6 +1213,8 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1.0.2" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -1293,6 +1365,8 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="], + "rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], "rss-parser": ["rss-parser@3.13.0", "", { "dependencies": { "entities": "2.2.0", "xml2js": "0.5.0" } }, "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w=="], @@ -1397,7 +1471,7 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svelte": ["svelte@5.38.6", "", { "dependencies": { "@jridgewell/remapping": "2.3.5", "@jridgewell/sourcemap-codec": "1.5.5", "@sveltejs/acorn-typescript": "1.0.5", "@types/estree": "1.0.8", "acorn": "8.15.0", "aria-query": "5.3.2", "axobject-query": "4.1.0", "clsx": "2.1.1", "esm-env": "1.2.2", "esrap": "2.1.0", "is-reference": "3.0.3", "locate-character": "3.0.0", "magic-string": "0.30.18", "zimmerframe": "1.1.2" } }, "sha512-ltBPlkvqk3bgCK7/N323atUpP3O3Y+DrGV4dcULrsSn4fZaaNnOmdplNznwfdWclAgvSr5rxjtzn/zJhRm6TKg=="], + "svelte": ["svelte@5.55.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ=="], "symbol-observable": ["symbol-observable@1.2.0", "", {}, "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="], @@ -1423,7 +1497,7 @@ "tinycolor2": ["tinycolor2@1.4.2", "", {}, "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="], - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.3" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "tinypool": ["tinypool@0.7.0", "", {}, "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww=="], @@ -1477,7 +1551,7 @@ "validator": ["validator@13.15.15", "", {}, "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="], - "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "0.25.9", "fdir": "6.5.0", "picomatch": "4.0.3", "postcss": "8.5.6", "rollup": "4.49.0", "tinyglobby": "0.2.14" }, "optionalDependencies": { "@types/node": "24.3.0", "fsevents": "2.3.3", "jiti": "1.21.7", "sass": "1.91.0", "yaml": "2.8.1" }, "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], "vite-node": ["vite-node@0.34.6", "", { "dependencies": { "cac": "6.7.14", "debug": "4.4.1", "mlly": "1.8.0", "pathe": "1.1.2", "picocolors": "1.1.1", "vite": "5.4.19" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA=="], @@ -1485,7 +1559,7 @@ "vite-plugin-externalize-deps": ["vite-plugin-externalize-deps@0.7.0", "", { "peerDependencies": { "vite": "6.3.5" } }, "sha512-do2gPrR79Tm8UKcqsw3RTAtN4YO8GkVRBckWdJWINZ3Qdp3KN9S1oyUZxKszTB/iyg4zdOUweLOeBI8t86QVow=="], - "vitefu": ["vitefu@1.1.1", "", { "optionalDependencies": { "vite": "6.3.5" } }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], "vitest": ["vitest@0.34.6", "", { "dependencies": { "@types/chai": "4.3.20", "@types/chai-subset": "1.3.6", "@types/node": "24.3.0", "@vitest/expect": "0.34.6", "@vitest/runner": "0.34.6", "@vitest/snapshot": "0.34.6", "@vitest/spy": "0.34.6", "@vitest/utils": "0.34.6", "acorn": "8.15.0", "acorn-walk": "8.3.4", "cac": "6.7.14", "chai": "4.5.0", "debug": "4.4.1", "local-pkg": "0.4.3", "magic-string": "0.30.18", "pathe": "1.1.2", "picocolors": "1.1.1", "std-env": "3.9.0", "strip-literal": "1.3.0", "tinybench": "2.9.0", "tinypool": "0.7.0", "vite": "5.4.19", "vite-node": "0.34.6", "why-is-node-running": "2.3.0" }, "bin": { "vitest": "vitest.mjs" } }, "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q=="], @@ -1555,7 +1629,9 @@ "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@bedframe/core/@crxjs/vite-plugin": ["@crxjs/vite-plugin@2.0.2", "", { "dependencies": { "@rollup/pluginutils": "4.2.1", "@webcomponents/custom-elements": "1.6.0", "acorn-walk": "8.3.4", "cheerio": "1.1.2", "convert-source-map": "1.9.0", "debug": "4.4.1", "es-module-lexer": "0.10.5", "fast-glob": "3.3.3", "fs-extra": "10.1.0", "jsesc": "3.1.0", "magic-string": "0.30.18", "pathe": "2.0.3", "picocolors": "1.1.1", "react-refresh": "0.13.0", "rollup": "2.79.2", "rxjs": "7.5.7" } }, "sha512-BeaVEkCTmna2tzl5DL9nw1kxll1IpIFZ+wbl2+iILz4fNJy1xRD6c1nF8w8/CvrWUuPYTFTpyX9K+A30ISDXHA=="], + "@bedframe/cli/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + + "@bedframe/core/@crxjs/vite-plugin": ["@crxjs/vite-plugin@2.3.0", "", { "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" } }, "sha512-+0CNVGS4bB30OoaF1vUsHVwWU1Lm7MxI0XWY9Fd/Ob+ZVTZgEFNqJ1ZC69IVwQsoYhY0sMQLvpLWiFIuDz8htg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1591,6 +1667,8 @@ "@samverschueren/stream-to-observable/rxjs": ["rxjs@6.6.7", "", { "dependencies": { "tslib": "1.14.1" } }, "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ=="], + "@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "@vitest/runner/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "@vitest/snapshot/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], @@ -1621,6 +1699,8 @@ "concurrently/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "8.0.1", "escalade": "3.2.0", "get-caller-file": "2.0.5", "require-directory": "2.1.1", "string-width": "4.2.3", "y18n": "5.0.8", "yargs-parser": "21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "dependency-cruiser/commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-json-stable-stringify": "2.1.0", "json-schema-traverse": "0.4.1", "uri-js": "4.4.1" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -1633,6 +1713,8 @@ "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "listr/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], "listr/rxjs": ["rxjs@6.6.7", "", { "dependencies": { "tslib": "1.14.1" } }, "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ=="], @@ -1715,9 +1797,13 @@ "tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "3.1.3", "braces": "3.0.3", "glob-parent": "5.1.2", "is-binary-path": "2.1.0", "is-glob": "4.0.3", "normalize-path": "3.0.0", "readdirp": "3.6.0" }, "optionalDependencies": { "fsevents": "2.3.3" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "vite/rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="], + "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "vite/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "vite-node/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], @@ -1725,6 +1811,8 @@ "vite-plugin-dts/@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "1.0.8", "estree-walker": "2.0.2", "picomatch": "4.0.3" }, "optionalDependencies": { "rollup": "4.49.0" } }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], + "vite-plugin-dts/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "0.25.9", "fdir": "6.5.0", "picomatch": "4.0.3", "postcss": "8.5.6", "rollup": "4.49.0", "tinyglobby": "0.2.14" }, "optionalDependencies": { "@types/node": "24.3.0", "fsevents": "2.3.3", "jiti": "1.21.7", "sass": "1.91.0", "yaml": "2.8.1" }, "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + "vitest/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "vitest/vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "0.21.5", "postcss": "8.5.6", "rollup": "4.49.0" }, "optionalDependencies": { "@types/node": "24.3.0", "fsevents": "2.3.3", "sass": "1.91.0" }, "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="], @@ -1741,6 +1829,14 @@ "z-schema/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "@bedframe/cli/vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "@bedframe/cli/vite/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], + + "@bedframe/cli/vite/rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="], + + "@bedframe/core/@crxjs/vite-plugin/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -1865,6 +1961,10 @@ "vite-plugin-dts/@rollup/pluginutils/rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="], + "vite-plugin-dts/vite/rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="], + + "vite-plugin-dts/vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.3" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "vitest/vite/esbuild": ["esbuild@0.21.5", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], "vitest/vite/rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="], diff --git a/docs/CLOUD_SETTINGS_SYNC_SERVER.md b/docs/CLOUD_SETTINGS_SYNC_SERVER.md index c9c085af..d9b502e8 100644 --- a/docs/CLOUD_SETTINGS_SYNC_SERVER.md +++ b/docs/CLOUD_SETTINGS_SYNC_SERVER.md @@ -35,13 +35,16 @@ Upserts the caller’s settings backup. ```json { "schemaVersion": 1, + "themeId": "uuid-string-or-empty", "data": { - "...": "flat key-value map mirroring extension storage (see Payload shape)" + "...": "flat key-value map mirroring extension storage (see Payload shape)", + "selectedTheme": "uuid-or-empty-string" } } ``` - **`schemaVersion`**: integer. The extension currently sends `1`. The server may reject unknown major versions or store it for future migrations. +- **`themeId`**: optional but recommended duplicate of **`data.selectedTheme`**. Should be the UUID of the **installed** BetterSEQTA store theme (`selectedTheme`). This may be a normal theme id **or** a **flavour (slave) variant** id from themes with **`flavours[]`** — the extension uses it after restore to prefetch `theme.json` when missing locally (same **`GET …/themes/{id}/download`** as the store UI). Persist and return **`themeId`** in sync with **`data.selectedTheme`**. - **`data`**: object whose keys are storage keys (strings) and values are JSON-serializable values (same types as stored in `chrome.storage.local`). **Success response:** HTTP `200` (or `201` if you prefer create semantics). Example: @@ -67,15 +70,19 @@ Returns the caller’s latest settings backup. ```json { "schemaVersion": 1, + "themeId": "uuid-string-or-empty", "data": { }, "updated_at": "2026-04-07T12:00:00.000Z" } ``` - **`data`**: required for restore; must be the same shape as accepted in `PUT` (flat map of storage keys). +- **`themeId`**: optional; if present must match **`data.selectedTheme`** (see `PUT`). - **`schemaVersion`**: optional but recommended; should match what was stored. - **`updated_at`**: optional; included for UX if the client shows “last backup” time. +The extension resolves **`themeId`** (if non-empty), else **`data.selectedTheme`,** to [`resolveThemeIdForPostSyncDownload`](../src/seqta/utils/cloudSettingsSync.ts) after downloading the envelope — used only to reinstall theme assets from **`betterseqta.org`** when IndexedDB lacks that id (see **BetterSEQTA Cloud** flavour note in **[THEME_STORE_FLAVOURS_API](./THEME_STORE_FLAVOURS_API.md)** section “Cloud settings sync compatibility”). + **No backup yet:** HTTP **`404`**. The extension treats this as “nothing in the cloud” and shows an error to the user. **Error responses:** `401` if the token is invalid, etc. @@ -128,6 +135,6 @@ This uses standard **WebExtension** APIs (`browser.alarms`, `runtime` messages, ## Client reference (extension) -- Upload / dev export: `buildUploadPayload` / `getSnapshotForUpload` in `src/seqta/utils/cloudSettingsSync.ts` (strips OAuth-related keys, sensitive device keys, and **`bsplus_cloud_settings_known_remote_updated_at`**). -- Download: `applyDownloadedEnvelope` after `GET`; local auth keys, sensitive device keys, and the client-only watermark key are merged back after `chrome.storage.local.clear()`. +- Upload / dev export: `buildUploadPayload` / `getSnapshotForUpload` in `src/seqta/utils/cloudSettingsSync.ts` (strips OAuth-related keys, sensitive device keys, **`bsplus_pending_theme_ensure_after_cloud`**, and **`bsplus_cloud_settings_known_remote_updated_at`** — includes **`themeId`** aligned with **`selectedTheme`**). +- Download: resolve id via **`resolveThemeIdForPostSyncDownload`** → **`applyDownloadedEnvelope`** after `GET` → prefetch theme blobs in page context if needed (**`prepareThemeAfterCloudSync`** in **`ThemeManager`**) → reload SEQTA tabs; local auth keys, sensitive device keys, client-only watermark, and **`bsplus_pending_theme_ensure_after_cloud`** semantics preserved as documented above. - Auto sync (summary, debounced upload, alarms): `src/background/cloudSettingsAutoSync.ts`; content script triggers a poll on each verified SEQTA Learn/Engage page load (top frame) via `cloudSettingsPoll`. diff --git a/package.json b/package.json index 72bdfc2a..27826d94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betterseqtaplus", - "version": "3.6.3", + "version": "3.6.4", "type": "module", "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development and add heaps more features!", "browserslist": "> 0.5%, last 2 versions, not dead", diff --git a/src/background.ts b/src/background.ts index d0926ae7..32f22083 100644 --- a/src/background.ts +++ b/src/background.ts @@ -5,6 +5,7 @@ import { initCloudSettingsAutoSync, performCloudSettingsDownloadWithRetry, performCloudSettingsUploadWithRetry, + requestCloudSettingsDebouncedUpload, runCloudSettingsPoll, } from "./background/cloudSettingsAutoSync"; @@ -346,6 +347,10 @@ const MESSAGE_HANDLERS: Record = { void runCloudSettingsPoll(); return false; }, + cloudSettingsRequestDebouncedUpload: () => { + requestCloudSettingsDebouncedUpload(); + return false; + }, getSeqtaSession: (req: { baseUrl?: string }, sendResponse: MessageSender, sender?: browser.Runtime.MessageSender) => { (async () => { try { diff --git a/src/background/cloudSettingsAutoSync.ts b/src/background/cloudSettingsAutoSync.ts index a75de192..1ac8433f 100644 --- a/src/background/cloudSettingsAutoSync.ts +++ b/src/background/cloudSettingsAutoSync.ts @@ -3,8 +3,10 @@ import { applyDownloadedEnvelope, buildUploadPayload, BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY, + BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY, CLOUD_SETTINGS_SYNC_SCHEMA_VERSION, isKeyIncludedInCloudUploadPayload, + resolveThemeIdForPostSyncDownload, setKnownRemoteUpdatedAt, } from "@/seqta/utils/cloudSettingsSync"; @@ -220,7 +222,15 @@ async function getSettingsAndApplyOnce(token: string): Promise { error: data?.error ?? `Download failed (${r.status})`, }; } + const themeIdToEnsure = resolveThemeIdForPostSyncDownload(data); await applyDownloadedEnvelope(data); + if (themeIdToEnsure) { + await browser.storage.local.set({ + [BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY]: themeIdToEnsure, + }); + } else { + await browser.storage.local.remove(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY); + } reloadSeqtaPagesFn?.(); const updated_at = data?.updated_at as string | undefined; await setKnownRemoteUpdatedAt(updated_at); @@ -352,6 +362,17 @@ function scheduleDebouncedUpload(): void { }, UPLOAD_DEBOUNCE_MS); } +/** Call after store theme install (and similar) so cloud upload runs even if storage events are flaky. */ +export function requestCloudSettingsDebouncedUpload(): void { + void (async () => { + const all = (await browser.storage.local.get()) as Record; + if (!isAutoCloudSyncEnabled(all)) return; + if (suppressAutoUploadDuringRestore) return; + if (!(await getAccessToken())) return; + scheduleDebouncedUpload(); + })(); +} + async function runDebouncedUploadJob(): Promise { const all = (await browser.storage.local.get()) as Record; if (!isAutoCloudSyncEnabled(all)) return; diff --git a/src/css/injected.scss b/src/css/injected.scss index 261552c2..9c5fe51c 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -1,3 +1,4 @@ + @use "sass:meta"; @import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,600,700"); @@ -2609,7 +2610,7 @@ body { [class*="MessageList__unread___"] { position: relative; - background: rgb(228 225 225); + background: var(--background-secondary, rgb(228 225 225)); } .dark [class*="MessageList__unread___"] { @@ -2735,7 +2736,7 @@ body { [class*="MessageList__MessageList___"] > ol > li[class*="MessageList__selected___"] { - background: rgb(228 225 225); + background: var(--background-secondary, rgb(228 225 225)); color: var(--text-primary); box-shadow: none !important; position: relative; diff --git a/src/interface/components/store/CoverSwiper.svelte b/src/interface/components/store/CoverSwiper.svelte index 0b1a2c08..cd6eb126 100644 --- a/src/interface/components/store/CoverSwiper.svelte +++ b/src/interface/components/store/CoverSwiper.svelte @@ -1,10 +1,13 @@ -{#if coverThemes.length > 0} +{#if slides.length > 0}
-
- {#each coverThemes as theme} + {#each slides as slide (slide.imageUrl + slide.title + (slide.subtitle ?? ''))}
{ if (e.key === 'Enter') setDisplayTheme(theme) }} - onclick={() => setDisplayTheme(theme)} + onkeydown={(e) => { + if (e.key === 'Enter') setDisplayTheme(slide.openTheme); + }} + onclick={() => setDisplayTheme(slide.openTheme)} > - Theme Preview - {#if theme.featured === true} + + {#if slide.badgeFeatured === true}
- + Featured
{/if} -
-

{theme.name}

- {#if theme.author} -

By {theme.author}

+
+

{slide.title}

+ {#if slide.subtitle} +

{slide.subtitle}

+ {/if} + {#if slide.openTheme.author && !slide.subtitle} +

By {slide.openTheme.author}

+ {/if} + {#if slide.openTheme.description && !slide.subtitle} +

{slide.openTheme.description}

{/if} -

{theme.description}

-
+
{/each}
- -
- - -
-
-

- {theme.name} -

- {#if theme.featured === true} - - - - - Featured - - {/if} -
- {#if theme.author} -

- By {theme.author} -

- {/if} -
- - - - - {(theme.download_count ?? 0).toLocaleString()} downloads - - - - - - {(theme.favorite_count ?? 0).toLocaleString()} favorites - -
- Theme Cover -

- {theme.description} -

-
- {#if toggleFavorite && theme} + +
+ +
+ - {/if} - {#if currentThemes.includes(theme.id)} - - {:else} - - {/if} -
- {#if relatedThemes.length > 0} -
+ {'\ued8a'} + + -

- Related themes -

-
- {#each relatedThemes as relatedTheme (relatedTheme.id)} - - {/each}
- {/if} -
- {:else} -
- + +
+ +

+ + {theme.name} + +

+ + {#if theme.featured === true} + + + + + + + + + + Featured + + + + {/if} + +
+ + {#if theme.author} + +

+ + By {theme.author} + +

+ + {/if} + +
+ + + + + + + + + + {modalDisplayDownloadCount.toLocaleString()} downloads + + + + + + + + + + + + {(theme.favorite_count ?? 0).toLocaleString()} favorites + + + +
+ + + + {#if heroSlides.length > 0} + + {#key theme?.id} + +
+ +
+ +
+ + {#each heroSlides as slide, slideIdx (slideIdx)} + +
+ + {slide.caption} + +
+ + {/each} + +
+ +
+ + {#if heroSlides.length > 1} + +
+ + + + + +
+ + {/if} + +
+ + {/key} + + {/if} + + + + {#if hasFlavours} + + {@const masterThumb = masterCarouselImageUrl(theme)} + +

Variants

+ +
+ + {#if currentThemes.includes(theme.id)} + + + + {:else} + + + + {/if} + + {#each theme.flavours ?? [] as f, flavourIdx (f.id)} + + {@const thumb = flavourCarouselImageUrl(f)} + + {#if currentThemes.includes(f.id)} + + + + {:else} + + + + {/if} + + {/each} + +
+ + {/if} + + + +

+ + {theme.description} + +

+ + + +
+ + {#if toggleFavorite && theme} + + + + {/if} + + + + {#if !hasFlavours} + + {#if currentThemes.includes(theme.id)} + + + + {:else} + + + + {/if} + + {/if} + +
+ + + + {#if relatedThemes.length > 0} + +
+ + + +

+ + Related themes + +

+ +
+ + {#each relatedThemes as relatedTheme (relatedTheme.id)} + + + + {/each} + +
+ + {/if} +
+ + {:else} + +
+ + + +
+ {/if} +
+
+ + + diff --git a/src/interface/pages/store.svelte b/src/interface/pages/store.svelte index d9689bd4..92bcef14 100644 --- a/src/interface/pages/store.svelte +++ b/src/interface/pages/store.svelte @@ -7,6 +7,7 @@ import SkeletonLoader from '../components/SkeletonLoader.svelte'; import { settingsState } from '@/seqta/utils/listeners/SettingsState' import type { Theme } from '../types/Theme' + import { visibleStoreThemes, buildCoverSlidesForThemes } from '@/interface/utils/themeStoreFlavours' import browser from 'webextension-polyfill' import ThemeModal from '../components/store/ThemeModal.svelte' import Header from '../components/store/Header.svelte' @@ -26,7 +27,12 @@ // State variables let searchTerm = $state(''); let themes = $state([]); - let coverThemes = $state([]); + + /** Grid/search/cover: hides flat-listed slaves when API sends them */ + let listThemes = $derived(visibleStoreThemes(themes)); + + /** Cover marquee slides (master + flavour imagery for top masters) */ + let coverSlides = $derived(buildCoverSlidesForThemes(listThemes.slice(0, 3))); let loading = $state(true); let darkMode = $state(false); let displayTheme = $state(null); @@ -108,7 +114,6 @@ throw new Error(data?.error || 'Failed to fetch themes'); } themes = [...data.data.themes].sort(compareStoreThemes); - coverThemes = themes.slice(0, 3); loading = false; } catch (err) { @@ -128,13 +133,36 @@ // Filter themes (list is already featured-first, then newest; filter preserves order) let filteredThemes = $derived( - themes.filter( + listThemes.filter( (theme) => theme.name.toLowerCase().includes(searchTerm.toLowerCase()) || theme.description.toLowerCase().includes(searchTerm.toLowerCase()), ), ); + async function installThemeFromStore(themeId: string, meta: Theme) { + const fullRow = themes.find((x) => x.id === themeId); + if (fullRow) { + await themeManager.downloadTheme(fullRow); + } else { + const flavour = meta.flavours?.find((f) => f.id === themeId); + await themeManager.downloadTheme({ + id: themeId, + name: flavour?.name ?? meta.name, + } as Theme); + } + await themeManager.setTheme(themeId); + themeUpdates.triggerUpdate(); + await fetchCurrentThemes(); + void browser.runtime.sendMessage({ type: 'cloudSettingsRequestDebouncedUpload' }).catch(() => {}); + } + + async function removeThemeFromStore(themeId: string) { + await themeManager.deleteTheme(themeId); + themeUpdates.triggerUpdate(); + await fetchCurrentThemes(); + } + $effect(() => { loadBackground(); selectedBackground @@ -172,12 +200,13 @@ {#if activeTab === 'themes'} {#if searchTerm === ''} - + {/if} (showSignInOverlay = true)} - onInstall={async () => { - if (displayTheme) { - await themeManager.downloadTheme(displayTheme); - await themeManager.setTheme(displayTheme.id); - themeUpdates.triggerUpdate(); - await fetchCurrentThemes(); - } + onInstall={async (themeId: string) => { + if (displayTheme) await installThemeFromStore(themeId, displayTheme); }} - onRemove={async () => { - if (displayTheme?.id) { - console.debug('deleting theme', displayTheme.id); - await themeManager.deleteTheme(displayTheme.id); - themeUpdates.triggerUpdate(); - await fetchCurrentThemes(); - } + onRemove={async (themeId: string) => { + console.debug('deleting theme', themeId); + await removeThemeFromStore(themeId); }} /> {/if} diff --git a/src/interface/types/Theme.ts b/src/interface/types/Theme.ts index 407744ef..2c3c7268 100644 --- a/src/interface/types/Theme.ts +++ b/src/interface/types/Theme.ts @@ -1,3 +1,17 @@ +export type ThemeRole = "standard" | "master" | "slave"; + +/** List/detail metadata for variants of a master theme (full theme.json fetched at install by id). */ +export type ThemeFlavour = { + id: string; + name: string; + /** Mirrors theme.json accent (e.g. defaultColour); used for install picker buttons */ + accent_color: string; + cover_image: string; + marquee_image?: string; + /** Per-variant installs when slaves are not returned as flat `theme_role` rows */ + download_count?: number; +}; + export type Theme = { id: string; name: string; @@ -15,4 +29,22 @@ export type Theme = { created_at?: number; /** Unix seconds — last server update (GET /api/themes). */ updated_at?: number; + /** Omitted / `standard` — show in grid. `slave` hides from grid. `master` can list `flavours`. */ + theme_role?: ThemeRole; + /** Present when `theme_role === "slave"` and API returns a flat list during migration */ + master_id?: string; + /** Variants nested on master rows; installs use flavour `id` */ + flavours?: ThemeFlavour[]; +}; + +/** One marquee slide (cover hero or modal carousel). */ +export type ThemeCoverSlide = { + imageUrl: string; + /** Main line — usually master name */ + title: string; + /** Subline — flavour name when applicable */ + subtitle?: string; + /** Opening the modal uses this theme (always the grid row / master object) */ + openTheme: Theme; + badgeFeatured?: boolean; }; diff --git a/src/interface/utils/themeStoreFlavours.ts b/src/interface/utils/themeStoreFlavours.ts new file mode 100644 index 00000000..e6c55cd4 --- /dev/null +++ b/src/interface/utils/themeStoreFlavours.ts @@ -0,0 +1,165 @@ +import type { Theme, ThemeCoverSlide, ThemeFlavour } from "@/interface/types/Theme"; + +export function isHiddenStoreTheme(theme: Theme): boolean { + return theme.theme_role === "slave"; +} + +/** Grid and search: omit slave rows (when API sends a flattened list). */ +export function visibleStoreThemes(themes: Theme[]): Theme[] { + return themes.filter((t) => !isHiddenStoreTheme(t)); +} + +function marqueeOrCoverUrl(t: { marqueeImage?: string; coverImage: string }): string { + return t.marqueeImage || t.coverImage; +} + +/** + * Builds slides for CoverSwiper: for each top-N master, first master image then each flavour image. + */ +export function buildCoverSlidesForThemes(topThemes: Theme[]): ThemeCoverSlide[] { + const slides: ThemeCoverSlide[] = []; + for (const theme of topThemes) { + const flavours = theme.flavours ?? []; + if (flavours.length === 0) { + slides.push({ + imageUrl: marqueeOrCoverUrl(theme), + title: theme.name, + subtitle: theme.author ? `By ${theme.author}` : undefined, + openTheme: theme, + badgeFeatured: theme.featured === true, + }); + continue; + } + slides.push({ + imageUrl: marqueeOrCoverUrl(theme), + title: theme.name, + subtitle: theme.author ? `By ${theme.author}` : undefined, + openTheme: theme, + badgeFeatured: theme.featured === true, + }); + for (const f of flavours) { + slides.push({ + imageUrl: f.marquee_image || f.cover_image, + title: theme.name, + subtitle: f.name, + openTheme: theme, + badgeFeatured: theme.featured === true, + }); + } + } + return slides; +} + +export type ModalHeroSlide = { imageUrl: string; caption: string }; + +/** Preview image for carousel + flavour picker (matches hero slide order after master slide). */ +export function flavourCarouselImageUrl(f: ThemeFlavour): string { + const u = (f.marquee_image || f.cover_image || "").trim(); + return u; +} + +/** Preview image for master variant tile (modal hero slide 0). */ +export function masterCarouselImageUrl(t: Theme): string { + return (marqueeOrCoverUrl(t) || "").trim(); +} + +/** + * Ordered preview URLs for the store grid card rotator: master first, then each variant. + * Uses nested `flavours` when present; otherwise flat `slave` rows (same order as modal when possible). + */ +export function gridCardPreviewImageUrls(theme: Theme, allStoreRows?: Theme[]): string[] { + const urls: string[] = []; + const push = (raw: string) => { + const u = raw.trim(); + if (u && !urls.includes(u)) urls.push(u); + }; + + push(marqueeOrCoverUrl(theme) || ""); + + const flavours = theme.flavours ?? []; + if (flavours.length > 0) { + for (const f of flavours) { + push(flavourCarouselImageUrl(f)); + } + return urls.length > 0 ? urls : [(theme.coverImage || "").trim()].filter(Boolean); + } + + if (allStoreRows) { + const slaves = allStoreRows + .filter((t) => t.theme_role === "slave" && t.master_id === theme.id) + .sort((a, b) => a.id.localeCompare(b.id)); + for (const s of slaves) { + push((marqueeOrCoverUrl(s) || "").trim()); + } + } + + if (urls.length > 0) return urls; + const fallback = (theme.coverImage || "").trim(); + return fallback ? [fallback] : []; +} + +/** + * Downloads shown on the grid card for a master row: master's count plus each slave + * (flat `theme_role === "slave"` with `master_id`) and any flavour-only `download_count` + * when there is no matching flat slave id (nested-only API shape). + */ +export function masterGridDisplayDownloadCount(master: Theme, allStoreRows: Theme[]): number { + let total = master.download_count ?? 0; + const slaveRows = allStoreRows.filter( + (t) => t.theme_role === "slave" && t.master_id === master.id, + ); + const countedIds = new Set(slaveRows.map((r) => r.id)); + for (const r of slaveRows) { + total += r.download_count ?? 0; + } + for (const f of master.flavours ?? []) { + if (countedIds.has(f.id)) continue; + total += f.download_count ?? 0; + } + return total; +} + +/** Modal hero: master first, then each flavour (plan order). */ +export function buildModalHeroSlides(theme: Theme): ModalHeroSlide[] { + const slides: ModalHeroSlide[] = []; + slides.push({ + imageUrl: marqueeOrCoverUrl(theme), + caption: theme.name, + }); + const flavours = theme.flavours ?? []; + for (const f of flavours) { + slides.push({ + imageUrl: f.marquee_image || f.cover_image, + caption: `${theme.name} — ${f.name}`, + }); + } + return slides; +} + +/** + * Relative luminance 0–1 for rgba/rgb/hex-ish strings; fallback 0.5 → white text + */ +export function pickContrastingTextColor(backgroundCss: string): "#ffffff" | "#0a0a0a" { + const s = backgroundCss.trim(); + const rgba = s.match( + /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/i, + ); + if (rgba) { + const r = Number(rgba[1]) / 255; + const g = Number(rgba[2]) / 255; + const b = Number(rgba[3]) / 255; + const a = rgba[4] !== undefined ? Number(rgba[4]) : 1; + const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; + const effective = lum * a + 0.05 * (1 - a); + return effective > 0.45 ? "#0a0a0a" : "#ffffff"; + } + const hex = s.match(/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i); + if (hex) { + const r = parseInt(hex[1], 16) / 255; + const g = parseInt(hex[2], 16) / 255; + const b = parseInt(hex[3], 16) / 255; + const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return lum > 0.45 ? "#0a0a0a" : "#ffffff"; + } + return "#ffffff"; +} diff --git a/src/plugins/built-in/messageFolders/index.ts b/src/plugins/built-in/messageFolders/index.ts new file mode 100644 index 00000000..e8170e86 --- /dev/null +++ b/src/plugins/built-in/messageFolders/index.ts @@ -0,0 +1,748 @@ +import type { Plugin } from "../../core/types"; +import { booleanSetting } from "@/plugins/core/settingsHelpers"; +import { waitForElm } from "@/seqta/utils/waitForElm"; +import styles from "./styles.css?inline"; + +const messageFoldersSettings = { + showTagsInAllMessages: booleanSetting({ + default: true, + title: "Show folder tags in All Messages", + description: + "When off, folder tags are not shown on the message list until you select a folder.", + }), + hideFolderedMessagesInAll: booleanSetting({ + default: true, + title: "Hide foldered messages in All Messages", + description: + "When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.", + }), +} as const; + +interface Folder { + id: string; + name: string; + color: string; +} + +interface MessageFoldersStorage { + folders: Folder[]; + messageAssignments: Record; +} + +const FOLDER_COLORS = [ + "#3b82f6", "#ef4444", "#22c55e", "#f59e0b", + "#8b5cf6", "#ec4899", "#14b8a6", "#f97316", +]; + +const FOLDER_ICON_SVG = ``; +const PLUS_SVG = ``; +const CHECK_SVG_WHITE = ``; +const CLOSE_SVG = ``; +const EDIT_SVG = ``; +const TRASH_SVG = ``; + +function generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); +} + +const messageFoldersPlugin: Plugin = { + id: "messageFolders", + name: "Message Folders", + description: "Organize direct messages into custom folders", + version: "1.0.0", + settings: messageFoldersSettings, + disableToggle: true, + defaultEnabled: true, + + run: async (api) => { + const styleEl = document.createElement("style"); + styleEl.textContent = styles; + document.head.appendChild(styleEl); + + await api.storage.loaded; + + if (!api.storage.folders) api.storage.folders = []; + if (!api.storage.messageAssignments) api.storage.messageAssignments = {}; + + let activeFolderId: string | null = null; + let messageListObserver: MutationObserver | null = null; + let sidebarObserver: MutationObserver | null = null; + let actionsObserver: MutationObserver | null = null; + let openDropdown: HTMLElement | null = null; + let dropdownCloseHandler: ((e: MouseEvent) => void) | null = null; + const unregisters: Array<{ unregister: () => void }> = []; + + // ── Storage accessors ── + + const getFolders = (): Folder[] => api.storage.folders ?? []; + const getAssignments = (): Record => api.storage.messageAssignments ?? {}; + + const saveFolders = (folders: Folder[]) => { + api.storage.folders = [...folders]; + }; + + const saveAssignments = (assignments: Record) => { + api.storage.messageAssignments = { ...assignments }; + }; + + const getMessageFolderIds = (messageId: string): string[] => { + const assignments = getAssignments(); + const ids: string[] = []; + for (const [folderId, msgIds] of Object.entries(assignments)) { + if (msgIds.includes(messageId)) ids.push(folderId); + } + return ids; + }; + + const toggleMessageInFolder = (messageId: string, folderId: string) => { + const assignments = getAssignments(); + if (!assignments[folderId]) assignments[folderId] = []; + const idx = assignments[folderId].indexOf(messageId); + if (idx >= 0) { + assignments[folderId].splice(idx, 1); + } else { + assignments[folderId].push(messageId); + } + saveAssignments(assignments); + }; + + const getFolderMessageCount = (folderId: string): number => { + return (getAssignments()[folderId] ?? []).length; + }; + + const restoreSubjectPlain = (subject: Element) => { + subject.querySelector(".bsplus-msg-badges")?.remove(); + const textWrap = subject.querySelector(".bsplus-subject-text"); + if (textWrap) { + subject.textContent = textWrap.textContent ?? ""; + } + }; + + const isMessageInAnyCustomFolder = (messageId: string): boolean => { + for (const msgIds of Object.values(getAssignments())) { + if (msgIds.includes(messageId)) return true; + } + return false; + }; + + const shouldShowBadgesInList = (): boolean => { + return api.settings.showTagsInAllMessages || activeFolderId !== null; + }; + + // ── Confirm modal ── + + const showConfirmModal = ( + title: string, + message: string, + onConfirm: () => void, + ) => { + const overlay = document.createElement("div"); + overlay.className = "bsplus-modal-overlay"; + + const modal = document.createElement("div"); + modal.className = "bsplus-modal"; + modal.innerHTML = ` +

${title}

+

${message}

+
+ + +
+ `; + overlay.appendChild(modal); + + const remove = () => { + overlay.remove(); + document.removeEventListener("keydown", onKey); + }; + + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") remove(); + }; + + overlay.addEventListener("click", (e) => { + if (e.target === overlay) remove(); + }); + modal.querySelector(".bsplus-modal-btn-cancel")!.addEventListener("click", remove); + modal.querySelector(".bsplus-modal-btn-danger")!.addEventListener("click", () => { + onConfirm(); + remove(); + }); + + document.body.appendChild(overlay); + document.addEventListener("keydown", onKey); + }; + + // ── Sidebar folder UI ── + + const renderSidebarFolders = () => { + const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); + if (!sidebar) return; + + const ol = sidebar.querySelector("ol"); + if (!ol) return; + + let section = ol.querySelector(".bsplus-folders-section"); + if (!section) { + section = document.createElement("div"); + section.className = "bsplus-folders-section"; + ol.appendChild(section); + } + + const folders = getFolders(); + const existingInput = section.querySelector(".bsplus-folder-input"); + const existingColors = section.querySelector(".bsplus-folder-colors"); + + section.innerHTML = ""; + + // Header + const header = document.createElement("div"); + header.className = "bsplus-folders-header"; + + const label = document.createElement("span"); + label.textContent = "Folders"; + header.appendChild(label); + + const addBtn = document.createElement("button"); + addBtn.className = "bsplus-folders-add-btn"; + addBtn.title = "New folder"; + addBtn.innerHTML = PLUS_SVG; + addBtn.addEventListener("click", (e) => { + e.stopPropagation(); + showNewFolderInput(section!); + }); + header.appendChild(addBtn); + section.appendChild(header); + + // "All Messages" item + const allItem = document.createElement("div"); + allItem.className = `bsplus-folder-item${activeFolderId === null ? " bsplus-folder-active" : ""}`; + allItem.innerHTML = ` + + All Messages + `; + allItem.addEventListener("click", () => { + activeFolderId = null; + applyFolderFilter(); + applyBadges(); + renderSidebarFolders(); + }); + section.appendChild(allItem); + + // Folder items + for (const folder of folders) { + const item = document.createElement("div"); + item.className = `bsplus-folder-item${activeFolderId === folder.id ? " bsplus-folder-active" : ""}`; + item.dataset.folderId = folder.id; + + const dot = document.createElement("div"); + dot.className = "bsplus-folder-dot"; + dot.style.background = folder.color; + item.appendChild(dot); + + const name = document.createElement("span"); + name.className = "bsplus-folder-name"; + name.textContent = folder.name; + item.appendChild(name); + + const actions = document.createElement("div"); + actions.className = "bsplus-folder-actions"; + + const editBtn = document.createElement("button"); + editBtn.className = "bsplus-folder-action-btn"; + editBtn.title = "Rename"; + editBtn.innerHTML = EDIT_SVG; + editBtn.addEventListener("click", (e) => { + e.stopPropagation(); + showEditFolderInput(section!, folder); + }); + actions.appendChild(editBtn); + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "bsplus-folder-action-btn"; + deleteBtn.title = "Delete"; + deleteBtn.innerHTML = TRASH_SVG; + deleteBtn.addEventListener("click", (e) => { + e.stopPropagation(); + showConfirmModal( + "Delete folder", + `Remove "${folder.name}"? Messages won't be deleted.`, + () => { + const folders = getFolders().filter((f) => f.id !== folder.id); + saveFolders(folders); + const assignments = getAssignments(); + delete assignments[folder.id]; + saveAssignments(assignments); + if (activeFolderId === folder.id) activeFolderId = null; + applyFolderFilter(); + applyBadges(); + renderSidebarFolders(); + }, + ); + }); + actions.appendChild(deleteBtn); + + item.appendChild(actions); + + const count = document.createElement("span"); + count.className = "bsplus-folder-count"; + const c = getFolderMessageCount(folder.id); + count.textContent = c > 0 ? String(c) : ""; + item.appendChild(count); + + item.addEventListener("click", () => { + activeFolderId = folder.id; + applyFolderFilter(); + applyBadges(); + renderSidebarFolders(); + }); + + section.appendChild(item); + } + + // Restore input if it was open + if (existingInput || existingColors) { + // Don't restore – let user re-trigger + } + }; + + const showNewFolderInput = (container: Element, editFolder?: Folder) => { + const existing = container.querySelector(".bsplus-folder-input"); + if (existing) existing.remove(); + container.querySelector(".bsplus-folder-colors")?.remove(); + + let selectedColor = editFolder?.color ?? FOLDER_COLORS[Math.floor(Math.random() * FOLDER_COLORS.length)]; + + const row = document.createElement("div"); + row.className = "bsplus-folder-input"; + + const input = document.createElement("input"); + input.type = "text"; + input.placeholder = editFolder ? "Rename folder…" : "Folder name…"; + input.value = editFolder?.name ?? ""; + input.maxLength = 30; + + const confirmBtn = document.createElement("button"); + confirmBtn.className = "bsplus-folder-input-confirm"; + confirmBtn.innerHTML = CHECK_SVG_WHITE; + + const cancelBtn = document.createElement("button"); + cancelBtn.className = "bsplus-folder-input-cancel"; + cancelBtn.innerHTML = CLOSE_SVG; + + row.appendChild(input); + row.appendChild(confirmBtn); + row.appendChild(cancelBtn); + + // Color picker + const colorRow = document.createElement("div"); + colorRow.className = "bsplus-folder-colors"; + for (const color of FOLDER_COLORS) { + const swatch = document.createElement("button"); + swatch.className = `bsplus-folder-color-opt${color === selectedColor ? " bsplus-color-selected" : ""}`; + swatch.style.background = color; + swatch.addEventListener("click", (e) => { + e.stopPropagation(); + selectedColor = color; + colorRow.querySelectorAll(".bsplus-folder-color-opt").forEach((s) => + s.classList.toggle("bsplus-color-selected", (s as HTMLElement).style.background === color), + ); + }); + colorRow.appendChild(swatch); + } + + const confirm = () => { + const name = input.value.trim(); + if (!name) return; + + if (editFolder) { + const folders = getFolders().map((f) => + f.id === editFolder.id ? { ...f, name, color: selectedColor } : f, + ); + saveFolders(folders); + } else { + const folder: Folder = { id: generateId(), name, color: selectedColor }; + saveFolders([...getFolders(), folder]); + } + applyBadges(); + renderSidebarFolders(); + }; + + confirmBtn.addEventListener("click", (e) => { + e.stopPropagation(); + confirm(); + }); + cancelBtn.addEventListener("click", (e) => { + e.stopPropagation(); + renderSidebarFolders(); + }); + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") confirm(); + if (e.key === "Escape") renderSidebarFolders(); + }); + + container.appendChild(row); + container.appendChild(colorRow); + requestAnimationFrame(() => input.focus()); + }; + + const showEditFolderInput = (container: Element, folder: Folder) => { + showNewFolderInput(container, folder); + }; + + // ── Intercept native sidebar clicks to clear folder filter ── + + const attachNativeSidebarListeners = () => { + const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); + if (!sidebar) return; + + const ol = sidebar.querySelector("ol"); + if (!ol) return; + + ol.addEventListener("click", (e) => { + const target = e.target as HTMLElement; + if (target.closest(".bsplus-folders-section")) return; + + const li = target.closest("li"); + if (li && ol.contains(li)) { + if (activeFolderId !== null) { + activeFolderId = null; + applyFolderFilter(); + applyBadges(); + renderSidebarFolders(); + } + } + }); + }; + + // ── "Add to folder" button in message action bar ── + + const injectFolderButton = (actionsBar: Element) => { + if (actionsBar.querySelector(".bsplus-folder-btn")) return; + + const wrapper = document.createElement("div"); + wrapper.className = "bsplus-folder-btn"; + wrapper.style.position = "relative"; + wrapper.style.display = "inline-block"; + + const btn = document.createElement("button"); + const btnClasses = actionsBar.querySelector("button")?.className ?? ""; + btn.className = btnClasses; + btn.title = "Add to folder"; + btn.innerHTML = FOLDER_ICON_SVG; + + btn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + closeDropdown(); + + const selectedMsg = document.querySelector("[class*='MessageList__selected___']"); + const messageId = selectedMsg?.getAttribute("data-message"); + if (!messageId) return; + + showFolderDropdown(wrapper, messageId); + }); + + wrapper.appendChild(btn); + + const moreMenu = actionsBar.querySelector("[class*='MenuButton__Menu___']"); + if (moreMenu) { + actionsBar.insertBefore(wrapper, moreMenu); + } else { + actionsBar.appendChild(wrapper); + } + }; + + const showFolderDropdown = (anchor: HTMLElement, messageId: string) => { + const dropdown = document.createElement("div"); + dropdown.className = "bsplus-folder-dropdown"; + + const folders = getFolders(); + const currentFolderIds = getMessageFolderIds(messageId); + + if (folders.length === 0) { + const empty = document.createElement("div"); + empty.className = "bsplus-folder-dropdown-empty"; + empty.textContent = "No folders yet"; + dropdown.appendChild(empty); + } else { + for (const folder of folders) { + const isChecked = currentFolderIds.includes(folder.id); + const item = document.createElement("button"); + item.className = `bsplus-folder-dropdown-item${isChecked ? " bsplus-checked" : ""}`; + + const check = document.createElement("div"); + check.className = "bsplus-folder-dropdown-check"; + check.style.borderColor = isChecked ? folder.color : ""; + check.style.background = isChecked ? folder.color : ""; + check.innerHTML = CHECK_SVG_WHITE; + + const dot = document.createElement("div"); + dot.className = "bsplus-folder-dot"; + dot.style.background = folder.color; + + const name = document.createElement("span"); + name.textContent = folder.name; + + item.appendChild(check); + item.appendChild(dot); + item.appendChild(name); + + item.addEventListener("click", (e) => { + e.stopPropagation(); + toggleMessageInFolder(messageId, folder.id); + + const nowChecked = getMessageFolderIds(messageId).includes(folder.id); + item.classList.toggle("bsplus-checked", nowChecked); + check.style.borderColor = nowChecked ? folder.color : ""; + check.style.background = nowChecked ? folder.color : ""; + + applyBadges(); + applyFolderFilter(); + renderSidebarFolders(); + }); + + dropdown.appendChild(item); + } + } + + anchor.appendChild(dropdown); + openDropdown = dropdown; + + dropdownCloseHandler = (e: MouseEvent) => { + if (!dropdown.contains(e.target as Node) && !anchor.contains(e.target as Node)) { + closeDropdown(); + } + }; + setTimeout(() => { + document.addEventListener("click", dropdownCloseHandler!, true); + }, 0); + }; + + const closeDropdown = () => { + if (openDropdown) { + openDropdown.remove(); + openDropdown = null; + } + if (dropdownCloseHandler) { + document.removeEventListener("click", dropdownCloseHandler, true); + dropdownCloseHandler = null; + } + }; + + // ── Message badges ── + + const applyBadges = () => { + const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]"); + + if (!shouldShowBadgesInList()) { + for (const li of messageItems) { + const subject = li.querySelector("[class*='MessageList__subject___']"); + if (subject && (subject.querySelector(".bsplus-msg-badges") || subject.querySelector(".bsplus-subject-text"))) { + restoreSubjectPlain(subject); + } else { + li.querySelector(".bsplus-msg-badges")?.remove(); + } + } + return; + } + + const folders = getFolders(); + const assignments = getAssignments(); + + for (const li of messageItems) { + const msgId = li.getAttribute("data-message"); + if (!msgId) continue; + + let badgeContainer = li.querySelector(".bsplus-msg-badges") as HTMLElement | null; + + const folderIds = []; + for (const [fId, mIds] of Object.entries(assignments)) { + if (mIds.includes(msgId)) folderIds.push(fId); + } + + if (folderIds.length === 0) { + badgeContainer?.remove(); + continue; + } + + if (!badgeContainer) { + badgeContainer = document.createElement("div"); + badgeContainer.className = "bsplus-msg-badges"; + const subject = li.querySelector("[class*='MessageList__subject___']"); + if (subject) { + if (!subject.querySelector(".bsplus-subject-text")) { + const textWrap = document.createElement("span"); + textWrap.className = "bsplus-subject-text"; + textWrap.textContent = subject.textContent; + subject.textContent = ""; + subject.appendChild(textWrap); + } + subject.appendChild(badgeContainer); + } else { + li.appendChild(badgeContainer); + } + } + + badgeContainer.innerHTML = ""; + for (const fId of folderIds) { + const folder = folders.find((f) => f.id === fId); + if (!folder) continue; + const badge = document.createElement("span"); + badge.className = "bsplus-msg-badge"; + badge.style.background = folder.color; + badge.textContent = folder.name; + badge.title = `Filter by "${folder.name}"`; + badge.addEventListener("click", (e) => { + e.stopPropagation(); + activeFolderId = folder.id; + applyFolderFilter(); + applyBadges(); + renderSidebarFolders(); + }); + badgeContainer.appendChild(badge); + } + } + }; + + // ── Folder filtering ── + + const applyFolderFilter = () => { + const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]"); + const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button"); + + if (activeFolderId === null) { + if (api.settings.hideFolderedMessagesInAll) { + for (const li of messageItems) { + const msgId = li.getAttribute("data-message"); + if (msgId && isMessageInAnyCustomFolder(msgId)) { + li.classList.add("bsplus-folder-hidden"); + } else { + li.classList.remove("bsplus-folder-hidden"); + } + } + } else { + for (const li of messageItems) { + li.classList.remove("bsplus-folder-hidden"); + } + } + if (moreBtn) (moreBtn as HTMLElement).classList.remove("bsplus-folder-hidden"); + return; + } + + const folderMsgIds = getAssignments()[activeFolderId] ?? []; + + for (const li of messageItems) { + const msgId = li.getAttribute("data-message"); + if (msgId && folderMsgIds.includes(msgId)) { + li.classList.remove("bsplus-folder-hidden"); + } else { + li.classList.add("bsplus-folder-hidden"); + } + } + if (moreBtn) (moreBtn as HTMLElement).classList.add("bsplus-folder-hidden"); + }; + + // ── Observers ── + + const setupMessageListObserver = () => { + const messageList = document.querySelector("[class*='MessageList__MessageList___'] ol"); + if (!messageList || messageListObserver) return; + + messageListObserver = new MutationObserver(() => { + applyBadges(); + applyFolderFilter(); + }); + messageListObserver.observe(messageList, { childList: true, subtree: false }); + }; + + const setupActionsObserver = () => { + if (actionsObserver) return; + + const target = document.querySelector("[class*='Viewer__Viewer___']") ?? document.querySelector("div.messages"); + if (!target) return; + + actionsObserver = new MutationObserver(() => { + const actionsBar = document.querySelector("[class*='Message__actions___']"); + if (actionsBar && !actionsBar.querySelector(".bsplus-folder-btn")) { + injectFolderButton(actionsBar); + } + }); + actionsObserver.observe(target, { childList: true, subtree: true }); + }; + + // ── Main page handler ── + + const handleMessagesPage = async () => { + await waitForElm("[class*='Viewer__sidebar___'] ol", true, 50, 100); + + renderSidebarFolders(); + attachNativeSidebarListeners(); + + await waitForElm("[class*='MessageList__MessageList___'] ol", true, 50, 100); + applyBadges(); + applyFolderFilter(); + setupMessageListObserver(); + + // The actions bar only exists when a message is selected/open, + // so we observe the whole viewer for it to appear dynamically + setupActionsObserver(); + + // If a message is already selected, inject immediately + const actionsBar = document.querySelector("[class*='Message__actions___']"); + if (actionsBar) injectFolderButton(actionsBar); + + // Re-observe the sidebar for SEQTA re-renders + const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); + if (sidebar && !sidebarObserver) { + sidebarObserver = new MutationObserver(() => { + const ol = sidebar.querySelector("ol"); + if (ol && !ol.querySelector(".bsplus-folders-section")) { + renderSidebarFolders(); + attachNativeSidebarListeners(); + } + }); + sidebarObserver.observe(sidebar, { childList: true, subtree: true }); + } + }; + + // ── Lifecycle ── + + const mountUnsub = api.seqta.onMount("div.messages", handleMessagesPage); + unregisters.push(mountUnsub); + + unregisters.push( + api.settings.onChange("showTagsInAllMessages", () => { + applyBadges(); + }), + ); + unregisters.push( + api.settings.onChange("hideFolderedMessagesInAll", () => { + applyFolderFilter(); + }), + ); + + return () => { + for (const u of unregisters) u.unregister(); + messageListObserver?.disconnect(); + sidebarObserver?.disconnect(); + actionsObserver?.disconnect(); + closeDropdown(); + styleEl.remove(); + document.querySelectorAll(".bsplus-folders-section").forEach((el) => el.remove()); + document.querySelectorAll(".bsplus-folder-btn").forEach((el) => el.remove()); + document.querySelectorAll(".bsplus-msg-badges").forEach((el) => el.remove()); + document.querySelectorAll("[class*='MessageList__subject___']").forEach((subject) => { + if (subject.querySelector(".bsplus-subject-text")) { + restoreSubjectPlain(subject); + } + }); + document.querySelectorAll(".bsplus-folder-hidden").forEach((el) => + el.classList.remove("bsplus-folder-hidden"), + ); + document.querySelectorAll(".bsplus-modal-overlay").forEach((el) => el.remove()); + }; + }, +}; + +export default messageFoldersPlugin; diff --git a/src/plugins/built-in/messageFolders/styles.css b/src/plugins/built-in/messageFolders/styles.css new file mode 100644 index 00000000..e239a883 --- /dev/null +++ b/src/plugins/built-in/messageFolders/styles.css @@ -0,0 +1,491 @@ +/* ── Sidebar folder section ── */ +.bsplus-folders-section { + border-top: 1px solid var(--background-secondary, rgba(128, 128, 128, 0.2)); + margin-top: 4px; + padding-top: 4px; +} + +.bsplus-folders-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px 2px; + user-select: none; +} + +.bsplus-folders-header span { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-primary, #666); + opacity: 0.5; +} + +.bsplus-folders-add-btn { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 20px !important; + height: 20px !important; + min-width: 0 !important; + border: none !important; + background: transparent !important; + opacity: 0.5; + cursor: pointer; + border-radius: 4px !important; + padding: 0 !important; + margin: 0 !important; + transition: all 0.2s ease; + text-align: center !important; +} + +.bsplus-folders-add-btn:hover { + opacity: 1; + background: var(--background-secondary, rgba(128, 128, 128, 0.1)) !important; +} + +/* ── Folder list items ── */ +.bsplus-folder-item { + display: flex; + align-items: center; + padding: 6px 12px; + cursor: pointer; + transition: background 0.15s ease; + position: relative; + gap: 8px; + user-select: none; +} + +.bsplus-folder-item:hover { + background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.08)); +} + +.bsplus-folder-item.bsplus-folder-active { + background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.12)); +} + +.bsplus-folder-item.bsplus-folder-active::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--better-main, #007bff); + border-radius: 0 2px 2px 0; +} + +.bsplus-folder-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.bsplus-folder-name { + font-size: 13px; + color: var(--text-primary, #333); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bsplus-folder-count { + font-size: 11px; + color: var(--text-primary, #999); + opacity: 0.5; + flex-shrink: 0; +} + +.bsplus-folder-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.15s ease; +} + +.bsplus-folder-item:hover .bsplus-folder-actions { + opacity: 1; +} + +.bsplus-folder-action-btn { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 20px !important; + height: 20px !important; + min-width: 0 !important; + border: none !important; + background: transparent !important; + opacity: 0.6; + cursor: pointer; + border-radius: 4px !important; + padding: 0 !important; + margin: 0 !important; + transition: all 0.15s ease; +} + +.bsplus-folder-action-btn:hover { + opacity: 1; + background: var(--background-secondary, rgba(128, 128, 128, 0.15)) !important; +} + +/* ── Inline folder name input ── */ +.bsplus-folder-input { + display: flex; + align-items: center; + padding: 4px 12px; + gap: 6px; +} + +.bsplus-folder-input input { + flex: 1; + min-width: 0; + padding: 4px 8px; + font-size: 13px; + border: 1px solid var(--background-secondary, #ccc); + border-radius: 6px; + background: var(--background-secondary, #f5f5f5); + color: var(--text-primary, #333); + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.bsplus-folder-input input:focus { + border-color: var(--better-main, #007bff); + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2); +} + +.bsplus-folder-input-confirm, +.bsplus-folder-input-cancel { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 24px !important; + height: 24px !important; + min-width: 0 !important; + border: none !important; + border-radius: 4px !important; + cursor: pointer; + padding: 0 !important; + margin: 0 !important; + transition: all 0.15s ease; +} + +.bsplus-folder-input-confirm { + background: var(--better-main, #007bff) !important; +} + +.bsplus-folder-input-confirm:hover { + transform: scale(1.1); +} + +.bsplus-folder-input-cancel { + background: transparent !important; + opacity: 0.6; +} + +.bsplus-folder-input-cancel:hover { + opacity: 1; + background: var(--background-secondary, rgba(128, 128, 128, 0.1)) !important; +} + +/* ── Color picker row ── */ +.bsplus-folder-colors { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; + padding: 4px 12px 6px; + max-width: 120px; +} + +.bsplus-folder-color-opt { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), + border-color 0.2s ease, + box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + padding: 0; + background: none; + box-sizing: border-box; +} + +.bsplus-folder-color-opt:hover { + transform: scale(1.25); + box-shadow: 0 0 0 3px rgba(128, 128, 128, 0.15); +} + +.bsplus-folder-color-opt.bsplus-color-selected { + border-color: var(--text-primary, #333); + transform: scale(1.15); + box-shadow: 0 0 0 3px rgba(128, 128, 128, 0.2); +} + +.bsplus-folder-color-opt.bsplus-color-selected:hover { + transform: scale(1.25); +} + +/* ── "Add to folder" button in message actions bar ── */ +.bsplus-folder-btn { + position: relative; +} + +.bsplus-folder-btn svg { + fill: currentColor; +} + +/* ── Folder dropdown ── */ +.bsplus-folder-dropdown { + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + min-width: 180px; + background: var(--background-primary, #fff); + border: 1px solid var(--background-secondary, #e0e0e0); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + z-index: 1000; + overflow: hidden; + animation: bsplus-dropdown-in 0.15s ease-out; +} + +@keyframes bsplus-dropdown-in { + from { + opacity: 0; + transform: translateY(-4px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.bsplus-folder-dropdown-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + transition: background 0.1s ease; + border: none; + background: transparent; + width: 100%; + text-align: left; + color: var(--text-primary, #333); + font-size: 13px; +} + +.bsplus-folder-dropdown-item:hover { + background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.08)); +} + +.bsplus-folder-dropdown-check { + width: 16px; + height: 16px; + border: 2px solid var(--background-secondary, #ccc); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.15s ease; +} + +.bsplus-folder-dropdown-item.bsplus-checked .bsplus-folder-dropdown-check { + background: var(--better-main, #007bff); + border-color: var(--better-main, #007bff); +} + +.bsplus-folder-dropdown-check svg { + width: 10px; + height: 10px; + color: white; + opacity: 0; + transition: opacity 0.1s ease; +} + +.bsplus-folder-dropdown-item.bsplus-checked .bsplus-folder-dropdown-check svg { + opacity: 1; +} + +.bsplus-folder-dropdown-empty { + padding: 12px; + text-align: center; + font-size: 12px; + color: var(--text-primary, #999); + opacity: 0.5; +} + +/* ── Let primary column use available space instead of being clipped ── */ +[class*='MessageList__primary___'] { + flex: 1 1 0% !important; + min-width: 0 !important; + overflow: hidden !important; +} + +/* ── Make subject line a flex row so badges sit inline ── */ +[class*='MessageList__subject___'] { + display: flex !important; + align-items: center; + gap: 6px; + min-width: 0 !important; + overflow: hidden !important; +} + +/* ── Subject text truncates to make room for badges ── */ +.bsplus-subject-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1 1 auto; +} + +/* ── Shrink the secondary column to its content ── */ +[class*='MessageList__secondary___'] { + flex: 0 0 auto !important; + width: auto !important; + min-width: 0 !important; + max-width: 200px !important; +} + +/* ── Constrain the flags/attachment icon column ── */ +[class*='MessageList__flags___'] { + width: 24px !important; + min-width: 0 !important; + flex-shrink: 0 !important; +} + +/* ── Message list folder badges ── */ +.bsplus-msg-badges { + display: inline-flex; + align-items: center; + gap: 3px; + flex-shrink: 0; + margin-left: auto; +} + +.bsplus-msg-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 1px 6px; + border-radius: 8px; + font-size: 10px; + font-weight: 500; + line-height: 1.4; + color: white; + white-space: nowrap; + cursor: pointer; + transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.bsplus-msg-badge:hover { + opacity: 0.85; + transform: scale(1.05); +} + +/* ── Folder filtering (hide messages not in active folder) ── */ +.bsplus-folder-hidden { + display: none !important; +} + +/* ── Delete confirmation modal ── */ +@keyframes bsplus-modal-overlay-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes bsplus-modal-in { + from { + opacity: 0; + transform: scale(0.95) translateY(-8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.bsplus-modal-overlay { + position: fixed; + inset: 0; + z-index: 2147483647; + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + animation: bsplus-modal-overlay-in 0.2s ease-out forwards; +} + +.bsplus-modal { + padding: 1rem 1.5rem; + margin: 0 1rem; + min-width: 16rem; + max-width: 22rem; + width: 100%; + box-sizing: border-box; + background: var(--background-primary, #fff); + border-radius: 0.75rem; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + border: 1px solid var(--background-secondary, #e0e0e0); + animation: bsplus-modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +.bsplus-modal h3 { + margin: 0 0 0.5rem; + font-size: 1rem; + font-weight: 600; + color: var(--text-primary, #333); +} + +.bsplus-modal p { + margin: 0 0 1rem; + font-size: 0.875rem; + color: var(--text-primary, #666); + opacity: 0.8; +} + +.bsplus-modal-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; +} + +.bsplus-modal-actions button { + padding: 0.4rem 1rem; + font-size: 0.875rem; + font-weight: 500; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.bsplus-modal-btn-cancel { + background: transparent; + border: 1px solid var(--background-secondary, #ccc); + color: var(--text-primary, #333); +} + +.bsplus-modal-btn-cancel:hover { + background: var(--background-secondary, rgba(128, 128, 128, 0.1)); +} + +.bsplus-modal-btn-danger { + background: #e53e3e; + border: none; + color: white; +} + +.bsplus-modal-btn-danger:hover { + background: #c53030; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(229, 62, 62, 0.35); +} diff --git a/src/plugins/built-in/themes/index.ts b/src/plugins/built-in/themes/index.ts index b31abd86..9127d739 100644 --- a/src/plugins/built-in/themes/index.ts +++ b/src/plugins/built-in/themes/index.ts @@ -10,6 +10,7 @@ const themesPlugin: Plugin = { run: async (_) => { const themeManager = ThemeManager.getInstance(); + await themeManager.prepareThemeAfterCloudSync(); await themeManager.initialize(); }, }; diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index b3bc490a..0faf1449 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -6,6 +6,7 @@ import { type LoadedCustomTheme, shouldForceThemeAppearance, } from "@/types/CustomThemes"; +import { BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY } from "@/seqta/utils/cloudSettingsSync"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import debounce from "@/seqta/utils/debounce"; import { themeUpdates } from "@/interface/hooks/ThemeUpdates"; @@ -25,7 +26,9 @@ type ThemeContent = { CanChangeColour?: boolean; CustomCSS?: string; hideThemeName?: boolean; + forceTheme?: boolean; forceDark?: boolean; + adaptiveCssVariables?: string[]; images?: { id: string; variableName: string; data: string }[]; // data: base64 }; @@ -35,7 +38,7 @@ export type InstallThemeMeta = { serverUpdatedAtSec?: number; forceTheme?: boolean; adaptiveCssVariables?: string[]; - images: { id: string; variableName: string; data: string }[]; // data: base64 + images?: { id: string; variableName: string; data: string }[]; // data: base64 }; export class ThemeManager { @@ -164,6 +167,33 @@ export class ThemeManager { } } + /** + * After cloud restore, IndexedDB/theme storage is only reachable from page context (not MV3 SW). + * Background sets BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY; we fetch the store JSON here before setTheme(). + * The resolved id matches cloud sync **`themeId` / `selectedTheme`**: it may be a standard theme uuid or a + * flavour (slave) variant id — **`downloadAndInstallStoreTheme`** is the same code path as the theme store installer. + */ + public async prepareThemeAfterCloudSync(): Promise { + try { + const snap = await browser.storage.local.get(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY); + const pending = snap[BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY]; + if (pending === undefined) return; + + await browser.storage.local.remove(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY); + + if (typeof pending !== "string") return; + const id = pending.trim(); + if (!id) return; + + const existing = (await localforage.getItem(id)) as CustomTheme | null; + if (existing) return; + + await this.downloadAndInstallStoreTheme({ id, name: id }); + } catch (e) { + console.warn("[ThemeManager] prepareThemeAfterCloudSync:", e); + } + } + /** * Initialize the theme system and restore previous state */ diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 0648accb..aa8779ad 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -10,6 +10,7 @@ import assessmentsAveragePlugin from "./built-in/assessmentsAverage"; import profilePicturePlugin from "./built-in/profilePicture"; import assessmentsOverviewPlugin from "./built-in/assessmentsOverview"; import backgroundMusicPlugin from "./built-in/backgroundMusic"; +//import messageFoldersPlugin from "./built-in/messageFolders"; //import testPlugin from './built-in/test'; // Heavy plugins (lazy-loaded only when enabled) @@ -28,6 +29,7 @@ pluginManager.registerPlugin(timetableEditPlugin); pluginManager.registerPlugin(profilePicturePlugin); pluginManager.registerPlugin(assessmentsOverviewPlugin); pluginManager.registerPlugin(backgroundMusicPlugin); +//pluginManager.registerPlugin(messageFoldersPlugin); //pluginManager.registerPlugin(testPlugin); // Register heavy plugins with lazy loading diff --git a/src/resources/update-video.webm b/src/resources/update-video.webm index 9e583a06..32abbdbd 100644 Binary files a/src/resources/update-video.webm and b/src/resources/update-video.webm differ diff --git a/src/seqta/ui/colors/Manager.ts b/src/seqta/ui/colors/Manager.ts index d72c8844..71740da9 100644 --- a/src/seqta/ui/colors/Manager.ts +++ b/src/seqta/ui/colors/Manager.ts @@ -5,7 +5,7 @@ import { lightenAndPaleColor } from "./lightenAndPaleColor"; import ColorLuminance from "./ColorLuminance"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour"; -import { getCustomThemeAdaptiveCssVariables } from "@/seqta/ui/colors/customThemeAdaptiveBindings"; +import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/customThemeAdaptiveBindings"; import darkLogo from "@/resources/icons/betterseqta-light-full.png"; import lightLogo from "@/resources/icons/betterseqta-dark-full.png"; @@ -84,6 +84,21 @@ function cancelColorTransition() { } } +function getRepresentativeRgbChannels(s: string): { r: number; g: number; b: number } | null { + const parsedHex = parseRepresentativeHex(s); + if (!parsedHex) return null; + try { + const [r, g, b] = Color(parsedHex).rgb().array(); + return { + r: Math.round(r), + g: Math.round(g), + b: Math.round(b), + }; + } catch { + return null; + } +} + function applyColorsWith(selectedColor: string) { if (settingsState.transparencyEffects) { document.documentElement.classList.add("transparencyEffects"); @@ -129,8 +144,35 @@ function applyColorsWith(selectedColor: string) { applyProperties({ ...commonProps, ...modeProps, ...dynamicProps }); if (settingsState.selectedTheme) { - for (const name of getCustomThemeAdaptiveCssVariables()) { - setCSSVar(name, selectedColor); + const channels = getRepresentativeRgbChannels(selectedColor); + for (const binding of getCustomThemeAdaptiveCssVariableBindings()) { + if (!binding.channel) { + setCSSVar(binding.cssVarName, selectedColor); + continue; + } + + if (!channels) { + continue; + } + + if (binding.channel === "r") { + setCSSVar(binding.cssVarName, String(channels.r)); + } else if (binding.channel === "g") { + setCSSVar(binding.cssVarName, String(channels.g)); + } else { + setCSSVar(binding.cssVarName, String(channels.b)); + } + } + } + + // Let themes opt-in to overriding only adaptive accent output. + // A theme can define `--adaptive-better-main` from adaptive channel bindings. + if (settingsState.selectedTheme && settingsState.adaptiveThemeColour) { + const adaptiveOverride = getComputedStyle(document.documentElement) + .getPropertyValue("--adaptive-better-main") + .trim(); + if (adaptiveOverride) { + setCSSVar("--better-main", adaptiveOverride); } } diff --git a/src/seqta/ui/colors/customThemeAdaptiveBindings.ts b/src/seqta/ui/colors/customThemeAdaptiveBindings.ts index dba35e3f..cdd0c823 100644 --- a/src/seqta/ui/colors/customThemeAdaptiveBindings.ts +++ b/src/seqta/ui/colors/customThemeAdaptiveBindings.ts @@ -1,20 +1,49 @@ /** Tracks which author-declared CSS variables mirror the effective accent; not persisted in settings storage. */ const VALID_CUSTOM_PROP = /^--[a-zA-Z0-9_-]{1,120}$/; +const VALID_CHANNEL = /^(r|g|b)$/; -let boundNames: string[] = []; +export type AdaptiveChannel = "r" | "g" | "b"; -export function normalizeAdaptiveCssVariableNames( +export type AdaptiveCssVariableBinding = { + cssVarName: string; + channel?: AdaptiveChannel; +}; + +let boundBindings: AdaptiveCssVariableBinding[] = []; + +function parseAdaptiveBinding( + rawBinding: string, +): AdaptiveCssVariableBinding | null { + const trimmed = rawBinding.trim(); + if (!trimmed) return null; + + const [rawName, rawChannel] = trimmed.split(":", 2); + const cssVarName = rawName?.trim() ?? ""; + if (!VALID_CUSTOM_PROP.test(cssVarName)) return null; + + if (!rawChannel) return { cssVarName }; + + const channel = rawChannel.trim().toLowerCase(); + if (!VALID_CHANNEL.test(channel)) return null; + + return { cssVarName, channel: channel as AdaptiveChannel }; +} + +export function normalizeAdaptiveCssVariableBindings( names: string[] | undefined, -): string[] { +): AdaptiveCssVariableBinding[] { if (!names?.length) return []; - const out: string[] = []; + const out: AdaptiveCssVariableBinding[] = []; const seen = new Set(); + for (const raw of names) { - const s = raw.trim(); - if (!VALID_CUSTOM_PROP.test(s) || seen.has(s)) continue; - seen.add(s); - out.push(s); + const parsed = parseAdaptiveBinding(raw); + if (!parsed) continue; + const key = `${parsed.cssVarName}:${parsed.channel ?? "full"}`; + if (seen.has(key)) continue; + seen.add(key); + out.push(parsed); } return out; } @@ -22,19 +51,24 @@ export function normalizeAdaptiveCssVariableNames( export function setCustomThemeAdaptiveCssVariables( names: string[] | undefined, ): void { - for (const n of boundNames) { - document.documentElement.style.removeProperty(n); + for (const binding of boundBindings) { + document.documentElement.style.removeProperty(binding.cssVarName); } - boundNames = normalizeAdaptiveCssVariableNames(names); + boundBindings = normalizeAdaptiveCssVariableBindings(names); } +export function getCustomThemeAdaptiveCssVariableBindings(): AdaptiveCssVariableBinding[] { + return boundBindings; +} + +// Backward-compatible helper for existing callsites. export function getCustomThemeAdaptiveCssVariables(): string[] { - return boundNames; + return boundBindings.map((b) => b.cssVarName); } export function clearCustomThemeAdaptiveCssVariables(): void { - for (const n of boundNames) { - document.documentElement.style.removeProperty(n); + for (const binding of boundBindings) { + document.documentElement.style.removeProperty(binding.cssVarName); } - boundNames = []; + boundBindings = []; } diff --git a/src/seqta/utils/Openers/OpenWhatsNewPopup.ts b/src/seqta/utils/Openers/OpenWhatsNewPopup.ts index 371cc03b..6160f6e5 100644 --- a/src/seqta/utils/Openers/OpenWhatsNewPopup.ts +++ b/src/seqta/utils/Openers/OpenWhatsNewPopup.ts @@ -34,14 +34,20 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) { const text = stringToHTML(/* html */ `
-

3.6.4 - Assessment weighting override & fixes

+

3.6.5 - Assessment weighting override & fixes

  • Added the ability to override/add weightings to assessments (on assessment page).
  • Fixed the display of weightings that could not automatically be discovered.
  • Fixed the formatting of the weighting tag that was broken due to a SEQTA update.
  • +

    3.6.4 - Theme flavours and fixes, Upcoming Assements improvement

    +
  • Added advanced colour adjustments variables for theme customisation.
  • +
  • Improved logic for upcoming assements dashlet to improve compatibility.
  • +
  • BS Cloud can now automatically download themes from other devices.
  • +
  • Added theme flavours for multiple colour variations of the same theme.
  • +

    3.6.3 - Assessment overview fix

  • Fixed assessments overview failing to load.
  • - +

    3.6.2 - Cloud backup, various fixes & SEQTA Engage support

  • BetterSEQTA Cloud: back up and restore extension settings from your account (General settings).
  • Optional automatic cloud sync if signed in (on by default).
  • @@ -55,6 +61,7 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
  • Updated outdated in-app links and update some under the hood code (Vite 8).
  • Added a notifications panel animation to work like settings.
  • Fix timetable edit plugin not working correctly.
  • +

    3.5.3 - Adaptive theme updates

  • Fixed adaptive theming on current-year course and assessment pages.
  • diff --git a/src/seqta/utils/cloudSettingsSync.ts b/src/seqta/utils/cloudSettingsSync.ts index 8918cd80..d474fc76 100644 --- a/src/seqta/utils/cloudSettingsSync.ts +++ b/src/seqta/utils/cloudSettingsSync.ts @@ -10,6 +10,13 @@ export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1; export const BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY = "bsplus_cloud_settings_known_remote_updated_at"; +/** + * Written by the service worker after applying a cloud settings envelope; the SEQTA page’s + * ThemeManager reads and clears it (SW cannot share localforage/IndexedDB with the page). + */ +export const BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY = + "bsplus_pending_theme_ensure_after_cloud"; + /** * Never uploaded to the cloud backup (OAuth and legacy keys). * IndexedDB (e.g. Global Search’s `betterseqta-index` database) is not part of @@ -39,6 +46,7 @@ export const SENSITIVE_DEVICE_STORAGE_KEY_PREFIXES = ["plugin.global-search.stor const CLIENT_ONLY_CLOUD_KEYS_EXACT = [ BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY, "bsplus_lastCloudPoll", + BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY, ] as const; /** After restoring from cloud, keep local session so the user stays signed in. */ @@ -101,8 +109,15 @@ function stripExcludedKeysFromRemoteData(remote: Record): Recor return out; } +/** Stored theme id (`selectedTheme`); trims whitespace; empty string clears. */ +export function normalizeThemeIdForSync(raw: unknown): string { + if (typeof raw !== "string") return ""; + return raw.trim(); +} + export function buildUploadPayload(all: Record): { schemaVersion: number; + themeId: string; data: Record; } { const filtered: Record = {}; @@ -111,17 +126,57 @@ export function buildUploadPayload(all: Record): { filtered[k] = v; } const data = migrateLegacyToPluginSettings(filtered); - return { schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION, data }; + const themeId = normalizeThemeIdForSync(all.selectedTheme); + return { + schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION, + themeId, + data, + }; } export async function getSnapshotForUpload(): Promise<{ schemaVersion: number; + themeId: string; data: Record; }> { const all = await browser.storage.local.get(); return buildUploadPayload(all as Record); } +/** + * Theme to ensure is installed locally after a downloaded envelope (explicit `themeId` overrides `data.selectedTheme`). + * Works for any store-backed id, including **flavour (slave) variants** nested under masters in the catalogue. + */ +export function resolveThemeIdForPostSyncDownload(envelope: unknown): string | undefined { + if (envelope && typeof envelope === "object" && "themeId" in envelope) { + const top = normalizeThemeIdForSync( + (envelope as Record).themeId, + ); + if (top) return top; + } + + let remoteFlat: Record; + if ( + envelope && + typeof envelope === "object" && + "data" in envelope && + (envelope as { data?: unknown }).data !== undefined && + typeof (envelope as { data?: unknown }).data === "object" && + (envelope as { data?: unknown }).data !== null && + !Array.isArray((envelope as { data?: unknown }).data) + ) { + remoteFlat = (envelope as { data: Record }).data; + } else if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) { + remoteFlat = envelope as Record; + } else { + return undefined; + } + + const migrated = migrateLegacyToPluginSettings(remoteFlat); + const fromData = normalizeThemeIdForSync(migrated.selectedTheme); + return fromData === "" ? undefined : fromData; +} + export async function setKnownRemoteUpdatedAt(iso: string | undefined): Promise { if (!iso || typeof iso !== "string") return; await browser.storage.local.set({ [BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY]: iso });