diff --git a/docs/design/00-web-layout.JPG b/docs/design/00-web-layout.JPG new file mode 100644 index 0000000..f41a7f2 Binary files /dev/null and b/docs/design/00-web-layout.JPG differ diff --git a/docs/design/01-Login.png b/docs/design/01-Login.png new file mode 100644 index 0000000..3cdaef5 Binary files /dev/null and b/docs/design/01-Login.png differ diff --git a/docs/design/02-menu.jpeg b/docs/design/02-menu.jpeg new file mode 100644 index 0000000..1f0d969 Binary files /dev/null and b/docs/design/02-menu.jpeg differ diff --git a/docs/design/02-queue-delete.png b/docs/design/02-queue-delete.png new file mode 100644 index 0000000..a2afbc1 Binary files /dev/null and b/docs/design/02-queue-delete.png differ diff --git a/docs/design/02-queue-drag.png b/docs/design/02-queue-drag.png new file mode 100644 index 0000000..26f053d Binary files /dev/null and b/docs/design/02-queue-drag.png differ diff --git a/docs/design/02-queue-sorting.png b/docs/design/02-queue-sorting.png new file mode 100644 index 0000000..9959a8b Binary files /dev/null and b/docs/design/02-queue-sorting.png differ diff --git a/docs/design/02-queue.png b/docs/design/02-queue.png new file mode 100644 index 0000000..0aa005e Binary files /dev/null and b/docs/design/02-queue.png differ diff --git a/docs/design/03-menu current page and non-admin.png b/docs/design/03-menu current page and non-admin.png new file mode 100644 index 0000000..ed61e92 Binary files /dev/null and b/docs/design/03-menu current page and non-admin.png differ diff --git a/docs/design/03-menu playing (admin).png b/docs/design/03-menu playing (admin).png new file mode 100644 index 0000000..f91e85c Binary files /dev/null and b/docs/design/03-menu playing (admin).png differ diff --git a/docs/design/03-menu.png b/docs/design/03-menu.png new file mode 100644 index 0000000..117506a Binary files /dev/null and b/docs/design/03-menu.png differ diff --git a/docs/design/04-search typing .png b/docs/design/04-search typing .png new file mode 100644 index 0000000..07d1a83 Binary files /dev/null and b/docs/design/04-search typing .png differ diff --git a/docs/design/04-search-song info.png b/docs/design/04-search-song info.png new file mode 100644 index 0000000..3a9106f Binary files /dev/null and b/docs/design/04-search-song info.png differ diff --git a/docs/design/04-search.png b/docs/design/04-search.png new file mode 100644 index 0000000..72f948d Binary files /dev/null and b/docs/design/04-search.png differ diff --git a/docs/design/05-singers add.png b/docs/design/05-singers add.png new file mode 100644 index 0000000..a5a2f2b Binary files /dev/null and b/docs/design/05-singers add.png differ diff --git a/docs/design/05-singers.png b/docs/design/05-singers.png new file mode 100644 index 0000000..ee7ba5c Binary files /dev/null and b/docs/design/05-singers.png differ diff --git a/docs/design/06-artists (not admin).png b/docs/design/06-artists (not admin).png new file mode 100644 index 0000000..0ed7da2 Binary files /dev/null and b/docs/design/06-artists (not admin).png differ diff --git a/docs/design/06-artists .png b/docs/design/06-artists .png new file mode 100644 index 0000000..bdeb6c8 Binary files /dev/null and b/docs/design/06-artists .png differ diff --git a/docs/design/06-artists search.png b/docs/design/06-artists search.png new file mode 100644 index 0000000..eb7ca36 Binary files /dev/null and b/docs/design/06-artists search.png differ diff --git a/docs/design/06-artists songs.png b/docs/design/06-artists songs.png new file mode 100644 index 0000000..359d229 Binary files /dev/null and b/docs/design/06-artists songs.png differ diff --git a/docs/design/07-favorites.png b/docs/design/07-favorites.png new file mode 100644 index 0000000..66fe157 Binary files /dev/null and b/docs/design/07-favorites.png differ diff --git a/docs/design/08-history.png b/docs/design/08-history.png new file mode 100644 index 0000000..d263d0f Binary files /dev/null and b/docs/design/08-history.png differ diff --git a/docs/design/09- song lists songs expand.png b/docs/design/09- song lists songs expand.png new file mode 100644 index 0000000..89a9b86 Binary files /dev/null and b/docs/design/09- song lists songs expand.png differ diff --git a/docs/design/09-song lists - songs.png b/docs/design/09-song lists - songs.png new file mode 100644 index 0000000..7956b7d Binary files /dev/null and b/docs/design/09-song lists - songs.png differ diff --git a/docs/design/09-songs list.png b/docs/design/09-songs list.png new file mode 100644 index 0000000..ccd18b8 Binary files /dev/null and b/docs/design/09-songs list.png differ diff --git a/docs/design/10-Settings.png b/docs/design/10-Settings.png new file mode 100644 index 0000000..aec46c4 Binary files /dev/null and b/docs/design/10-Settings.png differ diff --git a/docs/design/11-top 100 songs.png b/docs/design/11-top 100 songs.png new file mode 100644 index 0000000..b781462 Binary files /dev/null and b/docs/design/11-top 100 songs.png differ diff --git a/docs/design/12-favorite .png b/docs/design/12-favorite .png new file mode 100644 index 0000000..3680214 Binary files /dev/null and b/docs/design/12-favorite .png differ diff --git a/docs/design/12-favorite lists.png b/docs/design/12-favorite lists.png new file mode 100644 index 0000000..8623c4f Binary files /dev/null and b/docs/design/12-favorite lists.png differ diff --git a/env.template b/env.template new file mode 100644 index 0000000..9a12539 --- /dev/null +++ b/env.template @@ -0,0 +1,12 @@ +# Firebase Configuration +VITE_FIREBASE_API_KEY=your-api-key +VITE_FIREBASE_AUTH_DOMAIN=your-project-id.firebaseapp.com +VITE_FIREBASE_DATABASE_URL=https://your-project-id-default-rtdb.firebaseio.com +VITE_FIREBASE_PROJECT_ID=your-project-id +VITE_FIREBASE_STORAGE_BUCKET=your-project-id.appspot.com +VITE_FIREBASE_MESSAGING_SENDER_ID=123456789 +VITE_FIREBASE_APP_ID=your-app-id + +# App Configuration +VITE_CONTROLLER_NAME=default +VITE_APP_TITLE=SingSalot AI \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7511c93..909dcdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "version": "0.0.0", "dependencies": { "@reduxjs/toolkit": "^2.8.2", + "@tailwindcss/postcss": "^4.1.11", "@types/react-router-dom": "^5.3.3", "firebase": "^11.10.0", "react": "^19.1.0", @@ -29,13 +30,36 @@ "tailwindcss": "^4.1.11", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", - "vite": "^7.0.4" + "vite": "^5.4.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -45,13 +69,13 @@ "aix" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -61,13 +85,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -77,13 +101,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -93,13 +117,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -109,13 +133,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -125,13 +149,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -141,13 +165,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -157,13 +181,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -173,13 +197,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -189,13 +213,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -205,13 +229,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -221,13 +245,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -237,13 +261,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -253,13 +277,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -269,13 +293,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -285,13 +309,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -301,29 +325,13 @@ "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -333,29 +341,13 @@ "netbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -365,29 +357,13 @@ "openbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -397,13 +373,13 @@ "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -413,13 +389,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -429,13 +405,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -445,7 +421,7 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1245,6 +1221,48 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@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==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1848,6 +1866,252 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "postcss": "^8.4.41", + "tailwindcss": "4.1.11" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2379,6 +2643,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2464,6 +2736,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.186", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.186.tgz", @@ -2475,45 +2755,54 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -2923,6 +3212,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3035,6 +3329,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3087,6 +3389,223 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3118,6 +3637,14 @@ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3152,6 +3679,39 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3162,7 +3722,6 @@ "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", @@ -3277,8 +3836,7 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3296,7 +3854,6 @@ "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", @@ -3637,7 +4194,6 @@ "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, "engines": { "node": ">=0.10.0" } @@ -3693,49 +4249,30 @@ "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "dev": true + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, "node_modules/to-regex-range": { @@ -3868,23 +4405,20 @@ } }, "node_modules/vite": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.6", - "picomatch": "^4.0.2", - "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3893,25 +4427,19 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", "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" + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "jiti": { - "optional": true - }, "less": { "optional": true }, @@ -3932,41 +4460,9 @@ }, "terser": { "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/web-vitals": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", @@ -4041,6 +4537,14 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -4080,185 +4584,178 @@ } }, "dependencies": { + "@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" + }, + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", - "dev": true, - "optional": true - }, - "@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "dev": true, "optional": true }, @@ -4839,6 +5336,42 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "requires": { + "minipass": "^7.0.4" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@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==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5193,6 +5726,133 @@ "@swc/counter": "^0.1.3" } }, + "@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "requires": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "requires": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11", + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + } + }, + "@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "optional": true + }, + "@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "optional": true + }, + "@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "optional": true + }, + "@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "optional": true + }, + "@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "optional": true + }, + "@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "optional": true + }, + "@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "optional": true + }, + "@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "optional": true, + "requires": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + } + }, + "@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "optional": true + }, + "@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "optional": true + }, + "@tailwindcss/postcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "requires": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "postcss": "^8.4.41", + "tailwindcss": "4.1.11" + } + }, "@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5529,6 +6189,11 @@ "supports-color": "^7.1.0" } }, + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -5594,6 +6259,11 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==" + }, "electron-to-chromium": { "version": "1.5.186", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.186.tgz", @@ -5605,38 +6275,44 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, "esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "requires": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "escalade": { @@ -5935,6 +6611,11 @@ "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -6016,6 +6697,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==" + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -6062,6 +6748,84 @@ "type-check": "~0.4.0" } }, + "lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "requires": { + "detect-libc": "^2.0.3", + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "optional": true + }, + "lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "optional": true + }, + "lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "optional": true + }, + "lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "optional": true + }, + "lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "optional": true + }, + "lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "optional": true + }, + "lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "optional": true + }, + "lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "optional": true + }, + "lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "optional": true + }, + "lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "optional": true + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6087,6 +6851,14 @@ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, + "magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6112,6 +6884,24 @@ "brace-expansion": "^1.1.7" } }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "requires": { + "minipass": "^7.1.2" + } + }, + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6121,8 +6911,7 @@ "nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, "natural-compare": { "version": "1.4.0", @@ -6198,8 +6987,7 @@ "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "picomatch": { "version": "2.3.1", @@ -6211,7 +6999,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "requires": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6411,8 +7198,7 @@ "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 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, "string-width": { "version": "4.2.3", @@ -6450,32 +7236,24 @@ "tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "dev": true + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" }, - "tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, + "tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==" + }, + "tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "requires": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "dependencies": { - "fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "requires": {} - }, - "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true - } + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" } }, "to-regex-range": { @@ -6557,33 +7335,15 @@ "requires": {} }, "vite": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "requires": { - "esbuild": "^0.25.0", - "fdir": "^6.4.6", + "esbuild": "^0.21.3", "fsevents": "~2.3.3", - "picomatch": "^4.0.2", - "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" - }, - "dependencies": { - "fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "requires": {} - }, - "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true - } + "postcss": "^8.4.43", + "rollup": "^4.20.0" } }, "web-vitals": { @@ -6636,6 +7396,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" + }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 4459322..1cf3e20 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@reduxjs/toolkit": "^2.8.2", + "@tailwindcss/postcss": "^4.1.11", "@types/react-router-dom": "^5.3.3", "firebase": "^11.10.0", "react": "^19.1.0", @@ -32,6 +33,6 @@ "tailwindcss": "^4.1.11", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", - "vite": "^7.0.4" + "vite": "^5.4.0" } } diff --git a/postcss.config.js b/postcss.config.js index 387612e..5132039 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { - tailwindcss: {}, + '@tailwindcss/postcss': {}, autoprefixer: {}, }, } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index d85a2a8..1402c5b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,38 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import Layout from './components/Layout/Layout'; import Navigation from './components/Navigation/Navigation'; -import { Search, Queue, History, TopPlayed } from './features'; +import { Search, Queue, History, Favorites, NewSongs, Artists, Singers, SongLists } from './features'; +import TopPlayed from './features/TopPlayed/Top100'; +import { FirebaseProvider } from './firebase/FirebaseProvider'; +import { ErrorBoundary } from './components/common'; +import { AuthInitializer } from './components/Auth'; function App() { return ( - - - - - } /> - } /> - } /> - } /> - } /> - - - + + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + ); } diff --git a/src/components/Auth/AuthInitializer.tsx b/src/components/Auth/AuthInitializer.tsx new file mode 100644 index 0000000..0b53a7d --- /dev/null +++ b/src/components/Auth/AuthInitializer.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import { setAuth } from '../../redux/authSlice'; +import { selectIsAuthenticated } from '../../redux/authSlice'; +import { CONTROLLER_NAME } from '../../constants'; +import { LoginPrompt } from './index'; +import type { Authentication } from '../../types'; + +interface AuthInitializerProps { + children: React.ReactNode; +} + +const AuthInitializer: React.FC = ({ children }) => { + const [searchParams] = useSearchParams(); + const [showLogin, setShowLogin] = useState(false); + const dispatch = useAppDispatch(); + const isAuthenticated = useAppSelector(selectIsAuthenticated); + + useEffect(() => { + // Check for admin parameter in URL + const isAdmin = searchParams.get('admin') === 'true'; + + // If admin parameter is present, auto-authenticate + if (isAdmin) { + const auth: Authentication = { + authenticated: true, + singer: 'Admin', + isAdmin: true, + controller: CONTROLLER_NAME, + }; + dispatch(setAuth(auth)); + + // Clean up URL + if (window.history.replaceState) { + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete('admin'); + window.history.replaceState({}, '', newUrl.toString()); + } + } else if (!isAuthenticated) { + // Show login prompt for regular users + setShowLogin(true); + } + }, [searchParams, dispatch, isAuthenticated]); + + // Show login prompt if not authenticated + if (showLogin && !isAuthenticated) { + return ( + setShowLogin(false)} + /> + ); + } + + return <>{children}; +}; + +export default AuthInitializer; \ No newline at end of file diff --git a/src/components/Auth/LoginPrompt.tsx b/src/components/Auth/LoginPrompt.tsx new file mode 100644 index 0000000..0081703 --- /dev/null +++ b/src/components/Auth/LoginPrompt.tsx @@ -0,0 +1,86 @@ +import { useState } from 'react'; +import { useAppDispatch } from '../../redux/hooks'; +import { setAuth } from '../../redux/authSlice'; +import type { Authentication } from '../../types'; + +interface LoginPromptProps { + isAdmin: boolean; + onComplete: () => void; +} + +const LoginPrompt: React.FC = ({ isAdmin, onComplete }) => { + const [singerName, setSingerName] = useState(isAdmin ? 'Admin' : ''); + const [partyId, setPartyId] = useState(''); + const [error, setError] = useState(''); + const dispatch = useAppDispatch(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!partyId.trim() || !singerName.trim()) { + setError('Please enter both Party Id and your name.'); + return; + } + setError(''); + const auth: Authentication = { + authenticated: true, + singer: singerName.trim(), + isAdmin: isAdmin, + controller: partyId.trim(), + }; + dispatch(setAuth(auth)); + onComplete(); + }; + + return ( +
+
+
+

