diff --git a/.gitignore b/.gitignore index a585bc9..807eca2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ # You might also want to ignore node_modules and build outputs node_modules/ dist/ -build/ \ No newline at end of file +build/ + +package-lock.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f6c9049 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,59 @@ +# Contributing + +We welcome contributions! Here's how to get started: + +1. **Fork the Repository** +2. **Create a Feature Branch**: `git checkout -b feature/amazing-feature` +3. **Make Your Changes**: Follow the existing code style +4. **Test Thoroughly**: Ensure both light/dark themes work +5. **Submit a Pull Request**: Describe your changes clearly + +## Development Guidelines +- **Follow existing patterns** for consistency +- **Test on multiple screen sizes** (mobile, tablet, desktop) +- **Verify theme compatibility** (light/dark modes) +- **Update documentation** for new features +- **Add error handling** for robust operation + +## Workflow + +You can use docker compose like building from source, or you can run the app directly with npm. + + +For local development with hot reloading: + +#### Step 1: Install Dependencies +```bash +# Backend +cd server +npm install + +# Frontend +cd ../client +npm install +``` + +#### Step 2: Configure Environment +Create `server/.env`: +```env +PORT=5000 +``` + +Create `client/.env`: +```env +VITE_REACT_APP_API_URL=http://localhost:5000 +VITE_OPENWEATHER_API_KEY=your_openweather_api_key_here +``` + +#### Step 3: Run Development Servers +```bash +# Terminal 1 - Backend +cd server +npm start + +# Terminal 2 - Frontend +cd client +npm run dev +``` + +You'll want to edit the env variable PORT and set it to 5001 since the client on port 3001 will look for a server existing on port 5001 diff --git a/README.md b/README.md index 8bdd86a..566d725 100644 --- a/README.md +++ b/README.md @@ -141,20 +141,7 @@ DISCLAIMER: This project uses AI in its development process. While you are right ## 🤝 Contributing -We welcome contributions! Here's how to get started: - -1. **Fork the Repository** -2. **Create a Feature Branch**: `git checkout -b feature/amazing-feature` -3. **Make Your Changes**: Follow the existing code style -4. **Test Thoroughly**: Ensure both light/dark themes work -5. **Submit a Pull Request**: Describe your changes clearly - -### Development Guidelines -- **Follow existing patterns** for consistency -- **Test on multiple screen sizes** (mobile, tablet, desktop) -- **Verify theme compatibility** (light/dark modes) -- **Update documentation** for new features -- **Add error handling** for robust operation +Read our [contributing](CONTRIBUTING.md) guidelines. ## 📄 License diff --git a/client/nginx.conf b/client/nginx.conf index 012f3b3..e83e069 100644 --- a/client/nginx.conf +++ b/client/nginx.conf @@ -30,13 +30,15 @@ server { proxy_read_timeout 90; } - # Uploads proxy (case-sensitive paths) + # Uploads proxy (case-sensitive paths) - must come before static file caching location /uploads/ { proxy_pass http://${BACKEND_SERVICE}:${BACKEND_PORT}/uploads/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $arg_nocache; + add_header X-Proxy-Cache $upstream_cache_status; } location /Uploads/ { @@ -45,6 +47,8 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $arg_nocache; + add_header X-Proxy-Cache $upstream_cache_status; } # Widgets proxy @@ -56,14 +60,15 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } + # Cache static assets from build - but NOT from /Uploads/ or /uploads/ + location ~* ^/(?!Uploads|uploads|widgets).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + try_files $uri =404; + expires 1y; + add_header Cache-Control "public, immutable"; + } + # React Router - serve index.html for all routes location / { try_files $uri $uri/ /index.html; } - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } } diff --git a/client/package-lock.json b/client/package-lock.json deleted file mode 100644 index 4b7ebf4..0000000 --- a/client/package-lock.json +++ /dev/null @@ -1,3559 +0,0 @@ -{ - "name": "homeglow-client", - "version": "1.3.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "homeglow-client", - "version": "1.3.0", - "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^7.3.6", - "@mui/material": "^7.3.6", - "axios": "^1.13.2", - "cron-parser": "^4.9.0", - "extend": "3.0.2", - "geopattern": "^1.2.3", - "hammerjs": "^2.0.8", - "moment": "^2.30.1", - "react": "^19.2.1", - "react-big-calendar": "^1.19.4", - "react-color": "2.19.3", - "react-dom": "^19.2.1", - "react-grid-layout": "^2.1.0", - "react-rnd": "^10.5.2", - "recharts": "^3.5.1" - }, - "devDependencies": { - "@vitejs/plugin-react": "^5.1.2", - "vite": "^7.2.7" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@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/core/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/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@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.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "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.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@icons/material": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", - "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "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==", - "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==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz", - "integrity": "sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.6.tgz", - "integrity": "sha512-0FfkXEj22ysIq5pa41A2NbcAhJSvmcZQ/vcTIbjDsd6hlslG82k5BEBqqS0ZJprxwIL3B45qpJ+bPHwJPlF7uQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^7.3.6", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", - "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/core-downloads-tracker": "^7.3.6", - "@mui/system": "^7.3.6", - "@mui/types": "^7.4.9", - "@mui/utils": "^7.3.6", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.2.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.6", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.6.tgz", - "integrity": "sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.6", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.6.tgz", - "integrity": "sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz", - "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/private-theming": "^7.3.6", - "@mui/styled-engine": "^7.3.6", - "@mui/types": "^7.4.9", - "@mui/utils": "^7.3.6", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.9.tgz", - "integrity": "sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.6.tgz", - "integrity": "sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/types": "^7.4.9", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.1.tgz", - "integrity": "sha512-HjhlEREguAyBTGNzRlGNiDHGQ2EjLSPWwdhhpoEqHYy8hWak3Dp6/fU72OfqVsiMb8S6rbfPsWUF24fxpilrVA==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", - "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@restart/hooks": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", - "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@types/warning": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", - "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.5", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", - "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "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": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", - "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/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/date-arithmetic": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", - "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==", - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-toolkit": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", - "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-equals": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/geopattern": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/geopattern/-/geopattern-1.2.3.tgz", - "integrity": "sha512-UzrR9D0xUrXx71ROZTKbTg1isWdcfcYAXsJrqtvhDkQV2JCsRyqws6TuqUdkBKncg5CPevb0vyXXUJrZpjGFXw==", - "license": "MIT", - "dependencies": { - "extend": "~1.2.1" - } - }, - "node_modules/geopattern/node_modules/extend": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz", - "integrity": "sha512-2/JwIYRpMBDSjbQjUUppNSrmc719crhFaWIdT+TRSVA8gE+6HEobQWqJ6VkPt/H8twS7h/0WWs7veh8wmp98Ng==" - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/globalize": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz", - "integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hammerjs": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", - "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "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/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "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/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/material-colors": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/re-resizable": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", - "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", - "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-big-calendar": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.19.4.tgz", - "integrity": "sha512-FrvbDx2LF6JAWFD96LU1jjloppC5OgIvMYUYIPzAw5Aq+ArYFPxAjLqXc4DyxfsQDN0TJTMuS/BIbcSB7Pg0YA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.7", - "clsx": "^1.2.1", - "date-arithmetic": "^4.1.0", - "dayjs": "^1.11.7", - "dom-helpers": "^5.2.1", - "globalize": "^0.1.1", - "invariant": "^2.2.4", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "luxon": "^3.2.1", - "memoize-one": "^6.0.0", - "moment": "^2.29.4", - "moment-timezone": "^0.5.40", - "prop-types": "^15.8.1", - "react-overlays": "^5.2.1", - "uncontrollable": "^7.2.1" - }, - "peerDependencies": { - "react": "^16.14.0 || ^17 || ^18 || ^19", - "react-dom": "^16.14.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-big-calendar/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react-color": { - "version": "2.19.3", - "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", - "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", - "license": "MIT", - "dependencies": { - "@icons/material": "^0.2.4", - "lodash": "^4.17.15", - "lodash-es": "^4.17.15", - "material-colors": "^1.2.1", - "prop-types": "^15.5.10", - "reactcss": "^1.2.0", - "tinycolor2": "^1.4.1" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.1" - } - }, - "node_modules/react-draggable": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", - "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, - "node_modules/react-grid-layout": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.1.0.tgz", - "integrity": "sha512-d2UOqsTokpua1iaVN6wpxHxum6OE3+DOEKFzDn3UEOsSHxnb9m4Lzwkh3FaNTvQd4Z/2gjcqt1dfy3AnBfZiQw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "fast-equals": "^4.0.3", - "prop-types": "^15.8.1", - "react-draggable": "^4.4.6", - "react-resizable": "^3.0.5", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, - "node_modules/react-is": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz", - "integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==", - "license": "MIT" - }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "license": "MIT" - }, - "node_modules/react-overlays": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.8", - "@popperjs/core": "^2.11.6", - "@restart/hooks": "^0.4.7", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-resizable": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", - "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", - "license": "MIT", - "dependencies": { - "prop-types": "15.x", - "react-draggable": "^4.0.3" - }, - "peerDependencies": { - "react": ">= 16.3" - } - }, - "node_modules/react-rnd": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", - "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", - "license": "MIT", - "dependencies": { - "re-resizable": "6.11.2", - "react-draggable": "4.4.6", - "tslib": "2.6.2" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "node_modules/react-rnd/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react-rnd/node_modules/react-draggable": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", - "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", - "license": "MIT", - "dependencies": { - "clsx": "^1.1.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.0.1" - } - }, - "node_modules/recharts": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.1.tgz", - "integrity": "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA==", - "license": "MIT", - "workspaces": [ - "www" - ], - "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^10.1.1", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.1.1", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "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/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "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", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "license": "0BSD" - }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", - "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/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/vite": { - "version": "7.2.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", - "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "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/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - } - } -} diff --git a/client/package.json b/client/package.json index ccc9ca1..878e738 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "homeglow-client", - "version": "1.3.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "vite", @@ -23,7 +23,7 @@ "react-color": "2.19.3", "recharts": "^3.5.1", "react-rnd": "^10.5.2", - "react-grid-layout":"^2.1.0", + "react-grid-layout": "^2.1.0", "cron-parser": "^4.9.0" }, "devDependencies": { diff --git a/client/src/app.jsx b/client/src/app.jsx index ee59eca..0c6f3aa 100644 --- a/client/src/app.jsx +++ b/client/src/app.jsx @@ -1,7 +1,7 @@ // client/src/app.jsx -import React, { useState, useEffect } from 'react'; -import { Container, IconButton, Box, Dialog, DialogContent } from '@mui/material'; -import { Brightness4, Brightness7, Lock, LockOpen } from '@mui/icons-material'; +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import { IconButton, Box, Dialog, DialogContent } from '@mui/material'; +import { Brightness4, Brightness7, Lock, LockOpen, Close } from '@mui/icons-material'; import SettingsIcon from '@mui/icons-material/Settings'; import RefreshIcon from '@mui/icons-material/Refresh'; @@ -11,24 +11,41 @@ import PhotoWidget from './components/PhotoWidget.jsx'; import AdminPanel from './components/AdminPanel.jsx'; import WeatherWidget from './components/WeatherWidget.jsx'; import ChoreWidget from './components/ChoreWidget.jsx'; -import WidgetGallery from './components/WidgetGallery.jsx'; +import PluginWidgetWrapper from './components/PluginWidgetWrapper.jsx'; import WidgetContainer from './components/WidgetContainer.jsx'; +import TabBar from './components/TabBar.jsx'; +import TabIconModal from './components/TabIconModal.jsx'; +import ScreenSaver from './components/ScreenSaver.jsx'; +import ScreensaverCountdown from './components/ScreensaverCountdown.jsx'; import { API_BASE_URL } from './utils/apiConfig.js'; +import { getDeviceApiBase } from './utils/deviceName.js'; import './index.css'; const App = () => { + const API_DEVICE_URL = getDeviceApiBase(API_BASE_URL); const [theme, setTheme] = useState('light'); const [widgetsLocked, setWidgetsLocked] = useState(() => { const saved = localStorage.getItem('widgetsLocked'); - return saved !== null ? JSON.parse(saved) : true; // Default to locked + return saved !== null ? JSON.parse(saved) : true; }); + const [screensaverActive, setScreensaverActive] = useState(false); + const [screensaverSettings, setScreensaverSettings] = useState(() => { + const saved = localStorage.getItem('screensaverSettings'); + return saved ? JSON.parse(saved) : { + enabled: false, + mode: 'tabs', + timeout: 5, + slideshowInterval: 10 + }; + }); + const inactivityTimerRef = useRef(null); + const lastActivityRef = useRef(Date.now()); const [widgetSettings, setWidgetSettings] = useState(() => { const defaultSettings = { chores: { enabled: false, transparent: false }, calendar: { enabled: false, transparent: false }, photos: { enabled: false, transparent: false }, weather: { enabled: false, transparent: false }, - widgetGallery: { enabled: true, transparent: false }, lightGradientStart: '#00ddeb', lightGradientEnd: '#ff6b6b', darkGradientStart: '#2e2767', @@ -47,12 +64,48 @@ const App = () => { WEATHER_API_KEY: '', ICS_CALENDAR_URL: '', }); - const [widgetGalleryKey, setWidgetGalleryKey] = useState(0); + const [installedPlugins, setInstalledPlugins] = useState([]); + const [activeTab, setActiveTab] = useState(1); + const [tabs, setTabs] = useState([]); + const [widgetAssignments, setWidgetAssignments] = useState({}); + const [showTabIconModal, setShowTabIconModal] = useState(false); - const refreshWidgetGallery = () => { - setWidgetGalleryKey(prev => prev + 1); + const fetchInstalledPlugins = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/api/widgets`); + setInstalledPlugins(Array.isArray(response.data) ? response.data : []); + } catch { + setInstalledPlugins([]); + } }; + useEffect(() => { + const oldEnabled = localStorage.getItem('enabledWidgets'); + if (oldEnabled) { + try { + const parsed = JSON.parse(oldEnabled); + const existing = JSON.parse(localStorage.getItem('pluginSettings') || '{}'); + Object.entries(parsed).forEach(([filename, isEnabled]) => { + if (!existing[filename]) { + existing[filename] = { enabled: !!isEnabled, transparent: false, refreshInterval: 0 }; + } + }); + localStorage.setItem('pluginSettings', JSON.stringify(existing)); + } catch { } + localStorage.removeItem('enabledWidgets'); + } + const ws = localStorage.getItem('widgetSettings'); + if (ws) { + try { + const parsed = JSON.parse(ws); + if (parsed.widgetGallery) { + delete parsed.widgetGallery; + localStorage.setItem('widgetSettings', JSON.stringify(parsed)); + } + } catch { } + } + }, []); + useEffect(() => { const fetchApiKeys = async () => { try { @@ -63,13 +116,61 @@ const App = () => { } }; fetchApiKeys(); + fetchTabs(); + fetchWidgetAssignments(); + fetchInstalledPlugins(); }, []); + const fetchTabs = async () => { + try { + const response = await axios.get(`${API_DEVICE_URL}/tabs`); + setTabs(Array.isArray(response.data) ? response.data : []); + } catch (error) { + console.error('Error fetching tabs:', error); + setTabs([]); + } + }; + + const fetchWidgetAssignments = async () => { + try { + const response = await axios.get(`${API_DEVICE_URL}/widget-assignments`); + const assignments = Array.isArray(response.data) ? response.data : []; + + const groupedAssignments = {}; + assignments.forEach(assignment => { + if (!groupedAssignments[assignment.widget_name]) { + groupedAssignments[assignment.widget_name] = []; + } + groupedAssignments[assignment.widget_name].push({ + tabNumber: assignment.tab_number, + layout_x: assignment.layout_x, + layout_y: assignment.layout_y, + layout_w: assignment.layout_w, + layout_h: assignment.layout_h, + }); + }); + + setWidgetAssignments(groupedAssignments); + } catch (error) { + console.error('Error fetching widget assignments:', error); + setWidgetAssignments({}); + } + }; + useEffect(() => { const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); setTheme(savedTheme); document.documentElement.setAttribute('data-theme', savedTheme); setCurrentGeoPatternSeed(Math.random().toString()); + + if (savedTheme === 'light') { + const savedSettings = localStorage.getItem('widgetSettings'); + if (savedSettings) { + const parsed = JSON.parse(savedSettings); + const primaryColor = parsed.primary || '#f5f5f5'; + document.documentElement.style.setProperty('--background', primaryColor); + } + } }, []); useEffect(() => { @@ -84,11 +185,100 @@ const App = () => { document.documentElement.style.setProperty('--bottom-bar-height', '60px'); }, [widgetSettings]); + const screensaverActiveRef = useRef(false); + const screensaverSettingsRef = useRef(screensaverSettings); + const showAdminPanelRef = useRef(showAdminPanel); + const tabsRef = useRef(tabs); + + useEffect(() => { screensaverSettingsRef.current = screensaverSettings; }, [screensaverSettings]); + useEffect(() => { showAdminPanelRef.current = showAdminPanel; }, [showAdminPanel]); + useEffect(() => { tabsRef.current = tabs; }, [tabs]); + + const startInactivityTimer = useCallback(() => { + if (inactivityTimerRef.current) { + clearTimeout(inactivityTimerRef.current); + } + + const settings = screensaverSettingsRef.current; + if (!settings.enabled || showAdminPanelRef.current) return; + + lastActivityRef.current = Date.now(); + + inactivityTimerRef.current = setTimeout(() => { + screensaverActiveRef.current = true; + setScreensaverActive(true); + if (settings.mode === 'tabs' && tabsRef.current.length > 0) { + document.documentElement.requestFullscreen?.().catch(() => { }); + } + }, settings.timeout * 60 * 1000); + }, []); + + useEffect(() => { + const savedScreensaverSettings = localStorage.getItem('screensaverSettings'); + if (savedScreensaverSettings) { + setScreensaverSettings(JSON.parse(savedScreensaverSettings)); + } + }, []); + + useEffect(() => { + if (!screensaverSettings.enabled) { + if (inactivityTimerRef.current) { + clearTimeout(inactivityTimerRef.current); + } + return; + } + + const handleActivity = () => { + if (screensaverActiveRef.current) return; + lastActivityRef.current = Date.now(); + startInactivityTimer(); + }; + + const events = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart']; + + events.forEach(event => { + window.addEventListener(event, handleActivity, { passive: true }); + }); + + startInactivityTimer(); + + return () => { + events.forEach(event => { + window.removeEventListener(event, handleActivity); + }); + if (inactivityTimerRef.current) { + clearTimeout(inactivityTimerRef.current); + } + }; + }, [screensaverSettings.enabled, screensaverSettings.timeout, startInactivityTimer]); + + const handleExitScreensaver = useCallback(() => { + screensaverActiveRef.current = false; + setScreensaverActive(false); + if (document.fullscreenElement) { + document.exitFullscreen?.().catch(() => { }); + } + setTimeout(() => startInactivityTimer(), 500); + }, [startInactivityTimer]); + + const handleScreensaverTabChange = useCallback((tabNumber) => { + setActiveTab(tabNumber); + }, []); + const toggleTheme = () => { const newTheme = theme === 'light' ? 'dark' : 'light'; setTheme(newTheme); document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); + + if (newTheme === 'light') { + const savedSettings = localStorage.getItem('widgetSettings'); + const parsed = savedSettings ? JSON.parse(savedSettings) : {}; + const primaryColor = parsed.primary || '#f5f5f5'; + document.documentElement.style.setProperty('--background', primaryColor); + } else { + document.documentElement.style.removeProperty('--background'); + } }; const toggleWidgetsLock = () => { @@ -104,6 +294,10 @@ const App = () => { const parsed = JSON.parse(savedSettings); setWidgetSettings(prev => ({ ...prev, ...parsed })); } + const savedScreensaver = localStorage.getItem('screensaverSettings'); + if (savedScreensaver) { + setScreensaverSettings(JSON.parse(savedScreensaver)); + } } setShowAdminPanel(!showAdminPanel); }; @@ -112,16 +306,72 @@ const App = () => { window.location.reload(); }; - const buildWidgetsArray = () => { - const widgets = []; + const handleTabChange = (tabNumber) => { + setActiveTab(tabNumber); + }; + + const handleAddTab = () => { + setShowTabIconModal(true); + }; + + const handleSaveTab = async (tabData) => { + try { + const response = await axios.post(`${API_DEVICE_URL}/tabs`, tabData); + await fetchTabs(); + setShowTabIconModal(false); + } catch (error) { + console.error('Error creating tab:', error); + alert('Failed to create tab. Please try again.'); + } + }; + + const handleDeleteTab = async (tabNumber) => { + if (!window.confirm('Are you sure you want to delete this tab? Widgets will be moved to the Home tab.')) { + return; + } + + try { + await axios.delete(`${API_DEVICE_URL}/tabs/${tabNumber}`); + await fetchTabs(); + await fetchWidgetAssignments(); + + if (activeTab === tabNumber) { + setActiveTab(1); + } + } catch (error) { + console.error('Error deleting tab:', error); + alert('Failed to delete tab. Please try again.'); + } + }; + + const isWidgetAssignedToTab = (widgetName, tabNumber) => { + const assignments = widgetAssignments[widgetName]; + if (!assignments || assignments.length === 0) { + return tabNumber === 1; + } + return assignments.some(a => a.tabNumber === tabNumber); + }; + + const getWidgetLayoutForTab = (widgetName, tabNumber) => { + const assignments = widgetAssignments[widgetName]; + if (!assignments) return null; + const match = assignments.find(a => a.tabNumber === tabNumber); + if (!match || match.layout_x == null) return null; + return { x: match.layout_x, y: match.layout_y, w: match.layout_w, h: match.layout_h }; + }; + + const widgets = useMemo(() => { + const result = []; - if (widgetSettings.calendar.enabled) { - widgets.push({ + if (widgetSettings.calendar.enabled && isWidgetAssignedToTab('calendar', activeTab)) { + const dbLayout = getWidgetLayoutForTab('calendar', activeTab); + result.push({ id: 'calendar-widget', defaultPosition: { x: 0, y: 0 }, defaultSize: { width: 8, height: 5 }, minWidth: 2, minHeight: 2, + savedLayout: dbLayout, content: { }); } - if (widgetSettings.weather.enabled) { - widgets.push({ + if (widgetSettings.weather.enabled && isWidgetAssignedToTab('weather', activeTab)) { + const dbLayout = getWidgetLayoutForTab('weather', activeTab); + result.push({ id: 'weather-widget', defaultPosition: { x: 8, y: 0 }, defaultSize: { width: 4, height: 3 }, minWidth: 2, minHeight: 2, + savedLayout: dbLayout, content: { }); } - if (widgetSettings.chores.enabled) { - widgets.push({ + if (widgetSettings.chores.enabled && isWidgetAssignedToTab('chores', activeTab)) { + const dbLayout = getWidgetLayoutForTab('chores', activeTab); + result.push({ id: 'chores-widget', defaultPosition: { x: 0, y: 5 }, defaultSize: { width: 6, height: 4 }, minWidth: 2, minHeight: 2, + savedLayout: dbLayout, content: , }); } - if (widgetSettings.photos.enabled) { - widgets.push({ + if (widgetSettings.photos.enabled && isWidgetAssignedToTab('photos', activeTab)) { + const dbLayout = getWidgetLayoutForTab('photos', activeTab); + result.push({ id: 'photos-widget', defaultPosition: { x: 6, y: 5 }, defaultSize: { width: 6, height: 4 }, minWidth: 2, minHeight: 2, + savedLayout: dbLayout, content: , }); } - return widgets; - }; + const pluginSettings = JSON.parse(localStorage.getItem('pluginSettings') || '{}'); + installedPlugins.forEach((plugin, index) => { + const pSettings = pluginSettings[plugin.filename] || {}; + if (!pSettings.enabled) return; - const widgets = buildWidgetsArray(); + const pluginWidgetName = `plugin:${plugin.filename}`; + if (!isWidgetAssignedToTab(pluginWidgetName, activeTab)) return; + + const dbLayout = getWidgetLayoutForTab(pluginWidgetName, activeTab); + result.push({ + id: `plugin-${plugin.filename}`, + defaultPosition: { x: 0, y: 0 }, + defaultSize: { width: 6, height: 4 }, + minWidth: 2, + minHeight: 2, + savedLayout: dbLayout, + content: , + }); + }); + + return result; + }, [widgetSettings, activeTab, apiKeys, widgetAssignments, installedPlugins, theme]); + + const activeTabId = useMemo(() => { + const active = tabs.find(tab => tab.number === activeTab); + return active?.id ?? 1; + }, [tabs, activeTab]); return ( <> - {widgets.length > 0 && } - - {widgetSettings.widgetGallery?.enabled && ( - 0 ? 1 : 0 }}> - - - )} + {widgets.length > 0 && } - - + + + + + { + await fetchTabs(); + await fetchWidgetAssignments(); + }} + /> @@ -212,22 +508,19 @@ const App = () => { zIndex: 1000, }} > - {/* Left: Logo */} - - HomeGlow Logo - + {/* Left: TabBar */} + {/* Center: Control Buttons */} - { onClick={toggleWidgetsLock} aria-label={widgetsLocked ? "Unlock widgets" : "Lock widgets"} sx={{ - color: widgetsLocked + color: widgetsLocked ? (theme === 'light' ? 'action.active' : 'white') : 'var(--accent)', transition: 'color 0.2s ease', @@ -278,9 +571,31 @@ const App = () => { - {/* Right: Empty space for balance */} - + + + + + setShowTabIconModal(false)} + onSave={handleSaveTab} + /> + + {screensaverActive && screensaverSettings.enabled && ( + + )} ); }; diff --git a/client/src/components/AdminPanel.jsx b/client/src/components/AdminPanel.jsx index 4bead58..760f020 100644 --- a/client/src/components/AdminPanel.jsx +++ b/client/src/components/AdminPanel.jsx @@ -38,10 +38,14 @@ import { Paper, Backdrop, RadioGroup, - Radio + Radio, + Autocomplete, + Tooltip, + Slider } from '@mui/material'; import { Delete, + ContentCopy, Edit, Save, Cancel, @@ -52,21 +56,34 @@ import { Warning, RestartAlt, Timer, - ViewCompact, - ViewModule, - ViewQuilt, - Lock + Lock, + Nightlight, + Tab as TabIcon, + DragIndicator, + PhotoLibrary, + Info } from '@mui/icons-material'; -import { ChromePicker } from 'react-color'; +import ColorPickerPopover from './ColorPickerPopover'; import axios from 'axios'; import { API_BASE_URL } from '../utils/apiConfig.js'; +import { getDeviceApiBase, getDeviceName, setDeviceName } from '../utils/deviceName.js'; import PinModal from './PinModal'; import ChoreSchedulesTab from './ChoreSchedulesTab'; import ChoreHistoryTab from './ChoreHistoryTab'; - -const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { +import TabIconModal from './TabIconModal'; + +const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => { + const [currentDeviceName, setCurrentDeviceName] = useState(() => getDeviceName()); + const API_DEVICE_URL = getDeviceApiBase(API_BASE_URL); + const CORE_WIDGET_DEFAULT_SIZES = { + calendar: { w: 8, h: 5 }, + weather: { w: 4, h: 3 }, + chores: { w: 6, h: 4 }, + photos: { w: 6, h: 4 }, + }; const [activeTab, setActiveTab] = useState(0); const [choresSubTab, setChoresSubTab] = useState(0); + const [widgetsSubTab, setWidgetsSubTab] = useState(0); const [settings, setSettings] = useState({ WEATHER_API_KEY: '', PROXY_WHITELIST: '', @@ -76,10 +93,8 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { chores: { enabled: false, transparent: false, refreshInterval: 0 }, calendar: { enabled: false, transparent: false, refreshInterval: 0 }, photos: { enabled: false, transparent: false, refreshInterval: 0 }, - weather: { enabled: false, transparent: false, refreshInterval: 0, layoutMode: 'medium' }, - widgetGallery: { enabled: true, transparent: false, refreshInterval: 0 }, - // Accent colors (shared) - only these are customizable - primary: '#9E7FFF', + weather: { enabled: false, transparent: false, refreshInterval: 0 }, + primary: '#f5f5f5', secondary: '#38bdf8', accent: '#f472b6' }); @@ -93,7 +108,7 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { const [uploadedWidgets, setUploadedWidgets] = useState([]); const [githubWidgets, setGithubWidgets] = useState([]); const [loadingGithub, setLoadingGithub] = useState(false); - const [showColorPicker, setShowColorPicker] = useState({}); + const [colorPickerAnchor, setColorPickerAnchor] = useState({ key: null, el: null }); const [deleteUserDialog, setDeleteUserDialog] = useState({ open: false, user: null }); const [choreModal, setChoreModal] = useState({ open: false, user: null, userChores: [] }); const [isLoading, setIsLoading] = useState(false); @@ -102,6 +117,40 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { const [pinModal, setPinModal] = useState({ open: false, mode: 'verify', title: '' }); const [isAuthenticated, setIsAuthenticated] = useState(false); const [checkingPin, setCheckingPin] = useState(true); + const [tabs, setTabs] = useState([]); + const [widgetAssignments, setWidgetAssignments] = useState({}); + const [pluginSettings, setPluginSettings] = useState(() => { + const saved = localStorage.getItem('pluginSettings'); + return saved ? JSON.parse(saved) : {}; + }); + const [pluginAssignments, setPluginAssignments] = useState({}); + const [photoSources, setPhotoSources] = useState([]); + const [screensaverSettings, setScreensaverSettings] = useState(() => { + const saved = localStorage.getItem('screensaverSettings'); + return saved ? JSON.parse(saved) : { + enabled: false, + mode: 'tabs', + timeout: 5, + slideshowInterval: 10 + }; + }); + const [tabIconModalState, setTabIconModalState] = useState({ + open: false, + mode: 'create', + originalNumber: null, + initialData: null, + }); + const [deleteTabDialog, setDeleteTabDialog] = useState({ open: false, tab: null }); + const [draggingTabNumber, setDraggingTabNumber] = useState(null); + const [devices, setDevices] = useState([]); + const [copyDeviceDialog, setCopyDeviceDialog] = useState({ open: false, device: null }); + const [deleteDeviceDialog, setDeleteDeviceDialog] = useState({ open: false, device: null }); + const [renameDeviceDialog, setRenameDeviceDialog] = useState({ + open: false, + currentName: '', + newName: '', + error: '', + }); // Refresh interval options in milliseconds const refreshIntervalOptions = [ @@ -120,7 +169,7 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { const savedSettings = localStorage.getItem('widgetSettings'); if (savedSettings) { const parsed = JSON.parse(savedSettings); - // Ensure refresh intervals and layout mode are included, default to 'medium' if 'auto' or not set + // Ensure refresh intervals are included for all widgets. const settingsWithDefaults = { chores: { enabled: false, transparent: false, refreshInterval: 0, ...parsed.chores }, calendar: { enabled: false, transparent: false, refreshInterval: 0, ...parsed.calendar }, @@ -129,11 +178,9 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { enabled: false, transparent: false, refreshInterval: 0, - layoutMode: (parsed.weather?.layoutMode === 'auto' || !parsed.weather?.layoutMode) ? 'medium' : parsed.weather.layoutMode, ...parsed.weather }, - widgetGallery: { enabled: true, transparent: false, refreshInterval: 0, ...parsed.widgetGallery }, - primary: parsed.primary || '#9E7FFF', + primary: parsed.primary || '#f5f5f5', secondary: parsed.secondary || '#38bdf8', accent: parsed.accent || '#f472b6' }; @@ -149,9 +196,33 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { fetchChores(); fetchPrizes(); fetchUploadedWidgets(); + fetchTabs(); + fetchWidgetAssignments(); + fetchPhotoSources(); + fetchDevices(); } }, [isAuthenticated]); + const fetchDevices = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/api/devices`); + setDevices(Array.isArray(response.data) ? response.data : []); + } catch (error) { + console.error('Error fetching devices:', error); + setDevices([]); + } + }; + + const fetchPhotoSources = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/api/photo-sources`); + setPhotoSources(Array.isArray(response.data) ? response.data : []); + } catch (error) { + console.error('Error fetching photo sources:', error); + setPhotoSources([]); + } + }; + const checkPinStatus = async () => { try { const response = await axios.get(`${API_BASE_URL}/api/admin-pin/exists`); @@ -172,7 +243,7 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { const fetchSettings = async () => { try { - const response = await axios.get(`${API_BASE_URL}/api/settings`); + const response = await axios.post(`${API_BASE_URL}/api/settings/search`, ['*']); setSettings(response.data); } catch (error) { console.error('Error fetching settings:', error); @@ -219,6 +290,46 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { } }; + const fetchTabs = async () => { + try { + const response = await axios.get(`${API_DEVICE_URL}/tabs`); + setTabs(Array.isArray(response.data) ? response.data : []); + } catch (error) { + console.error('Error fetching tabs:', error); + setTabs([]); + } + }; + + const fetchWidgetAssignments = async () => { + try { + const response = await axios.get(`${API_DEVICE_URL}/widget-assignments`); + const assignments = Array.isArray(response.data) ? response.data : []; + + const coreAssignments = {}; + const pluginAssign = {}; + assignments.forEach(assignment => { + if (assignment.widget_name.startsWith('plugin:')) { + if (!pluginAssign[assignment.widget_name]) { + pluginAssign[assignment.widget_name] = []; + } + pluginAssign[assignment.widget_name].push(assignment.tab_number); + } else { + if (!coreAssignments[assignment.widget_name]) { + coreAssignments[assignment.widget_name] = []; + } + coreAssignments[assignment.widget_name].push(assignment.tab_number); + } + }); + + setWidgetAssignments(coreAssignments); + setPluginAssignments(pluginAssign); + } catch (error) { + console.error('Error fetching widget assignments:', error); + setWidgetAssignments({}); + setPluginAssignments({}); + } + }; + const fetchGithubWidgets = async () => { setLoadingGithub(true); try { @@ -269,45 +380,590 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { } }; - const saveWidgetSettings = () => { - localStorage.setItem('widgetSettings', JSON.stringify(widgetSettings)); - setWidgetSettings(widgetSettings); - setSaveMessage({ show: true, type: 'success', text: 'Widget settings saved successfully! Refresh page to see changes.' }); - setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + const getWidgetIdFromWidgetName = (widgetName) => { + const coreMap = { + calendar: 'calendar-widget', + weather: 'weather-widget', + chores: 'chores-widget', + photos: 'photos-widget', + }; + + if (coreMap[widgetName]) return coreMap[widgetName]; + if (widgetName.startsWith('plugin:')) return `plugin-${widgetName.slice(7)}`; + return null; + }; + + const getDefaultSizeForWidgetName = (widgetName) => { + if (CORE_WIDGET_DEFAULT_SIZES[widgetName]) return CORE_WIDGET_DEFAULT_SIZES[widgetName]; + if (widgetName.startsWith('plugin:')) return { w: 6, h: 4 }; + return null; + }; + + const ensureLocalLayoutKeyForAssignment = (widgetName, tabNumber) => { + const tab = tabs.find(t => t.number === tabNumber); + if (!tab?.id) return; + + const widgetId = getWidgetIdFromWidgetName(widgetName); + const defaultSize = getDefaultSizeForWidgetName(widgetName); + if (!widgetId || !defaultSize) return; + + const localKey = `widget-layout-id-${tab.id}-${widgetId}`; + if (localStorage.getItem(localKey)) return; + + // Store only dimensions; WidgetContainer will apply widget default position for x/y. + localStorage.setItem(localKey, JSON.stringify({ w: defaultSize.w, h: defaultSize.h })); + }; + + const collectAffectedTabNumbers = (existingTabNumbers, desiredTabNumbers) => { + const affected = new Set([...existingTabNumbers, ...desiredTabNumbers]); + if (existingTabNumbers.length === 0 || desiredTabNumbers.length === 0) { + affected.add(1); + } + return Array.from(affected); + }; + + const recalculateLayoutsForAffectedTabs = (affectedTabNumbers, coreAssignments, pluginAssignmentMap) => { + const tabNumbers = Array.from(new Set(affectedTabNumbers.filter(Boolean))); + if (tabNumbers.length === 0) return; + + const getAssignedWidgetsForTab = (tabNumber) => { + const assigned = []; + + Object.keys(CORE_WIDGET_DEFAULT_SIZES).forEach((widgetName) => { + const assignedTabs = coreAssignments[widgetName] || []; + if (assignedTabs.length === 0) { + if (tabNumber === 1) { + assigned.push(widgetName); + } + return; + } + if (assignedTabs.includes(tabNumber)) { + assigned.push(widgetName); + } + }); + + Object.entries(pluginAssignmentMap || {}).forEach(([widgetName, assignedTabs]) => { + const tabsForWidget = Array.isArray(assignedTabs) ? assignedTabs : []; + if (tabsForWidget.length === 0) { + if (tabNumber === 1) { + assigned.push(widgetName); + } + return; + } + if (tabsForWidget.includes(tabNumber)) { + assigned.push(widgetName); + } + }); + + return assigned + .map((widgetName) => { + const widgetId = getWidgetIdFromWidgetName(widgetName); + const defaultSize = getDefaultSizeForWidgetName(widgetName); + if (!widgetId || !defaultSize) return null; + return { widgetId, defaultSize }; + }) + .filter(Boolean) + .sort((a, b) => a.widgetId.localeCompare(b.widgetId)); + }; + + const findFreePosition = (placed, w, h, cols = 12) => { + const collides = (x, y) => placed.some((p) => ( + x < p.x + p.w && x + w > p.x && y < p.y + p.h && y + h > p.y + )); + + for (let row = 0; row < 200; row += 1) { + for (let col = 0; col <= cols - w; col += 1) { + if (!collides(col, row)) { + return { x: col, y: row }; + } + } + } + + return { x: 0, y: 0 }; + }; + + tabNumbers.forEach((tabNumber) => { + const tab = tabs.find(t => t.number === tabNumber); + if (!tab?.id) return; + + const widgetsForTab = getAssignedWidgetsForTab(tabNumber); + if (widgetsForTab.length === 0) return; + + const placed = []; + widgetsForTab.forEach(({ widgetId, defaultSize }) => { + const key = `widget-layout-id-${tab.id}-${widgetId}`; + let parsed = null; + try { + parsed = JSON.parse(localStorage.getItem(key) || 'null'); + } catch { + parsed = null; + } + + const w = Math.max(Number(parsed?.w) || defaultSize.w, 1); + const h = Math.max(Number(parsed?.h) || defaultSize.h, 1); + const pos = findFreePosition(placed, w, h, 12); + const layoutData = { x: pos.x, y: pos.y, w, h }; + + placed.push(layoutData); + localStorage.setItem(key, JSON.stringify(layoutData)); + }); + }); + }; + + const saveWidgetSettings = async () => { + setIsLoading(true); + try { + localStorage.setItem('widgetSettings', JSON.stringify(widgetSettings)); + setWidgetSettings(widgetSettings); + + const currentResponse = await axios.get(`${API_DEVICE_URL}/widget-assignments`); + const currentAssignments = Array.isArray(currentResponse.data) ? currentResponse.data : []; + const affectedTabs = new Set(); + const nextCoreAssignments = {}; + + for (const [widgetName, desiredTabNumbers] of Object.entries(widgetAssignments)) { + const existing = currentAssignments.filter(a => a.widget_name === widgetName); + const existingTabNumbers = existing.map(a => a.tab_number); + desiredTabNumbers.forEach(number => { + if (Number.isFinite(number)) { + nextCoreAssignments[widgetName] = nextCoreAssignments[widgetName] || []; + nextCoreAssignments[widgetName].push(number); + } + }); + + collectAffectedTabNumbers(existingTabNumbers, desiredTabNumbers).forEach((tabNumber) => affectedTabs.add(tabNumber)); + + const toRemove = existing.filter(a => !desiredTabNumbers.includes(a.tab_number)); + const toAdd = desiredTabNumbers.filter(number => !existingTabNumbers.includes(number)); + + for (const assignment of toRemove) { + await axios.delete(`${API_DEVICE_URL}/widget-assignments/${assignment.id}`); + } + + for (const tabNumber of toAdd) { + await axios.post(`${API_DEVICE_URL}/widget-assignments`, { + widget_name: widgetName, + tabNumber: tabNumber, + }); + ensureLocalLayoutKeyForAssignment(widgetName, tabNumber); + } + } + + Object.keys(CORE_WIDGET_DEFAULT_SIZES).forEach((widgetName) => { + if (!nextCoreAssignments[widgetName]) { + nextCoreAssignments[widgetName] = []; + } + }); + + recalculateLayoutsForAffectedTabs(Array.from(affectedTabs), nextCoreAssignments, pluginAssignments); + + if (onTabsChanged) { + await onTabsChanged(); + } + + setSaveMessage({ show: true, type: 'success', text: 'Widget settings saved successfully.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } catch (error) { + console.error('Error saving widget settings:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to save widget settings. Please try again.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } + }; + + const savePluginSettings = async () => { + setIsLoading(true); + try { + localStorage.setItem('pluginSettings', JSON.stringify(pluginSettings)); + + const currentResponse = await axios.get(`${API_DEVICE_URL}/widget-assignments`); + const currentAssignments = Array.isArray(currentResponse.data) ? currentResponse.data : []; + const affectedTabs = new Set(); + const nextPluginAssignments = {}; + + for (const [pluginWidgetName, desiredTabNumbers] of Object.entries(pluginAssignments)) { + const existing = currentAssignments.filter(a => a.widget_name === pluginWidgetName); + const existingTabNumbers = existing.map(a => a.tab_number); + desiredTabNumbers.forEach(number => { + if (Number.isFinite(number)) { + nextPluginAssignments[pluginWidgetName] = nextPluginAssignments[pluginWidgetName] || []; + nextPluginAssignments[pluginWidgetName].push(number); + } + }); + + collectAffectedTabNumbers(existingTabNumbers, desiredTabNumbers).forEach((tabNumber) => affectedTabs.add(tabNumber)); + + const toRemove = existing.filter(a => !desiredTabNumbers.includes(a.tab_number)); + const toAdd = desiredTabNumbers.filter(number => !existingTabNumbers.includes(number)); + + for (const assignment of toRemove) { + await axios.delete(`${API_DEVICE_URL}/widget-assignments/${assignment.id}`); + } + + for (const tabNumber of toAdd) { + await axios.post(`${API_DEVICE_URL}/widget-assignments`, { + widget_name: pluginWidgetName, + tabNumber: tabNumber, + }); + ensureLocalLayoutKeyForAssignment(pluginWidgetName, tabNumber); + } + } + + Object.keys(pluginAssignments).forEach((pluginWidgetName) => { + if (!nextPluginAssignments[pluginWidgetName]) { + nextPluginAssignments[pluginWidgetName] = []; + } + }); + + recalculateLayoutsForAffectedTabs(Array.from(affectedTabs), widgetAssignments, nextPluginAssignments); + + if (onTabsChanged) { + await onTabsChanged(); + } + + if (onPluginsChanged) onPluginsChanged(); + setSaveMessage({ show: true, type: 'success', text: 'Plugin settings saved successfully.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } catch (error) { + console.error('Error saving plugin settings:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to save plugin settings. Please try again.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } + }; + + const handleWidgetAssignmentChange = (widgetName, selectedTabNumbers) => { + setWidgetAssignments(prev => ({ + ...prev, + [widgetName]: selectedTabNumbers + })); + }; + + const openCreateTabDialog = () => { + setTabIconModalState({ + open: true, + mode: 'create', + originalNumber: null, + initialData: null, + }); + }; + + const openEditTabDialog = (tab) => { + setTabIconModalState({ + open: true, + mode: 'edit', + originalNumber: tab.number, + initialData: { + label: tab.label || '', + icon: tab.icon || 'star', + show_label: Boolean(tab.show_label), + }, + }); + }; + + const closeTabEditorDialog = () => { + setTabIconModalState(prev => ({ ...prev, open: false })); + }; + + const saveTabDefinition = async (tabData) => { + const trimmedLabel = (tabData.label || '').trim(); + if (!trimmedLabel) { + setSaveMessage({ show: true, type: 'error', text: 'Tab label is required.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + return; + } + + try { + setIsLoading(true); + if (tabIconModalState.mode === 'edit') { + await axios.patch(`${API_DEVICE_URL}/tabs/${tabIconModalState.originalNumber}`, { + label: trimmedLabel, + icon: tabData.icon, + show_label: tabData.show_label, + }); + } else { + await axios.post(`${API_DEVICE_URL}/tabs`, { + label: trimmedLabel, + icon: tabData.icon, + show_label: tabData.show_label, + }); + } + + closeTabEditorDialog(); + await fetchTabs(); + if (onTabsChanged) { + await onTabsChanged(); + } + setSaveMessage({ show: true, type: 'success', text: `Tab ${tabIconModalState.mode === 'edit' ? 'updated' : 'created'} successfully.` }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } catch (error) { + console.error('Error saving tab:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to save tab. Please try again.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } + }; + + const requestDeleteTab = (tab) => { + setDeleteTabDialog({ open: true, tab }); + }; + + const confirmDeleteTab = async () => { + if (!deleteTabDialog.tab) { + return; + } + + try { + setIsLoading(true); + await axios.delete(`${API_DEVICE_URL}/tabs/${deleteTabDialog.tab.number}`); + setDeleteTabDialog({ open: false, tab: null }); + await fetchTabs(); + await fetchWidgetAssignments(); + if (onTabsChanged) { + await onTabsChanged(); + } + setSaveMessage({ show: true, type: 'success', text: 'Tab deleted successfully.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } catch (error) { + console.error('Error deleting tab:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to delete tab. Please try again.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } + }; + + const saveTabOrder = async (orderedTabNumbers) => { + try { + setIsLoading(true); + await axios.patch(`${API_DEVICE_URL}/tabs/reorder`, { orderedTabNumbers }); + await fetchTabs(); + await fetchWidgetAssignments(); + if (onTabsChanged) { + await onTabsChanged(); + } + } catch (error) { + console.error('Error reordering tabs:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to reorder tabs. Please try again.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } + }; + + const toggleTabShowLabel = async (tab) => { + try { + setIsLoading(true); + await axios.patch(`${API_DEVICE_URL}/tabs/${tab.number}`, { + label: tab.label, + icon: tab.icon, + show_label: !Boolean(tab.show_label), + }); + + await fetchTabs(); + if (onTabsChanged) { + await onTabsChanged(); + } + + setSaveMessage({ show: true, type: 'success', text: 'Tab label visibility updated.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } catch (error) { + console.error('Error updating tab label visibility:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to update tab label visibility.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } + }; + + const handleTabDragStart = (tabNumber) => { + setDraggingTabNumber(tabNumber); + }; + + const handleTabDrop = async (targetTabNumber) => { + if (draggingTabNumber == null || draggingTabNumber === targetTabNumber) { + setDraggingTabNumber(null); + return; + } + + const draggableTabs = tabs + .filter(tab => tab.number !== 1) + .sort((a, b) => a.number - b.number); + + const fromIndex = draggableTabs.findIndex(tab => tab.number === draggingTabNumber); + const toIndex = draggableTabs.findIndex(tab => tab.number === targetTabNumber); + + if (fromIndex === -1 || toIndex === -1) { + setDraggingTabNumber(null); + return; + } + + const next = [...draggableTabs]; + const [moved] = next.splice(fromIndex, 1); + next.splice(toIndex, 0, moved); + const orderedTabNumbers = next.map(tab => tab.number); + + setDraggingTabNumber(null); + await saveTabOrder(orderedTabNumbers); + }; + + const openCopyDeviceDialog = (device) => { + setCopyDeviceDialog({ open: true, device }); + }; + + const confirmCopyDeviceToCurrent = async () => { + if (!copyDeviceDialog.device?.name) { + return; + } + + try { + setIsLoading(true); + await axios.post(`${API_DEVICE_URL}/copy-from/${encodeURIComponent(copyDeviceDialog.device.name)}`); + + setCopyDeviceDialog({ open: false, device: null }); + await fetchTabs(); + await fetchWidgetAssignments(); + await fetchDevices(); + if (onTabsChanged) { + await onTabsChanged(); + } + + setSaveMessage({ show: true, type: 'success', text: 'Device tabs and widget settings copied successfully.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } catch (error) { + console.error('Error copying device settings:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to copy device settings. Please try again.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } + }; + + const openDeleteDeviceDialog = (device) => { + setDeleteDeviceDialog({ open: true, device }); + }; + + const openRenameDeviceDialog = () => { + setRenameDeviceDialog({ + open: true, + currentName: currentDeviceName, + newName: currentDeviceName, + error: '', + }); + }; + + const confirmRenameDevice = async () => { + const nextName = (renameDeviceDialog.newName || '').trim(); + if (!nextName) { + setRenameDeviceDialog(prev => ({ ...prev, error: 'Device Name is required.' })); + return; + } + + if (nextName === renameDeviceDialog.currentName) { + setRenameDeviceDialog(prev => ({ ...prev, open: false, error: '' })); + return; + } + + try { + setIsLoading(true); + await axios.patch(`${API_BASE_URL}/api/devices/${encodeURIComponent(renameDeviceDialog.currentName)}`, { + name: nextName, + }); + + setDeviceName(nextName); + setCurrentDeviceName(nextName); + setRenameDeviceDialog({ open: false, currentName: '', newName: '', error: '' }); + await fetchDevices(); + setSaveMessage({ show: true, type: 'success', text: 'Device Name updated. Reloading to apply changes.' }); + setTimeout(() => { + window.location.reload(); + }, 400); + } catch (error) { + console.error('Error renaming device:', error); + const errorMessage = error?.response?.data?.error || 'Failed to update device name. Please try again.'; + setRenameDeviceDialog(prev => ({ ...prev, error: errorMessage })); + } finally { + setIsLoading(false); + } + }; + + const confirmDeleteDevice = async () => { + const deviceName = deleteDeviceDialog.device?.name; + if (!deviceName) { + return; + } + + if (deviceName === currentDeviceName) { + setSaveMessage({ show: true, type: 'error', text: 'You cannot delete the current device.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + setDeleteDeviceDialog({ open: false, device: null }); + return; + } + + try { + setIsLoading(true); + await axios.delete(`${API_BASE_URL}/api/devices/${encodeURIComponent(deviceName)}`); + setDeleteDeviceDialog({ open: false, device: null }); + await fetchDevices(); + setSaveMessage({ show: true, type: 'success', text: 'Device deleted successfully.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } catch (error) { + console.error('Error deleting device:', error); + setSaveMessage({ show: true, type: 'error', text: 'Failed to delete device. Please try again.' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + } finally { + setIsLoading(false); + } }; const saveInterfaceSettings = () => { localStorage.setItem('widgetSettings', JSON.stringify(widgetSettings)); setWidgetSettings(widgetSettings); - + // Apply CSS variables immediately applyAccentColors(); - + setSaveMessage({ show: true, type: 'success', text: 'Accent colors saved! Refresh page to see all changes.' }); setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); }; const applyAccentColors = () => { const root = document.documentElement; - - // Apply only accent colors + const isLight = root.getAttribute('data-theme') === 'light'; + root.style.setProperty('--primary', widgetSettings.primary); root.style.setProperty('--secondary', widgetSettings.secondary); root.style.setProperty('--accent', widgetSettings.accent); + + if (isLight) { + root.style.setProperty('--background', widgetSettings.primary); + } }; const resetToDefaults = () => { const defaultSettings = { - primary: '#9E7FFF', + primary: '#f5f5f5', secondary: '#38bdf8', accent: '#f472b6' }; - + setLocalWidgetSettings(prev => ({ ...prev, ...defaultSettings })); setSaveMessage({ show: true, type: 'info', text: 'Reset to default colors. Click Save to apply.' }); setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); }; + const saveScreensaverSettings = () => { + localStorage.setItem('screensaverSettings', JSON.stringify(screensaverSettings)); + setSaveMessage({ show: true, type: 'success', text: 'Screensaver settings saved!' }); + setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000); + }; + + const hasImmichConfigured = photoSources.some(source => source.type === 'Immich' && source.enabled === 1); + const hasTabsCreated = tabs.length > 0; + const handleWidgetToggle = (widget, field) => { setLocalWidgetSettings(prev => ({ ...prev, @@ -322,18 +978,8 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { setLocalWidgetSettings(prev => ({ ...prev, [widget]: { - ...prev[widget], - refreshInterval: interval - } - })); - }; - - const handleWeatherLayoutModeChange = (mode) => { - setLocalWidgetSettings(prev => ({ - ...prev, - weather: { - ...prev.weather, - layoutMode: mode + ...prev[widget], + refreshInterval: interval } })); }; @@ -460,7 +1106,7 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { headers: { 'Content-Type': 'multipart/form-data' } }); fetchUploadedWidgets(); - if (onWidgetUploaded) onWidgetUploaded(); + if (onPluginsChanged) onPluginsChanged(); } catch (error) { console.error('Error uploading widget:', error); alert('Failed to upload widget. Please try again.'); @@ -474,8 +1120,21 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { try { setIsLoading(true); await axios.delete(`${API_BASE_URL}/api/widgets/${filename}`); + const pluginWidgetName = `plugin:${filename}`; + await axios.delete(`${API_DEVICE_URL}/widget-assignments/widget/${encodeURIComponent(pluginWidgetName)}`).catch(() => { }); + setPluginSettings(prev => { + const updated = { ...prev }; + delete updated[filename]; + localStorage.setItem('pluginSettings', JSON.stringify(updated)); + return updated; + }); + setPluginAssignments(prev => { + const updated = { ...prev }; + delete updated[pluginWidgetName]; + return updated; + }); fetchUploadedWidgets(); - if (onWidgetUploaded) onWidgetUploaded(); + if (onPluginsChanged) onPluginsChanged(); } catch (error) { console.error('Error deleting widget:', error); } finally { @@ -493,7 +1152,7 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { name: widget.name }); fetchUploadedWidgets(); - if (onWidgetUploaded) onWidgetUploaded(); + if (onPluginsChanged) onPluginsChanged(); alert(`Widget "${widget.name}" installed successfully!`); } catch (error) { console.error('Error installing GitHub widget:', error); @@ -535,28 +1194,43 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { const handleProfilePictureUpload = async (userId, event) => { const file = event.target.files[0]; - if (!file) return; + if (!file) { + console.log('No file selected'); + return; + } + + console.log('File selected:', file.name, 'Size:', file.size, 'Type:', file.type); const formData = new FormData(); formData.append('file', file); try { setIsLoading(true); + console.log(`Uploading picture for user ${userId}...`); + const response = await axios.post( `${API_BASE_URL}/api/users/${userId}/upload-picture`, formData, { headers: { 'Content-Type': 'multipart/form-data' } } ); - fetchUsers(); + + console.log('Upload response:', response.data); + + await fetchUsers(); + console.log('Users fetched after upload'); } catch (error) { console.error('Error uploading profile picture:', error); + console.error('Error details:', error.response?.data); alert('Failed to upload profile picture. Please try again.'); } finally { setIsLoading(false); + event.target.value = ''; } }; - const renderUserAvatar = (user) => { + const UserAvatar = ({ user }) => { + const [imageError, setImageError] = useState(false); + let imageUrl = null; if (user.profile_picture) { if (user.profile_picture.startsWith('data:')) { @@ -566,23 +1240,27 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { } } - return imageUrl ? ( - {user.username} { - e.target.style.display = 'none'; - e.target.nextSibling.style.display = 'flex'; - }} - /> - ) : ( + if (imageUrl && !imageError) { + return ( + {user.username} { + console.error('Failed to load image:', imageUrl); + setImageError(true); + }} + /> + ); + } + + return ( {user.username.charAt(0).toUpperCase()} @@ -670,7 +1348,13 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { boxShadow: '0 4px 12px rgba(0,0,0,0.2)' } }} - onClick={() => setShowColorPicker(prev => ({ ...prev, [key]: !prev[key] }))} + onClick={(e) => { + if (colorPickerAnchor.key === key) { + setColorPickerAnchor({ key: null, el: null }); + } else { + setColorPickerAnchor({ key, el: e.currentTarget }); + } + }} /> { placeholder="#000000" /> - {showColorPicker[key] && ( - - setShowColorPicker(prev => ({ ...prev, [key]: false }))} - /> - - handleColorChange(key, color)} - /> - - - )} + handleColorChange(key, color)} + onClose={() => setColorPickerAnchor({ key: null, el: null })} + /> ); @@ -709,15 +1378,14 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { return option ? option.label : 'Disabled'; }; - const tabs = [ - 'APIs', + const adminTabs = [ 'Widgets', 'Interface', 'Users', 'Chores', 'Prizes', - 'Plugins', - 'Security' + 'Security', + 'APIs' ]; if (checkingPin) { @@ -749,16 +1417,23 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { setActiveTab(newValue)} sx={{ mb: 3 }}> - {tabs.map((tab, index) => ( + {adminTabs.map((tab, index) => ( ))} - {/* APIs Tab */} + {/* Widgets Tab */} {activeTab === 0 && ( - API Configuration + + setWidgetsSubTab(v)} size="small"> + + + + + + {saveMessage.show && ( @@ -766,334 +1441,591 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { )} - - setSettings(prev => ({ ...prev, WEATHER_API_KEY: e.target.value }))} - sx={{ mb: 2 }} - helperText="Get your free API key from openweathermap.org/api" - /> - - setSettings(prev => ({ ...prev, PROXY_WHITELIST: e.target.value }))} - sx={{ mb: 2 }} - helperText="Domains allowed for proxy requests (e.g., api.example.com, another-api.com)" - /> - - setSettings(prev => ({ ...prev, daily_completion_clam_reward: e.target.value }))} - sx={{ mb: 2 }} - helperText="Number of clams awarded when a user completes all their daily chores" - inputProps={{ min: 0, max: 100 }} - /> - - - - - - )} + {widgetsSubTab === 0 && ( + <> + + Enable widgets to show them on the dashboard. Click to select a widget, then drag to move or resize from corners. + - {/* Widgets Tab */} - {activeTab === 1 && ( - - - Widget Settings + {Object.entries(widgetSettings).filter(([key]) => + ['chores', 'calendar', 'photos'].includes(key) + ).map(([widget, config]) => ( + + + {widget} Widget + - {saveMessage.show && ( - - {saveMessage.text} - - )} + + + handleWidgetToggle(widget, 'enabled')} + /> + } + label="Enabled" + /> + handleWidgetToggle(widget, 'transparent')} + /> + } + label="Transparent Background" + sx={{ ml: 2 }} + /> + + + + + + + + Auto-Refresh Interval + + + + + + + + + option.label} + value={tabs.filter(tab => widgetAssignments[widget]?.includes(tab.number))} + onChange={(e, newValue) => { + handleWidgetAssignmentChange(widget, newValue.map(tab => tab.number)); + }} + renderInput={(params) => ( + + )} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + )) + } + /> + - - Enable widgets to show them on the dashboard. Click to select a widget, then drag to move or resize from corners. - + {config.refreshInterval > 0 && ( + }> + This widget will automatically refresh every {getRefreshIntervalLabel(config.refreshInterval).toLowerCase()} + + )} + + ))} + + + + Weather Widget + + + + + handleWidgetToggle('weather', 'enabled')} + /> + } + label="Enabled" + /> + handleWidgetToggle('weather', 'transparent')} + /> + } + label="Transparent Background" + sx={{ ml: 2 }} + /> + + + + + + + + Auto-Refresh Interval + + + + + + - {/* Core Widgets */} - {Object.entries(widgetSettings).filter(([key]) => - ['chores', 'calendar', 'photos'].includes(key) - ).map(([widget, config]) => ( - - - {widget} Widget - - - - - handleWidgetToggle(widget, 'enabled')} - /> - } - label="Enabled" - /> - handleWidgetToggle(widget, 'transparent')} + + option.label} + value={tabs.filter(tab => widgetAssignments['weather']?.includes(tab.number))} + onChange={(e, newValue) => { + handleWidgetAssignmentChange('weather', newValue.map(tab => tab.number)); + }} + renderInput={(params) => ( + + )} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + )) } - label="Transparent Background" - sx={{ ml: 2 }} /> + + + {widgetSettings.weather?.refreshInterval > 0 && ( + }> + Weather widget will automatically refresh every {getRefreshIntervalLabel(widgetSettings.weather.refreshInterval).toLowerCase()} + + )} + + + + + )} + + {widgetsSubTab === 1 && ( + <> + + + Upload Custom Widget + + + Uploaded Widgets + + {uploadedWidgets.map((widget) => ( + + + + deleteWidget(widget.filename)} color="error"> + + + + + ))} + - - - - - - - Auto-Refresh Interval - - - - - - - - {config.refreshInterval > 0 && ( - }> - This widget will automatically refresh every {getRefreshIntervalLabel(config.refreshInterval).toLowerCase()} - - )} - - ))} + Refresh + + - {/* Weather Widget with Layout Mode */} - - - 🌤️ Weather Widget - - - - - handleWidgetToggle('weather', 'enabled')} - /> - } - label="Enabled" - /> - handleWidgetToggle('weather', 'transparent')} - /> - } - label="Transparent Background" - sx={{ ml: 2 }} - /> - - - - - - - - Auto-Refresh Interval - - - - + + - - {/* Weather Layout Mode Selection */} - - - - Layout Mode - - - handleWeatherLayoutModeChange(e.target.value)} - > - } - label={ - - - - - Compact - - - Current weather only (minimal space) - - - - } - /> - - } - label={ - - - - - Medium - - - Current weather + 3-day forecast - - - - } - /> - - } - label={ - - - - - Full - - - All information with charts and air quality - + {uploadedWidgets.length > 0 && ( + <> + + Plugin Settings + + Configure each installed plugin below. Enable them, set transparency, refresh intervals, and assign to tabs just like core widgets. + + + {uploadedWidgets.map((plugin) => { + const pSettings = pluginSettings[plugin.filename] || {}; + const pluginWidgetName = `plugin:${plugin.filename}`; + return ( + + + + + {plugin.name} + + + {plugin.filename} + + + deleteWidget(plugin.filename)} color="error" size="small"> + + + + + + + { + setPluginSettings(prev => ({ + ...prev, + [plugin.filename]: { ...prev[plugin.filename], enabled: !(prev[plugin.filename]?.enabled) } + })); + }} + /> + } + label="Enabled" + /> + { + setPluginSettings(prev => ({ + ...prev, + [plugin.filename]: { ...prev[plugin.filename], transparent: !(prev[plugin.filename]?.transparent) } + })); + }} + /> + } + label="Transparent Background" + sx={{ ml: 2 }} + /> + + + + + + + + Auto-Refresh Interval + + + + + + + + + option.label} + value={tabs.filter(tab => pluginAssignments[pluginWidgetName]?.includes(tab.number))} + onChange={(e, newValue) => { + setPluginAssignments(prev => ({ + ...prev, + [pluginWidgetName]: newValue.map(tab => tab.number) + })); + }} + renderInput={(params) => ( + + )} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + )) + } + /> + - - } - /> - + ); + })} - - The selected layout mode will be used regardless of widget size. Resize the widget to fit your preferred layout. - - - - {widgetSettings.weather?.refreshInterval > 0 && ( - }> - Weather widget will automatically refresh every {getRefreshIntervalLabel(widgetSettings.weather.refreshInterval).toLowerCase()} - - )} - + + + )} + + )} - {/* Widget Gallery Settings */} - - - 🎨 Widget Gallery - - - The Widget Gallery displays custom uploaded widgets below the main dashboard widgets. - - - - - handleWidgetToggle('widgetGallery', 'enabled')} - /> - } - label="Show Widget Gallery" - /> - handleWidgetToggle('widgetGallery', 'transparent')} - /> - } - label="Transparent Background" - sx={{ ml: 2 }} - /> - - - - - - - - Auto-Refresh Interval - - - - - - - - {widgetSettings.widgetGallery?.refreshInterval > 0 && ( - }> - Widget Gallery will automatically refresh every {getRefreshIntervalLabel(widgetSettings.widgetGallery.refreshInterval).toLowerCase()} + {widgetsSubTab === 2 && ( + <> + + + Manage dashboard tabs. Drag rows to reorder tabs. Home tab cannot be edited or deleted. + + + + + + + + + Order + Label + Icon + Show Label + Actions + + + + {[...tabs].sort((a, b) => a.number - b.number).map((tab) => { + const isHome = tab.number === 1; + return ( + handleTabDragStart(tab.number)} + onDragOver={(e) => { + if (!isHome) { + e.preventDefault(); + } + }} + onDrop={() => { + if (!isHome) { + handleTabDrop(tab.number); + } + }} + sx={{ + cursor: isHome ? 'default' : 'grab', + opacity: draggingTabNumber === tab.number ? 0.65 : 1, + }} + > + + + {!isHome && } + + + + + {tab.label} + {isHome && ( + + )} + + + + + + e.stopPropagation()} + onChange={() => toggleTabShowLabel(tab)} + disabled={isLoading} + inputProps={{ 'aria-label': `Toggle show label for ${tab.label}` }} + /> + + + openEditTabDialog(tab)} + color="primary" + size="small" + disabled={isHome} + > + + + requestDeleteTab(tab)} + color="error" + size="small" + disabled={isHome} + > + + + + + ); + })} + +
+
+ + )} + + {widgetsSubTab === 3 && ( + <> + + Manage devices and copy tabs/widget settings between them. Copying will overwrite the current device tabs and widget assignments. - )} -
- - + + + + Current Device Name + + + + + {currentDeviceName} + + + + + + + + + Name + Last Updated + Widgets + Actions + + + + {devices.map((device) => { + const isCurrent = device.name === currentDeviceName; + return ( + + + + {isCurrent && } + + {device.name} + + + + + {device.updateTime ? new Date(device.updateTime).toLocaleString() : 'Unknown'} + + + + + + {isCurrent ? ( + + + + ) : ( + openCopyDeviceDialog(device)} + color="primary" + size="small" + title="Copy this device to current" + > + + + )} + openDeleteDeviceDialog(device)} + color="error" + size="small" + title="Delete device" + disabled={isCurrent} + > + + + + + ); + })} + +
+
+ + )}
)} {/* Interface Tab */} - {activeTab === 2 && ( + {activeTab === 1 && ( @@ -1115,18 +2047,18 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { )} - Customize the accent colors used throughout the dashboard. These colors are shared between light and dark themes. + Background color applies to light mode only. Accent color is used throughout the dashboard for highlights and interactive elements. - + - {renderColorPicker('primary', '🎨 Primary Color')} + {renderColorPicker('primary', '🎨 Background Color (Light Mode)')} {renderColorPicker('secondary', '💎 Secondary Color')} {renderColorPicker('accent', '✨ Accent Color')} - + - + + + + + + Screensaver + + + + The screensaver activates after a period of inactivity, cycling through tabs or displaying a photo slideshow. + + + + setScreensaverSettings(prev => ({ ...prev, enabled: e.target.checked }))} + /> + } + label="Enable Screensaver" + sx={{ mb: 3 }} + /> + + + Screensaver Mode + + + setScreensaverSettings(prev => ({ ...prev, mode: e.target.value }))} + sx={{ mb: 3 }} + > + + } + label={ + + + + + Cycle Through Tabs + + + {hasTabsCreated + ? `Automatically switch between ${tabs.length} tab${tabs.length !== 1 ? 's' : ''}` + : 'No tabs created yet'} + + + {!hasTabsCreated && ( + + + + )} + + } + sx={{ opacity: !hasTabsCreated ? 0.6 : 1 }} + /> + + + + } + label={ + + + + + Immich Photo Slideshow + + + {hasImmichConfigured + ? 'Display photos from your Immich library' + : 'Immich not configured'} + + + {!hasImmichConfigured && ( + + + + )} + + } + sx={{ opacity: !hasImmichConfigured ? 0.6 : 1 }} + /> + + + + + Inactivity Timeout: {screensaverSettings.timeout} minute{screensaverSettings.timeout !== 1 ? 's' : ''} + + setScreensaverSettings(prev => ({ ...prev, timeout: value }))} + min={1} + max={30} + marks={[ + { value: 1, label: '1m' }, + { value: 5, label: '5m' }, + { value: 10, label: '10m' }, + { value: 15, label: '15m' }, + { value: 30, label: '30m' } + ]} + sx={{ mb: 4 }} + /> + + {screensaverSettings.mode === 'photos' && ( + <> + + Photo Slideshow Interval: {screensaverSettings.slideshowInterval} second{screensaverSettings.slideshowInterval !== 1 ? 's' : ''} + + setScreensaverSettings(prev => ({ ...prev, slideshowInterval: value }))} + min={3} + max={60} + marks={[ + { value: 3, label: '3s' }, + { value: 10, label: '10s' }, + { value: 30, label: '30s' }, + { value: 60, label: '60s' } + ]} + sx={{ mb: 4 }} + /> + + )} + + {screensaverSettings.mode === 'tabs' && ( + <> + + Tab Cycle Interval: {screensaverSettings.slideshowInterval} second{screensaverSettings.slideshowInterval !== 1 ? 's' : ''} + + setScreensaverSettings(prev => ({ ...prev, slideshowInterval: value }))} + min={5} + max={120} + marks={[ + { value: 5, label: '5s' }, + { value: 30, label: '30s' }, + { value: 60, label: '60s' }, + { value: 120, label: '2m' } + ]} + sx={{ mb: 4 }} + /> + + )} + + + )} {/* Users Tab */} - {activeTab === 3 && ( + {activeTab === 2 && ( User Management - + Add New User @@ -1203,7 +2319,7 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { - {renderUserAvatar(user)} + - - Uploaded Widgets - - {uploadedWidgets.map((widget) => ( - - - - deleteWidget(widget.filename)} color="error"> - - - - - ))} - - - - - - GitHub Widget Repository - - - - - {githubWidgets.map((widget) => ( - - - - - - - ))} - - - - - - )} - {/* Security Tab */} - {activeTab === 7 && ( + {activeTab === 5 && ( Security Settings @@ -1604,6 +2639,203 @@ const AdminPanel = ({ setWidgetSettings, onWidgetUploaded }) => { )} + {/* APIs Tab */} + {activeTab === 6 && ( + + + API Configuration + + {saveMessage.show && ( + + {saveMessage.text} + + )} + + + setSettings(prev => ({ ...prev, WEATHER_API_KEY: e.target.value }))} + sx={{ mb: 2 }} + helperText="Get your free API key from openweathermap.org/api" + /> + + setSettings(prev => ({ ...prev, PROXY_WHITELIST: e.target.value }))} + sx={{ mb: 2 }} + helperText="Domains allowed for proxy requests (e.g., api.example.com, another-api.com)" + /> + + setSettings(prev => ({ ...prev, daily_completion_clam_reward: e.target.value }))} + sx={{ mb: 2 }} + helperText="Number of clams awarded when a user completes all their daily chores" + inputProps={{ min: 0, max: 100 }} + /> + + + + + + )} + + {/* User Delete Confirmation Dialog */} + + + setDeleteTabDialog({ open: false, tab: null })} + maxWidth="sm" + fullWidth + > + + + + Delete Tab + + + + + Widgets assigned to this tab will be moved by the server rules for deleted tabs. + + + Are you sure you want to delete {deleteTabDialog.tab?.label}? + + + + + + + + + setCopyDeviceDialog({ open: false, device: null })} + maxWidth="sm" + fullWidth + > + + + + Copy Device Settings + + + + + This action will COPY all tabs and widget settings from the selected device to this current client. + + + This will overwrite all current tabs and widget assignments. + + + Source device: {copyDeviceDialog.device?.name} + + + Destination device: {currentDeviceName} + + + + + + + + + setRenameDeviceDialog({ open: false, currentName: '', newName: '', error: '' })} + maxWidth="sm" + fullWidth + > + + + + Rename Current Device Name + + + + setRenameDeviceDialog(prev => ({ ...prev, newName: e.target.value, error: '' }))} + error={Boolean(renameDeviceDialog.error)} + helperText={renameDeviceDialog.error || 'This updates the current device name in both server and local storage.'} + sx={{ mt: 1 }} + autoFocus + /> + + + + + + + + setDeleteDeviceDialog({ open: false, device: null })} + maxWidth="sm" + fullWidth + > + + + + Delete Device + + + + + This action cannot be undone. + + + Are you sure you want to delete device {deleteDeviceDialog.device?.name}? + + + + + + + + {/* User Delete Confirmation Dialog */} { ))} - + { > Processing... - + { const [events, setEvents] = useState([]); const [upcomingEvents, setUpcomingEvents] = useState([]); @@ -35,6 +56,28 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { bulletSize: 10 }; }); + const [dayOfWeekSettings, setDayOfWeekSettings] = useState(() => { + const saved = localStorage.getItem('calendarDayOfWeekSettings'); + if (!saved) { + return { + weekViewStart: 'today', + monthViewStart: 'sunday' + }; + } + + try { + const parsed = JSON.parse(saved); + return { + weekViewStart: parsed.weekViewStart || 'today', + monthViewStart: parsed.monthViewStart || 'sunday' + }; + } catch { + return { + weekViewStart: 'today', + monthViewStart: 'sunday' + }; + } + }); const [showColorPicker, setShowColorPicker] = useState({ background: false, text: false }); const [calendarSources, setCalendarSources] = useState([]); const [showCalendarDialog, setShowCalendarDialog] = useState(false); @@ -50,11 +93,27 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { const [testingConnection, setTestingConnection] = useState(false); const [testResult, setTestResult] = useState(null); const [savingCalendar, setSavingCalendar] = useState(false); + const [syncStatus, setSyncStatus] = useState({}); + const [syncIntervals, setSyncIntervals] = useState({}); + const [isSyncing, setIsSyncing] = useState({}); + + const syncIntervalOptions = [ + { label: 'Disabled', value: 0 }, + { label: '5 minutes', value: 5 }, + { label: '15 minutes', value: 15 }, + { label: '30 minutes', value: 30 }, + { label: '1 hour', value: 60 }, + { label: '2 hours', value: 120 }, + { label: '6 hours', value: 360 }, + { label: '12 hours', value: 720 }, + { label: '24 hours', value: 1440 } + ]; // Initial data fetch useEffect(() => { fetchCalendarSources(); fetchCalendarEvents(); + fetchSyncStatus(); }, []); // Auto-refresh functionality @@ -64,7 +123,7 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { if (refreshInterval > 0) { console.log(`CalendarWidget: Auto-refresh enabled (${refreshInterval}ms)`); - + const intervalId = setInterval(() => { console.log('CalendarWidget: Auto-refreshing data...'); fetchCalendarSources(); @@ -104,10 +163,14 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { return () => clearTimeout(timeoutId); }, [displaySettings]); + useEffect(() => { + localStorage.setItem('calendarDayOfWeekSettings', JSON.stringify(dayOfWeekSettings)); + }, [dayOfWeekSettings]); + useEffect(() => { const loadDisplaySettings = async () => { try { - const response = await axios.get(`${API_BASE_URL}/api/settings`); + const response = await axios.post(`${API_BASE_URL}/api/settings/search`, ['CALENDAR_*']); const settings = response.data; if (settings.CALENDAR_TEXT_SIZE || settings.CALENDAR_BULLET_SIZE) { @@ -133,6 +196,75 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { } }; + const fetchSyncStatus = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/api/calendar-sync/status`); + const statusMap = {}; + const intervalsMap = {}; + if (Array.isArray(response.data)) { + response.data.forEach(status => { + statusMap[status.source_id] = status; + intervalsMap[status.source_id] = status.sync_interval_minutes || 15; + }); + } + setSyncStatus(statusMap); + setSyncIntervals(intervalsMap); + } catch (error) { + console.error('Error fetching sync status:', error); + } + }; + + const handleSyncSource = async (sourceId) => { + setIsSyncing(prev => ({ ...prev, [sourceId]: true })); + try { + await axios.post(`${API_BASE_URL}/api/calendar-sync/${sourceId}`); + await fetchCalendarEvents(); + await fetchSyncStatus(); + } catch (error) { + console.error('Error syncing calendar source:', error); + } finally { + setIsSyncing(prev => ({ ...prev, [sourceId]: false })); + } + }; + + const handleSyncAll = async () => { + setIsSyncing(prev => ({ ...prev, all: true })); + try { + await axios.post(`${API_BASE_URL}/api/calendar-sync/all`); + await fetchCalendarEvents(); + await fetchSyncStatus(); + } catch (error) { + console.error('Error syncing all calendar sources:', error); + } finally { + setIsSyncing(prev => ({ ...prev, all: false })); + } + }; + + const handleSyncIntervalChange = async (sourceId, intervalMinutes) => { + try { + await axios.patch(`${API_BASE_URL}/api/calendar-sync/${sourceId}/interval`, { + interval_minutes: intervalMinutes + }); + setSyncIntervals(prev => ({ ...prev, [sourceId]: intervalMinutes })); + } catch (error) { + console.error('Error setting sync interval:', error); + } + }; + + const formatLastSync = (timestamp) => { + if (!timestamp) return 'Never'; + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now - date; + const diffMins = Math.floor(diffMs / 60000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `${diffHours}h ago`; + return moment(date).format('MMM D, h:mm A'); + }; + const fetchCalendarEvents = async () => { try { setLoading(true); @@ -352,13 +484,28 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { return { isStart, isEnd }; }; + const getWeekStartDate = () => { + const baseDate = moment(currentDate).startOf('day'); + let startDate = baseDate.clone(); + + if (dayOfWeekSettings.weekViewStart === 'yesterday') { + startDate = baseDate.clone().subtract(1, 'day'); + } else if (dayOfWeekSettings.weekViewStart !== 'today') { + const targetDay = WEEKDAY_INDEX[dayOfWeekSettings.weekViewStart]; + if (typeof targetDay === 'number') { + startDate = baseDate.clone().startOf('week').add(targetDay, 'days'); + } + } + + return startDate; + }; + const getNext7Days = () => { - const startDate = new Date(currentDate); + const startDate = getWeekStartDate(); + const dates = []; for (let i = 0; i < 7; i++) { - const date = new Date(startDate); - date.setDate(startDate.getDate() + i); - dates.push(date); + dates.push(startDate.clone().add(i, 'days').toDate()); } const weekStart = moment(dates[0]).startOf('day'); @@ -391,7 +538,7 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { for (let s = 0; s < slots.length; s++) { const overlaps = slots[s].some(slotEvent => { return moment(event.start).isBefore(moment(slotEvent.end)) && - moment(event.end).isAfter(moment(slotEvent.start)); + moment(event.end).isAfter(moment(slotEvent.start)); }); if (!overlaps) { slots[s].push(event); @@ -496,9 +643,9 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { if (viewMode === 'month') { return moment(currentDate).format('MMMM YYYY'); } else { - const startOfWeek = moment(currentDate); - const endOfWeek = moment(currentDate).add(6, 'days'); - + const startOfWeek = getWeekStartDate(); + const endOfWeek = startOfWeek.clone().add(6, 'days'); + if (startOfWeek.month() === endOfWeek.month()) { return `${startOfWeek.format('MMM D')}-${endOfWeek.format('D, YYYY')}`; } else { @@ -523,9 +670,9 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { if (loading) { return ( - { } return ( - { exclusive onChange={handleViewModeChange} size="small" + sx={{ + '& .MuiToggleButton-root': { color: 'var(--text-color)', borderColor: 'var(--card-border)' }, + '& .MuiToggleButton-root.Mui-selected': { color: 'var(--text-color)', backgroundColor: 'rgba(var(--accent-rgb), 0.15)' }, + }} > @@ -591,7 +742,7 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { - + {error && ( @@ -603,7 +754,15 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { {viewMode === 'month' ? ( - {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => ( + {(() => { + if (dayOfWeekSettings.monthViewStart === 'first-day-of-month') { + const monthStart = moment(currentDate).startOf('month'); + return Array.from({ length: 7 }, (_, idx) => monthStart.clone().add(idx, 'days').format('ddd')); + } + + const firstDayIndex = WEEKDAY_INDEX[dayOfWeekSettings.monthViewStart] ?? 0; + return Array.from({ length: 7 }, (_, idx) => WEEKDAY_LABELS[(firstDayIndex + idx) % 7]); + })().map((day) => ( {day} @@ -613,8 +772,29 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { {(() => { const monthStart = moment(currentDate).startOf('month'); const monthEnd = moment(currentDate).endOf('month'); - const startDate = moment(monthStart).startOf('week'); - const endDate = moment(monthEnd).endOf('week'); + const isFirstDayOfMonthMode = dayOfWeekSettings.monthViewStart === 'first-day-of-month'; + + const startDate = (() => { + if (isFirstDayOfMonthMode) { + return monthStart.clone(); + } + + const firstDayIndex = WEEKDAY_INDEX[dayOfWeekSettings.monthViewStart] ?? 0; + const offset = (monthStart.day() - firstDayIndex + 7) % 7; + return monthStart.clone().subtract(offset, 'days'); + })(); + + const endDate = (() => { + if (isFirstDayOfMonthMode) { + return monthEnd.clone(); + } + + const firstDayIndex = WEEKDAY_INDEX[dayOfWeekSettings.monthViewStart] ?? 0; + const lastColumnDay = (firstDayIndex + 6) % 7; + const trailing = (lastColumnDay - monthEnd.day() + 7) % 7; + return monthEnd.clone().add(trailing, 'days'); + })(); + const numWeeks = Math.ceil((endDate.diff(startDate, 'days') + 1) / 7); const isMultiDaySpanning = (event) => { @@ -627,10 +807,16 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { for (let w = 0; w < numWeeks; w++) { const weekDates = []; for (let d = 0; d < 7; d++) { - weekDates.push(startDate.clone().add(w * 7 + d, 'days')); + const day = startDate.clone().add(w * 7 + d, 'days'); + if (isFirstDayOfMonthMode && day.isAfter(monthEnd, 'day')) { + weekDates.push(null); + } else { + weekDates.push(day); + } } - const weekStart = weekDates[0]; - const weekEnd = weekDates[6]; + const validWeekDates = weekDates.filter(Boolean); + const weekStart = validWeekDates[0] || startDate; + const weekEnd = validWeekDates[validWeekDates.length - 1] || weekStart; const weekMultiDayEvents = events .filter(e => isMultiDaySpanning(e) && moment(e.start).isSameOrBefore(weekEnd.clone().endOf('day')) && moment(e.end).isSameOrAfter(weekStart.clone().startOf('day'))) @@ -656,16 +842,36 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { const multiDaySlotCount = slots.length; const getSlot = (event) => { for (let s = 0; s < slots.length; s++) if (slots[s].includes(event)) return s; return -1; }; - weeks.push({ weekDates, weekMultiDayEvents, slots, multiDaySlotCount, getSlot }); + weeks.push({ + weekDates, + weekMultiDayEvents, + slots, + multiDaySlotCount, + getSlot, + weekKey: weekStart.format('YYYY-MM-DD') + }); } const allWeekCells = []; - weeks.forEach(({ weekDates, weekMultiDayEvents, multiDaySlotCount, getSlot }) => { - weekDates.forEach(day => { + weeks.forEach(({ weekDates, weekMultiDayEvents, multiDaySlotCount, getSlot, weekKey }) => { + weekDates.forEach((day, dayIdx) => { + if (!day) { + allWeekCells.push( + + ); + return; + } + const dayDate = day.toDate(); const isCurrentMonth = day.month() === moment(currentDate).month(); const isToday = day.isSame(moment(), 'day'); - const dayClone = day.clone(); const dayMultiDay = weekMultiDayEvents.filter(e => eventSpansDay(e, dayDate)); const multiDaySlottedRows = Array(multiDaySlotCount).fill(null).map((_, slotIdx) => @@ -679,141 +885,26 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { !e.all_day && !isMultiDaySpanning(e) && eventSpansDay(e, dayDate) ).sort((a, b) => a.start - b.start); - const maxItems = 3; const pillHeight = `${displaySettings.textSize * 1.5}px`; - - // Count total events for this day const totalEventCount = multiDaySlottedRows.filter(e => e !== null).length + dayAllDaySingle.length + dayTimed.length; - let shownCount = 0; - - const renderMonthPill = (event, key, clickHandler) => { - if (!event) { - return null; - } - if (shownCount >= maxItems) { - return null; - } - shownCount++; - const { isStart, isEnd } = getMultiDayPosition(event, dayDate); - const color = event.source_color || eventColors.backgroundColor; - const isContinuing = !isStart; - return ( - - - - {isContinuing ? `← ${event.title}` : event.title} - - - - ); - }; allWeekCells.push( - handleSelectSlot({ start: dayClone.toDate() })} - sx={{ - border: '1px solid var(--card-border)', - borderRadius: 1, - p: 0.75, - cursor: 'pointer', - bgcolor: isToday ? 'rgba(var(--accent-rgb), 0.1)' : 'transparent', - opacity: isCurrentMonth ? 1 : 0.4, - display: 'flex', - flexDirection: 'column', - minHeight: 0, - overflow: 'hidden', - '&:hover': { - bgcolor: isToday ? 'rgba(var(--accent-rgb), 0.15)' : 'rgba(0,0,0,0.05)' - } - }} - > - - {day.format('D')} - - - - {multiDaySlottedRows.map((event, slotIdx) => { - const result = renderMonthPill(event, `multi-${slotIdx}`, (e) => { e.stopPropagation(); handleSelectEvent(event); }); - return result; - })} - - {dayAllDaySingle.map((event, evIdx) => { - if (shownCount >= maxItems) { return null; } - shownCount++; - const color = event.source_color || eventColors.backgroundColor; - return ( - { e.stopPropagation(); handleSelectEvent(event); }} - sx={{ mb: 0.25, height: pillHeight, minHeight: pillHeight, display: 'flex', alignItems: 'stretch', cursor: 'pointer' }}> - - - {event.title} - - - - ); - })} - - {dayTimed.map((event, evIdx) => { - if (shownCount >= maxItems) { return null; } - shownCount++; - const color = event.source_color || eventColors.backgroundColor; - return ( - { e.stopPropagation(); handleSelectEvent(event); }} - sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.25, cursor: 'pointer', borderRadius: 0.5, px: 0.25, '&:hover': { bgcolor: 'rgba(0,0,0,0.05)' } }}> - - - {event.title} - - - ); - })} - - {totalEventCount > maxItems && ( - - +{totalEventCount - maxItems} more - - )} - - + day={day} + isCurrentMonth={isCurrentMonth} + isToday={isToday} + multiDaySlottedRows={multiDaySlottedRows} + dayAllDaySingle={dayAllDaySingle} + dayTimed={dayTimed} + totalEventCount={totalEventCount} + pillHeight={pillHeight} + displaySettings={displaySettings} + eventColors={eventColors} + getMultiDayPosition={getMultiDayPosition} + onSlotClick={handleSelectSlot} + onEventClick={handleSelectEvent} + /> ); }); }); @@ -924,7 +1015,7 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { {day.dayNumber} - + {day.monthName} @@ -1037,8 +1128,8 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { )} - setShowDayModal(false)} maxWidth="md" fullWidth @@ -1080,8 +1171,8 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { flexShrink: 0 }} /> - { - + - + {calendar.url} + + {calendar.enabled && ( + + + + + + + + + + handleSyncSource(calendar.id)} + disabled={isSyncing[calendar.id]} + > + {isSyncing[calendar.id] ? ( + + ) : ( + + )} + + + + + + Last sync: {formatLastSync(syncStatus[calendar.id]?.last_sync_at)} + + {syncStatus[calendar.id]?.last_sync_status === 'error' && ( + + )} + {syncStatus[calendar.id]?.event_count > 0 && ( + + )} + + + )} ))} )} + + {calendarSources.length > 0 && ( + + )} + + + + + Days of the week + + + + Week View Start + + + + + Month View Start + + Default Event Colors - + Event Background Color @@ -1242,9 +1432,10 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { {showColorPicker.background && ( - handleColorChange('background', color)} + disableAlpha /> )} @@ -1268,9 +1459,10 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { {showColorPicker.text && ( - handleColorChange('text', color)} + disableAlpha /> )} @@ -1486,9 +1678,10 @@ const CalendarWidget = ({ transparentBackground, icsCalendarUrl }) => { sx={{ position: 'fixed', top: 0, right: 0, bottom: 0, left: 0 }} onClick={() => setShowColorPicker({ ...showColorPicker, calendar: false })} /> - setCalendarForm({ ...calendarForm, color: color.hex })} + disableAlpha /> )} diff --git a/client/src/components/ChoreWidget.jsx b/client/src/components/ChoreWidget.jsx index e09acc9..0027017 100644 --- a/client/src/components/ChoreWidget.jsx +++ b/client/src/components/ChoreWidget.jsx @@ -541,7 +541,7 @@ const ChoreWidget = ({ transparentBackground }) => { {userChores.length === 0 ? ( - + No chores for today ) : ( diff --git a/client/src/components/ColorPickerPopover.jsx b/client/src/components/ColorPickerPopover.jsx new file mode 100644 index 0000000..8815b30 --- /dev/null +++ b/client/src/components/ColorPickerPopover.jsx @@ -0,0 +1,217 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import { CustomPicker } from 'react-color'; +import { Saturation, Hue, EditableInput } from 'react-color/lib/components/common'; +import { Box, Typography } from '@mui/material'; + +const SaturationSlider = ({ hsv, onChange }) => { + const containerRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + + const saturation = Math.round((hsv?.s || 0) * 100); + + const getPositionFromEvent = useCallback((e, container) => { + const rect = container.getBoundingClientRect(); + const clientX = e.touches ? e.touches[0].clientX : e.clientX; + const x = Math.max(0, Math.min(clientX - rect.left, rect.width)); + return x / rect.width; + }, []); + + const handleChange = useCallback((e) => { + if (!containerRef.current) return; + const newSaturation = getPositionFromEvent(e, containerRef.current); + onChange({ s: newSaturation }); + }, [getPositionFromEvent, onChange]); + + const handleMouseDown = useCallback((e) => { + e.preventDefault(); + setIsDragging(true); + handleChange(e); + }, [handleChange]); + + useEffect(() => { + if (!isDragging) return; + const handleMouseMove = (e) => handleChange(e); + const handleMouseUp = () => setIsDragging(false); + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, handleChange]); + + const thumbLeft = `${saturation}%`; + + return ( + + + + + + ); +}; + +const ColorPickerInner = ({ hex, hsv, hsl, onChange }) => { + const handleSaturationChange = ({ s }) => { + onChange({ ...hsv, s, source: 'hsv' }); + }; + + const handleHexChange = (val) => { + if (typeof val === 'string' && /^#?[0-9A-Fa-f]{6}$/.test(val)) { + onChange({ hex: val.startsWith('#') ? val : `#${val}`, source: 'hex' }); + } + }; + + return ( + + + + + + + + + + + + + + + + HEX + + + + + ); +}; + +const WrappedPicker = CustomPicker(ColorPickerInner); + +const ColorPickerPopover = ({ anchorEl, color, onChange, onClose }) => { + const popoverRef = useRef(null); + const [position, setPosition] = useState({ top: 0, left: 0 }); + + useEffect(() => { + if (!anchorEl) return; + const rect = anchorEl.getBoundingClientRect(); + const popoverWidth = 252; + const popoverHeight = 320; + const margin = 8; + + let top = rect.bottom + margin; + let left = rect.left; + + if (left + popoverWidth > window.innerWidth - margin) { + left = window.innerWidth - popoverWidth - margin; + } + if (top + popoverHeight > window.innerHeight - margin) { + top = rect.top - popoverHeight - margin; + } + + setPosition({ top, left }); + }, [anchorEl]); + + useEffect(() => { + const handleClick = (e) => { + if (popoverRef.current && !popoverRef.current.contains(e.target)) { + if (anchorEl && !anchorEl.contains(e.target)) { + onClose(); + } + } + }; + const handleKey = (e) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('mousedown', handleClick); + document.addEventListener('keydown', handleKey); + return () => { + document.removeEventListener('mousedown', handleClick); + document.removeEventListener('keydown', handleKey); + }; + }, [onClose, anchorEl]); + + if (!anchorEl) return null; + + return createPortal( + + + , + document.body + ); +}; + +export default ColorPickerPopover; diff --git a/client/src/components/MonthDayCell.jsx b/client/src/components/MonthDayCell.jsx new file mode 100644 index 0000000..9432f45 --- /dev/null +++ b/client/src/components/MonthDayCell.jsx @@ -0,0 +1,204 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Box, Typography } from '@mui/material'; + +const MonthDayCell = ({ + day, + isCurrentMonth, + isToday, + multiDaySlottedRows, + dayAllDaySingle, + dayTimed, + totalEventCount, + pillHeight, + displaySettings, + eventColors, + getMultiDayPosition, + onSlotClick, + onEventClick, +}) => { + const cellRef = useRef(null); + const dateLabelRef = useRef(null); + const maxItemsRef = useRef(3); + const rafRef = useRef(null); + const [maxItems, setMaxItems] = useState(3); + + const computeMax = useCallback(() => { + const cellEl = cellRef.current; + if (!cellEl) return; + + const cellHeight = cellEl.getBoundingClientRect().height; + const labelHeight = dateLabelRef.current + ? dateLabelRef.current.getBoundingClientRect().height + : 20; + const padding = 12; + const availableHeight = cellHeight - labelHeight - padding; + const itemH = parseFloat(pillHeight) + 2; + const moreLineH = 16; + + let newMax; + if (availableHeight <= 0) { + newMax = 0; + } else { + const fitAll = Math.floor(availableHeight / itemH); + if (totalEventCount <= fitAll) { + newMax = fitAll; + } else { + newMax = Math.max(1, Math.floor((availableHeight - moreLineH) / itemH)); + } + } + + if (newMax !== maxItemsRef.current) { + maxItemsRef.current = newMax; + setMaxItems(newMax); + } + }, [pillHeight, totalEventCount]); + + useEffect(() => { + const cellEl = cellRef.current; + if (!cellEl) return; + + computeMax(); + + const observer = new ResizeObserver(() => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(computeMax); + }); + + observer.observe(cellEl); + return () => { + observer.disconnect(); + if (rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [computeMax]); + + let shownCount = 0; + + const renderPill = (event, key, clickHandler) => { + if (!event || shownCount >= maxItems) return null; + shownCount++; + const dayDate = day.toDate(); + const { isStart, isEnd } = getMultiDayPosition(event, dayDate); + const color = event.source_color || eventColors.backgroundColor; + const isContinuing = !isStart; + return ( + + + + {isContinuing ? `\u2190 ${event.title}` : event.title} + + + + ); + }; + + const remaining = totalEventCount > maxItems ? totalEventCount - maxItems : 0; + + return ( + onSlotClick({ start: day.toDate() })} + sx={{ + border: '1px solid var(--card-border)', + borderRadius: 1, + p: 0.75, + cursor: 'pointer', + bgcolor: isToday ? 'rgba(var(--accent-rgb), 0.1)' : 'transparent', + opacity: isCurrentMonth ? 1 : 0.4, + display: 'flex', + flexDirection: 'column', + minHeight: 0, + overflow: 'hidden', + '&:hover': { + bgcolor: isToday ? 'rgba(var(--accent-rgb), 0.15)' : 'rgba(0,0,0,0.05)' + } + }} + > + + {day.format('D')} + + + + {multiDaySlottedRows.map((event, slotIdx) => + renderPill(event, `multi-${slotIdx}`, (e) => { e.stopPropagation(); onEventClick(event); }) + )} + + {dayAllDaySingle.map((event, evIdx) => { + if (shownCount >= maxItems) return null; + shownCount++; + const color = event.source_color || eventColors.backgroundColor; + return ( + { e.stopPropagation(); onEventClick(event); }} + sx={{ mb: 0.25, height: pillHeight, minHeight: pillHeight, display: 'flex', alignItems: 'stretch', cursor: 'pointer' }}> + + + {event.title} + + + + ); + })} + + {dayTimed.map((event, evIdx) => { + if (shownCount >= maxItems) return null; + shownCount++; + const color = event.source_color || eventColors.backgroundColor; + return ( + { e.stopPropagation(); onEventClick(event); }} + sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.25, cursor: 'pointer', borderRadius: 0.5, px: 0.25, '&:hover': { bgcolor: 'rgba(0,0,0,0.05)' } }}> + + + {event.title} + + + ); + })} + + {remaining > 0 && ( + + +{remaining} more + + )} + + + ); +}; + +export default React.memo(MonthDayCell); diff --git a/client/src/components/PhotoWidget.jsx b/client/src/components/PhotoWidget.jsx index 5f78bdb..84ac99b 100644 --- a/client/src/components/PhotoWidget.jsx +++ b/client/src/components/PhotoWidget.jsx @@ -37,7 +37,8 @@ const PhotoWidget = ({ transparentBackground }) => { const loadPreferences = async () => { try { - const response = await axios.get(`${API_BASE_URL}/api/settings`); + // If we make it clear what we are searching for in settings, our db can be happier. + const response = await axios.post(`${API_BASE_URL}/api/settings/search`, ['PHOTO_WIDGET_*']); const settings = response.data; if (settings.PHOTO_WIDGET_PHOTOS_PER_VIEW) { @@ -258,13 +259,13 @@ const PhotoWidget = ({ transparentBackground }) => { 📷 Photos - + {isPlaying ? : } - + - + @@ -280,7 +281,7 @@ const PhotoWidget = ({ transparentBackground }) => { {!loading && !error && photos.length === 0 && ( - No photos available. Add a photo source in settings. + No photos available. Add a photo source in settings. )} diff --git a/client/src/components/PluginWidgetWrapper.jsx b/client/src/components/PluginWidgetWrapper.jsx new file mode 100644 index 0000000..088244e --- /dev/null +++ b/client/src/components/PluginWidgetWrapper.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Box } from '@mui/material'; +import { API_BASE_URL } from '../utils/apiConfig.js'; + +const PluginWidgetWrapper = ({ filename, name, theme, transparentBackground = false }) => { + return ( + +