From c7fceee11fb002c0a436c309aab0f5f0548565bd Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Fri, 3 Jul 2026 21:05:07 +0200 Subject: [PATCH] Visually makeover --- README.md | 1 - RadioWebApp/data/radio-stations.json | 51 + RadioWebApp/sw.js | 6 +- about.html | 144 + docs.html | 328 +++ eslint.config.js | 61 + index.html | 14 +- package-lock.json | 3295 ++++++++++++++++++++-- package.json | 31 +- playwright.config.js | 28 + privacy.html | 101 +- public/api/managed-stations.php | 3 +- public/data/radio-stations-sync.json | 51 + public/data/radio-stations.json | 51 + public/images/76bit_logo.webp | Bin 0 -> 8992 bytes public/robots.txt | 8 + public/stations.json | 24 - public/sw.js | 64 +- scripts/bump-sw-cache-version.mjs | 17 - scripts/stamp-dist-sw.mjs | 32 + src/App.jsx | 491 +++- src/main.jsx | 72 +- src/player.js | 1449 ++++++---- src/player/constants.js | 32 + src/player/domRefs.js | 134 + src/player/stationHelpers.js | 220 ++ src/player/stationHelpers.test.js | 24 + src/radio/loadRadioStations.ts | 3 +- src/radio/radioConfig.json | 48 + src/radio/radioCountries.test.ts | 9 + src/radio/radioCountries.ts | 52 +- src/shared/serviceWorkerConfig.json | 16 + src/storage/playerPersistence.ts | 77 +- src/storage/stationCatalogPersistence.ts | 63 + src/styles.css | 991 ++++++- src/vite-env.d.ts | 4 +- sync.sh | 1 + tests/e2e/app.smoke.spec.js | 28 + vite.config.js | 17 +- vitest.config.js | 11 + 40 files changed, 6851 insertions(+), 1201 deletions(-) create mode 100644 about.html create mode 100644 docs.html create mode 100644 eslint.config.js create mode 100644 playwright.config.js create mode 100644 public/images/76bit_logo.webp create mode 100644 public/robots.txt delete mode 100644 scripts/bump-sw-cache-version.mjs create mode 100644 scripts/stamp-dist-sw.mjs create mode 100644 src/player/constants.js create mode 100644 src/player/domRefs.js create mode 100644 src/player/stationHelpers.js create mode 100644 src/player/stationHelpers.test.js create mode 100644 src/radio/radioConfig.json create mode 100644 src/radio/radioCountries.test.ts create mode 100644 src/shared/serviceWorkerConfig.json create mode 100644 tests/e2e/app.smoke.spec.js create mode 100644 vitest.config.js diff --git a/README.md b/README.md index 855b435..f535b1d 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,6 @@ public/ stations.json sw.js scripts/ - bump-sw-cache-version.mjs cron-refresh-stations.sh cron-update-stations.sh import-radio-stations.ts diff --git a/RadioWebApp/data/radio-stations.json b/RadioWebApp/data/radio-stations.json index 86216f9..e5eb06c 100644 --- a/RadioWebApp/data/radio-stations.json +++ b/RadioWebApp/data/radio-stations.json @@ -51139,6 +51139,57 @@ "source": "radio-browser", "sourceStationUuid": "b3efdf8f-dba0-11e9-a8ba-52543be04c81" }, + { + "id": "a3e4b1b3-0e4c-4fd6-9f69-6a4c0b4f7e01", + "name": "Aktual Dalmacija", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualDalmacija", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "a3e4b1b3-0e4c-4fd6-9f69-6a4c0b4f7e01" + }, + { + "id": "b1d6a9d8-0d3d-49be-9c7b-9a7f2d52b5bc", + "name": "Aktual Galama - Fešta", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualGalama", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "b1d6a9d8-0d3d-49be-9c7b-9a7f2d52b5bc" + }, + { + "id": "c8e0a7d4-2ce1-4a1e-a1bc-2fbb1c40d8f9", + "name": "Aktual Nostalgija", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualNostalgija", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "c8e0a7d4-2ce1-4a1e-a1bc-2fbb1c40d8f9" + }, { "id": "3cd65080-c8b5-4d06-86bb-62481e5d6e06", "name": "Radio1", diff --git a/RadioWebApp/sw.js b/RadioWebApp/sw.js index ef6e39f..6a4827a 100644 --- a/RadioWebApp/sw.js +++ b/RadioWebApp/sw.js @@ -1,9 +1,9 @@ // NOTE: This service worker is for the web/PWA build. // For development we aggressively unregister SWs in `src/player.js`. // -// This value is rewritten automatically before each build so deployed clients -// refresh to the newest shell and cached assets. -const CACHE_NAME = 'radioplayer-pwa-v5-1777223362895'; +// This checked-in file stays stable. The Vite web build stamps the matching +// token in `dist/sw.js` without mutating source files in the repo. +const CACHE_NAME = 'radioplayer-pwa-v5-__APP_BUILD_ID__'; const CORE_ASSETS = [ './', diff --git a/about.html b/about.html new file mode 100644 index 0000000..7b7a482 --- /dev/null +++ b/about.html @@ -0,0 +1,144 @@ + + + + + + + About RadioPlayer | Online Radio Player by 76bit.net + + + + + + + + + + + + + + + + + +
+ + +
+

About

+
+
+

About RadioPlayer

+

RadioPlayer is a simple online radio player that helps you find stations, start listening quickly, and move between stations without extra clutter.

+

It works well on desktop and mobile, keeps the experience clear, and makes it easy to come back and continue listening.

+
+ + +
+ +
+
+

What it does

+

Browse radio stations, search the list, switch between stations, and start playback with a small and simple interface.

+
+
+

Basic features

+

The player helps you discover stations, keep favorites close, and return to listening quickly on both phone and desktop screens.

+
+
+

Why people use it

+

It is made for listeners who want radio to feel fast, clear, and easy to use without unnecessary steps.

+
+
+
+
+ + + \ No newline at end of file diff --git a/docs.html b/docs.html new file mode 100644 index 0000000..fce8d05 --- /dev/null +++ b/docs.html @@ -0,0 +1,328 @@ + + + + + + + Docs | RadioPlayer + + + + + + + + + + +
+ + +
+

Docs

+
+
+

Simple guide to using RadioPlayer.

+

RadioPlayer helps you browse stations, start listening quickly, and move between stations with a clean player that works on desktop and mobile.

+

This page covers the basics only, so it stays easy to read and useful for everyday listening.

+
+ + +
+ + + +
+
+

Overview

+

RadioPlayer is an online radio player made to keep listening simple. You can open the player, browse available stations, search for something specific, and switch stations quickly without getting lost in a busy interface.

+
+
+

What you can do

+
    +
  • Browse a station library with a simple player view.
  • +
  • Search stations and move between them quickly.
  • +
  • Keep favorites and recent stations easy to reach.
  • +
  • Use the player on desktop and mobile screens.
  • +
+
+
+

What the player focuses on

+
    +
  • Fast access to stations without extra clutter.
  • +
  • Clear playback controls that are easy to understand.
  • +
  • A lightweight experience that is easy to return to.
  • +
  • Simple listening whether you are on phone or desktop.
  • +
+
+
+
+ +
+

Using the player

+
+
+

Quick start

+
    +
  1. Open the player and press play to start the current station.
  2. +
  3. Use next and previous to move through the available station list.
  4. +
  5. Open the station library to search and choose something else.
  6. +
  7. Save stations as favorites for faster access later.
  8. +
+
+
+

While you listen

+
    +
  • The player is designed to stay simple while you switch stations.
  • +
  • Search helps narrow the station list quickly.
  • +
  • Recent stations help you get back to what you heard before.
  • +
  • Favorites keep your usual choices close at hand.
  • +
+
+
+

Playback and status

+
    +
  • You can control playback, volume, and station changes from the main player.
  • +
  • Status messages help explain what the player is doing when needed.
  • +
  • The layout stays compact so listening feels quick and direct.
  • +
+
+
+
+ +
+

Stations and countries

+

RadioPlayer can help you narrow the station list by country, so it is easier to explore the stations you care about instead of scrolling through everything at once.

+
+
+

Finding stations

+
    +
  • Use the library to browse the available station list.
  • +
  • Search helps you get to a station faster.
  • +
  • Country filters can make the list easier to manage.
  • +
  • Your listening flow stays simple even with a larger catalog.
  • +
+
+
+

Keeping things current

+
    +
  • The station list can refresh so the player stays useful over time.
  • +
  • Saved data helps the player open quickly when you come back.
  • +
  • Recent and favorite stations help reduce repeated searching.
  • +
  • The goal is faster listening with less friction.
  • +
+
+
+

If you return often, favorites and recents help make the player feel faster and more personal.

+
+ +
+

Install and offline

+
+
+

Install and offline

+
    +
  • You can install the app on supported devices for easier access.
  • +
  • Installed use can feel faster and closer to a native app.
  • +
  • Some player data can stay available so reopening feels quicker.
  • +
  • The app is designed to come back up smoothly after the first visit.
  • +
+
+
+

Listening on different devices

+
    +
  • The layout is made to work well on both desktop and mobile.
  • +
  • Supported devices and browsers can offer extra playback options.
  • +
  • The player keeps controls visible so switching and listening stay easy.
  • +
+
+
+
+ +
+

Tips

+
+
+

For faster listening

+
    +
  • Use favorites for the stations you play most often.
  • +
  • Check recents when you want to go back quickly.
  • +
  • Use search instead of scrolling long lists.
  • +
+
+
+

For a cleaner experience

+
    +
  • Use country filters when the station list feels too wide.
  • +
  • Open the player on the device you listen on most.
  • +
  • Keep the installed version handy if your browser supports it.
  • +
+
+
+
+ +
+

Help

+

If you just want the basics, this page is enough: browse, search, play, and save favorites. For general app information, open About. For privacy details, open Privacy.

+

This public docs page is intentionally short so it stays clean and easy to scan.

+
+
+
+
+ + + \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5c81319 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,61 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: [ + 'dist/**', + 'node_modules/**', + 'RadioWebApp/**', + 'coverage/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{js,mjs,cjs,jsx,ts,tsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + ...globals.browser, + ...globals.node, + ...globals.serviceworker, + __APP_BUILD_ID__: 'readonly', + __RADIO_COUNTRIES_JSON__: 'readonly', + __MANAGED_COUNTRY_CODE__: 'readonly', + cast: 'readonly', + chrome: 'readonly', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrors: 'none', + }], + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, + { + files: ['src/player.js'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + }, + }, +); diff --git a/index.html b/index.html index aa759d8..79207ed 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,20 @@ RadioPlayer - + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 790e6ff..9bc481a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,478 @@ "version": "0.1.0", "dependencies": { "idb": "^8.0.3", - "react": "^19.2.5", - "react-dom": "^19.2.5" + "react": "^19.2.7", + "react-dom": "^19.2.7" }, "devDependencies": { - "@types/node": "^24.10.1", - "@vitejs/plugin-react": "^6.0.1", - "tsx": "^4.20.6", + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.61.1", + "@types/node": "^24.13.2", + "@vitejs/plugin-react": "^6.0.3", + "eslint": "^10.6.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.7.0", + "jsdom": "^29.1.1", + "terser": "^5.48.0", + "tsx": "^4.23.0", "typescript": "^5.9.3", - "vite": "^8.0.10" + "typescript-eslint": "^8.62.1", + "vite": "^8.1.3", + "vitest": "^4.1.9" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -32,9 +489,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -49,9 +506,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -66,9 +523,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -83,9 +540,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -100,9 +557,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -117,9 +574,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -134,9 +591,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -151,9 +608,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -168,9 +625,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -185,9 +642,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -202,9 +659,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -219,9 +676,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -236,9 +693,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -253,9 +710,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -270,9 +727,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -287,9 +744,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -304,9 +761,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -321,9 +778,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -338,9 +795,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -355,9 +812,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -372,9 +829,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -389,9 +846,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -406,9 +863,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -423,9 +880,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -440,9 +897,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -457,9 +914,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -473,15 +930,288 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -493,19 +1223,35 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -520,9 +1266,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -537,9 +1283,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -554,9 +1300,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -571,9 +1317,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -588,9 +1334,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], @@ -605,9 +1351,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], @@ -622,9 +1368,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], @@ -639,9 +1385,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], @@ -656,9 +1402,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], @@ -673,9 +1419,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], @@ -690,9 +1436,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -707,9 +1453,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -717,18 +1463,41 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -743,9 +1512,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -760,16 +1529,23 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -777,25 +1553,308 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", + "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/type-utils": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.62.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", + "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", + "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -814,6 +1873,378 @@ } } }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.41", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz", + "integrity": "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -824,10 +2255,37 @@ "node": ">=8" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.385", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz", + "integrity": "sha512-78sa/M08MNAYHQfjoWMvOlKQqZ0ElhSm/L5HNUc96VZ3b+KvDVnngFm8sYQy0XrhTRgAhggHr5abA7yTvRdo4Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -838,34 +2296,284 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "dev": true, + "license": "MIT", + "peer": true, + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -884,6 +2592,57 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -899,17 +2658,70 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/idb": { @@ -918,6 +2730,193 @@ "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", "license": "ISC" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -1179,10 +3178,76 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -1198,6 +3263,127 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1206,9 +3392,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "peer": true, @@ -1219,10 +3405,57 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -1240,7 +3473,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1248,10 +3481,30 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "peer": true, "engines": { @@ -1259,36 +3512,36 @@ } }, "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.5" + "react": "^19.2.7" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "engines": { + "node": ">=0.10.0" } }, "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -1297,29 +3550,35 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } }, "node_modules/scheduler": { "version": "0.27.0", @@ -1327,6 +3586,56 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1337,10 +3646,79 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -1354,6 +3732,75 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.6.tgz", + "integrity": "sha512-rbP0Gyx8b3Ae9yO//CU2wbSnQNoQ66m1nJdSbSHmnwKwzkkz/u8mERYU8T2rmlmy+bJvRNn84yNCW8gYqox44Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.6" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.6.tgz", + "integrity": "sha512-TkQNGJIhlEphpHCjKodMTSe23egUZr/g+flI2qkLgiJ/maAzSgXypSLRTNH3nCmqgayEmtcJBiLcfODSAr1xoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1363,15 +3810,14 @@ "optional": true }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -1383,12 +3829,26 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1397,26 +3857,101 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", + "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.62.1", + "@typescript-eslint/parser": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -1432,7 +3967,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -1482,6 +4017,248 @@ "optional": true } } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index 697f34f..7643d2a 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "type": "module", "scripts": { "dev": "vite", - "prebuild": "node scripts/bump-sw-cache-version.mjs", - "build": "vite build", + "build": "vite build && node scripts/stamp-dist-sw.mjs", + "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \"scripts/**/*.{js,mjs,ts}\" \"tests/**/*.js\" \"*.js\" \"public/sw.js\"", "preview": "vite preview", - "serve:backend": "node server/index.mjs", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "check": "npm run lint && npm run typecheck && npm run test", "update:stations": "node scripts/update-stations.mjs", "update:stations:build": "npm run update:stations && npm run build", "radio:import": "tsx scripts/import-radio-stations.ts", @@ -16,14 +19,24 @@ }, "dependencies": { "idb": "^8.0.3", - "react": "^19.2.5", - "react-dom": "^19.2.5" + "react": "^19.2.7", + "react-dom": "^19.2.7" }, "devDependencies": { - "@types/node": "^24.10.1", - "@vitejs/plugin-react": "^6.0.1", - "tsx": "^4.20.6", + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.61.1", + "@types/node": "^24.13.2", + "@vitejs/plugin-react": "^6.0.3", + "eslint": "^10.6.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.7.0", + "jsdom": "^29.1.1", + "terser": "^5.48.0", + "tsx": "^4.23.0", "typescript": "^5.9.3", - "vite": "^8.0.10" + "typescript-eslint": "^8.62.1", + "vite": "^8.1.3", + "vitest": "^4.1.9" } } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..6a25f15 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,28 @@ +import { defineConfig, devices } from '@playwright/test'; + +const HOST = '127.0.0.1'; +const PORT = 4174; +const BASE_URL = `http://${HOST}:${PORT}`; + +export default defineConfig({ + testDir: './tests/e2e', + retries: 0, + use: { + baseURL: BASE_URL, + trace: 'on-first-retry', + }, + webServer: { + command: `npm run build && npm run preview -- --host ${HOST} --port ${PORT} --strictPort`, + url: BASE_URL, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}); diff --git a/privacy.html b/privacy.html index 403fd13..94ab2b4 100644 --- a/privacy.html +++ b/privacy.html @@ -1,13 +1,13 @@ - + - Pravilnik o zasebnosti | RadioPlayer - + Privacy Policy | RadioPlayer + - + @@ -248,103 +248,100 @@ RadioPlayer RadioPlayer - Spletni predvajalnik radijskih postaj + Web radio player - Nazaj v aplikacijo + Back to the app
-

Pravilnik

-

Pravilnik o zasebnosti

+

Policy

+

Privacy Policy

- Ta stran pojasnjuje, katere podatke RadioPlayer shrani samo v vašem brskalniku, zakaj jih uporablja in kako - jih lahko izbrišete. Besedilo velja za spletno uporabo aplikacije in za njeno PWA namestitev. + This page explains which data RadioPlayer stores only in your browser, why it uses that data, and how you + can remove it. This text applies to the web version of the app and to its PWA installation.

-
Velja od: 29. 4. 2026
+
Effective date: April 29, 2026
-

Kateri podatki nastanejo pri uporabi

+

What data is created during use

- RadioPlayer ne zahteva registracije ali ustvarjanja uporabniškega računa. Aplikacija pa lahko v vašem - brskalniku lokalno shrani nastavitve in sezname, ki jih ustvarite sami, da si jih ob naslednjem obisku - zapomni. + RadioPlayer does not require registration or a user account. The app may, however, store settings and + lists that you create locally in your browser so they are available again the next time you visit.

    -
  • glasnost predvajanja, zadnjo izbrano postajo in zadnji izbran filter države,
  • -
  • seznam priljubljenih postaj in lokalno statistiko predvajanja za prikaz nedavno poslušanih postaj,
  • -
  • postaje, ki jih dodate ročno v urejevalniku,
  • -
  • nastavitev načina oddajanja zvoka pri Google Cast povezavi.
  • +
  • playback volume, the last selected station, and the last selected country filter,
  • +
  • your list of favorite stations and local playback statistics used to show recently played stations,
  • +
  • stations that you add manually in the editor,
  • +
  • the selected audio casting mode for a Google Cast connection.
-

Kje se podatki hranijo

+

Where the data is stored

- Zgoraj navedeni podatki se hranijo lokalno v vašem brskalniku oziroma v podatkih spletnega mesta na vaši - napravi. Za trajnejše shranjevanje nastavitev in postaj aplikacija uporablja brskalnikovo lokalno bazo - IndexedDB. Ob prvi uporabi nove različice lahko prenese tudi stare lokalne nastavitve iz prejšnje lokalne - hrambe, da se vaši podatki ohranijo. RadioPlayer teh nastavitev ne pošilja na lasten strežnik in jih brez - vašega dejanja ne deli z drugimi uporabniki. + The data listed above is stored locally in your browser or in this website's data on your device. For + more persistent storage of settings and stations, the app uses the browser's local IndexedDB database. + When you use a new version for the first time, it may also migrate older local settings from previous + local storage so your data is preserved. RadioPlayer does not send these settings to its own server and + does not share them with other users without your action.

- Aplikacija uporablja tudi predpomnjenje datotek aplikacije za hitrejši zagon in delovanje brez ponovnega - prenosa vseh statičnih datotek. To predpomnjenje lahko kadarkoli izbrišete v nastavitvah brskalnika. + The app also uses cached application files for faster startup and operation without downloading all static + files again. You can clear this cache at any time in your browser settings.

-

Zunanji ponudniki in prenosi

+

External providers and streaming

- Ko zaženete radijski tok, se vaš brskalnik poveže neposredno s strežnikom radijske postaje ali drugega - ponudnika toka. Ti zunanji ponudniki praviloma prejmejo tehnične podatke, ki so potrebni za prenos, na - primer IP naslov, podatke o brskalniku in čas zahteve. + When you start a radio stream, your browser connects directly to the radio station's server or another + stream provider. These external providers typically receive the technical data required to deliver the + stream, such as your IP address, browser details, and the time of the request.

- Če uporabite funkcijo Google Cast, se za vzpostavitev in upravljanje oddajanja uporabi Google Cast Web - Sender SDK. Pri tem lahko Google oziroma naprava za predvajanje obdelujeta podatke, potrebne za sejo - oddajanja in usmerjanje medijev. + If you use Google Cast, the Google Cast Web Sender SDK is used to establish and manage casting. In that + case, Google or the playback device may process data required for the casting session and media routing.

- Na napravah Apple lahko aplikacija ponudi tudi AirPlay izbiro izhoda. V tem primeru se medijski tok - preusmeri na izbrano AirPlay napravo prek zmožnosti vašega brskalnika in operacijskega sistema, zato lahko - zunanja naprava ali Apple obdelujeta podatke, potrebne za vzpostavitev in upravljanje predvajanja. + On Apple devices, the app may also offer AirPlay output selection. In that case, the media stream is + routed to the selected AirPlay device through your browser and operating system capabilities, so the + external device or Apple may process data required to establish and manage playback.

-

Piškotki in analitika

+

Cookies and analytics

- Trenutna različica aplikacije ne uporablja analitičnih skript za profiliranje uporabnikov in ne nastavlja - lastnih piškotkov za oglaševanje. Aplikacija za shranjevanje nastavitev uporablja lokalno hrambo brskalnika - in IndexedDB, ne klasičnih piškotkov. + The current version of the app does not use analytics scripts for user profiling and does not set its own + advertising cookies. To store settings, the app uses the browser's local storage and IndexedDB rather + than traditional cookies.

-

Dnevniki gostovanja

+

Hosting logs

- Gostovanje spletne strani lahko zaradi varnosti, stabilnosti ali odpravljanja napak samodejno vodi osnovne - tehnične dnevnike dostopa. Obseg teh dnevnikov je odvisen od ponudnika gostovanja in ni del nastavitev, - ki jih upravlja uporabniški vmesnik RadioPlayer. + The website host may automatically keep basic technical access logs for security, stability, or debugging + purposes. The scope of these logs depends on the hosting provider and is not part of the settings managed + by the RadioPlayer user interface.

-

Kako izbrišete lokalne podatke

+

How to delete local data

- Če želite odstraniti shranjene priljubljene postaje, uporabniške postaje ali druge nastavitve, izbrišite - podatke tega spletnega mesta v svojem brskalniku. S tem se izbrišejo lokalne nastavitve in predpomnjene - datoteke aplikacije na tej napravi. + If you want to remove saved favorite stations, custom stations, or other settings, delete this website's + data in your browser. This removes local settings and cached application files from this device.

-

Posodobitve pravilnika

+

Policy updates

- Pravilnik se lahko posodobi, če se spremeni delovanje aplikacije, način gostovanja ali vključeni zunanji - ponudniki. Na tej strani bo vedno objavljena veljavna različica besedila. + This policy may be updated if the app's behavior, hosting setup, or external providers change. The + current version of the text will always be published on this page.

diff --git a/public/api/managed-stations.php b/public/api/managed-stations.php index 6fcb78e..c953c6c 100644 --- a/public/api/managed-stations.php +++ b/public/api/managed-stations.php @@ -2,7 +2,8 @@ /** * Managed catalog API endpoint. * Served at /api/managed-stations.json via .htaccess rewrite. - * Returns the same JSON envelope as the Node backend: { schemaVersion, updatedAt, stations }. + * Returns the same JSON envelope used by the Vite dev/preview middleware: + * { schemaVersion, updatedAt, stations }. */ header('Content-Type: application/json; charset=utf-8'); diff --git a/public/data/radio-stations-sync.json b/public/data/radio-stations-sync.json index 86216f9..e5eb06c 100644 --- a/public/data/radio-stations-sync.json +++ b/public/data/radio-stations-sync.json @@ -51139,6 +51139,57 @@ "source": "radio-browser", "sourceStationUuid": "b3efdf8f-dba0-11e9-a8ba-52543be04c81" }, + { + "id": "a3e4b1b3-0e4c-4fd6-9f69-6a4c0b4f7e01", + "name": "Aktual Dalmacija", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualDalmacija", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "a3e4b1b3-0e4c-4fd6-9f69-6a4c0b4f7e01" + }, + { + "id": "b1d6a9d8-0d3d-49be-9c7b-9a7f2d52b5bc", + "name": "Aktual Galama - Fešta", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualGalama", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "b1d6a9d8-0d3d-49be-9c7b-9a7f2d52b5bc" + }, + { + "id": "c8e0a7d4-2ce1-4a1e-a1bc-2fbb1c40d8f9", + "name": "Aktual Nostalgija", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualNostalgija", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "c8e0a7d4-2ce1-4a1e-a1bc-2fbb1c40d8f9" + }, { "id": "3cd65080-c8b5-4d06-86bb-62481e5d6e06", "name": "Radio1", diff --git a/public/data/radio-stations.json b/public/data/radio-stations.json index 86216f9..e5eb06c 100644 --- a/public/data/radio-stations.json +++ b/public/data/radio-stations.json @@ -51139,6 +51139,57 @@ "source": "radio-browser", "sourceStationUuid": "b3efdf8f-dba0-11e9-a8ba-52543be04c81" }, + { + "id": "a3e4b1b3-0e4c-4fd6-9f69-6a4c0b4f7e01", + "name": "Aktual Dalmacija", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualDalmacija", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "a3e4b1b3-0e4c-4fd6-9f69-6a4c0b4f7e01" + }, + { + "id": "b1d6a9d8-0d3d-49be-9c7b-9a7f2d52b5bc", + "name": "Aktual Galama - Fešta", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualGalama", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "b1d6a9d8-0d3d-49be-9c7b-9a7f2d52b5bc" + }, + { + "id": "c8e0a7d4-2ce1-4a1e-a1bc-2fbb1c40d8f9", + "name": "Aktual Nostalgija", + "country": "Slovenia", + "countryCode": "SI", + "language": "slovenian", + "tags": [], + "codec": "MP3", + "bitrate": 0, + "streamUrl": "https://live.radio.si/AktualNostalgija", + "homepage": "https://radio.svet24.si/", + "logoUrl": "https://datacache.radio.si/api/radiostations/logo/aktual.svg", + "votes": 0, + "clickcount": 0, + "source": "radio-browser", + "sourceStationUuid": "c8e0a7d4-2ce1-4a1e-a1bc-2fbb1c40d8f9" + }, { "id": "3cd65080-c8b5-4d06-86bb-62481e5d6e06", "name": "Radio1", diff --git a/public/images/76bit_logo.webp b/public/images/76bit_logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..0b93fbe81bc0889d2b4a6cbbcc063e64bdf8cffe GIT binary patch literal 8992 zcmV+*Bj4OoNk&E(BLDzbMM6+kP&il$0000G000120sy=K06|PpNHPEb009>u5J>!W z8(@uyhy_!TY&^}BpqbkD=_2|+0Xs-9P>V_HKmDiw^q>Cw>gm5g-9dU*P&goJA^-re zjR2hiDp&%&06uLrmPjNbq9G)-+Bkp>iA~pP0Bhr(Xdmx9^#oquEhvHmODXV>rUzyJSrFWb-m&~K%2P_XdrOv~2CNWE9ux`<`8@X-v<-4?5rLf7bgU(BRS;mhG zL<8EpWh&TlD_clbo=l)4;tTY7Lj;5}9sBEaX|}iv1pI!WFXs)L zm;sVIR(xcvzWpDD^8ZS7u2^9o{sG0CYDVmo+;^X%Xl>d5d*Mc?;&O6=9nk+l98s$28CE#zRCuz;;qVw z74tuutaAv#U-%btvyNM6DWvjv`sW!z*6`x~a)i5T^}SG?2fBWdLHsvU_g|YYOXM1~ zmweh&Y}a#QLgj(gj`c;8ofQB@wgx~sKu|8vg9o`J4KH$Oq9Y+kz7RHrv*)D(6+9R5 zMOyQH?D(;b_f(ngE!c4+tG1p`d;}2!N0x$9kA39IIv>h=tpEsZ;Nv&CWq}T*^`I?;du}_On-cqM?~QMMKb5-oNG^tp)l-$=}L$H~Xu?E;o=ui(!K1geN+w&5UCp~D@f zvOqv`<87H_z??djZz@bcz!3Vk$G0yQ>bw;B^97=ZcNPBi`Cvavq)xcbrT7U@0092{ zQV+NW#-^(q<{pdyDn3ymzJLG#4-g-yvjp>V9r*h;+o*R%)}gsd6<-XDji+I6aM1+1 z+}_}F+(QJVkeUEexrsuU&m)Wi=gayYq!$5>=(C99gejG4g%A|qBYPkKKL`K}`Em#W z>ByQQ$3)Y}N^Vwp#PNzA}!ZvMruayty91UH-|= zr@h5lv`29xd4LAA{QF=0QQ7JHPltx2bYHZ$38Qy-BGwiR z1M#!aypb%ONRdF(q3uV_Uv728)1Cn^?r*DQB|rQ`-o&g<8+BLSmJCcipm$CEkGat5 z0m6KTjQvz|zM_sj3eMv%wS6jK{)`ozCGP1~nXx?Ed?f%7gThdg0S;MuAS_6ydaQ#m zRJt`&!kvV~nw##5RRKt-2PoEM_PN02i3z>dNgs#-bY*RBtrCiPv9;){zwMKE^tm*- zt7JG_{cpba!EZYO>iMP2-H!djHaDqvYl`CB+ zc{um401@0*{QC<9?e4_iXnF*+9$5}D3m$&DQgw}nj^^E)eYqP_1}+z5ae&iVei#vK z;+5M&pWDSK%C77kbdu5T;mb?#y3iE8@j-T4ZqMY+S#n+q-jaYlfkVxB$%ov- z9QIYHRRFY;QzU3V{iJ%^fuKHDjDGbepB=uZ@8Ht2ffUE-CVH8wrpGw^Y9-*BjDZOVjVA z^L$q&vbwd(=!Jy-(8(2qjU8Oh@N~Y0@jUxrEqbxh`O}iD4|>b24NJ6!rjwCQUWk_P z3qR(dkiSiHhztx{@y@}*Cv#f6&@*?XzVKkhA$)hrrIm*SMnCHej*i00FaPK2@i`dv z>xM^=*2^%`HaYXx)8HR~4ZvvPlxPz;K-o{{wRf<;Q6l{frocjv-L(hU4r+bdHyZZH z;Ijn4btnft=9FUQPzz{tU4005=a*V?*bV+@|A|Uy*a=E}g=Y%%1*XdTli*i=6B!dR zeF-j-2e-cnmDIBS*gd7d&POou5I#P3vGz)wzSBV2_i|3VUe><|l+Dp`y@(w{sjo5S z+_%UQa5c@}=DO@berI@iIc`hMnErp{gb^do(r5FBk^bTI8?O1RUyR|W=zh>&Kp&wq zG@D_02Uag4_-(BuJ0^h!5%Ska)RB7YhbI=JqMa{+jZXN`Y4=8$fgxyBm3Ew+FWUsD~A{{0-hhzQW zxd!yQQb&Z<4PtONIF4VRwm$<$bJ(nHS&QyeQQg>y(i(T24_ZyL+8nOTj%jlKmiDZ{ z`$`I5eQ2<`wr~yR8Q^BoGbr-@*f4wbC@^C_3%FZ`U;mvSw zMX3j$#5^MDspnh4Q;HN~2Sk^rFf-R*X2909tqwZtj4&js+Ct%d$Jm^K2y)}H=}(OK zoYovBnK71U{z~Opa{XnRjUb92P%55-k?Q=Wy@T2Yh5X2CjB?Qz`|VVLoCW9Q3O3$P9qY66R^o(EmETIZez&QwF>6`WOKvF??vTIF`mV$zyw1<@NBd$HLIKdYaLaDFuSHwB8h{N?j%*a{pK&Ch5e#hfTdT=95vB4 z*a34!(-!fjknR5O@uzq(D=m!sC~`0Bf7imHqspJtYvN=O5dZ)kS86=e5_8H!hd62d zu`u&~h4T^z;@v<+Rsy#Gj)i+(_s5P)71JPps#hoI-o(uu52P_r_;+kV3cJ&i+|fzQ zeX||zlI=F$!{2gJ(gB^%M&Y12r&A!978m$Eb7*;#Aa4 z8qbi~r10kUJ7Mg`NZNGa6h5i`(Yn=h3J&Jmca%2bs`wrm6xK@@Afe^94sjty`^Y@`-BR4=*dTlV~jqlUF0^mE9mj?1dTNYgDQi=!U z%@TD)J!t=&Id~uR0!p7<+>br#*zHZCM0MD5?I{=(r$i+dSD>lDc{N}Zu( zJCY(U?e(qmKtBv$cdNocSL|jZi$Bm2A`sv`f#a&}ux_$~Qx~3g;Ld2WXpZcsRC%ck z?CFz^Hk(FFJ^5_`n*U@vWZw+HR_t}Z?ei+OmNwS+4i}onZ&O+GFLL|S4R;9zE7?%#3ySgJ9-iLS`{>F)%U*e0HU| zA)NKN5{wKz$Qfrwg;6-1JSU3J&2*s0=Mn=Ah4tslN{`3|$;iUBNQi)NN@*6Ef;!+t z#~SE*Kvww=U=xZM0u%POUVjaxchmx@J;fpY53rQ)!vi=FH-U2xeySPYbV^Z9Tvtg3 zOG^`xc*)%xJycG!%ZIbRY0PpQh&R^~{O%A*3@gCt>8mEV$+F7t=zPMA|-%mFNh2>@CgoW2^9YbBvs6D@@ARHmCb*$+mTw zG78hDvN#IuX`Te$Esy}$JTs}xO7xHI6mwWHV8V_DLp~rN00005k2jtZ6mM$b1o`iWd2}{OIK~~g zpxWRzD0+4rV+G@U_3bSZ+^Q7){ph~?1fij83{o)L%jpEs@Xf$Zw6?A`L4>vqwL4l# zr_+82(RUg%V>OyqjY^PCG2fCp+=0={VH9J1)S%iBc3*Z@&zqs3e4`(9h5``kp{RHr zWfQmXHMbaVm>=2xTn1S@n1C2OlKYm8B4`R4u?bQh!a-c|kfnQc&yw)`LLx)VaS2&t zVl(Np!7Q6M3d!}ku4%Kn1dUZ5K_*n8!Z@eEEwI$rPo8`8^^G2R>ktHV>1h84q=&eG zLt%XSscjS}H?_*l1VqKrhqd9K23J2<(93jNk-Cabk;dtQbM43ilY|jO!mU!Kwt)zo zr0N9(l(C^Exg$-7u=AaMCwwPil>-~|`aZT*RhIoq>L}@^NTw40oleCD@!pt^`m?)j z`YQH27~Fm27P^WY_aCjwKkoD^w#Qd48yR5)HpL$KLYFQEuT@bJ{gu4E1|ipfzLbgE z2?dtNu2W&sv`NA@2fI~uD?&hF{sH8qfrqDh4%vm}(C?CV25B2811jD7O)dnD1RsVg z@;~$OsgR@;9dCha;lr6XmpAo*b`}_(6fqGgT@r5*JDZ>9xixzP|64>zf9A+gQs?)A zoZjTkC-i}n^1z|KGdw@VB6M=Wk124_5kVKyb2ZZIwFv|8`LgbmvG}QTNwag0(FcIv zR&oIO8KT0;fZo|xqpk4#$Q9(Z+}+S&6OER5G(GBa)%-#GUpeM$e^h)=M>RT?CB}Ng z7~6GKX2Ut5dOhpeP3fI`G-$uacn_pDY_3FvaCvkr2gH({s#sGS#ntiY1HhY_y#O!|M)SYfY&9k zEhE+he+3i5>xrjV{noVs`!crvXnJGfEWD<4w}LtqZdONR`=qaEQp!zyw4Ntz_-5(a zfrOI($Ulahb*A8F!kSSxwcT)*c>MtgUFRXAdr7Veb&G>d1hj@MLe1HEEK~q0-*x!S@td zRDO87NTuaZ|0VnX-@ao$p(FmSCF}Y^bu|P%FRfvf-wJ2j(h8kL=!!@iS#n3UiNqw* zD}3SWy8Y2ecn&sll49M9aR!XX2zJMkuiqEy&o8ZTV`PmP^yJXmHou6%F7nj-MV)wT z59;Z6a#mdbmn(3jO3{G`@XE))OpuTyl^IoUEOkfa2atyxE=c1s&4Z9pA^erwGy$?( zPcv|HjrQ%;42n+5GQYI1FG6|=Ph%Dd{n@FWZr3P8S%{$~t9`GBjZQTf>tL&cFVzcy z&d73^1~G5zq@vy>;+|;!oA(rZVXy4K2Sr{I+rDtg4`^BhDeNVc9^@F+1Mb8`>Hs38L=Ph ztE=7CD2qNhn#mbKb(0bLeLE z6ok1)cQekUQbeqLt{>?P#TM3z(uk35_g2P5wd?#%7>hJod!b`0nG7K#@Ndu)%c`tC ze7+E=KzM!l2Z3q@`$VK$1_{c8IVDa2!KpYkd-iJ(w+DhfnqJ&?lZs|L|I7XF>g2=a z1yL4x7zvbA%0ha(w|w>OjP`Ks;HMHwPmN6|=j)tfTp_Kttfr5>b9XpO0%lkv&gfpFCA>Z*{8kk_6J|o!@p2!e#}&e*RFuf*f*b_P zm{jW7>$ofIzBlRr;PBP_j&c`mXc4eIr-!LbDfuw*GVJes7Dix6i*rP(_ztt3--#$j z?xt}?zdo`tK_v#8f4Oi1%Jh3Qj}QWQ)wlngL-^+#$PSSiO*x zN3aU+;_&zyvY23xs`teIyvDxW65=X3DX|=}x$!ubwDrc$iY(wP^96)nE2Qv~Uzi^{ zIN!Zqo8`qZ_k57UI(Vz58$%O3dR(K-OlN7W zLL4Bks*YFSpyRJ&9}2C;dd}#IDSyf{5kDO|W-Kz@?cv}5n5{uN4Hf$hmeFqnEMC=n zHQ+7W*-$sPX)ke)v-o2$64HD?ufIZ<(@ov9u>PQ?6d-x{9PKNU_5gLbXx9`xwi?E& z)5QO@32Fs0kID&=)EnONTnB=&OZEwe3PyhD}lw| zE7Z6YX7(8L^sj}Eh$r)R;PP--Ny9*HK)D-5Cx}0C6?!KK+?Qd-OU2i=e)(j|m{xu+ zO`QGTUaKuML^Ug|rd@O|8+L;tp z32kDUu#&!f@lJS23zrTjqnFv=B5xkG3}i7zkdNgT!M7_((JutQtFgxc+n+SbI+k#b2KAre zBcR&m-5f7!(L(-r#ITEXe|Z9SCHflF=38%s---NyN|0E~WGK~st!@HLqR*({yowLs zfL?~SQ#IWAnqBGGm%G{S?%&2Pl2`V(`i2GZ0~lZxOo@4;5gFwYDY>Lnzg#ZBC8^3_ zGRo0FdY6GT`yZD}n)y3AG|(a;SLqzYVXM>Y>BHpT8bau=VqBnm>`IeMN~6&4w`mRl zuNYq;xiOr0GBvvpUoWw;y4=LofYcP3^!l7erq#2rfNjXs5w}dSqc5DrAq47s?Ardlt6KA2JW}4fm}f` z$8kI#ChH@v_6R!qO^{1$&rqkt>s+J3OxOZ;PGIGxUccW8S)^!yAUjVa%AvVS!BpN- z}UptD=Vn2cWvWdes>}aPlnL!>|wEL&Wn37;ZNcAAK^q3PR%L0BnXf_t|~ zzO*4(ZW)KcX|@Wp0UhCL=x)qIq-pC^P9L+)fe1B%(K|ZUaw44HZNjZeljy&|mNRm) z(X1rq4HxYUdL@j>&IP;N))t@a5k$Oj%B`@};86*ObxjCuwO3;f3d+B{a+mPvBigA! zj5k6<7WX%mWufY7r0Rgr$E?2(Ouk#47Pw^juTrRz>Rq~xk(7hY18$1T@8F)&N5q-} zWx`1g(+#^Eb=HS9$1!*-UN-%eFFy(|1Kh@z{gKzii3>H4QI5h9cjrCXhJ+RG%^xj| zz{rz}Zb2aJB1N=(nF6X=guQ;6b~npwv0rHMHiy4+nkU2b0RJ}zSMHLMz4lk?K8>ba zCb&V$NsC|<5VxQkMOae#zx%aVGk_+tzeq7*GUI04zLkmsb4Jj2EnlC)OR*P|;g0c? zgF2hNFlKpIQjRuDEAwrJDY!EshK=j?6l3?3192&UFnK&zsg9BKFa_@LBwRcIqW}c5 zWix7JR6wGIX}~i+DhY~WgQ{`+Xwl~+Xq#HDI&=bECvprLF zS?p3Vv5O#j1W}frW9!HXgGQ9I^~dP`C9_=LKBFMEyv-o(gr2m1f`ZZ_( zQaE&W75xI|ygr63ke_2)t+120_@E7!W~eqW$LV^BLM>k?g*}Zkb?qibC@dA;PXu@A zLw0KEHC+efqzSuTe$bp`{hX!lF&+l#tYIN49j;@ZDM=avxo}-G+}$>i?zNt;Fcu^G zE?>LI&H@s{O+4x)4z_(4lSPPq?Bq1M+CJkFn_c6hpNsk!>6sWWb|Y_*08jH_x<0dg zPd8TQLuKrg*X1khyFrb@c?>=OAN~i@)HqYn0-a5Mm6y>^o8D4Ds7E+U=7-_rEKR?ukNNC2;r)s zTOEdVuv)De(qWSNs3m-C6Q632X|4LFY;&kNh@^D{`1yZy-N9E^C&eWgfRxZ+fWZn$ zSCm=557>?MwYH_W9Z;u?>L48IZdF@YZm;~4P%uevmsRP@yR3EWs9DHsz+IDb!F!75 z)(60FP=(nd=9VcRZ=n8TRHZjZ6oKyq4gO_nXQ+(DEbO>EkR;>cVI|zT9}V7kTEcRo zY|OacFy>xK+l5Og3^5;-ESdo}^}?F84MUAdR6%E~j8Q#EL(ckGR?nW^ol^+GDF-e9gE&h8F0`NecR1?abl+ zD^NwZ4Z5x2AkwH}KTk|xn@q%xaNk&drA@6$^Qre7@XaUkF=ifjbyHb@$in3{!Loz5 zhuED`TiRXHf8Bom_{C=8TaNkUJirHRB!~dyC=4dd>-f$cDy!?N*ObA%$-pkb7ELH?aw8X^va7EzK`59NIUS8`#9hUhb*(wStpf=4I!^qw3bU{8p;$F#AMqpR5=!^!UzQ#);!XrAgGP4YuXj zt&spa1O9Z>`pen>v-J+k2kt;6B-f%ImK0>~J1G^6*sJue!_~0jG=&LElex1lxu+rV zoqGcfj`N8of>WAa*Yax5XAH7bk&n&dHX^hR0g1x(eL2|WL4JkfyIi3kEq`IFhlgh= z$1<*711hC~v~yHru1$@8CZi8JD{mP$l?kDdY9+jtwQg=G6|k6rdgtuz;I*)|g-;bG z0zBUY!Mpqmz;np+S$t>ww2-g}&I}Yb&LhbLMEBDeS)Hx&eCl1$)R5R)FrEQ9fEb*5 zeO;T_-CAvOs^_jWyq7)A8s7}K1!P=wOU)ZHEP z+Qju%nFLJ-wKTk2bIv1CNG{0Fz!>M|JTS>|rx}dj7KQ;pX)P>40eP?KpLf^0smpot zBPae;@T)JtxMR80^$}VXZ2QqiRHTK&9^C`0q!fKJHbVcJeVKSa;;Ri2*a|<}0#6AF zsFtH}p(z4a2`I31wmgSj!;nWfYl^;q?t87Vi&20vIYg@6!-nHT)-R`3jTwvI?AbRU z)r)w#LmPutH^B%&pS`wg3Hc{p+hA country.code); -const MANAGED_COUNTRY_CODE = 'SI'; +const MANAGED_COUNTRY_CODE = '__MANAGED_COUNTRY_CODE__'; const MAX_TAGS = 12; const OBVIOUSLY_UNSUPPORTED_CODECS = new Set([ 'wma', @@ -97,7 +53,7 @@ const DATA_PATHS = new Set([ const IMAGE_FALLBACK_PATH = new URL('images/radio-placeholder.svg', self.registration.scope).pathname; const SYNC_CATALOG_URL = new URL('data/radio-stations-sync.json', self.registration.scope).href; const SYNC_CATALOG_PATH = new URL('data/radio-stations-sync.json', self.registration.scope).pathname; -const SYNC_META_URL = new URL('data/radio-stations-sync-meta.json', self.registration.scope).href; +const SYNC_META_URL = new URL('__SW_STATION_SYNC_META_PATH__', self.registration.scope).href; const SYNC_SETTINGS_URL = new URL('data/radio-stations-sync-settings.json', self.registration.scope).href; const SYNC_COUNTRY_PREFIX_PATH = new URL('data/countries/', self.registration.scope).pathname; const BUNDLED_MANAGED_CATALOG_URL = new URL('stations.json', self.registration.scope).href; diff --git a/scripts/bump-sw-cache-version.mjs b/scripts/bump-sw-cache-version.mjs deleted file mode 100644 index adfb2de..0000000 --- a/scripts/bump-sw-cache-version.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import { readFile, writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; - -const swPath = resolve(process.cwd(), 'public', 'sw.js'); -const buildStamp = `${Date.now()}`; - -const source = await readFile(swPath, 'utf8'); -const next = source.replace( - /const CACHE_NAME = 'radioplayer-pwa-v5(?:-[^']+)?';/, - `const CACHE_NAME = 'radioplayer-pwa-v5-${buildStamp}';`, -); - -if (next === source) { - throw new Error('Failed to update service worker cache version.'); -} - -await writeFile(swPath, next, 'utf8'); \ No newline at end of file diff --git a/scripts/stamp-dist-sw.mjs b/scripts/stamp-dist-sw.mjs new file mode 100644 index 0000000..e2c89bc --- /dev/null +++ b/scripts/stamp-dist-sw.mjs @@ -0,0 +1,32 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const repoRoot = process.cwd(); +const publicSwPath = resolve(repoRoot, 'public', 'sw.js'); +const distSwPath = resolve(repoRoot, 'dist', 'sw.js'); +const distIndexHtmlPath = resolve(repoRoot, 'dist', 'index.html'); +const radioConfigPath = resolve(repoRoot, 'src', 'radio', 'radioConfig.json'); +const serviceWorkerConfigPath = resolve(repoRoot, 'src', 'shared', 'serviceWorkerConfig.json'); + +const distIndexHtml = await readFile(distIndexHtmlPath, 'utf8'); +const buildStampMatch = distIndexHtml.match(/assets\/(?:main|player)-(\d+)-/); +if (!buildStampMatch) { + throw new Error('Unable to infer build stamp from dist/index.html.'); +} + +const buildStamp = buildStampMatch[1]; +const radioConfig = JSON.parse(await readFile(radioConfigPath, 'utf8')); +const serviceWorkerConfig = JSON.parse(await readFile(serviceWorkerConfigPath, 'utf8')); +const swTemplate = await readFile(publicSwPath, 'utf8'); + +const stampedSource = swTemplate + .replaceAll('__APP_BUILD_ID__', buildStamp) + .replace('__RADIO_COUNTRIES_JSON__', JSON.stringify(radioConfig.radioCountries)) + .replaceAll('__MANAGED_COUNTRY_CODE__', String(radioConfig.managedCountryCode ?? 'SI')) + .replaceAll('__SW_STATION_SYNC_TAG__', serviceWorkerConfig.stationSyncTag) + .replaceAll('__SW_STATION_PERIODIC_SYNC_TAG__', serviceWorkerConfig.stationPeriodicSyncTag) + .replaceAll('__SW_MANAGED_CATALOG_PERIODIC_SYNC_TAG__', serviceWorkerConfig.managedCatalogPeriodicSyncTag) + .replaceAll('__SW_STATION_SYNC_META_PATH__', serviceWorkerConfig.stationSyncMetaPath); + +await writeFile(distSwPath, stampedSource, 'utf8'); +console.log(`[stamp-dist-sw] Updated dist/sw.js with build stamp ${buildStamp}`); diff --git a/src/App.jsx b/src/App.jsx index f39ca8f..870c722 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -73,6 +73,74 @@ function AirPlayIcon({ size = 22, className = '' }) { ); } +function InfoIcon({ size = 14 }) { + return ( + + ); +} + +function TimerIcon({ size = 18 }) { + return ( + + ); +} + +function HelpIcon({ size = 18 }) { + return ( + + ); +} + +function AlarmIcon({ size = 18 }) { + return ( + + ); +} + +function DocsIcon({ size = 14 }) { + return ( + + ); +} + +function PrivacyIcon({ size = 14 }) { + return ( + + ); +} + function VolumeIcon() { return ( @@ -121,6 +189,13 @@ function HeaderControls() { +
+ ); } diff --git a/src/main.jsx b/src/main.jsx index 9b4120f..29ca974 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,18 +1,56 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { flushSync } from 'react-dom'; import App from './App.jsx'; +import serviceWorkerConfig from './shared/serviceWorkerConfig.json'; import './styles.css'; +const APP_BUILD_ID = __APP_BUILD_ID__; + const hadServiceWorkerControllerAtLoad = 'serviceWorker' in navigator ? Boolean(navigator.serviceWorker.controller) : false; let hasReloadedForServiceWorkerUpdate = false; -const STATION_SYNC_TAG = 'radio-stations-refresh'; -const STATION_PERIODIC_SYNC_TAG = 'radio-stations-periodic-refresh'; -const STATION_PERIODIC_SYNC_INTERVAL = 12 * 60 * 60 * 1000; -const MANAGED_CATALOG_PERIODIC_SYNC_TAG = 'radioplayer-managed-catalog-refresh'; -const MANAGED_CATALOG_PERIODIC_SYNC_INTERVAL = 24 * 60 * 60 * 1000; +const STATION_SYNC_TAG = serviceWorkerConfig.stationSyncTag; +const STATION_PERIODIC_SYNC_TAG = serviceWorkerConfig.stationPeriodicSyncTag; +const STATION_PERIODIC_SYNC_INTERVAL = serviceWorkerConfig.stationPeriodicSyncIntervalMs; +const MANAGED_CATALOG_PERIODIC_SYNC_TAG = serviceWorkerConfig.managedCatalogPeriodicSyncTag; +const MANAGED_CATALOG_PERIODIC_SYNC_INTERVAL = serviceWorkerConfig.managedCatalogPeriodicSyncIntervalMs; +const STATION_SYNC_CACHE_PREFIX = serviceWorkerConfig.stationSyncCachePrefix; +const STATION_SYNC_META_URL = `${import.meta.env.BASE_URL}${serviceWorkerConfig.stationSyncMetaPath}`; + +async function shouldScheduleImmediateStationSync() { + if (!('caches' in window)) { + return true; + } + + try { + const cacheKeys = await caches.keys(); + const syncCacheName = cacheKeys.find((cacheName) => cacheName.startsWith(STATION_SYNC_CACHE_PREFIX)); + if (!syncCacheName) { + return true; + } + + const syncCache = await caches.open(syncCacheName); + const cachedMeta = await syncCache.match(STATION_SYNC_META_URL); + if (!cachedMeta?.ok) { + return true; + } + + const payload = await cachedMeta.json(); + const syncedAt = typeof payload?.syncedAt === 'string' + ? Date.parse(payload.syncedAt) + : Number.NaN; + + if (!Number.isFinite(syncedAt)) { + return true; + } + + return (Date.now() - syncedAt) >= STATION_PERIODIC_SYNC_INTERVAL; + } catch (error) { + console.debug('Station sync freshness check failed:', error); + return true; + } +} async function registerStationCatalogSync(registration) { if (!registration) { @@ -21,8 +59,12 @@ async function registerStationCatalogSync(registration) { if ('sync' in registration) { try { - await registration.sync.register(STATION_SYNC_TAG); - console.debug('ServiceWorker background sync registered:', STATION_SYNC_TAG); + if (await shouldScheduleImmediateStationSync()) { + await registration.sync.register(STATION_SYNC_TAG); + console.debug('ServiceWorker background sync registered:', STATION_SYNC_TAG); + } else { + console.debug('ServiceWorker background sync skipped: cached station catalog is fresh.'); + } } catch (error) { console.debug('ServiceWorker background sync registration failed:', error); } @@ -68,7 +110,7 @@ function setupServiceWorker() { return; } - const swUrl = `${import.meta.env.BASE_URL}sw.js`; + const swUrl = `${import.meta.env.BASE_URL}sw.js?v=${encodeURIComponent(APP_BUILD_ID)}`; navigator.serviceWorker.register(swUrl, { updateViaCache: 'none' }) .then(async (reg) => { try { @@ -102,13 +144,11 @@ setupServiceWorker(); const rootEl = document.getElementById('root'); const root = createRoot(rootEl); -flushSync(() => { - root.render( - - - , - ); -}); +root.render( + + + , +); document.getElementById('app-loading')?.remove(); diff --git a/src/player.js b/src/player.js index 1f2b53c..1fa60ec 100644 --- a/src/player.js +++ b/src/player.js @@ -16,6 +16,7 @@ import { getPersistedLastStationCountry, getPersistedLastStationId, getPersistedRecentStationHistory, + getPersistedStationSongHistory, getPersistedStationHealth, getPersistedStationUsageCounts, getPersistedUserStations, @@ -30,11 +31,44 @@ import { persistLastStationId, persistSelectedRadioCountryCodes, persistRecentStationHistory, + persistStationSongHistory, persistStationHealth, persistStationUsageCounts, persistVolume, upsertPersistedUserStation, } from './storage/playerPersistence.ts'; +import { clearPersistedManagedStationCatalog } from './storage/stationCatalogPersistence.ts'; +import { + ARTWORK_SHINE_EFFECTS, + ICON_SPARKLE_EFFECTS, + IMPORT_EXPORT_SCHEMA, + IMPORT_EXPORT_VERSION, + PLAYBACK_START_TIMEOUT_MS, + RADIO_BROWSER_COUNTRIES_API_ENDPOINT, + RADIO_PLACEHOLDER_LOGO, + RECENT_STATION_HISTORY_LIMIT, + SERVICE_WORKER_MANAGED_CATALOG_UPDATED_MESSAGE, + SERVICE_WORKER_MANAGED_CATALOG_UPDATE_FAILED_MESSAGE, + SERVICE_WORKER_STATION_CATALOG_UPDATED_MESSAGE, + SERVICE_WORKER_STATION_CATALOG_UPDATE_FAILED_MESSAGE, + SERVICE_WORKER_SYNC_COUNTRIES_MESSAGE, + STATION_LIBRARY_PAGE_SIZE, +} from './player/constants.js'; +import { playerDom } from './player/domRefs.js'; +import { + applyStationTheme, + getMetadataFetchUrl, + getStationHomepage, + getStationMetadataUrl, + isMobileInstallViewport, + isMobileViewport, + isStandalonePwa, + normalizeStationRecord, + prefersReducedMotion, + setImgWithFallback, + toHttpsIfHttp, + uniqueNonEmpty, +} from './player/stationHelpers.js'; // Web version of RadioPlayer — HTML5 Audio + Google Cast Web Sender SDK. @@ -64,6 +98,10 @@ let stationLibraryQuery = ''; let stationLibraryCategory = 'all'; let stationLibraryCountry = 'all'; let stationLibrarySort = 'recommended'; +let stationLibraryLanguage = 'all'; +let stationLibraryCodec = 'all'; +let stationLibraryBitrate = 'all'; +let stationLibraryHealthyOnly = false; let selectedRadioCountryCodes = new Set(defaultSelectedRadioCountryCodes); let draftSelectedRadioCountryCodes = new Set(defaultSelectedRadioCountryCodes); let availableRadioCountries = radioCountries.map((country) => ({ @@ -80,6 +118,13 @@ let stationLibraryPageTotal = 1; let stationCatalogState = 'idle'; let stationCatalogError = ''; let playbackError = ''; +let sleepTimerTimeoutId = null; +let sleepTimerDeadline = null; +let sleepTimerSelectionMinutes = 0; +let wakeAlarmTimeoutId = null; +let wakeAlarmDeadline = null; +let wakeAlarmSelectionMinutes = 0; +let sessionCountdownIntervalId = null; let playbackStartupAttemptId = 0; let playbackStartupTimeoutId = null; let activeLocalPlaybackAttempt = null; @@ -88,153 +133,156 @@ let artworkShineTimeoutId = null; let artworkShineClearTimeoutId = null; let stationTitleRafId = null; let catalogSyncStatusTimeoutId = null; +let catalogSyncHistoryTimeoutId = null; +let managedCatalogStatusTimeoutId = null; -const STATION_LIBRARY_PAGE_SIZE = 24; -const RECENT_STATION_HISTORY_LIMIT = 40; -const ARTWORK_SHINE_EFFECTS = ['shine-sweep', 'shine-glint', 'shine-flare']; -const ICON_SPARKLE_EFFECTS = ['icon-flash', 'icon-gradient-sweep', 'icon-glow-pulse']; let iconSparkleTimeoutId = null; - -const RADIO_PLACEHOLDER_LOGO = `${import.meta.env.BASE_URL}images/radio-placeholder.svg`; - -// UI Elements -const stationNameEl = document.getElementById('station-name'); -const stationSubtitleEl = document.getElementById('station-subtitle'); -const stationHealthSummaryEl = document.getElementById('station-health-summary'); -const stationHealthDetailEl = document.getElementById('station-health-detail'); -const managedCatalogStatusEl = document.getElementById('managed-catalog-status'); -const catalogSyncStatusEl = document.getElementById('catalog-sync-status'); -const catalogSyncStatusTextEl = document.getElementById('catalog-sync-status-text'); -const catalogSyncStatusTimeEl = document.getElementById('catalog-sync-status-time'); -const catalogSyncHistoryEl = document.getElementById('catalog-sync-history'); -const nowPlayingEl = document.getElementById('now-playing'); -const nowArtistEl = document.getElementById('now-artist'); -const nowTitleEl = document.getElementById('now-title'); -const statusTextEl = document.getElementById('status-text'); -const statusDotEl = document.querySelector('.status-dot'); -const engineBadgeEl = document.getElementById('engine-badge'); -const engineLabelEl = document.getElementById('engine-label'); -const playBtn = document.getElementById('play-btn'); -const iconPlay = document.getElementById('icon-play'); -const iconStop = document.getElementById('icon-stop'); -const prevBtn = document.getElementById('prev-btn'); -const nextBtn = document.getElementById('next-btn'); -const volumeSlider = document.getElementById('volume-slider'); -const volumeValue = document.getElementById('volume-value'); -const muteBtn = document.getElementById('mute-btn'); -const iconVolume = document.getElementById('icon-volume'); -const iconMuted = document.getElementById('icon-muted'); -const castOverlay = document.getElementById('cast-overlay'); -const closeOverlayBtn = document.getElementById('close-overlay'); -const deviceListEl = document.getElementById('device-list'); -const coverflowStageEl = document.getElementById('artwork-coverflow-stage'); -const coverflowPrevBtn = document.getElementById('artwork-prev'); -const coverflowNextBtn = document.getElementById('artwork-next'); -const artworkPlaceholder = document.querySelector('.artwork-placeholder'); -const logoTextEl = document.querySelector('.station-logo-text'); -const logoImgEl = document.getElementById('station-logo-img'); -const stationLibraryEl = document.getElementById('station-library'); -const playerLayoutEl = document.querySelector('.player-layout'); -const stationLibraryListEl = document.getElementById('station-library-list'); -const stationLibrarySummaryEl = document.getElementById('station-library-summary'); -const stationSearchInput = document.getElementById('station-search-input'); -const stationLibraryCloseBtn = document.getElementById('station-library-close'); -const stationCategoryListEl = document.getElementById('station-category-list'); -const stationCountryFilterWrapEl = document.querySelector('[data-country-filter]'); -const stationCountryFilterBtn = document.getElementById('station-country-filter-btn'); -const stationCountryFilterMenu = document.getElementById('station-country-filter-menu'); -const stationCountryFilterText = document.getElementById('station-country-filter-text'); -const stationCountryFilterFlag = document.getElementById('station-country-filter-flag'); -const countrySelectionOverlay = document.getElementById('country-selection-overlay'); -const countrySelectionSearchInput = document.getElementById('country-selection-search-input'); -const countrySelectionSummaryEl = document.getElementById('country-selection-summary'); -const countrySelectionEmptyEl = document.getElementById('country-selection-empty'); -const countrySelectionListEl = document.getElementById('country-selection-list'); -const countrySelectionDefaultsBtn = document.getElementById('country-selection-defaults-btn'); -const countrySelectionAllBtn = document.getElementById('country-selection-all-btn'); -const countrySelectionSaveBtn = document.getElementById('country-selection-save-btn'); -const countrySelectionCancelBtn = document.getElementById('country-selection-cancel-btn'); -const stationSortSelect = document.getElementById('station-sort-select'); -const stationSortBtn = document.getElementById('station-sort-btn'); -const stationSortText = document.getElementById('station-sort-text'); -const stationSortMenu = document.getElementById('station-sort-menu'); -const stationSortWrapEl = document.querySelector('[data-sort-filter]'); +const STATION_SONG_HISTORY_LIMIT = 8; +const { + stationNameEl, + stationSubtitleEl, + stationHealthSummaryEl, + stationHealthDetailEl, + sessionSchedulerBtn, + sessionSchedulerBadge, + sessionSummaryBtn, + sessionSummaryText, + sleepTimerStatusEl, + wakeAlarmStatusEl, + sleepTimerButtons, + wakeAlarmButtons, + songHistoryPanelEl, + songHistoryListEl, + songHistoryClearBtn, + managedCatalogStatusEl, + catalogSyncStatusEl, + catalogSyncStatusTextEl, + catalogSyncStatusTimeEl, + catalogSyncHistoryEl, + nowPlayingEl, + nowArtistEl, + nowTitleEl, + statusTextEl, + statusDotEl, + engineBadgeEl, + playBtn, + iconPlay, + iconStop, + prevBtn, + nextBtn, + volumeSlider, + volumeValue, + muteBtn, + iconVolume, + iconMuted, + castOverlay, + closeOverlayBtn, + deviceListEl, + coverflowStageEl, + coverflowPrevBtn, + coverflowNextBtn, + artworkPlaceholder, + logoTextEl, + logoImgEl, + stationLibraryEl, + playerLayoutEl, + stationLibraryListEl, + stationLibrarySummaryEl, + stationSearchInput, + stationLibraryCloseBtn, + stationLibraryFiltersToggleBtn, + clearManagedCatalogBtn, + stationCategoryListEl, + stationCountryFilterWrapEl, + stationCountryFilterBtn, + stationCountryFilterMenu, + stationCountryFilterText, + stationCountryFilterFlag, + stationAdvancedFiltersBtn, + stationAdvancedFiltersOverlay, + stationAdvancedFiltersCloseBtn, + stationAdvancedFiltersDoneBtn, + countrySelectionOverlay, + countrySelectionSearchInput, + countrySelectionSummaryEl, + countrySelectionEmptyEl, + countrySelectionListEl, + countrySelectionDefaultsBtn, + countrySelectionAllBtn, + countrySelectionSaveBtn, + countrySelectionCancelBtn, + stationSortBtn, + stationSortText, + stationSortMenu, + stationSortWrapEl, + stationLibraryPaginationEl, + stationLibraryPagePrevBtn, + stationLibraryPageNextBtn, + stationLibraryPageInfo, + stationLanguageFilterWrapEl, + stationLanguageFilterBtn, + stationLanguageFilterText, + stationLanguageFilterMenu, + stationCodecFilterWrapEl, + stationCodecFilterBtn, + stationCodecFilterText, + stationCodecFilterMenu, + stationBitrateFilterWrapEl, + stationBitrateFilterBtn, + stationBitrateFilterText, + stationBitrateFilterMenu, + stationHealthFilterBtn, + stationHealthFilterText, + stationTabBtns, + installPromptBannerEl, + installPromptActionBtn, + installPromptDismissBtn, + editBtn, + stationsListBtn, + shortcutHelpBtn, + installAppBtn, + castBtn, + castBtnCastIcon, + castBtnAirPlayIcon, + editorOverlay, + sessionSchedulerOverlay, + sessionSchedulerCloseBtn, + sessionSchedulerDoneBtn, + shortcutHelpOverlay, + shortcutHelpCloseBtn, + editorCloseBtn, + editorListEl, + editorPersistenceNoteEl, + editorPersistenceBackendEl, + editorBackupActivityNoteEl, + exportUserDataBtn, + importUserDataBtn, + importUserDataInput, + resetUserDataBtn, + resetUserDataBackupCheckbox, + addStationForm, + usTitle, + usUrl, + usLogo, + usWww, + usId, + castOutputRow, + castOutputBtn, + castOutputText, +} = playerDom; let stationSortOpen = false; -const stationLibraryPaginationEl = document.getElementById('station-library-pagination'); -const stationLibraryPagePrevBtn = document.getElementById('station-library-page-prev'); -const stationLibraryPageNextBtn = document.getElementById('station-library-page-next'); -const stationLibraryPageInfo = document.getElementById('station-library-page-info'); -const stationTabBtns = document.querySelectorAll('[data-station-tab]'); -const installPromptBannerEl = document.getElementById('install-prompt-banner'); -const installPromptActionBtn = document.getElementById('install-prompt-action'); -const installPromptDismissBtn = document.getElementById('install-prompt-dismiss'); +let stationLanguageFilterOpen = false; +let stationCodecFilterOpen = false; +let stationBitrateFilterOpen = false; +let stationAdvancedFiltersOpen = false; +let stationLibraryFiltersCollapsed = false; const radioCountryByCode = new Map(radioCountries.map((country) => [country.code, country])); const radioCountryCodeByName = new Map(radioCountries.map((country) => [country.name, country.code])); const radioCountryNameByCode = new Map(radioCountries.map((country) => [country.code, country.name])); -// Editor -const editBtn = document.getElementById('edit-stations-btn'); -const stationsListBtn = document.getElementById('stations-list-btn'); -const installAppBtn = document.getElementById('install-app-btn'); -const castBtn = document.getElementById('cast-btn'); -const castBtnText = document.getElementById('cast-btn-text'); -const castBtnCastIcon = castBtn?.querySelector('svg:not(.hidden)'); -const castBtnAirPlayIcon = castBtn?.querySelectorAll('svg')?.[1] ?? null; -const editorOverlay = document.getElementById('editor-overlay'); -const editorCloseBtn = document.getElementById('editor-close-btn'); -const editorListEl = document.getElementById('editor-list'); -const editorPersistenceNoteEl = document.getElementById('editor-persistence-note'); -const editorPersistenceBackendEl = document.getElementById('editor-persistence-backend'); -const editorBackupActivityNoteEl = document.getElementById('editor-backup-activity-note'); -const exportUserDataBtn = document.getElementById('export-user-data-btn'); -const importUserDataBtn = document.getElementById('import-user-data-btn'); -const importUserDataInput = document.getElementById('import-user-data-input'); -const resetUserDataBtn = document.getElementById('reset-user-data-btn'); -const resetUserDataBackupCheckbox = document.getElementById('reset-user-data-backup-checkbox'); -const addStationForm = document.getElementById('add-station-form'); -const usTitle = document.getElementById('us_title'); -const usUrl = document.getElementById('us_url'); -const usLogo = document.getElementById('us_logo'); -const usWww = document.getElementById('us_www'); -const usId = document.getElementById('us_id'); - -// Cast output toggle -const castOutputRow = document.getElementById('cast-output-row'); -const castOutputBtn = document.getElementById('cast-output-btn'); -const castOutputText = document.getElementById('cast-output-text'); - -const IMPORT_EXPORT_SCHEMA = 'radioplayer-local-data'; -const IMPORT_EXPORT_VERSION = 1; -const PLAYBACK_START_TIMEOUT_MS = 12000; -const RADIO_BROWSER_COUNTRIES_API_ENDPOINT = 'https://de1.api.radio-browser.info/json/countries'; -const SERVICE_WORKER_SYNC_COUNTRIES_MESSAGE = 'set-sync-countries'; -const SERVICE_WORKER_STATION_CATALOG_UPDATED_MESSAGE = 'station-catalog-updated'; -const SERVICE_WORKER_STATION_CATALOG_UPDATE_FAILED_MESSAGE = 'station-catalog-update-failed'; -const SERVICE_WORKER_MANAGED_CATALOG_UPDATED_MESSAGE = 'managed-catalog-updated'; -const SERVICE_WORKER_MANAGED_CATALOG_UPDATE_FAILED_MESSAGE = 'managed-catalog-update-failed'; -const CATALOG_SYNC_HISTORY_STORAGE_KEY = 'radioplayer.catalogSyncHistory'; - // ── Utilities ──────────────────────────────────────────────────────────────── -function prefersReducedMotion() { - try { - return window.matchMedia('(prefers-reduced-motion: reduce)').matches; - } catch (e) { - return false; - } -} - -function isMobileViewport() { - return window.matchMedia('(max-width: 760px)').matches; -} - -function isStandalonePwa() { - return window.matchMedia('(display-mode: standalone)').matches - || window.matchMedia('(display-mode: fullscreen)').matches - || window.navigator.standalone === true; -} - async function lockPortraitOrientation() { if (!isMobileViewport()) return; if (!isStandalonePwa()) return; @@ -248,10 +296,6 @@ async function lockPortraitOrientation() { } } -function isMobileInstallViewport() { - return window.matchMedia('(max-width: 760px)').matches; -} - function hideInstallPromptUI() { installAppBtn?.classList.add('hidden'); installPromptBannerEl?.classList.add('hidden'); @@ -345,209 +389,6 @@ function scheduleIconSparkle() { iconSparkleTimeoutId = window.setTimeout(trigger, 3000 + Math.floor(Math.random() * 5000)); } -const STATION_THEMES = [ - { accent: '#4dd7c8', accent2: '#ffb45c', accent3: '#8fb3ff', page: '#171b22', panel: '#111821' }, - { accent: '#ff7aa8', accent2: '#ffd166', accent3: '#8fb3ff', page: '#22151d', panel: '#1b131a' }, - { accent: '#7ce38b', accent2: '#53c7ff', accent3: '#f0c86a', page: '#132018', panel: '#101a16' }, - { accent: '#b28cff', accent2: '#6ee7d8', accent3: '#ff9f6e', page: '#19162a', panel: '#141221' }, - { accent: '#ff9f43', accent2: '#4dd7c8', accent3: '#feca57', page: '#241912', panel: '#1c1510' }, - { accent: '#6ecbff', accent2: '#b28cff', accent3: '#ff7aa8', page: '#111a25', panel: '#101720' }, - { accent: '#f7d36b', accent2: '#ff7a7a', accent3: '#6ee7d8', page: '#211b10', panel: '#1a160e' }, - { accent: '#9cff6e', accent2: '#ff8bd1', accent3: '#78a7ff', page: '#14200f', panel: '#111a0e' }, - { accent: '#ff6f61', accent2: '#ffd166', accent3: '#80ed99', page: '#241513', panel: '#1c1110' }, - { accent: '#64dfdf', accent2: '#c77dff', accent3: '#f6bd60', page: '#101f22', panel: '#0e191c' }, - { accent: '#a3cef1', accent2: '#f28482', accent3: '#f6bd60', page: '#121b24', panel: '#101720' }, - { accent: '#d0f4de', accent2: '#ff99c8', accent3: '#a9def9', page: '#10201a', panel: '#0e1815' }, -]; - -function toHttpsIfHttp(url) { - if (!url || typeof url !== 'string') return ''; - return url.startsWith('http://') ? ('https://' + url.slice('http://'.length)) : url; -} - -function hashString(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash); -} - -function hexToRgb(hex) { - const normalized = String(hex || '').replace('#', '').trim(); - if (!/^[0-9a-f]{6}$/i.test(normalized)) return null; - const num = parseInt(normalized, 16); - return `${(num >> 16) & 255}, ${(num >> 8) & 255}, ${num & 255}`; -} - -function getStationTheme(station) { - const rawTheme = station?.raw?.theme || station?.theme || {}; - const key = `${station?.id || ''}:${station?.name || station?.title || ''}`; - const fallback = STATION_THEMES[hashString(key) % STATION_THEMES.length]; - return { - accent: rawTheme.accent || rawTheme.primary || station?.raw?.color || fallback.accent, - accent2: rawTheme.accent2 || rawTheme.secondary || fallback.accent2, - accent3: rawTheme.accent3 || fallback.accent3, - page: rawTheme.page || rawTheme.background || fallback.page, - panel: rawTheme.panel || fallback.panel, - }; -} - -function applyStationTheme(station) { - if (!station) return; - const theme = getStationTheme(station); - const root = document.documentElement; - root.style.setProperty('--accent', theme.accent); - root.style.setProperty('--accent-2', theme.accent2); - root.style.setProperty('--accent-3', theme.accent3); - root.style.setProperty('--page-bg', theme.page); - root.style.setProperty('--theme-panel', theme.panel); - root.style.setProperty('--accent-rgb', hexToRgb(theme.accent) || '77, 215, 200'); - root.style.setProperty('--accent-2-rgb', hexToRgb(theme.accent2) || '255, 180, 92'); - root.style.setProperty('--accent-3-rgb', hexToRgb(theme.accent3) || '143, 179, 255'); - root.style.setProperty('--theme-page-rgb', hexToRgb(theme.page) || '17, 19, 24'); - root.style.setProperty('--theme-panel-rgb', hexToRgb(theme.panel) || '16, 20, 27'); - root.style.setProperty('--accent-glow', `rgba(${hexToRgb(theme.accent) || '77, 215, 200'}, 0.34)`); -} - -function getStationStreamUrl(station) { - if (!station) return ''; - return station.streamUrl - || station.url - || station.streams?.audio - || station.streams?.primary - || station.liveAudio - || station.liveStream - || station.liveVideo - || ''; -} - -function getStationHomepage(station) { - if (!station) return ''; - return station.url - || station.homepage - || station.website - || station.www - || station.raw?.homepage - || station.raw?.website - || station.raw?.www - || ''; -} - -function getStationVideoUrl(station) { - return station?.streams?.video || station?.liveVideo || ''; -} - -function getStationMetadataUrl(station) { - return station?.metadata?.lastSongs - || station?.currentSong - || station?.lastSongs - || station?.raw?.metadata?.lastSongs - || station?.raw?.currentSong - || station?.raw?.lastSongs - || ''; -} - -function normalizeStationRecord(station, isUserStation = false) { - const name = station?.name || station?.title || station?.id || 'Unknown'; - const url = getStationStreamUrl(station); - const bitrate = Number(station?.bitrate); - - return { - id: station?.id || `${isUserStation ? 'user' : 'station'}-${name.replace(/\s+/g, '-')}`, - name, - url, - logo: station?.logoUrl || station?.assets?.logo || station?.logo || '', - poster: station?.assets?.poster || station?.poster || '', - slogan: station?.slogan || station?.description || station?.defaultText || '', - category: station?.category || station?.genre || '', - country: station?.country || '', - countryCode: station?.countryCode || '', - language: station?.language || '', - region: station?.region || '', - tags: Array.isArray(station?.tags) ? station.tags : [], - codec: station?.codec || '', - bitrate: Number.isFinite(bitrate) && bitrate > 0 ? bitrate : null, - homepage: station?.homepage || station?.website || station?.www || '', - website: station?.website || station?.homepage || station?.www || '', - metadata: station?.metadata || (station?.lastSongs ? { lastSongs: station.lastSongs } : {}), - streams: station?.streams || { - audio: url, - ...(getStationVideoUrl(station) ? { video: getStationVideoUrl(station) } : {}), - }, - enabled: typeof station?.enabled === 'boolean' ? station.enabled : true, - source: station?.source || station?.raw?.source || '', - sourceStationUuid: station?.sourceStationUuid || station?.raw?.sourceStationUuid || '', - raw: station, - _user: isUserStation, - }; -} - -function getMetadataFetchUrl(url) { - if (!url || typeof url !== 'string') return ''; - - const safeUrl = toHttpsIfHttp(url) || url; - if (!import.meta.env.DEV) { - try { - const parsed = new URL(safeUrl); - if (parsed.hostname === 'data.radio.si') { - return ''; - } - } catch (e) { - return safeUrl; - } - - return safeUrl; - } - - try { - const parsed = new URL(safeUrl); - if (parsed.hostname === 'data.radio.si') { - return `/radio-si-data${parsed.pathname}${parsed.search}`; - } - } catch (e) { - return safeUrl; - } - - return safeUrl; -} - -function uniqueNonEmpty(urls) { - const out = []; - const seen = new Set(); - for (const u of urls) { - if (!u || typeof u !== 'string') continue; - const trimmed = u.trim(); - if (!trimmed || seen.has(trimmed)) continue; - seen.add(trimmed); - out.push(trimmed); - } - return out; -} - -function setImgWithFallback(imgEl, urls, onFinalError) { - const candidates = uniqueNonEmpty(urls); - let i = 0; - - if (!imgEl || candidates.length === 0) { - if (imgEl) { imgEl.onload = null; imgEl.onerror = null; imgEl.src = ''; } - if (onFinalError) onFinalError(); - return; - } - - const tryNext = () => { - if (i >= candidates.length) { - if (onFinalError) onFinalError(); - return; - } - imgEl.src = candidates[i++]; - }; - - imgEl.onerror = tryNext; - try { imgEl.referrerPolicy = 'no-referrer'; } catch (e) {} - tryNext(); -} // ── Global error handlers ──────────────────────────────────────────────────── @@ -654,6 +495,14 @@ function saveRecentStationHistory(history) { persistRecentStationHistory(Array.isArray(history) ? history : []); } +function loadStationSongHistory() { + return getPersistedStationSongHistory(); +} + +function saveStationSongHistory(history) { + persistStationSongHistory(history && typeof history === 'object' ? history : {}); +} + function loadStationHealth() { return getPersistedStationHealth(); } @@ -677,6 +526,27 @@ function recordRecentStationPlay(station) { saveRecentStationHistory(nextHistory); } +function recordSongHistory(station, songInfo) { + if (!station?.id || !songInfo?.artist || !songInfo?.title) return; + + const history = loadStationSongHistory(); + const nextEntry = { + artist: String(songInfo.artist).trim(), + title: String(songInfo.title).trim(), + seenAt: new Date().toISOString(), + }; + if (!nextEntry.artist || !nextEntry.title) return; + + const currentHistory = Array.isArray(history[station.id]) ? history[station.id] : []; + const dedupedHistory = currentHistory.filter((entry) => ( + String(entry?.artist || '').trim().toLowerCase() !== nextEntry.artist.toLowerCase() + || String(entry?.title || '').trim().toLowerCase() !== nextEntry.title.toLowerCase() + )); + + history[station.id] = [nextEntry, ...dedupedHistory].slice(0, STATION_SONG_HISTORY_LIMIT); + saveStationSongHistory(history); +} + function classifyPlaybackFailure(station, detail, options = {}) { const originalUrl = typeof station?.url === 'string' ? station.url.trim() : ''; const upgradedUrl = toHttpsIfHttp(originalUrl) || originalUrl; @@ -809,50 +679,6 @@ function armPlaybackStartupTimeout(station, attemptId) { }, PLAYBACK_START_TIMEOUT_MS); } -async function probeStreamResponse(streamUrl) { - const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now(); - - try { - const parsedUrl = new URL(streamUrl, window.location.href); - if (parsedUrl.origin !== window.location.origin) { - // Most radio streams are cross-origin and many do not support HEAD, - // which creates noisy false 400/405 errors in the console. - return { error: null, elapsedMillis: null }; - } - } catch (error) { - return { error: null, elapsedMillis: null }; - } - - const abortController = new AbortController(); - const timeoutId = window.setTimeout(() => abortController.abort(), 5000); - - try { - const response = await fetch(streamUrl, { - method: 'HEAD', - mode: 'cors', - cache: 'no-store', - signal: abortController.signal, - }); - const elapsedMillis = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt); - - if (!response.ok) { - return { error: `Server responded with ${response.status}`, elapsedMillis }; - } - - return { error: null, elapsedMillis }; - } catch (error) { - if (error?.name === 'AbortError') { - return { error: 'The stream server did not respond in time', elapsedMillis: null }; - } - - // CORS often prevents inspecting radio stream responses. In that case, - // we let the media element attempt playback and rely on its error events. - return { error: null, elapsedMillis: null }; - } finally { - clearTimeout(timeoutId); - } -} - function downloadJsonFile(filename, data) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const objectUrl = URL.createObjectURL(blob); @@ -1302,23 +1128,7 @@ function renderCountrySelectionOverlay() { const flag = document.createElement('span'); flag.className = 'country-selection-flag'; flag.setAttribute('aria-hidden', 'true'); - - const flagUrl = countryCodeToFlagUrl(country.code); - if (flagUrl) { - const flagImg = document.createElement('img'); - flagImg.src = flagUrl; - flagImg.alt = ''; - flagImg.className = 'country-selection-flag-img'; - flagImg.loading = 'lazy'; - flagImg.referrerPolicy = 'no-referrer'; - flagImg.addEventListener('error', () => { - flagImg.remove(); - flag.textContent = country.code === managedCountryCode ? '🇸🇮' : countryCodeToFlagEmoji(country.code); - }, { once: true }); - flag.appendChild(flagImg); - } else { - flag.textContent = country.code === managedCountryCode ? '🇸🇮' : countryCodeToFlagEmoji(country.code); - } + flag.textContent = country.code === managedCountryCode ? '🇸🇮' : countryCodeToFlagEmoji(country.code); const copy = document.createElement('div'); copy.className = 'country-selection-copy'; @@ -1660,9 +1470,42 @@ function createStationInfoIndicator(detail, variant = '') { return indicator; } +function buildStationLibraryInfoDetail(station, { healthBadge = null, proxySupportDetail = null, useCount = 0 } = {}) { + const lines = []; + const stationTags = getStationTags(station); + const subtitle = getStationSubtitle(station); + + if (healthBadge?.detail) { + lines.push(`${healthBadge.label}: ${healthBadge.detail}`); + } + + if (proxySupportDetail) { + lines.push(`Proxy: ${proxySupportDetail}`); + } + + if (stationTags.length > 0) { + lines.push(`Tags: ${stationTags.join(', ')}`); + } + + if (subtitle) { + lines.push(`Info: ${subtitle}`); + } + + if (useCount > 0) { + lines.push(`Plays: ${useCount}`); + } + + return lines.join('\n'); +} + function updateManagedCatalogStatus(source = 'unknown') { if (!managedCatalogStatusEl) return; + if (managedCatalogStatusTimeoutId) { + clearTimeout(managedCatalogStatusTimeoutId); + managedCatalogStatusTimeoutId = null; + } + const sourceLabels = { remote: 'Managed catalog: backend', 'cached-remote': 'Managed catalog: cached backend', @@ -1678,6 +1521,12 @@ function updateManagedCatalogStatus(source = 'unknown') { managedCatalogStatusEl.textContent = label; managedCatalogStatusEl.classList.remove('hidden'); + + managedCatalogStatusTimeoutId = window.setTimeout(() => { + managedCatalogStatusEl.textContent = ''; + managedCatalogStatusEl.classList.add('hidden'); + managedCatalogStatusTimeoutId = null; + }, 3500); } function formatCatalogSyncTime(value) { @@ -1692,50 +1541,26 @@ function formatCatalogSyncTime(value) { }).format(parsed); } -function formatCatalogSyncHistoryTime(value) { - const parsed = value ? new Date(value) : null; - if (!parsed || Number.isNaN(parsed.getTime())) { - return ''; - } - - return new Intl.DateTimeFormat(undefined, { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }).format(parsed); -} - -function readCatalogSyncHistory() { - try { - const raw = window.localStorage.getItem(CATALOG_SYNC_HISTORY_STORAGE_KEY); - if (!raw) { - return null; - } - - const parsed = JSON.parse(raw); - if (!parsed || typeof parsed !== 'object') { - return null; - } - - const kind = parsed.kind === 'managed' ? 'managed' : parsed.kind === 'stations' ? 'stations' : null; - const syncedAt = typeof parsed.syncedAt === 'string' ? parsed.syncedAt : null; - if (!kind || !syncedAt || Number.isNaN(new Date(syncedAt).getTime())) { - return null; - } - - return { kind, syncedAt }; - } catch { - return null; - } -} - function renderCatalogSyncHistory(record) { if (!catalogSyncHistoryEl) { return; } - const formattedTime = formatCatalogSyncHistoryTime(record?.syncedAt ?? null); + if (catalogSyncHistoryTimeoutId) { + clearTimeout(catalogSyncHistoryTimeoutId); + catalogSyncHistoryTimeoutId = null; + } + + const parsed = record?.syncedAt ? new Date(record.syncedAt) : null; + const formattedTime = parsed && !Number.isNaN(parsed.getTime()) + ? new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(parsed) + : ''; + if (!record || !formattedTime) { catalogSyncHistoryEl.textContent = ''; catalogSyncHistoryEl.classList.add('hidden'); @@ -1745,21 +1570,20 @@ function renderCatalogSyncHistory(record) { const label = record.kind === 'managed' ? 'Managed catalog' : 'Stations'; catalogSyncHistoryEl.textContent = `Last background refresh: ${label} · ${formattedTime}`; catalogSyncHistoryEl.classList.remove('hidden'); + + catalogSyncHistoryTimeoutId = window.setTimeout(() => { + catalogSyncHistoryEl.textContent = ''; + catalogSyncHistoryEl.classList.add('hidden'); + catalogSyncHistoryTimeoutId = null; + }, 3500); } function persistCatalogSyncHistory(kind, syncedAt) { const normalizedSyncedAt = syncedAt && !Number.isNaN(new Date(syncedAt).getTime()) ? new Date(syncedAt).toISOString() : new Date().toISOString(); - const record = { kind, syncedAt: normalizedSyncedAt }; - try { - window.localStorage.setItem(CATALOG_SYNC_HISTORY_STORAGE_KEY, JSON.stringify(record)); - } catch { - // Ignore storage failures and still render the in-memory state. - } - - renderCatalogSyncHistory(record); + renderCatalogSyncHistory({ kind, syncedAt: normalizedSyncedAt }); } function compactCatalogSyncError(error) { @@ -1822,7 +1646,7 @@ function showCatalogSyncStatus(message, syncedAt = null, tone = 'info', detail = catalogSyncStatusEl.classList.remove('is-warning'); catalogSyncStatusEl.classList.add('hidden'); catalogSyncStatusTimeoutId = null; - }, 5000); + }, 3500); } function getStationHealthBadge(station, stationHealth = loadStationHealth()) { @@ -1909,7 +1733,7 @@ function updateActiveStationHealthSummary(station) { const healthBadge = getStationHealthBadge(station, stationHealth); if (proxySupportDetail) { stationHealthSummaryEl.className = 'station-health-summary station-health-summary-info'; - stationHealthSummaryEl.replaceChildren(createStationInfoIndicator(proxySupportDetail, 'station-info-indicator-player')); + stationHealthSummaryEl.textContent = 'Proxy support'; stationHealthSummaryEl.title = proxySupportDetail; stationHealthSummaryEl.setAttribute('aria-label', proxySupportDetail); if (stationHealthDetailEl) { @@ -2050,7 +1874,7 @@ function updateStationTitleLayout(title) { } function getStationCountry(station) { - return station?.country || station?.countryCode || station?.region || ''; + return station?.countryCode || station?.country || station?.region || ''; } function getStationTags(station) { @@ -2064,8 +1888,49 @@ function getStationTechnicalLabel(station) { return parts.join(' • '); } +function getStationLanguageValues(station) { + const rawLanguages = [ + station?.language, + station?.languagecodes, + station?.languages, + station?.raw?.language, + station?.raw?.languages, + ].flatMap((value) => { + if (Array.isArray(value)) return value; + if (typeof value === 'string') return value.split(/[;,/|]/g); + return []; + }); + + return Array.from(new Set( + rawLanguages + .map((value) => String(value || '').trim()) + .filter(Boolean) + .map((value) => value.replace(/\s+/g, ' ')), + )); +} + +function getStationPrimaryLanguage(station) { + return getStationLanguageValues(station)[0] || ''; +} + +function getStationCodecValue(station) { + return String(station?.codec || '').trim().toUpperCase(); +} + +function getStationBitrateBucket(station) { + const bitrate = Number(station?.bitrate); + if (!Number.isFinite(bitrate) || bitrate <= 0) return 'unknown'; + if (bitrate <= 96) return 'low'; + if (bitrate < 192) return 'mid'; + return 'high'; +} + function getStationDetails(station) { - return [getCountryDisplayName(getStationCountry(station)), getStationTechnicalLabel(station)].filter(Boolean).join(' • '); + return [ + getCountryDisplayName(getStationCountry(station)), + getStationPrimaryLanguage(station), + getStationTechnicalLabel(station), + ].filter(Boolean).join(' • '); } function getStationSearchText(station) { @@ -2073,6 +1938,7 @@ function getStationSearchText(station) { getStationTitle(station), getStationCountry(station), getStationSubtitle(station), + getStationPrimaryLanguage(station), getStationTechnicalLabel(station), getStationCategory(station), ...getStationTags(station), @@ -2133,6 +1999,18 @@ function getCountryNames() { return Array.from(new Set(stations.map(getStationCountry).filter(Boolean))).sort((a, b) => a.localeCompare(b)); } +function getLanguageOptions() { + return Array.from(new Set( + stations.flatMap((station) => getStationLanguageValues(station)), + )).sort((a, b) => a.localeCompare(b)); +} + +function getCodecOptions() { + return Array.from(new Set( + stations.map((station) => getStationCodecValue(station)).filter(Boolean), + )).sort((a, b) => a.localeCompare(b)); +} + function getCountryDisplayName(countryValue) { const value = String(countryValue || '').trim(); if (!value) return ''; @@ -2188,10 +2066,116 @@ function countryCodeToFlagEmoji(countryCode) { return String.fromCodePoint(...code.split('').map((char) => 127397 + char.charCodeAt(0))); } -function countryCodeToFlagUrl(countryCode) { - const code = String(countryCode || '').trim().toLowerCase(); - if (!/^[a-z]{2}$/.test(code)) return ''; - return `https://flagcdn.com/w20/${code}.png`; +function renderLibrarySelectOptions({ wrapEl, btnEl, menuEl, textEl, open, value, fallbackLabel, options, onSelect }) { + if (!btnEl || !menuEl || !textEl) return; + + const currentLabel = options.find((option) => option.value === value)?.label ?? fallbackLabel; + textEl.textContent = currentLabel; + btnEl.setAttribute('aria-expanded', open ? 'true' : 'false'); + wrapEl?.classList.toggle('open', open); + menuEl.classList.toggle('open', open); + + if (!open) { + menuEl.innerHTML = ''; + return; + } + + menuEl.innerHTML = ''; + options.forEach((option) => { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'library-select-option' + (option.value === value ? ' active' : ''); + button.setAttribute('role', 'option'); + button.setAttribute('aria-selected', option.value === value ? 'true' : 'false'); + + const text = document.createElement('span'); + text.className = 'library-select-option-text'; + text.textContent = option.label; + button.appendChild(text); + + button.addEventListener('click', () => onSelect(option.value)); + menuEl.appendChild(button); + }); +} + +function syncStationLibraryMetadataFilters() { + const languageOptions = getLanguageOptions(); + if (stationLibraryLanguage !== 'all' && !languageOptions.includes(stationLibraryLanguage)) { + stationLibraryLanguage = 'all'; + } + renderLibrarySelectOptions({ + wrapEl: stationLanguageFilterWrapEl, + btnEl: stationLanguageFilterBtn, + menuEl: stationLanguageFilterMenu, + textEl: stationLanguageFilterText, + open: stationLanguageFilterOpen, + value: stationLibraryLanguage, + fallbackLabel: 'All languages', + options: [ + { value: 'all', label: 'All languages' }, + ...languageOptions.map((language) => ({ value: language, label: language })), + ], + onSelect: (nextValue) => { + stationLibraryLanguage = nextValue; + stationLanguageFilterOpen = false; + resetStationLibraryPage(); + renderStationLibrary(); + }, + }); + + const codecOptions = getCodecOptions(); + if (stationLibraryCodec !== 'all' && !codecOptions.includes(stationLibraryCodec)) { + stationLibraryCodec = 'all'; + } + renderLibrarySelectOptions({ + wrapEl: stationCodecFilterWrapEl, + btnEl: stationCodecFilterBtn, + menuEl: stationCodecFilterMenu, + textEl: stationCodecFilterText, + open: stationCodecFilterOpen, + value: stationLibraryCodec, + fallbackLabel: 'All codecs', + options: [ + { value: 'all', label: 'All codecs' }, + ...codecOptions.map((codec) => ({ value: codec, label: codec })), + ], + onSelect: (nextValue) => { + stationLibraryCodec = nextValue; + stationCodecFilterOpen = false; + resetStationLibraryPage(); + renderStationLibrary(); + }, + }); + + renderLibrarySelectOptions({ + wrapEl: stationBitrateFilterWrapEl, + btnEl: stationBitrateFilterBtn, + menuEl: stationBitrateFilterMenu, + textEl: stationBitrateFilterText, + open: stationBitrateFilterOpen, + value: stationLibraryBitrate, + fallbackLabel: 'Any bitrate', + options: [ + { value: 'all', label: 'Any bitrate' }, + { value: 'low', label: 'Up to 96 kbps' }, + { value: 'mid', label: '97-191 kbps' }, + { value: 'high', label: '192+ kbps' }, + ], + onSelect: (nextValue) => { + stationLibraryBitrate = nextValue; + stationBitrateFilterOpen = false; + resetStationLibraryPage(); + renderStationLibrary(); + }, + }); + + if (stationHealthFilterBtn) { + stationHealthFilterBtn.setAttribute('aria-pressed', stationLibraryHealthyOnly ? 'true' : 'false'); + stationHealthFilterBtn.classList.toggle('active', stationLibraryHealthyOnly); + } + if (stationHealthFilterText) { + stationHealthFilterText.textContent = stationLibraryHealthyOnly ? 'Works well recently' : 'All stations'; + } } function getCountryFlag(countryName) { @@ -2200,12 +2184,6 @@ function getCountryFlag(countryName) { return countryCode ? countryCodeToFlagEmoji(countryCode) : '🏳️'; } -function getCountryFlagUrl(countryName) { - if (!countryName || countryName === 'all') return 'https://flagcdn.com/w20/eu.png'; - const countryCode = getCountryCodeFromValue(countryName); - return countryCode ? countryCodeToFlagUrl(countryCode) : ''; -} - function toggleStationCountryFilter(forceOpen) { stationCountryFilterOpen = typeof forceOpen === 'boolean' ? forceOpen : !stationCountryFilterOpen; renderStationLibrary(); @@ -2225,6 +2203,42 @@ const SORT_OPTIONS = [ { value: 'health', label: 'Healthiest' }, ]; +function closeLanguageFilter() { + if (!stationLanguageFilterOpen) return; + stationLanguageFilterOpen = false; + syncStationLibraryMetadataFilters(); +} + +function closeCodecFilter() { + if (!stationCodecFilterOpen) return; + stationCodecFilterOpen = false; + syncStationLibraryMetadataFilters(); +} + +function closeBitrateFilter() { + if (!stationBitrateFilterOpen) return; + stationBitrateFilterOpen = false; + syncStationLibraryMetadataFilters(); +} + +function openAdvancedFiltersOverlay() { + if (!stationAdvancedFiltersOverlay) return; + stationAdvancedFiltersOpen = true; + stationAdvancedFiltersOverlay.classList.remove('hidden'); + stationAdvancedFiltersOverlay.setAttribute('aria-hidden', 'false'); +} + +function closeAdvancedFiltersOverlay() { + if (!stationAdvancedFiltersOverlay) return; + stationAdvancedFiltersOpen = false; + stationAdvancedFiltersOverlay.classList.add('hidden'); + stationAdvancedFiltersOverlay.setAttribute('aria-hidden', 'true'); + closeSortFilter(); + closeLanguageFilter(); + closeCodecFilter(); + closeBitrateFilter(); +} + function closeSortFilter() { if (!stationSortOpen) return; stationSortOpen = false; @@ -2280,7 +2294,7 @@ function renderCountryFilterOptions() { stationCountryFilterBtn.setAttribute('aria-expanded', stationCountryFilterOpen ? 'true' : 'false'); stationCountryFilterText.textContent = getCountryFilterDisplayName(stationLibraryCountry); if (stationCountryFilterFlag) { - stationCountryFilterFlag.src = getCountryFlagUrl(stationLibraryCountry); + stationCountryFilterFlag.textContent = getCountryFlag(stationLibraryCountry); } stationCountryFilterWrapEl?.classList.toggle('open', stationCountryFilterOpen); stationCountryFilterMenu.innerHTML = ''; @@ -2294,25 +2308,9 @@ function renderCountryFilterOptions() { option.setAttribute('aria-selected', stationLibraryCountry === value ? 'true' : 'false'); const flag = document.createElement('span'); - const flagUrl = getCountryFlagUrl(value); flag.className = 'library-select-option-flag'; flag.setAttribute('aria-hidden', 'true'); - if (flagUrl) { - const flagImg = document.createElement('img'); - flagImg.src = flagUrl; - flagImg.alt = ''; - flagImg.setAttribute('aria-hidden', 'true'); - flagImg.className = 'library-select-option-flag-img'; - flagImg.loading = 'lazy'; - flagImg.referrerPolicy = 'no-referrer'; - flagImg.addEventListener('error', () => { - flagImg.remove(); - flag.textContent = value === 'all' ? '🌐' : '🏳️'; - }, { once: true }); - flag.appendChild(flagImg); - } else { - flag.textContent = value === 'all' ? '🌐' : '🏳️'; - } + flag.textContent = getCountryFlag(value); const text = document.createElement('span'); text.className = 'library-select-option-text'; @@ -2383,6 +2381,22 @@ function getFilteredStationEntries() { entries = entries.filter(({ station }) => getStationCategory(station) === stationLibraryCategory); } + if (stationLibraryLanguage !== 'all') { + entries = entries.filter(({ station }) => getStationLanguageValues(station).includes(stationLibraryLanguage)); + } + + if (stationLibraryCodec !== 'all') { + entries = entries.filter(({ station }) => getStationCodecValue(station) === stationLibraryCodec); + } + + if (stationLibraryBitrate !== 'all') { + entries = entries.filter(({ station }) => getStationBitrateBucket(station) === stationLibraryBitrate); + } + + if (stationLibraryHealthyOnly) { + entries = entries.filter(({ station }) => getStationHealthScore(station, stationHealth) >= 0.7); + } + if (query) { entries = entries.filter(({ station }) => getStationSearchText(station).includes(query)); } @@ -2493,6 +2507,7 @@ function renderStationLibrary(options = {}) { renderCountryFilterOptions(); renderSortOptions(); + syncStationLibraryMetadataFilters(); renderCategoryChips(); if (stationCatalogState === 'loading') { @@ -2603,9 +2618,12 @@ function renderStationLibrary(options = {}) { const copy = document.createElement('div'); copy.className = 'library-station-copy'; + const titleRow = document.createElement('div'); + titleRow.className = 'library-station-title-row'; const titleEl = document.createElement('div'); titleEl.className = 'library-station-title'; titleEl.textContent = title; + titleRow.appendChild(titleEl); const meta = document.createElement('div'); meta.className = 'library-station-meta'; const country = document.createElement('span'); @@ -2616,44 +2634,18 @@ function renderStationLibrary(options = {}) { tech.textContent = getStationTechnicalLabel(station) || getStationCategory(station); meta.appendChild(country); meta.appendChild(tech); - copy.appendChild(titleEl); + copy.appendChild(titleRow); copy.appendChild(meta); + const useCount = Number(counts[station.id] ?? count) || 0; - const tags = document.createElement('div'); - tags.className = 'library-station-tags'; - if (proxySupportDetail) { - tags.appendChild(createStationInfoIndicator(proxySupportDetail, 'station-info-indicator-inline')); - } - if (healthBadge) { - const healthChip = document.createElement('span'); - healthChip.className = `library-tag library-tag-${healthBadge.tone}`; - healthChip.textContent = healthBadge.label; - healthChip.title = healthBadge.detail; - tags.appendChild(healthChip); - } - const stationTags = getStationTags(station).slice(0, 3); - stationTags.forEach((tag) => { - const chip = document.createElement('span'); - chip.className = 'library-tag'; - chip.textContent = tag; - tags.appendChild(chip); + const infoDetail = buildStationLibraryInfoDetail(station, { + healthBadge, + proxySupportDetail, + useCount, }); - const useCount = Number(counts[station.id] ?? count) || 0; - if (useCount > 0) { - const plays = document.createElement('span'); - plays.className = 'library-tag muted'; - plays.textContent = `${useCount} plays`; - tags.appendChild(plays); - } else if (stationTags.length === 0 && getStationSubtitle(station)) { - const fallback = document.createElement('span'); - fallback.className = 'library-tag muted'; - fallback.textContent = getStationSubtitle(station); - tags.appendChild(fallback); - } - - if (tags.childElementCount > 0) { - copy.appendChild(tags); + if (infoDetail) { + titleRow.appendChild(createStationInfoIndicator(infoDetail, 'station-info-indicator-inline station-info-indicator-neutral')); } row.appendChild(copy); @@ -2711,7 +2703,10 @@ function openStationLibrary() { } else { playerLayoutEl?.classList.remove('library-collapsed'); } - setTimeout(() => stationSearchInput?.focus(), 30); + setTimeout(() => { + if (stationLibraryFiltersCollapsed) stationLibraryFiltersToggleBtn?.focus(); + else stationSearchInput?.focus(); + }, 30); } function closeStationLibrary() { @@ -2741,6 +2736,36 @@ function toggleStationLibrary() { else closeStationLibrary(); } +function applyStationLibraryFiltersState() { + if (!stationLibraryEl || !stationLibraryFiltersToggleBtn) return; + + stationLibraryEl.classList.toggle('filters-collapsed', stationLibraryFiltersCollapsed); + stationLibraryFiltersToggleBtn.setAttribute('aria-expanded', stationLibraryFiltersCollapsed ? 'false' : 'true'); + + const label = stationLibraryFiltersCollapsed + ? 'Expand search and filters' + : 'Collapse search and filters'; + + stationLibraryFiltersToggleBtn.setAttribute('aria-label', label); + stationLibraryFiltersToggleBtn.title = label; +} + +function toggleStationLibraryFilters(forceCollapsed) { + stationLibraryFiltersCollapsed = typeof forceCollapsed === 'boolean' + ? forceCollapsed + : !stationLibraryFiltersCollapsed; + + if (stationLibraryFiltersCollapsed) { + closeStationCountryFilter(); + closeSortFilter(); + closeLanguageFilter(); + closeCodecFilter(); + closeBitrateFilter(); + } + + applyStationLibraryFiltersState(); +} + async function moveQuickPick(delta) { const entries = getQuickPickEntries(); if (!entries.length) return; @@ -3141,7 +3166,12 @@ async function fetchAndStoreCurrentSong(station, idx, url) { } if (now) { + const previousArtist = String(station.currentSongInfo?.artist || '').trim().toLowerCase(); + const previousTitle = String(station.currentSongInfo?.title || '').trim().toLowerCase(); station.currentSongInfo = now; + if (previousArtist !== String(now.artist || '').trim().toLowerCase() || previousTitle !== String(now.title || '').trim().toLowerCase()) { + recordSongHistory(station, now); + } if (idx === currentIndex) updateNowPlayingUI(); // If provider gives timing info, schedule a single-shot refresh at song end. @@ -3189,6 +3219,192 @@ async function fetchAndStoreCurrentSong(station, idx, url) { } } +function formatRelativeTimer(deadline) { + if (!deadline) return 'Off'; + const diffMs = deadline - Date.now(); + if (diffMs <= 0) return 'Now'; + const totalSeconds = Math.ceil(diffMs / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return seconds > 0 + ? `In ${hours}h ${minutes}m ${seconds}s` + : `In ${hours}h ${minutes}m`; + } + + if (minutes > 0) { + return `In ${minutes}m ${seconds}s`; + } + + return `In ${seconds}s`; +} + +function stopSessionCountdownTicker() { + if (!sessionCountdownIntervalId) return; + clearInterval(sessionCountdownIntervalId); + sessionCountdownIntervalId = null; +} + +function refreshSessionCountdownUI() { + updateSleepTimerUI(); + updateWakeAlarmUI(); +} + +function ensureSessionCountdownTicker() { + const hasActiveSession = Boolean(sleepTimerDeadline || wakeAlarmDeadline); + if (!hasActiveSession) { + stopSessionCountdownTicker(); + return; + } + + if (sessionCountdownIntervalId) return; + + sessionCountdownIntervalId = window.setInterval(() => { + refreshSessionCountdownUI(); + if (!sleepTimerDeadline && !wakeAlarmDeadline) { + stopSessionCountdownTicker(); + } + }, 1000); +} + +function buildSessionSummaryText() { + const sleepText = sleepTimerDeadline ? `Sleep ${formatRelativeTimer(sleepTimerDeadline).toLowerCase()}` : 'Sleep off'; + const wakeText = wakeAlarmDeadline ? `Wake ${formatRelativeTimer(wakeAlarmDeadline).toLowerCase()}` : 'Wake off'; + return `${sleepText} · ${wakeText}`; +} + +function updateSleepTimerUI() { + if (sleepTimerStatusEl) { + sleepTimerStatusEl.textContent = sleepTimerDeadline ? `Playback stops ${formatRelativeTimer(sleepTimerDeadline).toLowerCase()}.` : 'No sleep timer set.'; + } + sleepTimerButtons?.forEach((button) => { + const minutes = Number(button.getAttribute('data-sleep-minutes')); + button.classList.toggle('active', minutes > 0 && minutes === sleepTimerSelectionMinutes); + }); + const hasActiveSession = Boolean(sleepTimerDeadline || wakeAlarmDeadline); + sessionSchedulerBtn?.classList.toggle('session-active', hasActiveSession); + sessionSummaryBtn?.setAttribute('aria-expanded', sessionSchedulerOverlay && !sessionSchedulerOverlay.classList.contains('hidden') ? 'true' : 'false'); + if (sessionSchedulerBadge) { + sessionSchedulerBadge.classList.toggle('hidden', !hasActiveSession); + } + if (sessionSummaryText) { + sessionSummaryText.textContent = buildSessionSummaryText(); + } +} + +function updateWakeAlarmUI() { + if (wakeAlarmStatusEl) { + wakeAlarmStatusEl.textContent = wakeAlarmDeadline ? `Playback resumes ${formatRelativeTimer(wakeAlarmDeadline).toLowerCase()}.` : 'No wake alarm set.'; + } + wakeAlarmButtons?.forEach((button) => { + const minutes = Number(button.getAttribute('data-wake-minutes')); + button.classList.toggle('active', minutes > 0 && minutes === wakeAlarmSelectionMinutes); + }); + const hasActiveSession = Boolean(sleepTimerDeadline || wakeAlarmDeadline); + sessionSchedulerBtn?.classList.toggle('session-active', hasActiveSession); + if (sessionSchedulerBadge) { + sessionSchedulerBadge.classList.toggle('hidden', !hasActiveSession); + } + if (sessionSummaryText) { + sessionSummaryText.textContent = buildSessionSummaryText(); + } + sessionSummaryBtn?.setAttribute('aria-expanded', sessionSchedulerOverlay && !sessionSchedulerOverlay.classList.contains('hidden') ? 'true' : 'false'); +} + +function clearSleepTimer() { + if (sleepTimerTimeoutId) clearTimeout(sleepTimerTimeoutId); + sleepTimerTimeoutId = null; + sleepTimerDeadline = null; + sleepTimerSelectionMinutes = 0; + updateSleepTimerUI(); + ensureSessionCountdownTicker(); +} + +function clearWakeAlarm() { + if (wakeAlarmTimeoutId) clearTimeout(wakeAlarmTimeoutId); + wakeAlarmTimeoutId = null; + wakeAlarmDeadline = null; + wakeAlarmSelectionMinutes = 0; + updateWakeAlarmUI(); + ensureSessionCountdownTicker(); +} + +function scheduleSleepTimer(minutes) { + clearSleepTimer(); + if (!Number.isFinite(minutes) || minutes <= 0) return; + sleepTimerSelectionMinutes = minutes; + sleepTimerDeadline = Date.now() + minutes * 60000; + sleepTimerTimeoutId = window.setTimeout(async () => { + clearSleepTimer(); + await stop(); + if (statusTextEl) statusTextEl.textContent = 'Sleep timer finished'; + }, minutes * 60000); + updateSleepTimerUI(); + ensureSessionCountdownTicker(); +} + +function scheduleWakeAlarm(minutes) { + clearWakeAlarm(); + if (!Number.isFinite(minutes) || minutes <= 0) return; + wakeAlarmSelectionMinutes = minutes; + wakeAlarmDeadline = Date.now() + minutes * 60000; + wakeAlarmTimeoutId = window.setTimeout(async () => { + clearWakeAlarm(); + try { + await play(); + if (statusTextEl) statusTextEl.textContent = 'Wake alarm started playback'; + } catch (error) { + if (statusTextEl) statusTextEl.textContent = 'Wake alarm is ready'; + } + }, minutes * 60000); + updateWakeAlarmUI(); + ensureSessionCountdownTicker(); +} + +function openSessionSchedulerOverlay() { + if (!sessionSchedulerOverlay) return; + sessionSchedulerOverlay.classList.remove('hidden'); + sessionSchedulerOverlay.setAttribute('aria-hidden', 'false'); + sessionSummaryBtn?.setAttribute('aria-expanded', 'true'); +} + +function closeSessionSchedulerOverlay() { + if (!sessionSchedulerOverlay) return; + sessionSchedulerOverlay.classList.add('hidden'); + sessionSchedulerOverlay.setAttribute('aria-hidden', 'true'); + sessionSummaryBtn?.setAttribute('aria-expanded', 'false'); +} + +function renderSongHistory() { + if (!songHistoryPanelEl || !songHistoryListEl) return; + + const station = stations[currentIndex]; + const historyMap = loadStationSongHistory(); + const entries = station?.id ? (historyMap[station.id] || []) : []; + + songHistoryListEl.innerHTML = ''; + songHistoryPanelEl.classList.toggle('hidden', entries.length === 0); + if (!entries.length) return; + + entries.forEach((entry) => { + const item = document.createElement('li'); + item.className = 'song-history-item'; + + const title = document.createElement('div'); + title.className = 'song-history-track'; + title.textContent = entry.title; + + const meta = document.createElement('div'); + meta.className = 'song-history-meta'; + meta.textContent = `${entry.artist} · ${new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(new Date(entry.seenAt))}`; + + item.append(title, meta); + songHistoryListEl.appendChild(item); + }); +} + function updateNowPlayingUI() { const station = stations[currentIndex]; if (!station) return; @@ -3204,6 +3420,7 @@ function updateNowPlayingUI() { } } if (stationSubtitleEl) stationSubtitleEl.textContent = getStationDetails(station) || 'Live stream'; + renderSongHistory(); } // ── Load station UI ─────────────────────────────────────────────────────────── @@ -3220,6 +3437,7 @@ function loadStation(index) { if (nowPlayingEl) nowPlayingEl.classList.add('hidden'); if (nowArtistEl) nowArtistEl.textContent = ''; if (nowTitleEl) nowTitleEl.textContent = ''; + renderSongHistory(); try { if (logoTextEl && station.name) { @@ -3408,9 +3626,6 @@ function updateCastButtonUI() { ? (airPlayReady ? 'Cast or AirPlay to device' : 'Cast to device') : (airPlayReady ? 'AirPlay to device' : 'Cast and AirPlay are not available'); - if (castBtnText) { - castBtnText.textContent = label; - } castBtn.title = remoteActive ? `${label} active` : title; castBtn.setAttribute('aria-label', remoteActive ? `${label} active` : title); castBtnCastIcon?.classList.toggle('hidden', isAirPlayActive); @@ -3518,16 +3733,7 @@ async function playLocal() { if (statusDotEl) statusDotEl.style.backgroundColor = 'var(--text-muted)'; const streamUrl = toHttpsIfHttp(station.url) || station.url; - const probeResult = await probeStreamResponse(streamUrl); - beginLocalPlaybackAttempt(station, attemptId, probeResult?.elapsedMillis ?? null); - if (attemptId !== playbackStartupAttemptId) return; - if (probeResult?.error) { - completeLocalPlaybackAttemptAsFailure(station, probeResult.error, {}); - playbackError = getPlaybackFailureMessage(station, probeResult.error); - isPlaying = false; - updateUI(); - return; - } + beginLocalPlaybackAttempt(station, attemptId, null); audio.src = streamUrl; audio.volume = isMuted ? 0 : currentVolume; @@ -3661,6 +3867,18 @@ function toggleMute() { if (iconMuted) iconMuted.classList.toggle('hidden', !isMuted); } +function openShortcutHelpOverlay() { + if (!shortcutHelpOverlay) return; + shortcutHelpOverlay.classList.remove('hidden'); + shortcutHelpOverlay.setAttribute('aria-hidden', 'false'); +} + +function closeShortcutHelpOverlay() { + if (!shortcutHelpOverlay) return; + shortcutHelpOverlay.classList.add('hidden'); + shortcutHelpOverlay.setAttribute('aria-hidden', 'true'); +} + // ── Stations overlay ────────────────────────────────────────────────────────── async function openStationsOverlay() { @@ -3927,10 +4145,44 @@ function setupEventListeners() { resetUserDataBtn?.addEventListener('click', () => { void handleResetUserData(); }); + stationAdvancedFiltersBtn?.addEventListener('click', openAdvancedFiltersOverlay); + stationAdvancedFiltersCloseBtn?.addEventListener('click', closeAdvancedFiltersOverlay); + stationAdvancedFiltersDoneBtn?.addEventListener('click', closeAdvancedFiltersOverlay); + sessionSchedulerBtn?.addEventListener('click', openSessionSchedulerOverlay); + sessionSummaryBtn?.addEventListener('click', openSessionSchedulerOverlay); + sessionSchedulerCloseBtn?.addEventListener('click', closeSessionSchedulerOverlay); + sessionSchedulerDoneBtn?.addEventListener('click', closeSessionSchedulerOverlay); + shortcutHelpBtn?.addEventListener('click', openShortcutHelpOverlay); + shortcutHelpCloseBtn?.addEventListener('click', closeShortcutHelpOverlay); + songHistoryClearBtn?.addEventListener('click', () => { + const station = stations[currentIndex]; + if (!station?.id) return; + const history = loadStationSongHistory(); + delete history[station.id]; + saveStationSongHistory(history); + renderSongHistory(); + }); stationLibraryCloseBtn?.addEventListener('click', closeStationLibrary); + stationLibraryFiltersToggleBtn?.addEventListener('click', () => { + toggleStationLibraryFilters(); + }); + clearManagedCatalogBtn?.addEventListener('click', async () => { + try { + await clearPersistedManagedStationCatalog(); + showCatalogSyncStatus('Cleared persisted managed catalog'); + await loadStations(); + } catch (e) { + console.error('clearManagedCatalog failed', e); + showCatalogSyncStatus('Failed to clear managed catalog', null, 'warning', e?.message || ''); + } + }); stationCountryFilterBtn?.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); + closeSortFilter(); + closeLanguageFilter(); + closeCodecFilter(); + closeBitrateFilter(); toggleStationCountryFilter(); }); stationSearchInput?.addEventListener('input', () => { @@ -3938,15 +4190,55 @@ function setupEventListeners() { resetStationLibraryPage(); renderStationLibrary(); }); - stationSortSelect?.addEventListener('change', () => { - stationLibrarySort = stationSortSelect.value || 'recommended'; + stationLanguageFilterBtn?.addEventListener('click', (event) => { + event.stopPropagation(); + stationLanguageFilterOpen = !stationLanguageFilterOpen; + closeStationCountryFilter(); + closeSortFilter(); + closeCodecFilter(); + closeBitrateFilter(); + syncStationLibraryMetadataFilters(); + }); + stationCodecFilterBtn?.addEventListener('click', (event) => { + event.stopPropagation(); + stationCodecFilterOpen = !stationCodecFilterOpen; + closeStationCountryFilter(); + closeSortFilter(); + closeLanguageFilter(); + closeBitrateFilter(); + syncStationLibraryMetadataFilters(); + }); + stationBitrateFilterBtn?.addEventListener('click', (event) => { + event.stopPropagation(); + stationBitrateFilterOpen = !stationBitrateFilterOpen; + closeStationCountryFilter(); + closeSortFilter(); + closeLanguageFilter(); + closeCodecFilter(); + syncStationLibraryMetadataFilters(); + }); + stationHealthFilterBtn?.addEventListener('click', () => { + stationLibraryHealthyOnly = !stationLibraryHealthyOnly; resetStationLibraryPage(); renderStationLibrary(); }); + sleepTimerButtons?.forEach((button) => { + button.addEventListener('click', () => { + scheduleSleepTimer(Number(button.getAttribute('data-sleep-minutes'))); + }); + }); + wakeAlarmButtons?.forEach((button) => { + button.addEventListener('click', () => { + scheduleWakeAlarm(Number(button.getAttribute('data-wake-minutes'))); + }); + }); stationSortBtn?.addEventListener('click', (e) => { e.stopPropagation(); stationSortOpen = !stationSortOpen; closeStationCountryFilter(); + closeLanguageFilter(); + closeCodecFilter(); + closeBitrateFilter(); renderSortOptions(); }); stationTabBtns.forEach((btn) => { @@ -3968,6 +4260,21 @@ function setupEventListeners() { if (stationSortOpen && !stationSortWrapEl?.contains(ev.target)) { closeSortFilter(); } + if (stationLanguageFilterOpen && !stationLanguageFilterWrapEl?.contains(ev.target)) { + closeLanguageFilter(); + } + if (stationCodecFilterOpen && !stationCodecFilterWrapEl?.contains(ev.target)) { + closeCodecFilter(); + } + if (stationBitrateFilterOpen && !stationBitrateFilterWrapEl?.contains(ev.target)) { + closeBitrateFilter(); + } + if (stationAdvancedFiltersOverlay && ev.target === stationAdvancedFiltersOverlay) { + closeAdvancedFiltersOverlay(); + } + if (sessionSchedulerOverlay && ev.target === sessionSchedulerOverlay) { + closeSessionSchedulerOverlay(); + } }); // Keyboard shortcuts @@ -3983,17 +4290,56 @@ function setupEventListeners() { closeSortFilter(); return; } + if (stationLanguageFilterOpen && e.code === 'Escape') { + e.preventDefault(); + closeLanguageFilter(); + return; + } + if (stationCodecFilterOpen && e.code === 'Escape') { + e.preventDefault(); + closeCodecFilter(); + return; + } + if (stationBitrateFilterOpen && e.code === 'Escape') { + e.preventDefault(); + closeBitrateFilter(); + return; + } + if (stationAdvancedFiltersOpen && e.code === 'Escape') { + e.preventDefault(); + closeAdvancedFiltersOverlay(); + return; + } if (countrySelectionOverlay && !countrySelectionOverlay.classList.contains('hidden') && e.code === 'Escape') { e.preventDefault(); closeCountrySelectionOverlay(); return; } + if (shortcutHelpOverlay && !shortcutHelpOverlay.classList.contains('hidden') && e.code === 'Escape') { + e.preventDefault(); + closeShortcutHelpOverlay(); + return; + } + if (sessionSchedulerOverlay && !sessionSchedulerOverlay.classList.contains('hidden') && e.code === 'Escape') { + e.preventDefault(); + closeSessionSchedulerOverlay(); + return; + } + if ((e.key === '?' || (e.shiftKey && e.code === 'Slash'))) { + e.preventDefault(); + openShortcutHelpOverlay(); + return; + } if (e.code === 'Space') { e.preventDefault(); togglePlay(); } else if (e.code === 'ArrowRight') playNext(); else if (e.code === 'ArrowLeft') playPrev(); else if (e.code === 'KeyM') toggleMute(); - else if (e.code === 'Escape') { closeStationLibrary(); closeCastOverlay(); closeEditorOverlay(); } + else if (e.code === 'Escape') { closeStationLibrary(); closeCastOverlay(); closeEditorOverlay(); closeShortcutHelpOverlay(); closeSessionSchedulerOverlay(); } }); + + applyStationLibraryFiltersState(); + updateSleepTimerUI(); + updateWakeAlarmUI(); } async function promptInstallApp() { @@ -4043,7 +4389,6 @@ async function init() { restoreCastBothMode(); restoreLastStationCountry(); restoreSelectedRadioCountryCodes(); - renderCatalogSyncHistory(readCatalogSyncHistory()); await loadStations(); setupEventListeners(); setupServiceWorkerCatalogRefreshListener(); @@ -4090,6 +4435,38 @@ document.addEventListener('visibilitychange', () => { } }); +// Runtime debug helpers (use from console) +window.__rp_inspectManagedCatalog = async function() { + try { + const persisted = await loadPersistedManagedStations(); + console.log('Persisted managed catalog:', persisted); + console.log('In-memory stations count:', stations.length); + return { persisted, stations }; + } catch (e) { + console.error('inspectManagedCatalog failed', e); + } +}; + +window.__rp_clearManagedCatalog = async function() { + try { + await clearPersistedManagedStationCatalog(); + console.log('Cleared persisted managed catalog. Call loadStations() to reload catalogs.'); + } catch (e) { + console.error('clearManagedCatalog failed', e); + } +}; + +window.__rp_refreshManagedCatalog = async function() { + try { + await clearPersistedManagedStationCatalog(); + await loadManagedStations(); + await loadStations(); + console.log('Refreshed managed catalog from backend and reloaded stations.'); + } catch (e) { + console.error('refreshManagedCatalog failed', e); + } +}; + export function initPlayer() { return init(); } diff --git a/src/player/constants.js b/src/player/constants.js new file mode 100644 index 0000000..b7f1c9e --- /dev/null +++ b/src/player/constants.js @@ -0,0 +1,32 @@ +import serviceWorkerConfig from '../shared/serviceWorkerConfig.json'; + +export const STATION_LIBRARY_PAGE_SIZE = 24; +export const RECENT_STATION_HISTORY_LIMIT = 40; +export const ARTWORK_SHINE_EFFECTS = ['shine-sweep', 'shine-glint', 'shine-flare']; +export const ICON_SPARKLE_EFFECTS = ['icon-flash', 'icon-gradient-sweep', 'icon-glow-pulse']; +export const RADIO_PLACEHOLDER_LOGO = `${import.meta.env.BASE_URL}images/radio-placeholder.svg`; + +export const IMPORT_EXPORT_SCHEMA = 'radioplayer-local-data'; +export const IMPORT_EXPORT_VERSION = 1; +export const PLAYBACK_START_TIMEOUT_MS = 12000; +export const RADIO_BROWSER_COUNTRIES_API_ENDPOINT = 'https://de1.api.radio-browser.info/json/countries'; +export const SERVICE_WORKER_SYNC_COUNTRIES_MESSAGE = serviceWorkerConfig.messages.syncCountries; +export const SERVICE_WORKER_STATION_CATALOG_UPDATED_MESSAGE = serviceWorkerConfig.messages.stationCatalogUpdated; +export const SERVICE_WORKER_STATION_CATALOG_UPDATE_FAILED_MESSAGE = serviceWorkerConfig.messages.stationCatalogUpdateFailed; +export const SERVICE_WORKER_MANAGED_CATALOG_UPDATED_MESSAGE = serviceWorkerConfig.messages.managedCatalogUpdated; +export const SERVICE_WORKER_MANAGED_CATALOG_UPDATE_FAILED_MESSAGE = serviceWorkerConfig.messages.managedCatalogUpdateFailed; + +export const STATION_THEMES = [ + { accent: '#4dd7c8', accent2: '#ffb45c', accent3: '#8fb3ff', page: '#171b22', panel: '#111821' }, + { accent: '#ff7aa8', accent2: '#ffd166', accent3: '#8fb3ff', page: '#22151d', panel: '#1b131a' }, + { accent: '#7ce38b', accent2: '#53c7ff', accent3: '#f0c86a', page: '#132018', panel: '#101a16' }, + { accent: '#b28cff', accent2: '#6ee7d8', accent3: '#ff9f6e', page: '#19162a', panel: '#141221' }, + { accent: '#ff9f43', accent2: '#4dd7c8', accent3: '#feca57', page: '#241912', panel: '#1c1510' }, + { accent: '#6ecbff', accent2: '#b28cff', accent3: '#ff7aa8', page: '#111a25', panel: '#101720' }, + { accent: '#f7d36b', accent2: '#ff7a7a', accent3: '#6ee7d8', page: '#211b10', panel: '#1a160e' }, + { accent: '#9cff6e', accent2: '#ff8bd1', accent3: '#78a7ff', page: '#14200f', panel: '#111a0e' }, + { accent: '#ff6f61', accent2: '#ffd166', accent3: '#80ed99', page: '#241513', panel: '#1c1110' }, + { accent: '#64dfdf', accent2: '#c77dff', accent3: '#f6bd60', page: '#101f22', panel: '#0e191c' }, + { accent: '#a3cef1', accent2: '#f28482', accent3: '#f6bd60', page: '#121b24', panel: '#101720' }, + { accent: '#d0f4de', accent2: '#ff99c8', accent3: '#a9def9', page: '#10201a', panel: '#0e1815' }, +]; diff --git a/src/player/domRefs.js b/src/player/domRefs.js new file mode 100644 index 0000000..b7cdb22 --- /dev/null +++ b/src/player/domRefs.js @@ -0,0 +1,134 @@ +export const playerDom = { + stationNameEl: document.getElementById('station-name'), + stationSubtitleEl: document.getElementById('station-subtitle'), + stationHealthSummaryEl: document.getElementById('station-health-summary'), + stationHealthDetailEl: document.getElementById('station-health-detail'), + sessionSchedulerBtn: document.getElementById('session-scheduler-btn'), + sessionSchedulerBadge: document.getElementById('session-scheduler-badge'), + sessionSummaryBtn: document.getElementById('session-summary-btn'), + sessionSummaryText: document.getElementById('session-summary-text'), + sleepTimerStatusEl: document.getElementById('sleep-timer-status'), + wakeAlarmStatusEl: document.getElementById('wake-alarm-status'), + sleepTimerButtons: document.querySelectorAll('[data-sleep-minutes]'), + wakeAlarmButtons: document.querySelectorAll('[data-wake-minutes]'), + songHistoryPanelEl: document.getElementById('song-history-panel'), + songHistoryListEl: document.getElementById('song-history-list'), + songHistoryClearBtn: document.getElementById('song-history-clear-btn'), + managedCatalogStatusEl: document.getElementById('managed-catalog-status'), + catalogSyncStatusEl: document.getElementById('catalog-sync-status'), + catalogSyncStatusTextEl: document.getElementById('catalog-sync-status-text'), + catalogSyncStatusTimeEl: document.getElementById('catalog-sync-status-time'), + catalogSyncHistoryEl: document.getElementById('catalog-sync-history'), + nowPlayingEl: document.getElementById('now-playing'), + nowArtistEl: document.getElementById('now-artist'), + nowTitleEl: document.getElementById('now-title'), + statusTextEl: document.getElementById('status-text'), + statusDotEl: document.querySelector('.status-dot'), + engineBadgeEl: document.getElementById('engine-badge'), + engineLabelEl: document.getElementById('engine-label'), + playBtn: document.getElementById('play-btn'), + iconPlay: document.getElementById('icon-play'), + iconStop: document.getElementById('icon-stop'), + prevBtn: document.getElementById('prev-btn'), + nextBtn: document.getElementById('next-btn'), + volumeSlider: document.getElementById('volume-slider'), + volumeValue: document.getElementById('volume-value'), + muteBtn: document.getElementById('mute-btn'), + iconVolume: document.getElementById('icon-volume'), + iconMuted: document.getElementById('icon-muted'), + castOverlay: document.getElementById('cast-overlay'), + closeOverlayBtn: document.getElementById('close-overlay'), + deviceListEl: document.getElementById('device-list'), + coverflowStageEl: document.getElementById('artwork-coverflow-stage'), + coverflowPrevBtn: document.getElementById('artwork-prev'), + coverflowNextBtn: document.getElementById('artwork-next'), + artworkPlaceholder: document.querySelector('.artwork-placeholder'), + logoTextEl: document.querySelector('.station-logo-text'), + logoImgEl: document.getElementById('station-logo-img'), + stationLibraryEl: document.getElementById('station-library'), + playerLayoutEl: document.querySelector('.player-layout'), + stationLibraryListEl: document.getElementById('station-library-list'), + stationLibrarySummaryEl: document.getElementById('station-library-summary'), + stationSearchInput: document.getElementById('station-search-input'), + stationLibraryCloseBtn: document.getElementById('station-library-close'), + stationLibraryFiltersToggleBtn: document.getElementById('station-library-filters-toggle'), + clearManagedCatalogBtn: document.getElementById('clear-managed-catalog-btn'), + stationCategoryListEl: document.getElementById('station-category-list'), + stationCountryFilterWrapEl: document.querySelector('[data-country-filter]'), + stationCountryFilterBtn: document.getElementById('station-country-filter-btn'), + stationCountryFilterMenu: document.getElementById('station-country-filter-menu'), + stationCountryFilterText: document.getElementById('station-country-filter-text'), + stationCountryFilterFlag: document.getElementById('station-country-filter-flag'), + stationAdvancedFiltersBtn: document.getElementById('station-advanced-filters-btn'), + stationAdvancedFiltersOverlay: document.getElementById('station-advanced-filters-overlay'), + stationAdvancedFiltersCloseBtn: document.getElementById('station-advanced-filters-close-btn'), + stationAdvancedFiltersDoneBtn: document.getElementById('station-advanced-filters-done-btn'), + countrySelectionOverlay: document.getElementById('country-selection-overlay'), + countrySelectionSearchInput: document.getElementById('country-selection-search-input'), + countrySelectionSummaryEl: document.getElementById('country-selection-summary'), + countrySelectionEmptyEl: document.getElementById('country-selection-empty'), + countrySelectionListEl: document.getElementById('country-selection-list'), + countrySelectionDefaultsBtn: document.getElementById('country-selection-defaults-btn'), + countrySelectionAllBtn: document.getElementById('country-selection-all-btn'), + countrySelectionSaveBtn: document.getElementById('country-selection-save-btn'), + countrySelectionCancelBtn: document.getElementById('country-selection-cancel-btn'), + stationSortBtn: document.getElementById('station-sort-btn'), + stationSortText: document.getElementById('station-sort-text'), + stationSortMenu: document.getElementById('station-sort-menu'), + stationSortWrapEl: document.querySelector('[data-sort-filter]'), + stationLibraryPaginationEl: document.getElementById('station-library-pagination'), + stationLibraryPagePrevBtn: document.getElementById('station-library-page-prev'), + stationLibraryPageNextBtn: document.getElementById('station-library-page-next'), + stationLibraryPageInfo: document.getElementById('station-library-page-info'), + stationLanguageFilterWrapEl: document.querySelector('[data-language-filter]'), + stationLanguageFilterBtn: document.getElementById('station-language-filter-btn'), + stationLanguageFilterText: document.getElementById('station-language-filter-text'), + stationLanguageFilterMenu: document.getElementById('station-language-filter-menu'), + stationCodecFilterWrapEl: document.querySelector('[data-codec-filter]'), + stationCodecFilterBtn: document.getElementById('station-codec-filter-btn'), + stationCodecFilterText: document.getElementById('station-codec-filter-text'), + stationCodecFilterMenu: document.getElementById('station-codec-filter-menu'), + stationBitrateFilterWrapEl: document.querySelector('[data-bitrate-filter]'), + stationBitrateFilterBtn: document.getElementById('station-bitrate-filter-btn'), + stationBitrateFilterText: document.getElementById('station-bitrate-filter-text'), + stationBitrateFilterMenu: document.getElementById('station-bitrate-filter-menu'), + stationHealthFilterBtn: document.getElementById('station-health-filter-btn'), + stationHealthFilterText: document.getElementById('station-health-filter-text'), + stationTabBtns: document.querySelectorAll('[data-station-tab]'), + installPromptBannerEl: document.getElementById('install-prompt-banner'), + installPromptActionBtn: document.getElementById('install-prompt-action'), + installPromptDismissBtn: document.getElementById('install-prompt-dismiss'), + editBtn: document.getElementById('edit-stations-btn'), + stationsListBtn: document.getElementById('stations-list-btn'), + shortcutHelpBtn: document.getElementById('shortcut-help-btn'), + installAppBtn: document.getElementById('install-app-btn'), + castBtn: document.getElementById('cast-btn'), + editorOverlay: document.getElementById('editor-overlay'), + sessionSchedulerOverlay: document.getElementById('session-scheduler-overlay'), + sessionSchedulerCloseBtn: document.getElementById('session-scheduler-close-btn'), + sessionSchedulerDoneBtn: document.getElementById('session-scheduler-done-btn'), + shortcutHelpOverlay: document.getElementById('shortcut-help-overlay'), + shortcutHelpCloseBtn: document.getElementById('shortcut-help-close-btn'), + editorCloseBtn: document.getElementById('editor-close-btn'), + editorListEl: document.getElementById('editor-list'), + editorPersistenceNoteEl: document.getElementById('editor-persistence-note'), + editorPersistenceBackendEl: document.getElementById('editor-persistence-backend'), + editorBackupActivityNoteEl: document.getElementById('editor-backup-activity-note'), + exportUserDataBtn: document.getElementById('export-user-data-btn'), + importUserDataBtn: document.getElementById('import-user-data-btn'), + importUserDataInput: document.getElementById('import-user-data-input'), + resetUserDataBtn: document.getElementById('reset-user-data-btn'), + resetUserDataBackupCheckbox: document.getElementById('reset-user-data-backup-checkbox'), + addStationForm: document.getElementById('add-station-form'), + usTitle: document.getElementById('us_title'), + usUrl: document.getElementById('us_url'), + usLogo: document.getElementById('us_logo'), + usWww: document.getElementById('us_www'), + usId: document.getElementById('us_id'), + castOutputRow: document.getElementById('cast-output-row'), + castOutputBtn: document.getElementById('cast-output-btn'), + castOutputText: document.getElementById('cast-output-text'), +}; + +playerDom.castBtnCastIcon = playerDom.castBtn?.querySelector('svg:not(.hidden)') ?? null; +playerDom.castBtnAirPlayIcon = playerDom.castBtn?.querySelectorAll('svg')?.[1] ?? null; diff --git a/src/player/stationHelpers.js b/src/player/stationHelpers.js new file mode 100644 index 0000000..6b6c0e0 --- /dev/null +++ b/src/player/stationHelpers.js @@ -0,0 +1,220 @@ +import { STATION_THEMES } from './constants.js'; + +export function prefersReducedMotion() { + try { + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; + } catch { + return false; + } +} + +export function isMobileViewport() { + return window.matchMedia('(max-width: 760px)').matches; +} + +export function isStandalonePwa() { + return window.matchMedia('(display-mode: standalone)').matches + || window.matchMedia('(display-mode: fullscreen)').matches + || window.navigator.standalone === true; +} + +export function isMobileInstallViewport() { + return window.matchMedia('(max-width: 760px)').matches; +} + +export function toHttpsIfHttp(url) { + if (!url || typeof url !== 'string') return ''; + return url.startsWith('http://') ? (`https://${url.slice('http://'.length)}`) : url; +} + +function hashString(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = ((hash << 5) - hash) + str.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash); +} + +function hexToRgb(hex) { + const normalized = String(hex || '').replace('#', '').trim(); + if (!/^[0-9a-f]{6}$/i.test(normalized)) return null; + const num = parseInt(normalized, 16); + return `${(num >> 16) & 255}, ${(num >> 8) & 255}, ${num & 255}`; +} + +export function getStationTheme(station) { + const rawTheme = station?.raw?.theme || station?.theme || {}; + const key = `${station?.id || ''}:${station?.name || station?.title || ''}`; + const fallback = STATION_THEMES[hashString(key) % STATION_THEMES.length]; + return { + accent: rawTheme.accent || rawTheme.primary || station?.raw?.color || fallback.accent, + accent2: rawTheme.accent2 || rawTheme.secondary || fallback.accent2, + accent3: rawTheme.accent3 || fallback.accent3, + page: rawTheme.page || rawTheme.background || fallback.page, + panel: rawTheme.panel || fallback.panel, + }; +} + +export function applyStationTheme(station) { + if (!station) return; + const theme = getStationTheme(station); + const root = document.documentElement; + root.style.setProperty('--accent', theme.accent); + root.style.setProperty('--accent-2', theme.accent2); + root.style.setProperty('--accent-3', theme.accent3); + root.style.setProperty('--page-bg', theme.page); + root.style.setProperty('--theme-panel', theme.panel); + root.style.setProperty('--accent-rgb', hexToRgb(theme.accent) || '77, 215, 200'); + root.style.setProperty('--accent-2-rgb', hexToRgb(theme.accent2) || '255, 180, 92'); + root.style.setProperty('--accent-3-rgb', hexToRgb(theme.accent3) || '143, 179, 255'); + root.style.setProperty('--theme-page-rgb', hexToRgb(theme.page) || '17, 19, 24'); + root.style.setProperty('--theme-panel-rgb', hexToRgb(theme.panel) || '16, 20, 27'); + root.style.setProperty('--accent-glow', `rgba(${hexToRgb(theme.accent) || '77, 215, 200'}, 0.34)`); +} + +export function getStationStreamUrl(station) { + if (!station) return ''; + return station.streamUrl + || station.url + || station.streams?.audio + || station.streams?.primary + || station.liveAudio + || station.liveStream + || station.liveVideo + || ''; +} + +export function getStationHomepage(station) { + if (!station) return ''; + return station.url + || station.homepage + || station.website + || station.www + || station.raw?.homepage + || station.raw?.website + || station.raw?.www + || ''; +} + +export function getStationVideoUrl(station) { + return station?.streams?.video || station?.liveVideo || ''; +} + +export function getStationMetadataUrl(station) { + return station?.metadata?.lastSongs + || station?.currentSong + || station?.lastSongs + || station?.raw?.metadata?.lastSongs + || station?.raw?.currentSong + || station?.raw?.lastSongs + || ''; +} + +export function normalizeStationRecord(station, isUserStation = false) { + const name = station?.name || station?.title || station?.id || 'Unknown'; + const url = getStationStreamUrl(station); + const bitrate = Number(station?.bitrate); + + return { + id: station?.id || `${isUserStation ? 'user' : 'station'}-${name.replace(/\s+/g, '-')}`, + name, + url, + logo: station?.logoUrl || station?.assets?.logo || station?.logo || '', + poster: station?.assets?.poster || station?.poster || '', + slogan: station?.slogan || station?.description || station?.defaultText || '', + category: station?.category || station?.genre || '', + country: station?.country || '', + countryCode: station?.countryCode || '', + language: station?.language || '', + region: station?.region || '', + tags: Array.isArray(station?.tags) ? station.tags : [], + codec: station?.codec || '', + bitrate: Number.isFinite(bitrate) && bitrate > 0 ? bitrate : null, + homepage: station?.homepage || station?.website || station?.www || '', + website: station?.website || station?.homepage || station?.www || '', + metadata: station?.metadata || (station?.lastSongs ? { lastSongs: station.lastSongs } : {}), + streams: station?.streams || { + audio: url, + ...(getStationVideoUrl(station) ? { video: getStationVideoUrl(station) } : {}), + }, + enabled: typeof station?.enabled === 'boolean' ? station.enabled : true, + source: station?.source || station?.raw?.source || '', + sourceStationUuid: station?.sourceStationUuid || station?.raw?.sourceStationUuid || '', + raw: station, + _user: isUserStation, + }; +} + +export function getMetadataFetchUrl(url) { + if (!url || typeof url !== 'string') return ''; + + const safeUrl = toHttpsIfHttp(url) || url; + if (!import.meta.env.DEV) { + try { + const parsed = new URL(safeUrl); + if (parsed.hostname === 'data.radio.si') { + return ''; + } + } catch { + return safeUrl; + } + + return safeUrl; + } + + try { + const parsed = new URL(safeUrl); + if (parsed.hostname === 'data.radio.si') { + return `/radio-si-data${parsed.pathname}${parsed.search}`; + } + } catch { + return safeUrl; + } + + return safeUrl; +} + +export function uniqueNonEmpty(urls) { + const out = []; + const seen = new Set(); + for (const url of urls) { + if (!url || typeof url !== 'string') continue; + const trimmed = url.trim(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + out.push(trimmed); + } + return out; +} + +export function setImgWithFallback(imgEl, urls, onFinalError) { + const candidates = uniqueNonEmpty(urls); + let index = 0; + + if (!imgEl || candidates.length === 0) { + if (imgEl) { + imgEl.onload = null; + imgEl.onerror = null; + imgEl.src = ''; + } + if (onFinalError) onFinalError(); + return; + } + + const tryNext = () => { + if (index >= candidates.length) { + if (onFinalError) onFinalError(); + return; + } + imgEl.src = candidates[index++]; + }; + + imgEl.onerror = tryNext; + try { + imgEl.referrerPolicy = 'no-referrer'; + } catch { + // ignore + } + tryNext(); +} diff --git a/src/player/stationHelpers.test.js b/src/player/stationHelpers.test.js new file mode 100644 index 0000000..d10e03a --- /dev/null +++ b/src/player/stationHelpers.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { normalizeStationRecord, toHttpsIfHttp } from './stationHelpers.js'; + +describe('stationHelpers', () => { + it('upgrades plain http stream URLs to https', () => { + expect(toHttpsIfHttp('http://example.com/stream')).toBe('https://example.com/stream'); + expect(toHttpsIfHttp('https://example.com/stream')).toBe('https://example.com/stream'); + }); + + it('normalizes station records into the player shape', () => { + const station = normalizeStationRecord({ + id: 'station-1', + name: 'Radio Test', + streamUrl: 'https://example.com/live', + tags: ['news'], + }); + + expect(station.id).toBe('station-1'); + expect(station.name).toBe('Radio Test'); + expect(station.url).toBe('https://example.com/live'); + expect(station.streams.audio).toBe('https://example.com/live'); + expect(station.tags).toEqual(['news']); + }); +}); diff --git a/src/radio/loadRadioStations.ts b/src/radio/loadRadioStations.ts index 513375c..d3fd5a6 100644 --- a/src/radio/loadRadioStations.ts +++ b/src/radio/loadRadioStations.ts @@ -5,6 +5,7 @@ import { } from '../storage/stationCatalogPersistence.js'; const STATION_SYNC_CACHE_PREFIX = 'radioplayer-station-sync-'; +const APP_BUILD_ID = __APP_BUILD_ID__; async function loadSyncedCatalogFromCache(catalogUrl: string): Promise { if (typeof window === 'undefined' || !('caches' in window)) { @@ -32,7 +33,7 @@ export async function loadPersistedRadioStations(): Promise { const syncedCatalogUrl = `${import.meta.env.BASE_URL}data/radio-stations-sync.json`; - const bundledCatalogUrl = `${import.meta.env.BASE_URL}data/radio-stations.json`; + const bundledCatalogUrl = `${import.meta.env.BASE_URL}data/radio-stations.json?v=${encodeURIComponent(APP_BUILD_ID)}`; const hasServiceWorkerController = typeof navigator !== 'undefined' && 'serviceWorker' in navigator && Boolean(navigator.serviceWorker.controller); diff --git a/src/radio/radioConfig.json b/src/radio/radioConfig.json new file mode 100644 index 0000000..48d47e2 --- /dev/null +++ b/src/radio/radioConfig.json @@ -0,0 +1,48 @@ +{ + "managedCountryCode": "SI", + "radioCountries": [ + { "name": "Austria", "code": "AT" }, + { "name": "Belgium", "code": "BE" }, + { "name": "Bulgaria", "code": "BG" }, + { "name": "Cyprus", "code": "CY" }, + { "name": "Czechia", "code": "CZ" }, + { "name": "Denmark", "code": "DK" }, + { "name": "Estonia", "code": "EE" }, + { "name": "Finland", "code": "FI" }, + { "name": "France", "code": "FR" }, + { "name": "Germany", "code": "DE" }, + { "name": "Greece", "code": "GR" }, + { "name": "Russia", "code": "RU" }, + { "name": "Hungary", "code": "HU" }, + { "name": "Ireland", "code": "IE" }, + { "name": "Italy", "code": "IT" }, + { "name": "Japan", "code": "JP" }, + { "name": "Latvia", "code": "LV" }, + { "name": "Lithuania", "code": "LT" }, + { "name": "Luxembourg", "code": "LU" }, + { "name": "Malta", "code": "MT" }, + { "name": "Mexico", "code": "MX" }, + { "name": "Netherlands", "code": "NL" }, + { "name": "Poland", "code": "PL" }, + { "name": "Brazil", "code": "BR" }, + { "name": "Portugal", "code": "PT" }, + { "name": "Romania", "code": "RO" }, + { "name": "Croatia", "code": "HR" }, + { "name": "Serbia", "code": "RS" }, + { "name": "Montenegro", "code": "ME" }, + { "name": "Bosnia & Herzegovina", "code": "BA" }, + { "name": "Argentina", "code": "AR" }, + { "name": "United Kingdom", "code": "GB" }, + { "name": "Slovenia", "code": "SI" }, + { "name": "Slovakia", "code": "SK" }, + { "name": "Spain", "code": "ES" }, + { "name": "USA", "code": "US" }, + { "name": "Canada", "code": "CA" }, + { "name": "Australia", "code": "AU" }, + { "name": "China", "code": "CN" }, + { "name": "Sweden", "code": "SE" }, + { "name": "Switzerland", "code": "CH" }, + { "name": "Turkey", "code": "TR" }, + { "name": "Ukraine", "code": "UA" } + ] +} diff --git a/src/radio/radioCountries.test.ts b/src/radio/radioCountries.test.ts new file mode 100644 index 0000000..918c820 --- /dev/null +++ b/src/radio/radioCountries.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; +import { defaultSelectedRadioCountryCodes, managedCountryCode, radioCountries } from './radioCountries.js'; + +describe('radioCountries', () => { + it('includes the managed country in the default synced list', () => { + expect(radioCountries.some((country) => country.code === managedCountryCode)).toBe(true); + expect(defaultSelectedRadioCountryCodes).toContain(managedCountryCode); + }); +}); diff --git a/src/radio/radioCountries.ts b/src/radio/radioCountries.ts index 45339ca..c4a8a44 100644 --- a/src/radio/radioCountries.ts +++ b/src/radio/radioCountries.ts @@ -1,50 +1,8 @@ -export const radioCountries = [ - { name: 'Austria', code: 'AT' }, - { name: 'Belgium', code: 'BE' }, - { name: 'Bulgaria', code: 'BG' }, - { name: 'Cyprus', code: 'CY' }, - { name: 'Czechia', code: 'CZ' }, - { name: 'Denmark', code: 'DK' }, - { name: 'Estonia', code: 'EE' }, - { name: 'Finland', code: 'FI' }, - { name: 'France', code: 'FR' }, - { name: 'Germany', code: 'DE' }, - { name: 'Greece', code: 'GR' }, - { name: 'Russia', code: 'RU' }, - { name: 'Hungary', code: 'HU' }, - { name: 'Ireland', code: 'IE' }, - { name: 'Italy', code: 'IT' }, - { name: 'Japan', code: 'JP' }, - { name: 'Latvia', code: 'LV' }, - { name: 'Lithuania', code: 'LT' }, - { name: 'Luxembourg', code: 'LU' }, - { name: 'Malta', code: 'MT' }, - { name: 'Mexico', code: 'MX' }, - { name: 'Netherlands', code: 'NL' }, - { name: 'Poland', code: 'PL' }, - { name: 'Brazil', code: 'BR' }, - { name: 'Portugal', code: 'PT' }, - { name: 'Romania', code: 'RO' }, - { name: 'Croatia', code: 'HR' }, - { name: 'Serbia', code: 'RS' }, - { name: 'Montenegro', code: 'ME' }, - { name: 'Bosnia & Herzegovina', code: 'BA' }, - { name: 'Argentina', code: 'AR' }, - { name: 'United Kingdom', code: 'GB' }, - { name: 'Slovenia', code: 'SI' }, - { name: 'Slovakia', code: 'SK' }, - { name: 'Spain', code: 'ES' }, - { name: 'USA', code: 'US' }, - { name: 'Canada', code: 'CA' }, - { name: 'Australia', code: 'AU' }, - { name: 'China', code: 'CN' }, - { name: 'Sweden', code: 'SE' }, - { name: 'Switzerland', code: 'CH' }, - { name: 'Turkey', code: 'TR' }, - { name: 'Ukraine', code: 'UA' }, -] as const; +import radioConfig from './radioConfig.json' with { type: 'json' }; -export const managedCountryCode = 'SI'; +export const radioCountries = radioConfig.radioCountries; + +export const managedCountryCode = radioConfig.managedCountryCode; export const defaultSelectedRadioCountryCodes = radioCountries.map((country) => country.code); -export type RadioCountry = (typeof radioCountries)[number]; \ No newline at end of file +export type RadioCountry = (typeof radioCountries)[number]; diff --git a/src/shared/serviceWorkerConfig.json b/src/shared/serviceWorkerConfig.json new file mode 100644 index 0000000..72ce26f --- /dev/null +++ b/src/shared/serviceWorkerConfig.json @@ -0,0 +1,16 @@ +{ + "stationSyncTag": "radio-stations-refresh", + "stationPeriodicSyncTag": "radio-stations-periodic-refresh", + "managedCatalogPeriodicSyncTag": "radioplayer-managed-catalog-refresh", + "stationPeriodicSyncIntervalMs": 43200000, + "managedCatalogPeriodicSyncIntervalMs": 86400000, + "stationSyncCachePrefix": "radioplayer-station-sync-", + "stationSyncMetaPath": "data/radio-stations-sync-meta.json", + "messages": { + "syncCountries": "set-sync-countries", + "stationCatalogUpdated": "station-catalog-updated", + "stationCatalogUpdateFailed": "station-catalog-update-failed", + "managedCatalogUpdated": "managed-catalog-updated", + "managedCatalogUpdateFailed": "managed-catalog-update-failed" + } +} diff --git a/src/storage/playerPersistence.ts b/src/storage/playerPersistence.ts index 86cd80f..4f9c7fa 100644 --- a/src/storage/playerPersistence.ts +++ b/src/storage/playerPersistence.ts @@ -19,6 +19,12 @@ export type PersistedRecentStationEntry = { country: string | null; }; +export type PersistedSongHistoryEntry = { + artist: string; + title: string; + seenAt: string; +}; + type PersistedSettings = { volume: number | null; lastStationId: string | null; @@ -28,6 +34,7 @@ type PersistedSettings = { favoriteStationIds: string[]; stationUsageCounts: Record; recentStationHistory: PersistedRecentStationEntry[]; + stationSongHistory: Record; stationHealth: Record; lastExportedAt: string | null; lastImportedAt: string | null; @@ -48,6 +55,7 @@ type SettingRecordMap = { favoriteStationIds: string[]; stationUsageCounts: Record; recentStationHistory: PersistedRecentStationEntry[]; + stationSongHistory: Record; stationHealth: Record; lastExportedAt: string | null; lastImportedAt: string | null; @@ -88,6 +96,7 @@ const DEFAULT_SNAPSHOT: PersistedSnapshot = { favoriteStationIds: [], stationUsageCounts: {}, recentStationHistory: [], + stationSongHistory: {}, stationHealth: {}, lastExportedAt: null, lastImportedAt: null, @@ -103,6 +112,7 @@ const LOCAL_STORAGE_KEYS = { favoriteStationIds: 'favoriteStationIds', stationUsageCounts: 'stationUsageCounts', recentStationHistory: 'recentStationHistory', + stationSongHistory: 'stationSongHistory', stationHealth: 'stationHealth', lastExportedAt: 'lastExportedAt', lastImportedAt: 'lastImportedAt', @@ -238,6 +248,43 @@ function sanitizeRecentStationHistory(value: unknown): PersistedRecentStationEnt .filter((entry): entry is PersistedRecentStationEntry => Boolean(entry)); } +function sanitizeSongHistoryEntry(value: unknown): PersistedSongHistoryEntry | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return null; + } + + const input = value as Record; + const artist = typeof input.artist === 'string' ? input.artist.trim() : ''; + const title = typeof input.title === 'string' ? input.title.trim() : ''; + const seenAt = sanitizeIsoDate(input.seenAt); + + if (!artist || !title || !seenAt) { + return null; + } + + return { artist, title, seenAt }; +} + +function sanitizeStationSongHistoryMap(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + + return Object.fromEntries( + Object.entries(value as Record) + .map(([stationId, entries]) => { + const history = Array.isArray(entries) + ? entries + .map((entry) => sanitizeSongHistoryEntry(entry)) + .filter((entry): entry is PersistedSongHistoryEntry => Boolean(entry)) + : []; + + return [stationId, history] as const; + }) + .filter(([stationId, history]) => Boolean(stationId) && history.length > 0), + ); +} + function cloneStationHealthMap(map: Record) { return Object.fromEntries( Object.entries(map).map(([stationId, record]) => [stationId, { ...record }]), @@ -248,6 +295,12 @@ function cloneRecentStationHistory(history: PersistedRecentStationEntry[]) { return history.map((entry) => ({ ...entry })); } +function cloneSongHistoryMap(history: Record) { + return Object.fromEntries( + Object.entries(history).map(([stationId, entries]) => [stationId, entries.map((entry) => ({ ...entry }))]), + ); +} + function getLocalStorage(): Storage | null { try { return globalThis.localStorage ?? null; @@ -293,6 +346,9 @@ function mirrorSettingToLocalStorage(key: K, value: Settin case 'recentStationHistory': writeLocalStorageValue(LOCAL_STORAGE_KEYS.recentStationHistory, JSON.stringify(Array.isArray(value) ? value : [])); break; + case 'stationSongHistory': + writeLocalStorageValue(LOCAL_STORAGE_KEYS.stationSongHistory, JSON.stringify(value && typeof value === 'object' ? value : {})); + break; case 'stationHealth': writeLocalStorageValue(LOCAL_STORAGE_KEYS.stationHealth, JSON.stringify(value && typeof value === 'object' ? value : {})); break; @@ -357,6 +413,7 @@ function sanitizeImportedSnapshot(source: unknown): PersistedSnapshot { }) : []; const recentStationHistory = sanitizeRecentStationHistory(input.recentStationHistory); + const stationSongHistory = sanitizeStationSongHistoryMap(input.stationSongHistory); const stationHealth = sanitizeStationHealthMap(input.stationHealth); return { @@ -368,6 +425,7 @@ function sanitizeImportedSnapshot(source: unknown): PersistedSnapshot { favoriteStationIds, stationUsageCounts, recentStationHistory, + stationSongHistory, stationHealth, lastExportedAt, lastImportedAt, @@ -396,6 +454,7 @@ function readLegacyLocalStorage(): PersistedSnapshot { favoriteStationIds: safeJsonParse(storage?.getItem('favoriteStationIds') ?? null, []).filter(Boolean), stationUsageCounts: safeJsonParse>(storage?.getItem('stationUsageCounts') ?? null, {}), recentStationHistory: sanitizeRecentStationHistory(safeJsonParse(storage?.getItem('recentStationHistory') ?? null, [])), + stationSongHistory: sanitizeStationSongHistoryMap(safeJsonParse>(storage?.getItem('stationSongHistory') ?? null, {})), stationHealth: sanitizeStationHealthMap(safeJsonParse>(storage?.getItem('stationHealth') ?? null, {})), lastExportedAt: storage?.getItem('lastExportedAt') ?? null, lastImportedAt: storage?.getItem('lastImportedAt') ?? null, @@ -423,6 +482,7 @@ async function migrateFromLocalStorageIfNeeded(db: IDBPDatabase) tx.objectStore('settings').put(legacy.favoriteStationIds, 'favoriteStationIds'), tx.objectStore('settings').put(legacy.stationUsageCounts, 'stationUsageCounts'), tx.objectStore('settings').put(legacy.recentStationHistory, 'recentStationHistory'), + tx.objectStore('settings').put(legacy.stationSongHistory, 'stationSongHistory'), tx.objectStore('settings').put(legacy.stationHealth, 'stationHealth'), tx.objectStore('settings').put(legacy.lastExportedAt, 'lastExportedAt'), tx.objectStore('settings').put(legacy.lastImportedAt, 'lastImportedAt'), @@ -461,6 +521,7 @@ export async function hydratePlayerPersistence() { const favoriteStationIds = await settingsStore.get('favoriteStationIds') as SettingRecordMap['favoriteStationIds'] | undefined; const stationUsageCounts = await settingsStore.get('stationUsageCounts') as SettingRecordMap['stationUsageCounts'] | undefined; const recentStationHistory = await settingsStore.get('recentStationHistory') as SettingRecordMap['recentStationHistory'] | undefined; + const stationSongHistory = await settingsStore.get('stationSongHistory') as SettingRecordMap['stationSongHistory'] | undefined; const stationHealth = await settingsStore.get('stationHealth') as SettingRecordMap['stationHealth'] | undefined; const lastExportedAt = await settingsStore.get('lastExportedAt') as SettingRecordMap['lastExportedAt'] | undefined; const lastImportedAt = await settingsStore.get('lastImportedAt') as SettingRecordMap['lastImportedAt'] | undefined; @@ -479,6 +540,7 @@ export async function hydratePlayerPersistence() { ? stationUsageCounts : {}, recentStationHistory: sanitizeRecentStationHistory(recentStationHistory), + stationSongHistory: sanitizeStationSongHistoryMap(stationSongHistory), stationHealth: sanitizeStationHealthMap(stationHealth), lastExportedAt: typeof lastExportedAt === 'string' ? lastExportedAt : null, lastImportedAt: typeof lastImportedAt === 'string' ? lastImportedAt : null, @@ -500,6 +562,7 @@ export function getPlayerPersistenceSnapshot(): PersistedSnapshot { favoriteStationIds: [...DEFAULT_SNAPSHOT.favoriteStationIds], stationUsageCounts: { ...DEFAULT_SNAPSHOT.stationUsageCounts }, recentStationHistory: cloneRecentStationHistory(DEFAULT_SNAPSHOT.recentStationHistory), + stationSongHistory: cloneSongHistoryMap(DEFAULT_SNAPSHOT.stationSongHistory), stationHealth: cloneStationHealthMap(DEFAULT_SNAPSHOT.stationHealth), lastExportedAt: DEFAULT_SNAPSHOT.lastExportedAt, lastImportedAt: DEFAULT_SNAPSHOT.lastImportedAt, @@ -516,6 +579,7 @@ export function getPlayerPersistenceSnapshot(): PersistedSnapshot { favoriteStationIds: [...snapshot.favoriteStationIds], stationUsageCounts: { ...snapshot.stationUsageCounts }, recentStationHistory: cloneRecentStationHistory(snapshot.recentStationHistory), + stationSongHistory: cloneSongHistoryMap(snapshot.stationSongHistory), stationHealth: cloneStationHealthMap(snapshot.stationHealth), lastExportedAt: snapshot.lastExportedAt, lastImportedAt: snapshot.lastImportedAt, @@ -559,6 +623,10 @@ export function getPersistedRecentStationHistory() { return cloneRecentStationHistory(snapshot.recentStationHistory); } +export function getPersistedStationSongHistory() { + return cloneSongHistoryMap(snapshot.stationSongHistory); +} + export function getPersistedStationHealth() { return cloneStationHealthMap(snapshot.stationHealth); } @@ -620,6 +688,11 @@ export function persistRecentStationHistory(history: PersistedRecentStationEntry void writeSetting('recentStationHistory', snapshot.recentStationHistory); } +export function persistStationSongHistory(history: Record) { + snapshot.stationSongHistory = cloneSongHistoryMap(sanitizeStationSongHistoryMap(history)); + void writeSetting('stationSongHistory', snapshot.stationSongHistory); +} + export function persistStationHealth(health: Record) { snapshot.stationHealth = cloneStationHealthMap(sanitizeStationHealthMap(health)); void writeSetting('stationHealth', snapshot.stationHealth); @@ -745,6 +818,7 @@ export async function importPlayerPersistence(source: unknown) { mirrorSettingToLocalStorage('favoriteStationIds', snapshot.favoriteStationIds); mirrorSettingToLocalStorage('stationUsageCounts', snapshot.stationUsageCounts); mirrorSettingToLocalStorage('recentStationHistory', snapshot.recentStationHistory); + mirrorSettingToLocalStorage('stationSongHistory', snapshot.stationSongHistory); mirrorSettingToLocalStorage('stationHealth', snapshot.stationHealth); mirrorSettingToLocalStorage('lastExportedAt', snapshot.lastExportedAt); mirrorSettingToLocalStorage('lastImportedAt', snapshot.lastImportedAt); @@ -768,6 +842,7 @@ export async function importPlayerPersistence(source: unknown) { tx.objectStore('settings').put(snapshot.favoriteStationIds, 'favoriteStationIds'), tx.objectStore('settings').put(snapshot.stationUsageCounts, 'stationUsageCounts'), tx.objectStore('settings').put(snapshot.recentStationHistory, 'recentStationHistory'), + tx.objectStore('settings').put(snapshot.stationSongHistory, 'stationSongHistory'), tx.objectStore('settings').put(snapshot.stationHealth, 'stationHealth'), tx.objectStore('settings').put(snapshot.lastExportedAt, 'lastExportedAt'), tx.objectStore('settings').put(snapshot.lastImportedAt, 'lastImportedAt'), @@ -817,4 +892,4 @@ export async function clearPlayerPersistence() { } return getPlayerPersistenceSnapshot(); -} \ No newline at end of file +} diff --git a/src/storage/stationCatalogPersistence.ts b/src/storage/stationCatalogPersistence.ts index 20977e1..0e58506 100644 --- a/src/storage/stationCatalogPersistence.ts +++ b/src/storage/stationCatalogPersistence.ts @@ -23,6 +23,8 @@ const LOCAL_STORAGE_KEYS = { managed: 'radioplayer.stationCatalog.managed', }; +const BUILD_ID_KEY = 'radioplayer.stationCatalog.buildId'; + let dbPromise: Promise | null> | null = null; function getDb() { @@ -109,7 +111,59 @@ function writeCatalogToLocalStorage(record: StationCatalogRecord) { } } +function getStoredBuildId(): string | null { + const storage = getLocalStorage(); + if (!storage) return null; + try { + return storage.getItem(BUILD_ID_KEY); + } catch { + return null; + } +} + +function setStoredBuildId(buildId: string) { + const storage = getLocalStorage(); + if (!storage) return; + try { + storage.setItem(BUILD_ID_KEY, buildId); + } catch { + // ignore + } +} + +async function clearManagedCatalogPersistence() { + const storage = getLocalStorage(); + if (storage) { + try { + storage.removeItem(getLocalStorageKey('managed')); + storage.removeItem(BUILD_ID_KEY); + } catch { + // ignore + } + } + + const db = await getDb(); + if (db) { + try { + await db.delete('catalogs', 'managed'); + } catch { + // ignore + } + } +} + async function readCatalog(key: StationCatalogKey): Promise { + // If a previous build persisted a managed catalog, invalidate it when the + // current build id differs. This forces clients to fetch the updated managed + // catalog after a deploy without requiring manual cache clearing. + if (key === 'managed') { + const stored = getStoredBuildId(); + if (stored && stored !== __APP_BUILD_ID__) { + await clearManagedCatalogPersistence(); + return null; + } + } + const db = await getDb(); if (!db) { return readCatalogFromLocalStorage(key); @@ -133,6 +187,11 @@ async function writeCatalog(key: StationCatalogKey, stations: unknown[], source: writeCatalogToLocalStorage(record); + // Persist build id for managed catalog so we can invalidate on next run. + if (key === 'managed') { + setStoredBuildId(__APP_BUILD_ID__); + } + const db = await getDb(); if (!db) { return; @@ -166,4 +225,8 @@ export async function loadPersistedManagedStationCatalog(): Promise<{ stations: export async function persistManagedStationCatalog(stations: unknown[], source: string | null) { await writeCatalog('managed', stations, source); +} + +export async function clearPersistedManagedStationCatalog(): Promise { + await clearManagedCatalogPersistence(); } \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index d4d4c92..10c6087 100644 --- a/src/styles.css +++ b/src/styles.css @@ -153,7 +153,7 @@ input:focus-visible, position: relative; isolation: isolate; width: min(1340px, 100%); - height: clamp(600px, calc(100vh - 84px), 760px); + height: clamp(560px, calc(100vh - 118px), 700px); display: flex; flex-direction: row; align-items: stretch; @@ -163,7 +163,7 @@ input:focus-visible, .sidebar-wrap { flex-shrink: 0; - width: 360px; + width: 392px; margin-right: 18px; overflow: visible; will-change: width, margin-right; @@ -218,6 +218,12 @@ input:focus-visible, gap: 12px; } +.library-top-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + .library-eyebrow { margin: 0 0 4px; color: var(--accent); @@ -253,8 +259,51 @@ input:focus-visible, .library-filter-grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 4fr) minmax(56px, 1fr); gap: 10px; + position: relative; + z-index: 3; + overflow: visible; + max-height: 160px; + opacity: 1; + transform: translateY(0); + transition: max-height 0.28s ease, opacity 0.2s ease, transform 0.2s ease, margin 0.2s ease; +} + +.library-filter-grid > .library-search { + grid-column: 1 / -1; +} + +.library-filter-grid .library-select-trigger { + height: 42px; + gap: 9px; + padding: 0 12px; +} + +.library-filter-grid .library-select-value { + font-size: 0.86rem; + font-weight: 800; +} + +.library-filter-grid .library-select-caret { + margin-left: auto; +} + +.library-filter-grid .library-select-flag, +.library-filter-grid .library-select-sort-icon { + opacity: 0.9; +} + +.library-filter-grid .library-select-prefix { + display: none; +} + +.station-library.filters-collapsed .library-filter-grid { + overflow: hidden; + max-height: 0; + opacity: 0; + transform: translateY(-8px); + pointer-events: none; } .library-select-sort-icon { @@ -267,6 +316,10 @@ input:focus-visible, position: relative; } +.library-select.open { + z-index: 18; +} + .library-select-trigger { width: 100%; min-width: 0; @@ -293,12 +346,14 @@ input:focus-visible, flex: 0 0 auto; width: 1.4rem; height: 1rem; + display: inline-flex; + align-items: center; + justify-content: center; text-align: center; - font-size: 1rem; + font-size: 1.05rem; line-height: 1; } -.library-select-flag, .library-select-option-flag-img { display: block; width: 1.4rem; @@ -361,6 +416,73 @@ input:focus-visible, gap: 6px; } +.library-filter-settings-btn { + width: 100%; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid rgba(var(--accent-rgb), 0.26); + border-radius: 16px; + background: + linear-gradient(180deg, rgba(255,255,255,0.09), rgba(255,255,255,0.04)), + linear-gradient(135deg, rgba(var(--accent-rgb), 0.14), rgba(var(--accent-3-rgb), 0.08)); + color: var(--text-main); + box-shadow: 0 12px 28px rgba(0,0,0,0.16), inset 0 1px 0 rgba(255,255,255,0.1); + transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; +} + +.library-filter-settings-btn:hover { + transform: translateY(-1px); + border-color: rgba(var(--accent-rgb), 0.5); + box-shadow: 0 16px 32px rgba(0,0,0,0.2), 0 0 0 1px rgba(var(--accent-rgb), 0.08); +} + +.advanced-filters-modal { + width: min(620px, 100%); +} + +.advanced-filters-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + margin-bottom: 16px; +} + +.advanced-filters-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.advanced-filters-grid .library-select-prefix { + display: none; +} + +.advanced-filters-grid .library-select-trigger, +.advanced-filters-grid .library-select-toggle { + height: 46px; +} + +.advanced-filters-grid .library-select-toggle { + justify-content: flex-start; +} + +.advanced-filters-grid .library-select-toggle.active { + border-color: rgba(var(--accent-rgb), 0.5); + background: + linear-gradient(180deg, rgba(255,255,255,0.09), rgba(255,255,255,0.04)), + linear-gradient(135deg, rgba(var(--accent-rgb), 0.2), rgba(var(--accent-3-rgb), 0.1)); + box-shadow: 0 16px 32px rgba(0,0,0,0.2), 0 0 0 1px rgba(var(--accent-rgb), 0.08); +} + +.advanced-filters-actions { + justify-content: flex-end; + margin-top: 18px; +} + .library-select-manage-btn { width: 100%; min-height: 38px; @@ -690,7 +812,16 @@ input:focus-visible, overflow: hidden; } +.library-station-title-row { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + overflow: hidden; +} + .library-station-title { + flex: 1 1 auto; min-width: 0; overflow: hidden; color: var(--text-main); @@ -728,6 +859,7 @@ input:focus-visible, .library-station-tags { display: flex; + align-items: center; flex-wrap: wrap; gap: 6px; } @@ -736,13 +868,13 @@ input:focus-visible, display: inline-flex; align-items: center; justify-content: center; - width: 20px; - height: 20px; + width: 16px; + height: 16px; border: 1px solid rgba(255, 132, 132, 0.32); border-radius: 999px; background: rgba(255, 132, 132, 0.14); color: #ffc2c2; - font-size: 0.72rem; + font-size: 0.62rem; font-weight: 800; line-height: 1; cursor: help; @@ -758,40 +890,22 @@ input:focus-visible, flex: 0 0 auto; } -.library-tag { - max-width: 100%; - padding: 4px 8px; - border: 1px solid rgba(255,255,255,0.1); - border-radius: 999px; - background: rgba(255,255,255,0.045); - color: var(--text-main); - font-size: 0.7rem; - font-weight: 700; - line-height: 1.2; +.station-info-indicator-inline.station-info-indicator-neutral { + margin-top: 1px; } -.library-tag-healthy { - border-color: rgba(132,242,168,0.34); - background: rgba(132,242,168,0.14); - color: #b6ffd0; -} - -.library-tag-warning { - border-color: rgba(255, 184, 77, 0.34); - background: rgba(255, 184, 77, 0.14); - color: #ffd99a; -} - -.library-tag-proxy { - border-color: rgba(255, 132, 132, 0.32); - background: rgba(255, 132, 132, 0.14); - color: #ffc2c2; -} - -.library-tag.muted { +.station-info-indicator-neutral { + border-color: rgba(255,255,255,0.14); + background: rgba(255,255,255,0.06); color: var(--text-soft); } +.station-info-indicator-neutral:hover { + border-color: rgba(var(--accent-rgb), 0.32); + background: rgba(var(--accent-rgb), 0.14); + color: var(--text-main); +} + .favorite-btn { width: 34px; height: 34px; @@ -908,18 +1022,18 @@ input:focus-visible, overflow: hidden; flex: 1 1 0; min-width: 0; - align-self: flex-start; - margin-bottom: 12px; - height: calc(100% - 12px); + align-self: stretch; + margin-bottom: 0; + height: 100%; min-height: 0; display: grid; grid-template-columns: minmax(300px, 0.92fr) minmax(340px, 1.08fr); grid-template-areas: "header header" "artwork info" - "artwork progress" "artwork controls" "artwork volume" + "artwork session" "quickpick quickpick" "legal legal"; gap: 18px 28px; @@ -1085,29 +1199,48 @@ header { display: flex; justify-content: flex-end; align-items: center; + gap: 8px; min-width: 0; + margin-top: 8px; } .legal-link { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.04); color: var(--text-muted); - font-size: 0.84rem; - font-weight: 700; - letter-spacing: 0.01em; text-decoration: none; - border-bottom: 1px solid transparent; - transition: color 0.16s ease, border-color 0.16s ease, transform 0.16s ease; + transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease, transform 0.16s ease; +} + +.legal-link[data-variant="about"] { + color: var(--accent-3); +} + +.legal-link[data-variant="docs"] { + color: var(--accent-2); +} + +.legal-link[data-variant="privacy"] { + color: var(--text-muted); } .legal-link:hover { color: var(--text-main); - border-bottom-color: rgba(var(--accent-rgb), 0.5); + border-color: rgba(var(--accent-rgb), 0.35); + background: rgba(var(--accent-rgb), 0.09); transform: translateY(-1px); } .legal-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; - border-bottom-color: transparent; + border-color: rgba(var(--accent-rgb), 0.35); } .artwork-container { @@ -1506,11 +1639,20 @@ header { font-size: 0.98rem; } +.station-meta-row { + width: 100%; + margin-top: 12px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; +} + .station-health-summary { display: inline-flex; align-items: center; width: fit-content; - margin-top: 12px; + margin-top: 0; padding: 5px 10px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px; @@ -1522,15 +1664,9 @@ header { } .station-health-summary-info { - padding: 0; - border: 0; - background: transparent; -} - -.station-info-indicator-player { - width: 24px; - height: 24px; - font-size: 0.8rem; + border-color: rgba(255, 132, 132, 0.34); + background: rgba(255, 132, 132, 0.14); + color: #ffd0d0; } .station-health-summary-healthy { @@ -1567,14 +1703,314 @@ header { color: #ffc2c2; } +.session-summary-btn { + width: 100%; + display: grid; + grid-template-columns: 38px minmax(0, 1fr); + gap: 12px; + align-items: center; + padding: 12px; + border: 1px solid rgba(255,255,255,0.1); + border-radius: 18px; + background: + linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), rgba(var(--accent-3-rgb), 0.08)), + rgba(255,255,255,0.045); + color: var(--text-main); + text-align: left; + transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease; +} + +.session-summary-section { + grid-area: session; +} + +.session-summary-btn:hover { + transform: translateY(-1px); + border-color: rgba(var(--accent-rgb), 0.34); + box-shadow: 0 14px 30px rgba(0,0,0,0.18); +} + +.session-summary-icon { + width: 38px; + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: rgba(255,255,255,0.08); + color: var(--accent); +} + +.session-summary-copy { + min-width: 0; + display: grid; + gap: 3px; +} + +.session-summary-label { + color: var(--text-main); + font-size: 0.84rem; + font-weight: 850; +} + +.session-summary-text { + color: var(--text-soft); + font-size: 0.76rem; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.session-scheduler-btn { + position: relative; +} + +.session-scheduler-btn.session-active { + border-color: rgba(var(--accent-rgb), 0.42); + background: rgba(var(--accent-rgb), 0.14); + box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.08), 0 10px 22px rgba(var(--accent-rgb), 0.16); +} + +.session-scheduler-badge { + position: absolute; + top: 8px; + right: 8px; + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--accent); + box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.85); +} + +.session-scheduler-modal { + width: min(760px, 100%); + gap: 18px; + padding: 26px; + border-color: rgba(var(--accent-rgb), 0.18); + background: + radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.12), transparent 34%), + radial-gradient(circle at bottom right, rgba(var(--accent-3-rgb), 0.12), transparent 32%), + var(--panel-strong); +} + +.session-scheduler-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; +} + +.session-scheduler-eyebrow { + margin: 0 0 6px; + color: var(--accent); + font-size: 0.74rem; + font-weight: 850; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.session-scheduler-intro { + max-width: 52ch; + margin: 10px 0 0; + color: var(--text-muted); + font-size: 0.9rem; + line-height: 1.5; +} + +.session-scheduler-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.session-panel { + min-width: 0; + padding: 18px; + border: 1px solid rgba(255,255,255,0.1); + border-radius: 22px; + background: rgba(255,255,255,0.045); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.05); +} + +.session-panel-sleep { + background: + linear-gradient(180deg, rgba(var(--accent-rgb), 0.08), transparent 55%), + rgba(255,255,255,0.045); +} + +.session-panel-wake { + background: + linear-gradient(180deg, rgba(var(--accent-3-rgb), 0.1), transparent 55%), + rgba(255,255,255,0.045); +} + +.session-panel-heading { + display: grid; + grid-template-columns: 42px minmax(0, 1fr); + gap: 12px; + align-items: start; +} + +.session-panel-icon { + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 14px; + background: rgba(255,255,255,0.08); + color: var(--accent); +} + +.session-panel-heading h3 { + margin: 0; + color: var(--text-main); + font-size: 1.02rem; + font-weight: 850; +} + +.session-panel-heading p { + margin: 6px 0 0; + color: var(--text-soft); + font-size: 0.82rem; + line-height: 1.45; +} + +.session-tool-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} + +.session-chip, +.session-link-btn { + border: 1px solid rgba(255,255,255,0.12); + background: rgba(255,255,255,0.06); + color: var(--text-main); + font: inherit; + font-weight: 800; + transition: transform 0.14s ease, border-color 0.14s ease, background 0.14s ease, color 0.14s ease, box-shadow 0.14s ease; +} + +.session-chip { + min-height: 40px; + padding: 0 14px; + border-radius: 999px; + font-size: 0.8rem; +} + +.session-chip.secondary { + color: var(--text-soft); +} + +.session-chip.active, +.session-chip:hover, +.session-link-btn:hover { + transform: translateY(-1px); + border-color: rgba(var(--accent-rgb), 0.4); + background: rgba(var(--accent-rgb), 0.14); + box-shadow: 0 10px 18px rgba(0,0,0,0.12); +} + +.session-tool-status { + margin-top: 14px; + color: var(--text-soft); + font-size: 0.8rem; + font-weight: 700; +} + +.session-tool-status-strong { + color: var(--text-main); +} + +.session-scheduler-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.session-scheduler-footnote { + color: var(--text-soft); + font-size: 0.76rem; + font-weight: 700; + line-height: 1.4; +} + +.session-scheduler-footer .btn { + width: auto; + min-width: 140px; +} + +.song-history-panel { + width: 100%; + margin-top: 14px; + padding: 14px; + border: 1px solid rgba(255,255,255,0.1); + border-radius: 18px; + background: rgba(255,255,255,0.045); +} + +.song-history-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.song-history-header h3 { + margin: 0; + color: var(--text-main); + font-size: 0.9rem; + font-weight: 850; +} + +.session-link-btn { + min-height: 30px; + padding: 0 10px; + border-radius: 999px; + font-size: 0.74rem; +} + +.song-history-list { + margin: 12px 0 0; + padding: 0; + list-style: none; + display: grid; + gap: 10px; +} + +.song-history-item { + min-width: 0; +} + +.song-history-track { + color: var(--text-main); + font-size: 0.88rem; + font-weight: 800; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.song-history-meta { + margin-top: 3px; + color: var(--text-soft); + font-size: 0.74rem; + font-weight: 700; +} + .status-indicator-wrap { display: flex; flex-wrap: wrap; align-items: center; - gap: 9px; - margin-top: 14px; + gap: 8px; + margin-top: 0; color: var(--text-muted); - font-size: 0.88rem; + font-size: 0.8rem; + line-height: 1; } .managed-catalog-status { @@ -1585,26 +2021,50 @@ header { letter-spacing: 0.01em; } +.quickpick-sync-info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + width: min(340px, calc(100vw - 32px)); + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; + pointer-events: none; + position: fixed; + right: 16px; + bottom: 16px; + z-index: 24; + text-align: left; +} + +.quickpick-sync-info .managed-catalog-status, +.quickpick-sync-info .catalog-sync-history { + margin-top: 0; +} + .catalog-sync-status { display: inline-flex; align-items: center; gap: 8px; - margin-top: 8px; + margin-top: 0; width: fit-content; - padding: 6px 10px; - border: 1px solid rgba(var(--accent-rgb), 0.22); - border-radius: 999px; - background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-3-rgb), 0.1)); + padding: 0; + border: 0; + border-radius: 0; + background: transparent; color: var(--text-main); font-size: 0.75rem; font-weight: 800; letter-spacing: 0.01em; - box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18); + box-shadow: none; } .catalog-sync-status.is-warning { - border-color: rgba(255, 112, 135, 0.34); - background: linear-gradient(135deg, rgba(255, 112, 135, 0.18), rgba(255, 180, 92, 0.12)); + color: #ff9db0; } .catalog-sync-status-time { @@ -1633,13 +2093,18 @@ header { } .catalog-sync-history { - margin-top: 8px; color: var(--text-soft); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.01em; } +@media (max-width: 760px) { + .quickpick-sync-info { + display: none !important; + } +} + .status-dot { width: 8px; height: 8px; @@ -1652,7 +2117,7 @@ header { display: inline-flex; align-items: center; gap: 6px; - padding: 4px 9px; + padding: 5px 9px; border-radius: 999px; border: 1px solid var(--border); background: rgba(255,255,255,0.06); @@ -1686,7 +2151,7 @@ header { display: flex; align-items: center; gap: 8px; - margin-top: 10px; + margin-top: 12px; } .cast-output-label { @@ -1713,42 +2178,6 @@ header { color: var(--success); } -.progress-container { - grid-area: progress; - width: 100%; - height: 10px; - padding: 3px; - border-radius: 999px; - background: rgba(255,255,255,0.08); - border: 1px solid rgba(255,255,255,0.08); -} - -.progress-bar { - position: relative; - width: 100%; - height: 100%; -} - -.progress-fill { - width: 100%; - height: 100%; - border-radius: 999px; - background: linear-gradient(90deg, var(--accent), var(--accent-2)); - box-shadow: 0 0 18px var(--accent-glow); -} - -.progress-handle { - position: absolute; - right: -1px; - top: 50%; - width: 14px; - height: 14px; - border-radius: 50%; - background: var(--text-main); - box-shadow: 0 0 20px rgba(255,255,255,0.52); - transform: translate(50%, -50%); -} - .controls-section { grid-area: controls; display: grid; @@ -1917,6 +2346,43 @@ input[type=range]::-webkit-slider-thumb { letter-spacing: 0; } +.shortcut-help-modal { + max-width: min(460px, calc(100vw - 32px)); +} + +.shortcut-help-list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 10px; +} + +.shortcut-help-list li { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + gap: 12px; + align-items: center; + padding: 11px 12px; + border: 1px solid rgba(255,255,255,0.08); + border-radius: 14px; + background: rgba(255,255,255,0.045); +} + +.shortcut-help-list kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 32px; + padding: 0 10px; + border: 1px solid rgba(var(--accent-rgb), 0.28); + border-radius: 10px; + background: rgba(var(--accent-rgb), 0.12); + color: var(--text-main); + font-size: 0.8rem; + font-weight: 850; +} + .device-list { list-style: none; padding: 2px; @@ -2360,6 +2826,288 @@ input[type=range]::-webkit-slider-thumb { } } +@media (max-height: 780px) and (min-width: 761px) { + .player-layout { + height: calc(100vh - 72px); + } + + .glass-card { + grid-template-columns: minmax(250px, 0.8fr) minmax(300px, 1.2fr); + grid-template-areas: + "header header" + "artwork info" + "artwork controls" + "artwork volume" + "artwork session" + "quickpick quickpick" + "legal legal"; + gap: 14px 22px; + padding: 22px; + } + + .artwork-container { + width: min(100%, 300px); + padding: 8px; + border-radius: 26px; + } + + .artwork-placeholder { + border-radius: 20px; + } + + .station-logo-img { + padding: 10px; + } + + .track-info h2 { + font-size: clamp(1.9rem, 4vw, 2.8rem); + } + + .station-meta-row { + margin-top: 10px; + gap: 8px; + } + + .session-summary-btn { + padding: 11px; + } + + .song-history-panel { + margin-top: 12px; + padding: 12px; + } + + .controls-section { + grid-template-columns: 56px 84px 56px; + gap: 14px; + margin-top: 8px; + } + + .control-btn.secondary { + width: 56px; + height: 56px; + border-radius: 16px; + } + + .control-btn.primary { + width: 84px; + height: 84px; + border-radius: 24px; + } + + .icon-container { + width: 30px; + height: 30px; + } + + .volume-section { + grid-template-columns: 38px minmax(0, 1fr) 44px; + gap: 10px; + } + + .quickpick-section { + align-self: end; + } + + .artwork-coverflow { + height: 108px; + } + + .coverflow-item { + width: 82px; + height: 72px; + border-radius: 18px; + } + + .coverflow-item img { + padding: 8px; + } + + .legal-links { + margin-top: 0; + } +} + +@media (max-height: 700px) and (min-width: 761px) { + .player-layout { + height: calc(100vh - 32px); + width: min(1280px, 100%); + } + + .glass-card { + grid-template-columns: minmax(220px, 0.78fr) minmax(300px, 1.22fr); + grid-template-areas: + "header header" + "artwork info" + "artwork controls" + "artwork volume" + "artwork session" + "quickpick quickpick" + "legal legal"; + gap: 12px 18px; + padding: 18px 20px; + } + + .header-top-row { + gap: 12px; + } + + .brand-logo { + width: 40px; + height: 40px; + flex-basis: 40px; + border-radius: 12px; + } + + .brand-copy { + gap: 10px; + } + + .app-title { + font-size: 1rem; + } + + .datetime-time { + font-size: 0.9rem; + } + + .datetime-date { + font-size: 0.76rem; + } + + .icon-btn { + width: 38px; + height: 38px; + border-radius: 11px; + } + + .artwork-container { + width: min(100%, 260px); + padding: 7px; + border-radius: 22px; + } + + .artwork-placeholder { + border-radius: 18px; + } + + .track-info h2 { + font-size: clamp(1.7rem, 3.6vw, 2.5rem); + line-height: 0.94; + } + + #now-playing { + min-height: 44px; + margin-top: 12px; + } + + .track-info p { + margin-top: 9px; + font-size: 0.9rem; + } + + .station-meta-row { + margin-top: 9px; + gap: 7px; + } + + .station-health-summary { + padding: 4px 9px; + font-size: 0.72rem; + } + + .status-indicator-wrap { + gap: 7px; + font-size: 0.76rem; + } + + .engine-badge { + padding: 4px 8px; + font-size: 0.68rem; + } + + .station-health-detail { + margin-top: 6px; + font-size: 0.78rem; + } + + .song-history-panel { + margin-top: 10px; + padding: 10px 12px; + } + + .song-history-list { + gap: 8px; + } + + .controls-section { + grid-template-columns: 48px 72px 48px; + gap: 12px; + margin-top: 12px; + } + + .control-btn.secondary { + width: 48px; + height: 48px; + border-radius: 14px; + } + + .control-btn.primary { + width: 72px; + height: 72px; + border-radius: 22px; + } + + .icon-container { + width: 26px; + height: 26px; + } + + .volume-section { + grid-template-columns: 36px minmax(0, 1fr) 42px; + gap: 8px; + margin-top: 2px; + } + + .session-summary-btn { + padding: 10px 11px; + border-radius: 16px; + } + + .session-summary-icon { + width: 34px; + height: 34px; + border-radius: 11px; + } + + .session-summary-label { + font-size: 0.8rem; + } + + .session-summary-text { + font-size: 0.72rem; + } + + .artwork-coverflow { + height: 88px; + } + + .coverflow-item { + width: 70px; + height: 62px; + border-radius: 16px; + } + + .coverflow-item img { + padding: 7px; + } + + .legal-link { + width: 28px; + height: 28px; + } +} + @media (max-width: 760px) { body { overflow: hidden; @@ -2382,14 +3130,14 @@ input[type=range]::-webkit-slider-thumb { min-height: 100dvh; width: 100%; grid-template-columns: 1fr; - grid-template-rows: auto auto auto auto auto auto auto auto; + grid-template-rows: auto auto auto auto auto auto auto; grid-template-areas: "header" "artwork" "info" - "progress" "controls" "volume" + "session" "quickpick" "legal"; gap: 9px; @@ -2475,6 +3223,19 @@ input[type=range]::-webkit-slider-thumb { grid-template-columns: 1fr; } + .session-scheduler-grid { + grid-template-columns: 1fr; + } + + .session-scheduler-footer { + flex-direction: column; + align-items: stretch; + } + + .session-scheduler-footer .btn { + width: 100%; + } + .library-select-menu { max-height: 240px; } @@ -2715,11 +3476,12 @@ input[type=range]::-webkit-slider-thumb { .legal-links { justify-content: center; - padding-top: 2px; + margin-top: 0; } .legal-link { - font-size: 0.8rem; + width: 30px; + height: 30px; } .install-prompt-banner { @@ -2868,6 +3630,11 @@ input[type=range]::-webkit-slider-thumb { display: none !important; } + .quickpick-sync-info { + min-height: 44px; + margin-top: 10px; + } + .artwork-container { width: min(100%, 360px); } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 151aa68..707d124 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,3 @@ -/// \ No newline at end of file +/// + +declare const __APP_BUILD_ID__: string; \ No newline at end of file diff --git a/sync.sh b/sync.sh index 1a9c8b3..33bc784 100644 --- a/sync.sh +++ b/sync.sh @@ -26,6 +26,7 @@ if [[ "${SKIP_DEPLOY:-0}" == "1" ]]; then fi rsync -avz \ + --delete \ --chmod=D755,F644 \ --exclude ".phpintel/" \ --exclude ".env" \ diff --git a/tests/e2e/app.smoke.spec.js b/tests/e2e/app.smoke.spec.js new file mode 100644 index 0000000..f61dfb0 --- /dev/null +++ b/tests/e2e/app.smoke.spec.js @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test'; + +test('loads the app shell, shows playback UI, and registers a service worker', async ({ page }) => { + await page.goto('/'); + + await expect(page.locator('.app-title')).toHaveText('RadioPlayer'); + await expect(page.locator('#play-btn')).toBeVisible(); + await expect(page.locator('#mute-btn')).toBeVisible(); + await expect(page.locator('#station-library')).toBeVisible(); + await expect(page.locator('#station-name')).not.toHaveText('', { timeout: 20000 }); + + const serviceWorkerState = await page.evaluate(async () => { + if (!('serviceWorker' in navigator)) { + return { supported: false, controlled: false, scope: '' }; + } + + const registration = await navigator.serviceWorker.ready; + return { + supported: true, + controlled: Boolean(navigator.serviceWorker.controller), + scope: registration.scope, + }; + }); + + expect(serviceWorkerState.supported).toBe(true); + expect(serviceWorkerState.controlled).toBe(true); + expect(serviceWorkerState.scope).toContain('4174'); +}); diff --git a/vite.config.js b/vite.config.js index 10d9fdc..7d6f90c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -26,6 +26,15 @@ async function loadManagedCatalogEnvelope(repoRoot) { const buildStamp = `${Date.now()}`; +function htmlBuildVersionPlugin() { + return { + name: 'html-build-version', + transformIndexHtml(html) { + return html.replaceAll('__BUILD_STAMP__', buildStamp); + }, + }; +} + function managedCatalogApiPlugin() { async function handleManagedCatalogRequest(req, res, next) { if (!req.url) { @@ -71,12 +80,18 @@ function managedCatalogApiPlugin() { } export default defineConfig({ - plugins: [react(), managedCatalogApiPlugin()], + define: { + __APP_BUILD_ID__: JSON.stringify(buildStamp), + }, + plugins: [react(), managedCatalogApiPlugin(), htmlBuildVersionPlugin()], base: './', build: { + minify: 'terser', rollupOptions: { input: { main: resolve(process.cwd(), 'index.html'), + about: resolve(process.cwd(), 'about.html'), + docs: resolve(process.cwd(), 'docs.html'), privacy: resolve(process.cwd(), 'privacy.html'), }, output: { diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..a5029d9 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + include: ['src/**/*.test.{js,ts}'], + coverage: { + reporter: ['text', 'html'], + }, + }, +});