+ Welcome to Karaoke! 🎤 +

+

+ {isAdmin ? 'You have admin privileges' : 'Enter your Party Id and name to get started'} +

+
+
+
+ + setPartyId(e.target.value)} + placeholder="Enter your Party Id" + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + autoFocus + /> +
+
+ + setSingerName(e.target.value)} + placeholder={isAdmin ? 'Admin' : 'Enter your name'} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ {error &&
{error}
} + +
+
+
+ ); +}; + +export default LoginPrompt; \ No newline at end of file diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts new file mode 100644 index 0000000..58307d3 --- /dev/null +++ b/src/components/Auth/index.ts @@ -0,0 +1,2 @@ +export { default as AuthInitializer } from './AuthInitializer'; +export { default as LoginPrompt } from './LoginPrompt'; \ No newline at end of file diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 554a4a0..3bbbd22 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,13 +1,21 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import type { RootState } from '../../redux/store'; +import { useSelector, useDispatch } from 'react-redux'; +import { selectCurrentSinger, selectIsAdmin, selectControllerName } from '../../redux/authSlice'; +import { logout } from '../../redux/authSlice'; +import { ActionButton } from '../common'; import type { LayoutProps } from '../../types'; const Layout: React.FC = ({ children }) => { - // TODO: Replace with actual Redux selectors - const currentSinger = useSelector((state: RootState) => state.auth?.singer || ''); - const isAdmin = useSelector((state: RootState) => state.auth?.isAdmin || false); - const controllerName = useSelector((state: RootState) => state.auth?.controller || ''); + const currentSinger = useSelector(selectCurrentSinger); + const isAdmin = useSelector(selectIsAdmin); + const controllerName = useSelector(selectControllerName); + const dispatch = useDispatch(); + + const handleLogout = () => { + dispatch(logout()); + // Reload the page to return to login screen + window.location.reload(); + }; return (
@@ -22,21 +30,30 @@ const Layout: React.FC = ({ children }) => { {controllerName && ( - Controller: {controllerName} + Party: {controllerName} )}
- {/* User Info */} + {/* User Info & Logout */}
{currentSinger && ( -
- {currentSinger} - {isAdmin && ( - - Admin - - )} +
+
+ {currentSinger} + {isAdmin && ( + + Admin + + )} +
+ + Logout +
)}
diff --git a/src/components/Navigation/Navigation.tsx b/src/components/Navigation/Navigation.tsx index 390ac88..5581b2c 100644 --- a/src/components/Navigation/Navigation.tsx +++ b/src/components/Navigation/Navigation.tsx @@ -3,10 +3,15 @@ import { NavLink } from 'react-router-dom'; const Navigation: React.FC = () => { const navItems = [ - { path: '/', label: 'Search', icon: '🔍' }, { path: '/queue', label: 'Queue', icon: '📋' }, + { path: '/search', label: 'Search', icon: '🔍' }, + { path: '/favorites', label: 'Favorites', icon: '❤️' }, + { path: '/new-songs', label: 'New Songs', icon: '🆕' }, + { path: '/artists', label: 'Artists', icon: '🎤' }, + { path: '/song-lists', label: 'Song Lists', icon: '📝' }, { path: '/history', label: 'History', icon: '⏰' }, - { path: '/top-played', label: 'Top Played', icon: '🏆' }, + { path: '/top-played', label: 'Top 100', icon: '🏆' }, + { path: '/singers', label: 'Singers', icon: '👥' }, ]; return ( diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..8350ac1 --- /dev/null +++ b/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,60 @@ +import React, { Component, type ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+
+
⚠️
+

+ Something went wrong +

+

+ We're sorry, but something unexpected happened. Please try refreshing the page. +

+ +
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/src/components/common/InfiniteScrollList.tsx b/src/components/common/InfiniteScrollList.tsx new file mode 100644 index 0000000..c47059a --- /dev/null +++ b/src/components/common/InfiniteScrollList.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useRef } from 'react'; +import { SongItem, EmptyState } from './index'; +import type { Song } from '../../types'; + +interface InfiniteScrollListProps { + items: Song[]; + isLoading: boolean; + hasMore: boolean; + onLoadMore: () => void; + onAddToQueue: (song: Song) => void; + onToggleFavorite: (song: Song) => void; + onRemoveFromQueue?: (song: Song) => void; + context: 'search' | 'queue' | 'history' | 'topPlayed' | 'favorites'; + title: string; + subtitle?: string; + emptyTitle: string; + emptyMessage: string; + loadingTitle?: string; + loadingMessage?: string; + isAdmin?: boolean; + renderExtraContent?: (item: Song, index: number) => React.ReactNode; + debugInfo?: string; +} + +const InfiniteScrollList: React.FC = ({ + items, + isLoading, + hasMore, + onLoadMore, + onAddToQueue, + onToggleFavorite, + onRemoveFromQueue, + context, + title, + subtitle, + emptyTitle, + emptyMessage, + loadingTitle = "Loading...", + loadingMessage = "Please wait while data is being loaded", + isAdmin = false, + renderExtraContent, + debugInfo, +}) => { + const observerRef = useRef(null); + + // Intersection Observer for infinite scrolling + useEffect(() => { + console.log('InfiniteScrollList - Setting up observer:', { hasMore, isLoading, itemsLength: items.length }); + + const observer = new IntersectionObserver( + (entries) => { + console.log('InfiniteScrollList - Intersection detected:', { + isIntersecting: entries[0].isIntersecting, + hasMore, + isLoading + }); + + if (entries[0].isIntersecting && hasMore && !isLoading) { + console.log('InfiniteScrollList - Loading more items'); + onLoadMore(); + } + }, + { threshold: 0.1 } + ); + + if (observerRef.current) { + observer.observe(observerRef.current); + } + + return () => observer.disconnect(); + }, [onLoadMore, hasMore, isLoading, items.length]); + + return ( +
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} + + {/* Debug info */} + {debugInfo && ( +
+ {debugInfo} +
+ )} +
+ + {/* List */} +
+ {items.length === 0 && !isLoading ? ( + + + + } + /> + ) : isLoading && items.length === 0 ? ( + + + + } + /> + ) : ( +
+ {items.map((item, index) => ( +
+ {/* Song Info */} +
+ onAddToQueue(item)} + onToggleFavorite={() => onToggleFavorite(item)} + onRemoveFromQueue={onRemoveFromQueue ? () => onRemoveFromQueue(item) : undefined} + isAdmin={isAdmin} + /> +
+ + {/* Extra Content */} + {renderExtraContent && renderExtraContent(item, index)} +
+ ))} + + {/* Infinite scroll trigger */} + {hasMore && ( +
+
+ + + + + Loading more items... +
+
+ )} +
+ )} +
+ + {/* Stats */} + {items.length > 0 && ( +
+ Showing {items.length} item{items.length !== 1 ? 's' : ''} + {hasMore && ` • Scroll down to load more`} +
+ )} +
+ ); +}; + +export default InfiniteScrollList; \ No newline at end of file diff --git a/src/components/common/SongItem.tsx b/src/components/common/SongItem.tsx index 4d2b0fc..4764db4 100644 --- a/src/components/common/SongItem.tsx +++ b/src/components/common/SongItem.tsx @@ -96,6 +96,26 @@ const SongItem: React.FC = ({
); + case 'topPlayed': + return ( +
+ {})} + variant="primary" + size="sm" + > + Add to Queue + + {})} + variant={song.favorite ? 'danger' : 'secondary'} + size="sm" + > + {song.favorite ? '❤️' : '🤍'} + +
+ ); + default: return null; } diff --git a/src/components/common/index.ts b/src/components/common/index.ts index f19f7e1..89f35e4 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,4 +1,6 @@ export { default as EmptyState } from './EmptyState'; export { default as Toast } from './Toast'; export { default as ActionButton } from './ActionButton'; -export { default as SongItem } from './SongItem'; \ No newline at end of file +export { default as InfiniteScrollList } from './InfiniteScrollList'; +export { default as SongItem } from './SongItem'; +export { default as ErrorBoundary } from './ErrorBoundary'; \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index ff86ee3..3f011e3 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,6 +1,7 @@ // App constants export const APP_NAME = '🎤 Karaoke App'; export const APP_VERSION = '1.0.0'; +export const CONTROLLER_NAME = import.meta.env.VITE_CONTROLLER_NAME || 'default'; // Firebase configuration export const FIREBASE_CONFIG = { diff --git a/src/features/Artists/Artists.tsx b/src/features/Artists/Artists.tsx new file mode 100644 index 0000000..9e2441f --- /dev/null +++ b/src/features/Artists/Artists.tsx @@ -0,0 +1,157 @@ +import React, { useState } from 'react'; +import { InfiniteScrollList, ActionButton } from '../../components/common'; +import { useArtists } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectSongs } from '../../redux'; + + +const Artists: React.FC = () => { + const { + artists, + searchTerm, + handleSearchChange, + getSongsByArtist, + handleAddToQueue, + handleToggleFavorite, + } = useArtists(); + + const songs = useAppSelector(selectSongs); + const songsCount = Object.keys(songs).length; + const [selectedArtist, setSelectedArtist] = useState(null); + + // Debug logging + console.log('Artists component - artists count:', artists.length); + console.log('Artists component - selected artist:', selectedArtist); + + const handleArtistClick = (artist: string) => { + setSelectedArtist(artist); + }; + + const handleCloseArtistSongs = () => { + setSelectedArtist(null); + }; + + const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : []; + + return ( +
+
+

Artists

+ + {/* Search Input */} +
+ handleSearchChange(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ + + +
+
+ + {/* Debug info */} +
+ Total songs loaded: {songsCount} | Showing: {artists.length} artists | Search term: "{searchTerm}" +
+
+ + {/* Artists List */} +
+ {songsCount === 0 ? ( +
+
+ + + +
+

Loading artists...

+

Please wait while songs are being loaded from the database

+
+ ) : artists.length === 0 ? ( +
+
+ + + +
+

+ {searchTerm ? "No artists found" : "No artists available"} +

+

+ {searchTerm ? "Try adjusting your search terms" : "Artists will appear here once songs are loaded"} +

+
+ ) : ( +
+ {artists.map((artist) => ( +
+
+

+ {artist} +

+

+ {getSongsByArtist(artist).length} song{getSongsByArtist(artist).length !== 1 ? 's' : ''} +

+
+
+ handleArtistClick(artist)} + variant="primary" + size="sm" + > + View Songs + +
+
+ ))} +
+ )} +
+ + {/* Artist Songs Modal */} + {selectedArtist && ( +
+
+
+
+

+ Songs by {selectedArtist} +

+ + Close + +
+
+ +
+ {}} + onAddToQueue={handleAddToQueue} + onToggleFavorite={handleToggleFavorite} + context="search" + title="" + emptyTitle="No songs found" + emptyMessage="No songs found for this artist" + debugInfo="" + /> +
+
+
+ )} +
+ ); +}; + +export default Artists; \ No newline at end of file diff --git a/src/features/Favorites/Favorites.tsx b/src/features/Favorites/Favorites.tsx new file mode 100644 index 0000000..2257386 --- /dev/null +++ b/src/features/Favorites/Favorites.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { InfiniteScrollList } from '../../components/common'; +import { useFavorites } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectFavorites } from '../../redux'; + +const Favorites: React.FC = () => { + const { + favoritesItems, + hasMore, + loadMore, + handleAddToQueue, + handleToggleFavorite, + } = useFavorites(); + + const favorites = useAppSelector(selectFavorites); + const favoritesCount = Object.keys(favorites).length; + + // Debug logging + console.log('Favorites component - favorites count:', favoritesCount); + console.log('Favorites component - favorites items:', favoritesItems); + + return ( + + ); +}; + +export default Favorites; \ No newline at end of file diff --git a/src/features/History/History.tsx b/src/features/History/History.tsx index 8d0ef34..499e143 100644 --- a/src/features/History/History.tsx +++ b/src/features/History/History.tsx @@ -1,62 +1,57 @@ import React from 'react'; -import { SongItem, EmptyState } from '../../components/common'; +import { InfiniteScrollList } from '../../components/common'; import { useHistory } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectHistory } from '../../redux'; import { formatDate } from '../../utils/dataProcessing'; +import type { Song } from '../../types'; const History: React.FC = () => { const { historyItems, + hasMore, + loadMore, handleAddToQueue, handleToggleFavorite, } = useHistory(); + const history = useAppSelector(selectHistory); + const historyCount = Object.keys(history).length; + + // Debug logging + console.log('History component - history count:', historyCount); + console.log('History component - history items:', historyItems); + + // Render extra content for history items (play date) + const renderExtraContent = (item: Song) => { + if (item.date) { + return ( +
+ {formatDate(item.date)} +
+ ); + } + return null; + }; + return ( -
-
-

Recently Played

-

- {historyItems.length} song{historyItems.length !== 1 ? 's' : ''} in history -

-
- - {/* History List */} -
- {historyItems.length === 0 ? ( - - - - } - /> - ) : ( -
- {historyItems.map((song) => ( -
- {/* Song Info */} -
- handleAddToQueue(song)} - onToggleFavorite={() => handleToggleFavorite(song)} - /> -
- - {/* Play Date */} - {song.date && ( -
- {formatDate(song.date)} -
- )} -
- ))} -
- )} -
-
+ ); }; diff --git a/src/features/NewSongs/NewSongs.tsx b/src/features/NewSongs/NewSongs.tsx new file mode 100644 index 0000000..dbb860a --- /dev/null +++ b/src/features/NewSongs/NewSongs.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { InfiniteScrollList } from '../../components/common'; +import { useNewSongs } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectNewSongs } from '../../redux'; + +const NewSongs: React.FC = () => { + const { + newSongsItems, + hasMore, + loadMore, + handleAddToQueue, + handleToggleFavorite, + } = useNewSongs(); + + const newSongs = useAppSelector(selectNewSongs); + const newSongsCount = Object.keys(newSongs).length; + + // Debug logging + console.log('NewSongs component - newSongs count:', newSongsCount); + console.log('NewSongs component - newSongs items:', newSongsItems); + + return ( + + ); +}; + +export default NewSongs; \ No newline at end of file diff --git a/src/features/Queue/Queue.tsx b/src/features/Queue/Queue.tsx index a30afb9..5fc36ac 100644 --- a/src/features/Queue/Queue.tsx +++ b/src/features/Queue/Queue.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { SongItem, EmptyState, ActionButton } from '../../components/common'; import { useQueue } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectQueue } from '../../redux'; const Queue: React.FC = () => { const { @@ -13,6 +15,13 @@ const Queue: React.FC = () => { handleMoveDown, } = useQueue(); + const queue = useAppSelector(selectQueue); + const queueCount = Object.keys(queue).length; + + // Debug logging + console.log('Queue component - queue count:', queueCount); + console.log('Queue component - queue items:', queueItems); + return (
@@ -20,11 +29,16 @@ const Queue: React.FC = () => {

{queueStats.totalSongs} song{queueStats.totalSongs !== 1 ? 's' : ''} in queue

+ + {/* Debug info */} +
+ Queue items loaded: {queueCount} +
{/* Queue List */}
- {queueItems.length === 0 ? ( + {queueCount === 0 ? ( { } /> + ) : queueItems.length === 0 ? ( + + + + } + /> ) : (
{queueItems.map((queueItem, index) => ( diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index f6cdb65..2bdf7d8 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useAppSelector } from '../../redux'; -import { SongItem, EmptyState } from '../../components/common'; +import { InfiniteScrollList } from '../../components/common'; import { useSearch } from '../../hooks'; -import { selectIsAdmin } from '../../redux'; +import { useAppSelector } from '../../redux'; +import { selectIsAdmin, selectSongs } from '../../redux'; const Search: React.FC = () => { const { @@ -11,9 +11,27 @@ const Search: React.FC = () => { handleSearchChange, handleAddToQueue, handleToggleFavorite, + loadMore, } = useSearch(); const isAdmin = useAppSelector(selectIsAdmin); + const songs = useAppSelector(selectSongs); + const songsCount = Object.keys(songs).length; + + // Performance monitoring + React.useEffect(() => { + const startTime = performance.now(); + + return () => { + const endTime = performance.now(); + const renderTime = endTime - startTime; + console.log(`Search component render time: ${renderTime.toFixed(2)}ms`); + }; + }); + + // Debug logging + console.log('Search component - songs count:', songsCount); + console.log('Search component - search results:', searchResults); return (
@@ -35,40 +53,36 @@ const Search: React.FC = () => {
+ + {/* Debug info */} +
+ Total songs loaded: {songsCount} | Showing: {searchResults.songs.length} of {searchResults.count} | Page: {searchResults.currentPage}/{searchResults.totalPages} | Search term: "{searchTerm}" +
{/* Search Results */} -
- {searchResults.songs.length === 0 ? ( - - - - } - /> - ) : ( -
- {searchResults.songs.map((song) => ( - handleAddToQueue(song)} - onToggleFavorite={() => handleToggleFavorite(song)} - isAdmin={isAdmin} - /> - ))} -
- )} -
+ {/* Search Stats */} {searchTerm && (
- Found {searchResults.count} song{searchResults.count !== 1 ? 's' : ''} + Found {searchResults.count} song{searchResults.count !== 1 ? 's' : ''} + {searchResults.hasMore && ` • Scroll down to load more`}
)}
diff --git a/src/features/Singers/Singers.tsx b/src/features/Singers/Singers.tsx new file mode 100644 index 0000000..08bd6df --- /dev/null +++ b/src/features/Singers/Singers.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { ActionButton, EmptyState } from '../../components/common'; +import { useSingers } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectSingers } from '../../redux'; +import { formatDate } from '../../utils/dataProcessing'; + +const Singers: React.FC = () => { + const { + singers, + isAdmin, + handleRemoveSinger, + } = useSingers(); + + const singersData = useAppSelector(selectSingers); + const singersCount = Object.keys(singersData).length; + + // Debug logging + console.log('Singers component - singers count:', singersCount); + console.log('Singers component - singers:', singers); + + return ( +
+
+

Singers

+

+ {singers.length} singer{singers.length !== 1 ? 's' : ''} in the party +

+ + {/* Debug info */} +
+ Singers loaded: {singersCount} +
+
+ + {/* Singers List */} +
+ {singersCount === 0 ? ( + + + + } + /> + ) : singers.length === 0 ? ( + + + + } + /> + ) : ( +
+ {singers.map((singer) => ( +
+ {/* Singer Info */} +
+

+ {singer.name} +

+

+ Last login: {formatDate(singer.lastLogin)} +

+
+ + {/* Admin Controls */} + {isAdmin && ( +
+ handleRemoveSinger(singer)} + variant="danger" + size="sm" + > + Remove + +
+ )} +
+ ))} +
+ )} +
+
+ ); +}; + +export default Singers; \ No newline at end of file diff --git a/src/features/SongLists/SongLists.tsx b/src/features/SongLists/SongLists.tsx new file mode 100644 index 0000000..8ce38a0 --- /dev/null +++ b/src/features/SongLists/SongLists.tsx @@ -0,0 +1,180 @@ +import React, { useState } from 'react'; +import { ActionButton, SongItem, InfiniteScrollList } from '../../components/common'; +import { useSongLists } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectSongList } from '../../redux'; +import type { SongListSong, Song } from '../../types'; + +const SongLists: React.FC = () => { + const { + songLists, + hasMore, + loadMore, + checkSongAvailability, + handleAddToQueue, + handleToggleFavorite, + } = useSongLists(); + + const songListData = useAppSelector(selectSongList); + const songListCount = Object.keys(songListData).length; + const [selectedSongList, setSelectedSongList] = useState(null); + const [expandedSongs, setExpandedSongs] = useState>(new Set()); + + // Debug logging + console.log('SongLists component - songList count:', songListCount); + console.log('SongLists component - songLists:', songLists); + + const handleSongListClick = (songListKey: string) => { + setSelectedSongList(songListKey); + }; + + const handleCloseSongList = () => { + setSelectedSongList(null); + }; + + const handleToggleExpanded = (songKey: string) => { + const newExpanded = new Set(expandedSongs); + if (newExpanded.has(songKey)) { + newExpanded.delete(songKey); + } else { + newExpanded.add(songKey); + } + setExpandedSongs(newExpanded); + }; + + const selectedList = selectedSongList ? songLists.find(list => list.key === selectedSongList) : null; + + return ( +
+
+

Song Lists

+

+ {songLists.length} song list{songLists.length !== 1 ? 's' : ''} available +

+ + {/* Debug info */} +
+ Song lists loaded: {songListCount} +
+
+ + {/* Song Lists */} + ({ + ...songList, + title: songList.title, + artist: `${songList.songs.length} song${songList.songs.length !== 1 ? 's' : ''}`, + path: '', + disabled: false, + favorite: false, + }))} + isLoading={songListCount === 0} + hasMore={hasMore} + onLoadMore={loadMore} + onAddToQueue={() => {}} // Not applicable for song lists + onToggleFavorite={() => {}} // Not applicable for song lists + context="search" + title="Song Lists" + subtitle={`${songLists.length} song list${songLists.length !== 1 ? 's' : ''} available`} + emptyTitle="No song lists available" + emptyMessage="Song lists will appear here when they're available" + loadingTitle="Loading song lists..." + loadingMessage="Please wait while song lists are being loaded" + debugInfo={`Song lists loaded: ${songListCount}`} + renderExtraContent={(item) => ( +
+ handleSongListClick(item.key!)} + variant="primary" + size="sm" + > + View Songs + +
+ )} + /> + + {/* Song List Modal */} + {selectedList && ( +
+
+
+
+

+ {selectedList.title} +

+ + Close + +
+
+ +
+
+ {selectedList.songs.map((songListSong: SongListSong) => { + const availableSongs = checkSongAvailability(songListSong); + const isExpanded = expandedSongs.has(songListSong.key!); + const isAvailable = availableSongs.length > 0; + + return ( +
+ {/* Song List Song Row */} +
+
+

+ {songListSong.title} +

+

+ {songListSong.artist} • Position {songListSong.position} +

+ {!isAvailable && ( +

+ Not available in catalog +

+ )} +
+
+ {isAvailable && ( + handleToggleExpanded(songListSong.key!)} + variant="secondary" + size="sm" + > + {isExpanded ? 'Hide' : 'Show'} ({availableSongs.length}) + + )} +
+
+ + {/* Available Songs (when expanded) */} + {isExpanded && isAvailable && ( +
+ {availableSongs.map((song: Song) => ( +
+ handleAddToQueue(song)} + onToggleFavorite={() => handleToggleFavorite(song)} + /> +
+ ))} +
+ )} +
+ ); + })} +
+
+
+
+ )} +
+ ); +}; + +export default SongLists; \ No newline at end of file diff --git a/src/features/TopPlayed/Top100.tsx b/src/features/TopPlayed/Top100.tsx new file mode 100644 index 0000000..16907dd --- /dev/null +++ b/src/features/TopPlayed/Top100.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { InfiniteScrollList } from '../../components/common'; +import { useTopPlayed } from '../../hooks'; +import { useAppSelector } from '../../redux'; +import { selectTopPlayed } from '../../redux'; +import type { TopPlayed, Song } from '../../types'; + +const Top100: React.FC = () => { + const { + topPlayedItems, + hasMore, + loadMore, + handleAddToQueue, + handleToggleFavorite, + } = useTopPlayed(); + + const topPlayed = useAppSelector(selectTopPlayed); + const topPlayedCount = Object.keys(topPlayed).length; + + // Debug logging + console.log('TopPlayed component - topPlayed count:', topPlayedCount); + console.log('TopPlayed component - topPlayed items:', topPlayedItems); + + // Convert TopPlayed items to Song format for the InfiniteScrollList + const songItems = topPlayedItems.map((item: TopPlayed) => ({ + ...item, + path: '', // TopPlayed doesn't have path + disabled: false, + favorite: false, + })); + + // Render extra content for top played items (rank and play count) + const renderExtraContent = (item: Song, index: number) => { + return ( + <> + {/* Rank */} +
+
2 ? 'bg-gray-50 text-gray-600' : ''} + `}> + {index + 1} +
+
+ + {/* Play Count */} +
+
{item.count}
+
+ play{item.count !== 1 ? 's' : ''} +
+
+ + ); + }; + + // Wrapper functions to handle type conversion + const handleAddToQueueWrapper = (song: Song) => { + const topPlayedItem = topPlayedItems.find(item => item.key === song.key); + if (topPlayedItem) { + handleAddToQueue(topPlayedItem); + } + }; + + const handleToggleFavoriteWrapper = (song: Song) => { + const topPlayedItem = topPlayedItems.find(item => item.key === song.key); + if (topPlayedItem) { + handleToggleFavorite(topPlayedItem); + } + }; + + return ( + + ); +}; + +export default Top100; \ No newline at end of file diff --git a/src/features/TopPlayed/TopPlayed.tsx b/src/features/TopPlayed/TopPlayed.tsx deleted file mode 100644 index 57242cf..0000000 --- a/src/features/TopPlayed/TopPlayed.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import { SongItem, EmptyState } from '../../components/common'; -import { useTopPlayed } from '../../hooks'; - -const TopPlayed: React.FC = () => { - const { - topPlayedItems, - handleAddToQueue, - handleToggleFavorite, - } = useTopPlayed(); - - return ( -
-
-

Most Played

-

- Top {topPlayedItems.length} song{topPlayedItems.length !== 1 ? 's' : ''} by play count -

-
- - {/* Top Played List */} -
- {topPlayedItems.length === 0 ? ( - - - - } - /> - ) : ( -
- {topPlayedItems.map((song, index) => ( -
- {/* Rank */} -
-
2 ? 'bg-gray-50 text-gray-600' : ''} - `}> - {index + 1} -
-
- - {/* Song Info */} -
- handleAddToQueue(song)} - onToggleFavorite={() => handleToggleFavorite(song)} - /> -
- - {/* Play Count */} -
-
{song.count}
-
- play{song.count !== 1 ? 's' : ''} -
-
-
- ))} -
- )} -
-
- ); -}; - -export default TopPlayed; \ No newline at end of file diff --git a/src/features/index.ts b/src/features/index.ts index 783334d..202614f 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -1,4 +1,9 @@ export { default as Search } from './Search/Search'; export { default as Queue } from './Queue/Queue'; export { default as History } from './History/History'; -export { default as TopPlayed } from './TopPlayed/TopPlayed'; \ No newline at end of file +export { default as TopPlayed } from './TopPlayed/Top100'; +export { default as Favorites } from './Favorites/Favorites'; +export { default as NewSongs } from './NewSongs/NewSongs'; +export { default as Artists } from './Artists/Artists'; +export { default as Singers } from './Singers/Singers'; +export { default as SongLists } from './SongLists/SongLists'; \ No newline at end of file diff --git a/src/firebase/FirebaseContext.tsx b/src/firebase/FirebaseContext.tsx new file mode 100644 index 0000000..b73bef9 --- /dev/null +++ b/src/firebase/FirebaseContext.tsx @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +interface FirebaseContextType { + isConnected: boolean; + syncStatus: 'idle' | 'loading' | 'success' | 'error'; +} + +export const FirebaseContext = createContext(undefined); \ No newline at end of file diff --git a/src/firebase/FirebaseProvider.tsx b/src/firebase/FirebaseProvider.tsx new file mode 100644 index 0000000..9d36c35 --- /dev/null +++ b/src/firebase/FirebaseProvider.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState, type ReactNode } from 'react'; +import { database } from './config'; +import { ref, onValue, off } from 'firebase/database'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { setController } from '../redux/controllerSlice'; +import { type Controller, PlayerState } from '../types'; +import { FirebaseContext } from './FirebaseContext'; +import { selectAuth } from '../redux/authSlice'; + +interface FirebaseProviderProps { + children: ReactNode; +} + +export const FirebaseProvider = ({ children }: FirebaseProviderProps) => { + const [isConnected, setIsConnected] = useState(false); + const [syncStatus, setLocalSyncStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const dispatch = useAppDispatch(); + const auth = useAppSelector(selectAuth); + const controllerName = auth?.controller; + const isAuthenticated = auth?.authenticated; + + useEffect(() => { + if (!isAuthenticated || !controllerName) { + setIsConnected(false); + setLocalSyncStatus('idle'); + return; + } + const controllerRef = ref(database, `controllers/${controllerName}`); + setLocalSyncStatus('loading'); + onValue( + controllerRef, + (snapshot) => { + if (snapshot.exists()) { + const controllerData = snapshot.val() as Controller; + dispatch(setController(controllerData)); + setLocalSyncStatus('success'); + setIsConnected(true); + } else { + // Initialize empty controller if it doesn't exist + const emptyController: Controller = { + favorites: {}, + history: {}, + topPlayed: {}, + newSongs: {}, + player: { + queue: {}, + settings: { + autoadvance: false, + userpick: false + }, + singers: {}, + state: { + state: PlayerState.stopped + } + }, + songList: {}, + songs: {} + }; + dispatch(setController(emptyController)); + setLocalSyncStatus('success'); + setIsConnected(true); + } + }, + (error) => { + console.error('Firebase sync error:', error); + setLocalSyncStatus('error'); + setIsConnected(false); + } + ); + return () => { + off(controllerRef); + }; + }, [dispatch, isAuthenticated, controllerName]); + + const value = { + isConnected, + syncStatus + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/firebase/useFirebase.ts b/src/firebase/useFirebase.ts new file mode 100644 index 0000000..5afbbd4 --- /dev/null +++ b/src/firebase/useFirebase.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { FirebaseContext } from './FirebaseContext'; + +export const useFirebase = () => { + const context = useContext(FirebaseContext); + if (!context) { + throw new Error('useFirebase must be used within a FirebaseProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 6b43fbf..e6dfe24 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -4,4 +4,9 @@ export { useToast } from './useToast'; export { useSearch } from './useSearch'; export { useQueue } from './useQueue'; export { useHistory } from './useHistory'; -export { useTopPlayed } from './useTopPlayed'; \ No newline at end of file +export { useTopPlayed } from './useTopPlayed'; +export { useFavorites } from './useFavorites'; +export { useNewSongs } from './useNewSongs'; +export { useArtists } from './useArtists'; +export { useSingers } from './useSingers'; +export { useSongLists } from './useSongLists'; \ No newline at end of file diff --git a/src/hooks/useArtists.ts b/src/hooks/useArtists.ts new file mode 100644 index 0000000..2e7811c --- /dev/null +++ b/src/hooks/useArtists.ts @@ -0,0 +1,63 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useAppSelector, selectArtistsArray, selectSongsArray } from '../redux'; +import { useSongOperations } from './useSongOperations'; +import { useToast } from './useToast'; +import type { Song } from '../types'; + +export const useArtists = () => { + const allArtists = useAppSelector(selectArtistsArray); + const allSongs = useAppSelector(selectSongsArray); + const { addToQueue, toggleFavorite } = useSongOperations(); + const { showSuccess, showError } = useToast(); + + const [searchTerm, setSearchTerm] = useState(''); + + // Filter artists by search term + const filteredArtists = useMemo(() => { + if (!searchTerm.trim()) return allArtists; + + const term = searchTerm.toLowerCase(); + return allArtists.filter(artist => + artist.toLowerCase().includes(term) + ); + }, [allArtists, searchTerm]); + + // Get songs by artist + const getSongsByArtist = useCallback((artistName: string) => { + return allSongs.filter(song => + song.artist.toLowerCase() === artistName.toLowerCase() + ); + }, [allSongs]); + + const handleSearchChange = useCallback((value: string) => { + setSearchTerm(value); + }, []); + + const handleAddToQueue = useCallback(async (song: Song) => { + try { + await addToQueue(song); + showSuccess('Song added to queue'); + } catch { + showError('Failed to add song to queue'); + } + }, [addToQueue, showSuccess, showError]); + + const handleToggleFavorite = useCallback(async (song: Song) => { + try { + await toggleFavorite(song); + showSuccess(song.favorite ? 'Removed from favorites' : 'Added to favorites'); + } catch { + showError('Failed to update favorites'); + } + }, [toggleFavorite, showSuccess, showError]); + + return { + artists: filteredArtists, + allArtists, + searchTerm, + handleSearchChange, + getSongsByArtist, + handleAddToQueue, + handleToggleFavorite, + }; +}; \ No newline at end of file diff --git a/src/hooks/useFavorites.ts b/src/hooks/useFavorites.ts new file mode 100644 index 0000000..05f527a --- /dev/null +++ b/src/hooks/useFavorites.ts @@ -0,0 +1,62 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useAppSelector, selectFavoritesArray } from '../redux'; +import { useSongOperations } from './useSongOperations'; +import { useToast } from './useToast'; +import type { Song } from '../types'; + +const ITEMS_PER_PAGE = 20; + +export const useFavorites = () => { + const allFavoritesItems = useAppSelector(selectFavoritesArray); + const { addToQueue, toggleFavorite } = useSongOperations(); + const { showSuccess, showError } = useToast(); + + const [currentPage, setCurrentPage] = useState(1); + + // Paginate the favorites items - show all items up to current page + const favoritesItems = useMemo(() => { + const endIndex = currentPage * ITEMS_PER_PAGE; + return allFavoritesItems.slice(0, endIndex); + }, [allFavoritesItems, currentPage]); + + const hasMore = useMemo(() => { + // Only show "hasMore" if there are more items than currently loaded + return allFavoritesItems.length > ITEMS_PER_PAGE && favoritesItems.length < allFavoritesItems.length; + }, [favoritesItems.length, allFavoritesItems.length]); + + const loadMore = useCallback(() => { + console.log('useFavorites - loadMore called:', { hasMore, currentPage, allFavoritesItemsLength: allFavoritesItems.length }); + if (hasMore) { + setCurrentPage(prev => prev + 1); + } + }, [hasMore, currentPage, allFavoritesItems.length]); + + const handleAddToQueue = useCallback(async (song: Song) => { + try { + await addToQueue(song); + showSuccess('Song added to queue'); + } catch { + showError('Failed to add song to queue'); + } + }, [addToQueue, showSuccess, showError]); + + const handleToggleFavorite = useCallback(async (song: Song) => { + try { + await toggleFavorite(song); + showSuccess('Removed from favorites'); + } catch { + showError('Failed to remove from favorites'); + } + }, [toggleFavorite, showSuccess, showError]); + + return { + favoritesItems, + allFavoritesItems, + hasMore, + loadMore, + currentPage, + totalPages: Math.ceil(allFavoritesItems.length / ITEMS_PER_PAGE), + handleAddToQueue, + handleToggleFavorite, + }; +}; \ No newline at end of file diff --git a/src/hooks/useHistory.ts b/src/hooks/useHistory.ts index 8943d7d..2304923 100644 --- a/src/hooks/useHistory.ts +++ b/src/hooks/useHistory.ts @@ -1,14 +1,35 @@ -import { useCallback } from 'react'; -import { useAppSelector } from '../redux'; -import { selectHistoryArray } from '../redux/selectors'; +import { useCallback, useMemo, useState } from 'react'; +import { useAppSelector, selectHistoryArray } from '../redux'; import { useSongOperations } from './useSongOperations'; import { useToast } from './useToast'; import type { Song } from '../types'; +const ITEMS_PER_PAGE = 20; + export const useHistory = () => { - const historyItems = useAppSelector(selectHistoryArray); + const allHistoryItems = useAppSelector(selectHistoryArray); const { addToQueue, toggleFavorite } = useSongOperations(); const { showSuccess, showError } = useToast(); + + const [currentPage, setCurrentPage] = useState(1); + + // Paginate the history items - show all items up to current page + const historyItems = useMemo(() => { + const endIndex = currentPage * ITEMS_PER_PAGE; + return allHistoryItems.slice(0, endIndex); + }, [allHistoryItems, currentPage]); + + const hasMore = useMemo(() => { + // Only show "hasMore" if there are more items than currently loaded + return allHistoryItems.length > ITEMS_PER_PAGE && historyItems.length < allHistoryItems.length; + }, [historyItems.length, allHistoryItems.length]); + + const loadMore = useCallback(() => { + console.log('useHistory - loadMore called:', { hasMore, currentPage, allHistoryItemsLength: allHistoryItems.length }); + if (hasMore) { + setCurrentPage(prev => prev + 1); + } + }, [hasMore, currentPage, allHistoryItems.length]); const handleAddToQueue = useCallback(async (song: Song) => { try { @@ -30,6 +51,11 @@ export const useHistory = () => { return { historyItems, + allHistoryItems, + hasMore, + loadMore, + currentPage, + totalPages: Math.ceil(allHistoryItems.length / ITEMS_PER_PAGE), handleAddToQueue, handleToggleFavorite, }; diff --git a/src/hooks/useNewSongs.ts b/src/hooks/useNewSongs.ts new file mode 100644 index 0000000..01dd103 --- /dev/null +++ b/src/hooks/useNewSongs.ts @@ -0,0 +1,62 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useAppSelector, selectNewSongsArray } from '../redux'; +import { useSongOperations } from './useSongOperations'; +import { useToast } from './useToast'; +import type { Song } from '../types'; + +const ITEMS_PER_PAGE = 20; + +export const useNewSongs = () => { + const allNewSongsItems = useAppSelector(selectNewSongsArray); + const { addToQueue, toggleFavorite } = useSongOperations(); + const { showSuccess, showError } = useToast(); + + const [currentPage, setCurrentPage] = useState(1); + + // Paginate the new songs items - show all items up to current page + const newSongsItems = useMemo(() => { + const endIndex = currentPage * ITEMS_PER_PAGE; + return allNewSongsItems.slice(0, endIndex); + }, [allNewSongsItems, currentPage]); + + const hasMore = useMemo(() => { + // Only show "hasMore" if there are more items than currently loaded + return allNewSongsItems.length > ITEMS_PER_PAGE && newSongsItems.length < allNewSongsItems.length; + }, [newSongsItems.length, allNewSongsItems.length]); + + const loadMore = useCallback(() => { + console.log('useNewSongs - loadMore called:', { hasMore, currentPage, allNewSongsItemsLength: allNewSongsItems.length }); + if (hasMore) { + setCurrentPage(prev => prev + 1); + } + }, [hasMore, currentPage, allNewSongsItems.length]); + + const handleAddToQueue = useCallback(async (song: Song) => { + try { + await addToQueue(song); + showSuccess('Song added to queue'); + } catch { + showError('Failed to add song to queue'); + } + }, [addToQueue, showSuccess, showError]); + + const handleToggleFavorite = useCallback(async (song: Song) => { + try { + await toggleFavorite(song); + showSuccess(song.favorite ? 'Removed from favorites' : 'Added to favorites'); + } catch { + showError('Failed to update favorites'); + } + }, [toggleFavorite, showSuccess, showError]); + + return { + newSongsItems, + allNewSongsItems, + hasMore, + loadMore, + currentPage, + totalPages: Math.ceil(allNewSongsItems.length / ITEMS_PER_PAGE), + handleAddToQueue, + handleToggleFavorite, + }; +}; \ No newline at end of file diff --git a/src/hooks/useQueue.ts b/src/hooks/useQueue.ts index ed71f8b..12e58c6 100644 --- a/src/hooks/useQueue.ts +++ b/src/hooks/useQueue.ts @@ -1,6 +1,5 @@ import { useCallback } from 'react'; -import { useAppSelector } from '../redux'; -import { selectQueueWithUserInfo, selectQueueStats, selectCanReorderQueue } from '../redux/selectors'; +import { useAppSelector, selectQueueWithUserInfo, selectQueueStats, selectCanReorderQueue } from '../redux'; import { useSongOperations } from './useSongOperations'; import { useToast } from './useToast'; import type { QueueItem } from '../types'; diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts index c16fa5b..4ebfe7e 100644 --- a/src/hooks/useSearch.ts +++ b/src/hooks/useSearch.ts @@ -1,33 +1,62 @@ import { useState, useCallback, useMemo } from 'react'; -import { useAppSelector } from '../redux'; -import { selectSearchResults } from '../redux/selectors'; +import { useAppSelector, selectSongsArray } from '../redux'; import { useSongOperations } from './useSongOperations'; import { useToast } from './useToast'; import { UI_CONSTANTS } from '../constants'; +import { filterSongs } from '../utils/dataProcessing'; import type { Song } from '../types'; +const ITEMS_PER_PAGE = 20; + export const useSearch = () => { const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); const { addToQueue, toggleFavorite } = useSongOperations(); const { showSuccess, showError } = useToast(); - // Get filtered search results using selector - const searchResults = useAppSelector(state => - selectSearchResults(state, searchTerm) - ); + // Get all songs from Redux (this is memoized) + const allSongs = useAppSelector(selectSongsArray); - // Debounced search term for performance - const debouncedSearchTerm = useMemo(() => { - if (searchTerm.length < UI_CONSTANTS.SEARCH.MIN_SEARCH_LENGTH) { - return ''; + // Memoize filtered results to prevent unnecessary re-computations + const filteredSongs = useMemo(() => { + if (!searchTerm.trim() || searchTerm.length < UI_CONSTANTS.SEARCH.MIN_SEARCH_LENGTH) { + return allSongs; } - return searchTerm; - }, [searchTerm]); + + return filterSongs(allSongs, searchTerm); + }, [allSongs, searchTerm]); + + // Paginate the filtered results - show all items up to current page + const searchResults = useMemo(() => { + const endIndex = currentPage * ITEMS_PER_PAGE; + const paginatedSongs = filteredSongs.slice(0, endIndex); + + return { + songs: paginatedSongs, + count: filteredSongs.length, + hasMore: endIndex < filteredSongs.length, + currentPage, + totalPages: Math.ceil(filteredSongs.length / ITEMS_PER_PAGE), + }; + }, [filteredSongs, currentPage]); const handleSearchChange = useCallback((value: string) => { setSearchTerm(value); + setCurrentPage(1); // Reset to first page when searching }, []); + const loadMore = useCallback(() => { + console.log('useSearch - loadMore called:', { + hasMore: searchResults.hasMore, + currentPage, + filteredSongsLength: filteredSongs.length, + searchResultsCount: searchResults.count + }); + if (searchResults.hasMore) { + setCurrentPage(prev => prev + 1); + } + }, [searchResults.hasMore, currentPage, filteredSongs.length, searchResults.count]); + const handleAddToQueue = useCallback(async (song: Song) => { try { await addToQueue(song); @@ -48,10 +77,10 @@ export const useSearch = () => { return { searchTerm, - debouncedSearchTerm, searchResults, handleSearchChange, handleAddToQueue, handleToggleFavorite, + loadMore, }; }; \ No newline at end of file diff --git a/src/hooks/useSingers.ts b/src/hooks/useSingers.ts new file mode 100644 index 0000000..4fb7b02 --- /dev/null +++ b/src/hooks/useSingers.ts @@ -0,0 +1,30 @@ +import { useCallback } from 'react'; +import { useAppSelector, selectSingersArray, selectIsAdmin } from '../redux'; +import { useToast } from './useToast'; +import type { Singer } from '../types'; + +export const useSingers = () => { + const singers = useAppSelector(selectSingersArray); + const isAdmin = useAppSelector(selectIsAdmin); + const { showSuccess, showError } = useToast(); + + const handleRemoveSinger = useCallback(async (singer: Singer) => { + if (!isAdmin) { + showError('Only admins can remove singers'); + return; + } + + try { + // TODO: Implement remove singer functionality + showSuccess(`${singer.name} removed from singers list`); + } catch { + showError('Failed to remove singer'); + } + }, [isAdmin, showSuccess, showError]); + + return { + singers, + isAdmin, + handleRemoveSinger, + }; +}; \ No newline at end of file diff --git a/src/hooks/useSongLists.ts b/src/hooks/useSongLists.ts new file mode 100644 index 0000000..dbf6c73 --- /dev/null +++ b/src/hooks/useSongLists.ts @@ -0,0 +1,94 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useAppSelector, selectSongListArray, selectSongsArray } from '../redux'; +import { useSongOperations } from './useSongOperations'; +import { useToast } from './useToast'; +import type { SongListSong, Song } from '../types'; + +const ITEMS_PER_PAGE = 20; + +export const useSongLists = () => { + const allSongLists = useAppSelector(selectSongListArray); + const allSongs = useAppSelector(selectSongsArray); + const { addToQueue, toggleFavorite } = useSongOperations(); + const { showSuccess, showError } = useToast(); + + const [currentPage, setCurrentPage] = useState(1); + + // Paginate the song lists - show all items up to current page + const songLists = useMemo(() => { + const endIndex = currentPage * ITEMS_PER_PAGE; + return allSongLists.slice(0, endIndex); + }, [allSongLists, currentPage]); + + const hasMore = useMemo(() => { + // Show "hasMore" if there are more items than currently loaded + const hasMoreItems = songLists.length < allSongLists.length; + console.log('useSongLists - hasMore calculation:', { + songListsLength: songLists.length, + allSongListsLength: allSongLists.length, + hasMore: hasMoreItems, + currentPage + }); + return hasMoreItems; + }, [songLists.length, allSongLists.length, currentPage]); + + const loadMore = useCallback(() => { + console.log('useSongLists - loadMore called:', { + hasMore, + currentPage, + allSongListsLength: allSongLists.length, + songListsLength: songLists.length + }); + if (hasMore) { + console.log('useSongLists - Incrementing page from', currentPage, 'to', currentPage + 1); + setCurrentPage(prev => prev + 1); + } else { + console.log('useSongLists - Not loading more because hasMore is false'); + } + }, [hasMore, currentPage, allSongLists.length, songLists.length]); + + // Check if a song exists in the catalog + const checkSongAvailability = useCallback((songListSong: SongListSong) => { + if (songListSong.foundSongs && songListSong.foundSongs.length > 0) { + return songListSong.foundSongs; + } + + // Search for songs by artist and title + const matchingSongs = allSongs.filter(song => + song.artist.toLowerCase() === songListSong.artist.toLowerCase() && + song.title.toLowerCase() === songListSong.title.toLowerCase() + ); + + return matchingSongs; + }, [allSongs]); + + const handleAddToQueue = useCallback(async (song: Song) => { + try { + await addToQueue(song); + showSuccess('Song added to queue'); + } catch { + showError('Failed to add song to queue'); + } + }, [addToQueue, showSuccess, showError]); + + const handleToggleFavorite = useCallback(async (song: Song) => { + try { + await toggleFavorite(song); + showSuccess(song.favorite ? 'Removed from favorites' : 'Added to favorites'); + } catch { + showError('Failed to update favorites'); + } + }, [toggleFavorite, showSuccess, showError]); + + return { + songLists, + allSongLists, + hasMore, + loadMore, + currentPage, + totalPages: Math.ceil(allSongLists.length / ITEMS_PER_PAGE), + checkSongAvailability, + handleAddToQueue, + handleToggleFavorite, + }; +}; \ No newline at end of file diff --git a/src/hooks/useTopPlayed.ts b/src/hooks/useTopPlayed.ts index 8f3b6f4..dfa5550 100644 --- a/src/hooks/useTopPlayed.ts +++ b/src/hooks/useTopPlayed.ts @@ -1,14 +1,35 @@ -import { useCallback } from 'react'; -import { useAppSelector } from '../redux'; -import { selectTopPlayedArray } from '../redux/selectors'; +import { useCallback, useMemo, useState } from 'react'; +import { useAppSelector, selectTopPlayedArray } from '../redux'; import { useSongOperations } from './useSongOperations'; import { useToast } from './useToast'; import type { TopPlayed } from '../types'; +const ITEMS_PER_PAGE = 20; + export const useTopPlayed = () => { - const topPlayedItems = useAppSelector(selectTopPlayedArray); + const allTopPlayedItems = useAppSelector(selectTopPlayedArray); const { addToQueue, toggleFavorite } = useSongOperations(); const { showSuccess, showError } = useToast(); + + const [currentPage, setCurrentPage] = useState(1); + + // Paginate the top played items - show all items up to current page + const topPlayedItems = useMemo(() => { + const endIndex = currentPage * ITEMS_PER_PAGE; + return allTopPlayedItems.slice(0, endIndex); + }, [allTopPlayedItems, currentPage]); + + const hasMore = useMemo(() => { + // Only show "hasMore" if there are more items than currently loaded + return allTopPlayedItems.length > ITEMS_PER_PAGE && topPlayedItems.length < allTopPlayedItems.length; + }, [topPlayedItems.length, allTopPlayedItems.length]); + + const loadMore = useCallback(() => { + console.log('useTopPlayed - loadMore called:', { hasMore, currentPage, allTopPlayedItemsLength: allTopPlayedItems.length }); + if (hasMore) { + setCurrentPage(prev => prev + 1); + } + }, [hasMore, currentPage, allTopPlayedItems.length]); const handleAddToQueue = useCallback(async (song: TopPlayed) => { try { @@ -44,6 +65,11 @@ export const useTopPlayed = () => { return { topPlayedItems, + allTopPlayedItems, + hasMore, + loadMore, + currentPage, + totalPages: Math.ceil(allTopPlayedItems.length / ITEMS_PER_PAGE), handleAddToQueue, handleToggleFavorite, }; diff --git a/src/redux/controllerSlice.ts b/src/redux/controllerSlice.ts index 08aecfb..0c3a51e 100644 --- a/src/redux/controllerSlice.ts +++ b/src/redux/controllerSlice.ts @@ -157,6 +157,8 @@ export const selectQueue = (state: { controller: ControllerState }) => state.con export const selectFavorites = (state: { controller: ControllerState }) => state.controller.data?.favorites || {}; export const selectHistory = (state: { controller: ControllerState }) => state.controller.data?.history || {}; export const selectTopPlayed = (state: { controller: ControllerState }) => state.controller.data?.topPlayed || {}; +export const selectNewSongs = (state: { controller: ControllerState }) => state.controller.data?.newSongs || {}; +export const selectSongList = (state: { controller: ControllerState }) => state.controller.data?.songList || {}; export const selectPlayerState = (state: { controller: ControllerState }) => state.controller.data?.player?.state; export const selectSettings = (state: { controller: ControllerState }) => state.controller.data?.player?.settings; export const selectSingers = (state: { controller: ControllerState }) => state.controller.data?.player?.singers || {}; diff --git a/src/redux/index.ts b/src/redux/index.ts index 3ad554d..b1b7229 100644 --- a/src/redux/index.ts +++ b/src/redux/index.ts @@ -25,6 +25,8 @@ export { selectFavorites, selectHistory, selectTopPlayed, + selectNewSongs, + selectSongList, selectPlayerState, selectSettings, selectSingers, diff --git a/src/redux/selectors.ts b/src/redux/selectors.ts index 7a4dab6..134e4de 100644 --- a/src/redux/selectors.ts +++ b/src/redux/selectors.ts @@ -6,6 +6,9 @@ import { selectFavorites, selectHistory, selectTopPlayed, + selectNewSongs, + selectSongList, + selectSingers, selectIsAdmin, selectCurrentSinger } from './index'; @@ -15,6 +18,7 @@ import { sortQueueByOrder, sortHistoryByDate, sortTopPlayedByCount, + sortSongsByArtistAndTitle, limitArray, getQueueStats } from '../utils/dataProcessing'; @@ -23,11 +27,11 @@ import { UI_CONSTANTS } from '../constants'; // Enhanced selectors with data processing export const selectSongsArray = createSelector( [selectSongs], - (songs) => objectToArray(songs) + (songs) => sortSongsByArtistAndTitle(objectToArray(songs)) ); export const selectFilteredSongs = createSelector( - [selectSongsArray, (state: RootState, searchTerm: string) => searchTerm], + [selectSongsArray, (_state: RootState, searchTerm: string) => searchTerm], (songs, searchTerm) => filterSongs(songs, searchTerm) ); @@ -48,7 +52,35 @@ export const selectHistoryArray = createSelector( export const selectFavoritesArray = createSelector( [selectFavorites], - (favorites) => objectToArray(favorites) + (favorites) => sortSongsByArtistAndTitle(objectToArray(favorites)) +); + +export const selectNewSongsArray = createSelector( + [selectNewSongs], + (newSongs) => sortSongsByArtistAndTitle(objectToArray(newSongs)) +); + +export const selectSingersArray = createSelector( + [selectSingers], + (singers) => objectToArray(singers).sort((a, b) => a.name.localeCompare(b.name)) +); + +export const selectSongListArray = createSelector( + [selectSongList], + (songList) => objectToArray(songList) +); + +export const selectArtistsArray = createSelector( + [selectSongs], + (songs) => { + const artists = new Set(); + Object.values(songs).forEach(song => { + if (song.artist) { + artists.add(song.artist); + } + }); + return Array.from(artists).sort((a, b) => a.localeCompare(b)); + } ); export const selectTopPlayedArray = createSelector( diff --git a/src/types/index.ts b/src/types/index.ts index 2feb67e..abeb0f9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -90,7 +90,7 @@ export interface Controller { singers: Record; state: Player; }; - songList: Record; + songList: Record; songs: Record; } @@ -120,7 +120,7 @@ export interface ActionButtonProps { export interface SongItemProps { song: Song; - context: 'search' | 'queue' | 'history' | 'favorites'; + context: 'search' | 'queue' | 'history' | 'favorites' | 'topPlayed'; onAddToQueue?: () => void; onRemoveFromQueue?: () => void; onToggleFavorite?: () => void; diff --git a/src/utils/dataProcessing.ts b/src/utils/dataProcessing.ts index 8d354e3..008eb9c 100644 --- a/src/utils/dataProcessing.ts +++ b/src/utils/dataProcessing.ts @@ -39,6 +39,33 @@ export const sortTopPlayedByCount = (songs: TopPlayed[]): TopPlayed[] => { return [...songs].sort((a, b) => b.count - a.count); }; +// Sort songs by artist then title (case insensitive) +export const sortSongsByArtistAndTitle = (songs: Song[]): Song[] => { + const sortedSongs = [...songs].sort((a, b) => { + // First sort by artist (case insensitive) + const artistA = a.artist.toLowerCase(); + const artistB = b.artist.toLowerCase(); + + if (artistA !== artistB) { + return artistA.localeCompare(artistB); + } + + // If artists are the same, sort by title (case insensitive) + const titleA = a.title.toLowerCase(); + const titleB = b.title.toLowerCase(); + return titleA.localeCompare(titleB); + }); + + // Debug logging for first few songs to verify sorting + if (sortedSongs.length > 0) { + console.log('Songs sorted by artist and title. First 5 songs:', + sortedSongs.slice(0, 5).map(s => `${s.artist} - ${s.title}`) + ); + } + + return sortedSongs; +}; + // Limit array to specified length export const limitArray = (array: T[], limit: number): T[] => { return array.slice(0, limit);