Frontend Basics

This commit is contained in:
js0ny 2025-12-04 17:21:23 +00:00
parent fde16fa283
commit 3bbfed11fa
28 changed files with 2780 additions and 7 deletions

4
drone-frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
dist
.svelte-kit
.DS_Store

286
drone-frontend/bun.lock Normal file
View file

@ -0,0 +1,286 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "drone-frontend",
"dependencies": {
"leaflet": "^1.9.4",
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tailwindcss/postcss": "^4.1.17",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"svelte": "^4.2.18",
"tailwindcss": "^4.1.17",
"vite": "^5.4.8",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@3.1.2", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", "debug": "^4.3.4", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.10", "svelte-hmr": "^0.16.0", "vitefu": "^0.2.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@2.1.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001759", "", {}, "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw=="],
"code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="],
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.264", "", {}, "sha512-1tEf0nLgltC3iy9wtlYDlQDc5Rg9lEKVjEmIHJ21rI9OcqkvD45K1oyNIRA4rR1z3LgJ7KeGzEBojVcV6m4qjA=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
"periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"svelte": ["svelte@4.2.20", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q=="],
"svelte-hmr": ["svelte-hmr@0.16.0", "", { "peerDependencies": { "svelte": "^3.19.0 || ^4.0.0" } }, "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA=="],
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="],
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"vitefu": ["vitefu@0.2.5", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["vite"] }, "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
}
}

13
drone-frontend/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drone Path Explorer</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -0,0 +1,23 @@
{
"name": "drone-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"leaflet": "^1.9.4"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tailwindcss/postcss": "^4.1.17",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"svelte": "^4.2.18",
"tailwindcss": "^4.1.17",
"vite": "^5.4.8"
}
}

View file

@ -0,0 +1,7 @@
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};

View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#2dd4bf"/>
<stop offset="100%" stop-color="#0ea5e9"/>
</linearGradient>
</defs>
<circle cx="32" cy="32" r="30" fill="url(#grad)" />
<path d="M18 32h28M32 18v28" stroke="#0b172a" stroke-width="5" stroke-linecap="round"/>
<circle cx="32" cy="32" r="6" fill="#e2e8f0"/>
</svg>

After

Width:  |  Height:  |  Size: 465 B

View file

@ -0,0 +1,400 @@
<script>
import { onDestroy, onMount } from "svelte";
import L from "leaflet";
import { theme } from "./stores.js";
import {
STEP_SECONDS,
defaultDispatch,
samplePathResponse,
fallbackBounds,
} from "./sampleData.js";
import Sidebar from "./components/Sidebar.svelte";
import PlaybackControls from "./components/PlaybackControls.svelte";
import {
buildTimeline,
computeWallClock,
boundsKey,
colorFor,
} from "./utils.js";
let palette = [];
let lightTiles, darkTiles;
function updatePalette() {
if (typeof document === "undefined") return;
const style = getComputedStyle(document.documentElement);
const newPalette = Array.from({ length: 6 }, (_, i) =>
// Reads CSS custom property --p-1 through --p-6 (palette colors defined in CSS)
style.getPropertyValue(`--p-${i + 1}`).trim(),
);
// Only update if colors are valid and different to avoid unnecessary redraws
if (
newPalette.some((c) => c) &&
JSON.stringify(newPalette) !== JSON.stringify(palette)
) {
palette = newPalette;
}
}
// const fallbackBounds = {
// minLng: -3.19,
// maxLng: -3.17,
// minLat: 55.94,
// maxLat: 55.99,
// };
let mapContainer;
let map;
let pathLayer;
let markerLayer;
let lastBoundsKey = "";
let apiBase = "http://localhost:8080/api/v1";
let blackBoxBase = "http://localhost:3000";
let dispatchBody = JSON.stringify(defaultDispatch, null, 2);
let plannedPath = samplePathResponse;
let status = "System Ready. Waiting for dispatch payload.";
let snapshotStatus = "";
let snapshot = [];
let tick = 0;
let isPlaying = false;
let loading = false;
let playTimer;
let startTime = new Date().toISOString().slice(0, 16);
let sidebarOpen = true;
// Reactive Statements
$: timeline = buildTimeline(plannedPath);
$: totalSteps = Math.max(timeline.totalSteps, 1);
$: tick = Math.min(tick, totalSteps - 1);
$: playbackSeconds = tick * STEP_SECONDS;
$: wallClock = computeWallClock(startTime, playbackSeconds);
// Compute positions based on current tick
$: positionsNow = timeline.drones.map((drone) => ({
...drone,
current: drone.path[Math.min(tick, drone.path.length - 1)],
visited: drone.path.slice(0, Math.min(tick + 1, drone.path.length)),
}));
// Update Map
$: if (map && palette.length > 0) {
const nextKey = boundsKey(timeline.bounds);
if (nextKey && nextKey !== lastBoundsKey) {
fitMapToBounds();
lastBoundsKey = nextKey;
}
refreshMap(positionsNow);
}
// Update theme
$: {
if (typeof document !== "undefined") {
if ($theme === "dark") {
document.documentElement.classList.add("dark");
if (map && lightTiles && darkTiles) {
if (map.hasLayer(lightTiles)) map.removeLayer(lightTiles);
if (!map.hasLayer(darkTiles)) map.addLayer(darkTiles);
}
} else {
document.documentElement.classList.remove("dark");
if (map && lightTiles && darkTiles) {
if (map.hasLayer(darkTiles)) map.removeLayer(darkTiles);
if (!map.hasLayer(lightTiles)) map.addLayer(lightTiles);
}
}
// We need to wait for the browser to apply the new CSS variables
setTimeout(updatePalette, 50);
}
}
onMount(() => {
updatePalette(); // Set initial palette
map = L.map(mapContainer, {
preferCanvas: true,
zoomControl: false, // We'll move zoom control or rely on scroll
attributionControl: false, // Custom placement if needed, or keep minimal
});
// ADD ZOOM CONTROL to Top Right
L.control.zoom({ position: "topright" }).addTo(map);
const tileOptions = {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: "abcd",
maxZoom: 20,
};
lightTiles = L.tileLayer(
"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
tileOptions,
);
darkTiles = L.tileLayer(
"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
tileOptions,
);
if ($theme === "dark") {
darkTiles.addTo(map);
} else {
lightTiles.addTo(map);
}
pathLayer = L.layerGroup().addTo(map);
markerLayer = L.layerGroup().addTo(map);
fitMapToBounds();
refreshMap();
return () => map?.remove();
});
onDestroy(() => {
stopPlaying();
});
async function requestPath() {
let payload;
try {
payload = JSON.parse(dispatchBody);
} catch (err) {
status = "Error: Invalid JSON format.";
return;
}
if (!Array.isArray(payload)) {
status = "Error: Payload must be an array.";
return;
}
loading = true;
status = "Transmitting dispatch to API...";
stopPlaying();
try {
const res = await fetch(`${apiBase}/calcDeliveryPath`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
plannedPath = await res.json();
tick = 0;
status = "Flight path received. Ready for simulation.";
} catch (err) {
plannedPath = samplePathResponse;
status = `API Link Failed. Using offline sample data. (${err.message})`;
} finally {
loading = false;
}
}
function loadSample() {
plannedPath = samplePathResponse;
status = "Loaded sample simulation data.";
tick = 0;
stopPlaying();
}
function togglePlay() {
if (isPlaying) {
stopPlaying();
} else if (totalSteps > 1) {
isPlaying = true;
clearInterval(playTimer);
playTimer = setInterval(() => {
tick = (tick + 1) % totalSteps;
if (tick === totalSteps - 1) stopPlaying(); // Auto stop at end
}, 650);
}
}
function stopPlaying() {
isPlaying = false;
if (playTimer) clearInterval(playTimer);
}
async function fetchSnapshot() {
snapshotStatus = "Querying Black Box...";
try {
const res = await fetch(
`${blackBoxBase}/snapshot?time=${encodeURIComponent(wallClock)}`,
);
if (!res.ok) throw new Error(res.status);
snapshot = await res.json();
snapshotStatus = `Retrieved ${snapshot.length} events for ${wallClock}`;
} catch (err) {
snapshotStatus = `Connection Error: ${err.message}`;
snapshot = [];
}
}
function fitMapToBounds() {
if (!map || !timeline.bounds) return;
const b = timeline.bounds;
map.fitBounds(
[
[b.minLat, b.minLng],
[b.maxLat, b.maxLng],
],
{
padding: [50, 50],
animate: true,
},
);
}
function refreshMap(currentPositions = positionsNow) {
if (!map || !pathLayer || !markerLayer) return;
pathLayer.clearLayers();
markerLayer.clearLayers();
currentPositions.forEach((drone, idx) => {
const color = colorFor(idx, palette);
// Draw Full Trajectory (Dimmed)
if (drone.path.length > 1) {
L.polyline(
drone.path.map((p) => [p.lat, p.lng]),
{
color: color,
weight: 2,
opacity: 0.2,
dashArray: "4, 8", // Dashed line for future path
},
).addTo(pathLayer);
}
// Draw Visited Trajectory (Bright)
if (drone.visited.length > 1) {
L.polyline(
drone.visited.map((p) => [p.lat, p.lng]),
{
color: color,
weight: 3,
opacity: 0.9,
className: "neon-path", // Can add glow via CSS if desired
},
).addTo(pathLayer);
}
// Draw Drone Marker
if (drone.current) {
const marker = L.circleMarker(
[drone.current.lat, drone.current.lng],
{
radius: 6,
color: "#fff",
weight: 2,
fillColor: color,
fillOpacity: 1,
},
);
const isMoving =
drone.path.length > 1 &&
drone.visited.length < drone.path.length;
const popupContent = `
<div class="min-w-[120px]">
<h3 class="font-bold text-sm mb-1" style="color: ${color}">Drone ${drone.id}</h3>
<div class="text-[10px] space-y-0.5 font-mono opacity-80">
<div class="flex justify-between"><span>Lat:</span> <span>${drone.current.lat.toFixed(4)}</span></div>
<div class="flex justify-between"><span>Lng:</span> <span>${drone.current.lng.toFixed(4)}</span></div>
<div class="flex justify-between mt-1 pt-1 border-t border-white/10">
<span>Status:</span>
<span class="${isMoving ? "text-green-400" : "text-slate-400"}">${isMoving ? "MOVING" : "IDLE"}</span>
</div>
</div>
</div>
`;
// Tooltip style
marker.bindTooltip(`ID: ${drone.id}`, {
permanent: false,
direction: "top",
className: "drone-tooltip", // Custom class
});
marker.bindPopup(popupContent, {
className: "drone-popup",
closeButton: false,
});
marker.addTo(markerLayer);
}
});
}
function toggleTheme() {
theme.update((t) => (t === "dark" ? "light" : "dark"));
}
</script>
<div
id="map-background"
class="absolute inset-0 z-0 bg-gray-800"
bind:this={mapContainer}
></div>
<div
class="relative z-10 pointer-events-none h-screen w-screen grid transition-[grid-template-columns] duration-500 ease-in-out grid-rows-[1fr_auto]"
style="grid-template-columns: {sidebarOpen ? '380px' : '0px'} 1fr;"
>
<!-- Sidebar Container -->
<div class="row-span-full relative h-full">
<!-- Collapsed State Toggle Button (Visible when sidebar is closed) -->
<button
on:click={() => (sidebarOpen = true)}
class="pointer-events-auto absolute top-4 left-4 z-50 p-2.5 bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border border-slate-200 dark:border-slate-800 shadow-lg rounded-xl text-slate-500 hover:text-sky-500 transition-all duration-300 {sidebarOpen
? 'opacity-0 pointer-events-none -translate-x-full'
: 'opacity-100 translate-x-0'}"
aria-label="Open Sidebar"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><rect x="3" y="3" width="18" height="18" rx="2" ry="2"
></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg
>
</button>
<Sidebar
bind:open={sidebarOpen}
bind:apiBase
bind:blackBoxBase
bind:startTime
bind:dispatchBody
{status}
{loading}
on:close={() => (sidebarOpen = false)}
on:request={requestPath}
on:loadSample={loadSample}
/>
</div>
<PlaybackControls
{wallClock}
{playbackSeconds}
totalCost={plannedPath?.totalCost}
activeDrones={plannedPath?.dronePaths?.length}
{isPlaying}
bind:tick
{totalSteps}
{snapshotStatus}
{snapshot}
on:togglePlay={togglePlay}
on:seek={stopPlaying}
on:fetchSnapshot={fetchSnapshot}
/>
</div>

View file

@ -0,0 +1,58 @@
@import "leaflet/dist/leaflet.css";
@import "tailwindcss";
@layer base {
:root {
/* Light theme palette for drone paths - Distinct Colors */
--p-1: theme("colors.green.600");
--p-2: theme("colors.yellow.600");
--p-3: theme("colors.sky.500");
--p-4: theme("colors.pink.600");
--p-5: theme("colors.violet.600");
--p-6: theme("colors.red.600");
}
.dark {
/* Dark theme palette for drone paths - Neon Colors */
--p-1: #00ff00;
--p-2: #ffff00;
--p-3: #00ffff;
--p-4: #ff00ff;
--p-5: #bd00ff;
--p-6: #ff4d4d;
}
}
.leaflet-container {
background: #111 !important;
}
.drone-tooltip {
@apply bg-gray-800 text-white p-2 rounded-md border-gray-700 border;
}
/* Custom Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.3); /* Slate 400 with opacity */
border-radius: 20px;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.5);
}
/* Drone Popup Styles */
.drone-popup .leaflet-popup-content-wrapper {
@apply bg-slate-900/90 backdrop-blur-md border border-slate-700 text-slate-100 rounded-lg p-0 shadow-xl;
}
.drone-popup .leaflet-popup-tip {
@apply bg-slate-900/90 border-slate-700;
}
.drone-popup .leaflet-popup-content {
margin: 12px;
}

View file

@ -0,0 +1,164 @@
<script>
import { createEventDispatcher } from "svelte";
import { theme } from "../stores.js";
export let wallClock = "";
export let playbackSeconds = 0;
export let totalCost = 0;
export let activeDrones = 0;
export let isPlaying = false;
export let tick = 0;
export let totalSteps = 0;
export let snapshotStatus = "";
export let snapshot = [];
const dispatch = createEventDispatcher();
function formatDuration(sec) {
const m = Math.floor(sec / 60)
.toString()
.padStart(2, "0");
const s = Math.floor(sec % 60)
.toString()
.padStart(2, "0");
return `${m}:${s}`;
}
</script>
<div
class="pointer-events-auto bg-white/70 dark:bg-slate-900/70 backdrop-blur-xl border border-slate-200 dark:border-slate-800 shadow-2xl flex flex-col gap-3 self-end row-start-2 col-start-2 m-6 ml-8 rounded-2xl p-4"
>
<div class="flex gap-6">
<div class="flex flex-col">
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
>Mission Time</span
>
<span
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
>{wallClock.split("T")[1].slice(0, 8)}</span
>
</div>
<div class="flex flex-col">
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
>T+ Elapsed</span
>
<span
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
>{formatDuration(playbackSeconds)}</span
>
</div>
<div class="flex flex-col">
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
>Total Cost</span
>
<span
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
>{totalCost ?? "—"}</span
>
</div>
<div class="flex flex-col">
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
>Active Drones</span
>
<span
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
>{activeDrones ?? 0}</span
>
</div>
</div>
<div class="flex items-center gap-4">
<button
class="w-10 h-10 rounded-full flex items-center justify-center shrink-0 bg-slate-800 text-white dark:bg-slate-100 dark:text-slate-900 hover:bg-slate-900 dark:hover:bg-white transition-all disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => dispatch('togglePlay')}
disabled={totalSteps <= 1}
>
{#if isPlaying}
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"
><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /></svg
>
{:else}
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"
><path d="M8 5v14l11-7z" /></svg
>
{/if}
</button>
<input
type="range"
min="0"
max={Math.max(totalSteps - 1, 0)}
step="1"
bind:value={tick}
on:input={() => dispatch('seek')}
class="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-full appearance-none"
style="--thumb-color: {$theme === 'dark'
? '#f1f5f9'
: '#1e293b'};"
/>
<span
class="font-mono text-sm min-w-[60px] text-right text-slate-600 dark:text-slate-400"
>
{Math.round((tick / (totalSteps - 1 || 1)) * 100)}%
</span>
</div>
<div
class="mt-2 pt-2 border-t border-slate-200 dark:border-slate-800 flex justify-between items-center"
>
<div
class="text-xs text-slate-500 dark:text-slate-400 overflow-hidden whitespace-nowrap text-ellipsis max-w-[300px]"
>
{snapshotStatus || "Black Box Offline"}
</div>
<button
class="px-3 py-1 text-xs font-semibold rounded-md transition-all text-slate-800 dark:text-slate-100 bg-slate-500/20 hover:bg-slate-500/30"
on:click={() => dispatch('fetchSnapshot')}
>
FETCH SNAPSHOT
</button>
</div>
{#if snapshot.length > 0}
<div class="flex flex-wrap gap-1.5 mt-2">
{#each snapshot.slice(0, 3) as event}
<div
class="text-xs px-2 py-0.5 rounded-full bg-slate-500/20 font-mono text-slate-600 dark:text-slate-300"
>
Drone {event.drone_id} log found
</div>
{/each}
{#if snapshot.length > 3}
<div
class="text-xs px-2 py-0.5 rounded-full bg-slate-500/20 font-mono text-slate-600 dark:text-slate-300"
>
+{snapshot.length - 3} more
</div>
{/if}
</div>
{/if}
</div>
<style>
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 18px;
width: 18px;
border-radius: 9999px;
background-color: var(--thumb-color);
margin-top: -7px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]::-moz-range-thumb {
height: 18px;
width: 18px;
border-radius: 9999px;
background-color: var(--thumb-color);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
cursor: pointer;
transition: transform 0.1s;
}
</style>

View file

@ -0,0 +1,209 @@
<script>
import { createEventDispatcher } from "svelte";
import { theme } from "../stores.js";
export let open = true;
export let status = "";
export let loading = false;
// Configuration Bindings
export let apiBase = "";
export let blackBoxBase = "";
export let startTime = "";
export let dispatchBody = "";
let configOpen = false;
const dispatch = createEventDispatcher();
function toggleTheme() {
theme.update((t) => (t === "dark" ? "light" : "dark"));
}
</script>
<aside
class="absolute top-0 left-0 bottom-0 w-[380px] pointer-events-auto bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border border-slate-200 dark:border-slate-800 shadow-2xl flex flex-col m-4 rounded-2xl p-5 gap-5 transition-transform duration-500 ease-in-out {open ? 'translate-x-0' : '-translate-x-[120%]'}"
>
<div class="flex items-center justify-between">
<h1
class="text-xl font-bold text-slate-800 dark:text-slate-100 flex items-center gap-2"
>
<span
class="block w-2 h-2 bg-sky-500 rounded-full shadow-[0_0_10px_theme(colors.sky.500)]"
></span>
Drone Explorer
</h1>
<div class="flex items-center gap-1">
<button
on:click={toggleTheme}
class="p-2 rounded-lg text-slate-500 hover:text-sky-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
aria-label="Toggle Theme"
>
{#if $theme === "dark"}
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
></path></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><circle cx="12" cy="12" r="5"></circle><line
x1="12"
y1="1"
x2="12"
y2="3"
></line><line x1="12" y1="21" x2="12" y2="23"
></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"
></line><line
x1="18.36"
y1="18.36"
x2="19.78"
y2="19.78"
></line><line x1="1" y1="12" x2="3" y2="12"></line><line
x1="21"
y1="12"
x2="23"
y2="12"
></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"
></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"
></line></svg
>
{/if}
</button>
<button
on:click={() => dispatch('close')}
class="p-2 rounded-lg text-slate-500 hover:text-sky-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
aria-label="Close Sidebar"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
</div>
<div
class="font-mono text-xs text-slate-500 dark:text-slate-400 p-2.5 bg-slate-500/10 rounded-lg border-l-4 border-sky-500/50"
>
> {status}
</div>
<div class="space-y-3 overflow-y-auto pr-1 custom-scrollbar flex-1 flex flex-col">
<div class="border-b border-slate-200 dark:border-slate-800 pb-2">
<button
class="w-full flex items-center justify-between text-sm font-semibold tracking-wider uppercase text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors focus:outline-none"
on:click={() => (configOpen = !configOpen)}
>
<span>Configuration</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="transform transition-transform duration-300 {configOpen
? 'rotate-180'
: ''}"
><polyline points="6 9 12 15 18 9"></polyline></svg
>
</button>
{#if configOpen}
<div class="mt-3 space-y-3">
<div>
<label
for="api"
class="block text-xs text-slate-600 dark:text-slate-400 mb-1"
>Planner API Endpoint</label
>
<input
id="api"
type="text"
bind:value={apiBase}
placeholder="http://localhost..."
class="w-full bg-white/50 dark:bg-black/30 border border-slate-300 dark:border-slate-700 text-slate-800 dark:text-slate-100 p-2 rounded-lg font-mono text-xs transition-all focus:outline-none focus:border-sky-500 focus:bg-white dark:focus:bg-black/50 focus:ring-2 focus:ring-sky-500/20"
/>
</div>
<div>
<label
for="bb"
class="block text-xs text-slate-600 dark:text-slate-400 mb-1"
>Black Box API Endpoint</label
>
<input
id="bb"
type="text"
bind:value={blackBoxBase}
placeholder="http://localhost..."
class="w-full bg-white/50 dark:bg-black/30 border border-slate-300 dark:border-slate-700 text-slate-800 dark:text-slate-100 p-2 rounded-lg font-mono text-xs transition-all focus:outline-none focus:border-sky-500 focus:bg-white dark:focus:bg-black/50 focus:ring-2 focus:ring-sky-500/20"
/>
</div>
<div>
<label
for="startTime"
class="block text-xs text-slate-600 dark:text-slate-400 mb-1"
>Mission Start Time</label
>
<input
id="startTime"
type="datetime-local"
bind:value={startTime}
class="w-full bg-white/50 dark:bg-black/30 border border-slate-300 dark:border-slate-700 text-slate-800 dark:text-slate-100 p-2 rounded-lg font-mono text-xs transition-all focus:outline-none focus:border-sky-500 focus:bg-white dark:focus:bg-black/50 focus:ring-2 focus:ring-sky-500/20"
/>
</div>
</div>
{/if}
</div>
<div class="flex-1 flex flex-col min-h-0">
<h2
class="mb-2 text-sm font-semibold tracking-wider uppercase text-slate-500 dark:text-slate-400"
>
Dispatch Payload (JSON)
</h2>
<textarea
class="flex-1 min-h-[200px] text-xs w-full bg-white/50 dark:bg-black/30 border border-slate-300 dark:border-slate-700 text-slate-800 dark:text-slate-100 p-2.5 rounded-lg font-mono transition-all focus:outline-none focus:border-sky-500 focus:bg-white dark:focus:bg-black/50 focus:ring-2 focus:ring-sky-500/20 resize-none"
bind:value={dispatchBody}
spellcheck="false"
></textarea>
</div>
</div>
<div class="flex gap-2 mt-auto pt-4 border-t border-slate-200 dark:border-slate-800">
<button
class="flex-1 p-2.5 rounded-lg font-semibold text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed text-white bg-sky-500 shadow-[0_4px_12px_theme(colors.sky.500/30)] hover:bg-sky-600 disabled:hover:bg-sky-500"
on:click={() => dispatch('request')}
disabled={loading}
>
{loading ? "CALCULATING..." : "REQUEST PATH"}
</button>
<button
class="flex-1 p-2.5 rounded-lg font-semibold text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed text-slate-800 dark:text-slate-100 bg-slate-500/20 hover:bg-slate-500/30"
on:click={() => dispatch('loadSample')}
>
LOAD SAMPLE
</button>
</div>
</aside>

View file

@ -0,0 +1,8 @@
import './app.css';
import App from './App.svelte';
const app = new App({
target: document.getElementById('app')
});
export default app;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const theme = writable('dark');

108
drone-frontend/src/utils.js Normal file
View file

@ -0,0 +1,108 @@
import { fallbackBounds } from "./sampleData.js";
export function buildTimeline(response) {
const drones = [];
const allPoints = [];
let longest = 0;
if (!response || !response.dronePaths) {
return { drones, totalSteps: 0, bounds: fallbackBounds };
}
for (const path of response.dronePaths) {
const coords = flattenDeliveries(path.deliveries);
if (!coords.length) continue;
drones.push({ id: path.droneId, path: coords });
coords.forEach((p) => allPoints.push(p));
longest = Math.max(longest, coords.length);
}
// Add padding points for bounds calculation if empty
const basePoints =
allPoints.length > 0
? allPoints
: [
{
lng: fallbackBounds.minLng,
lat: fallbackBounds.minLat,
},
{
lng: fallbackBounds.maxLng,
lat: fallbackBounds.maxLat,
},
];
return {
drones,
totalSteps: longest || 0,
bounds: computeBounds(basePoints),
};
}
function flattenDeliveries(deliveries) {
const coords = [];
for (const delivery of deliveries || []) {
for (const point of delivery.flightPath || []) {
if (
!coords.length ||
!samePoint(coords[coords.length - 1], point)
) {
coords.push(point);
}
}
}
return coords;
}
function samePoint(a, b) {
return Math.abs(a.lng - b.lng) < 1e-9 && Math.abs(a.lat - b.lat) < 1e-9;
}
export function computeBounds(points) {
if (!points.length) return fallbackBounds;
let minLng = Infinity,
maxLng = -Infinity,
minLat = Infinity,
maxLat = -Infinity;
for (const p of points) {
minLng = Math.min(minLng, p.lng);
maxLng = Math.max(maxLng, p.lng);
minLat = Math.min(minLat, p.lat);
maxLat = Math.max(maxLat, p.lat);
}
const padding = 0.002; // Slightly more padding
return {
minLng: minLng - padding,
maxLng: maxLng + padding,
minLat: minLat - padding,
maxLat: maxLat + padding,
};
}
export function colorFor(idx, palette) {
if (!palette || palette.length === 0) return "#000";
return palette[idx % palette.length];
}
export function formatDuration(sec) {
const m = Math.floor(sec / 60)
.toString()
.padStart(2, "0");
const s = Math.floor(sec % 60)
.toString()
.padStart(2, "0");
return `${m}:${s}`;
}
export function computeWallClock(start, seconds) {
const base = start ? new Date(start) : new Date();
const ts = new Date(base.getTime() + seconds * 1000);
return isNaN(ts.getTime())
? new Date().toISOString()
: ts.toISOString();
}
export function boundsKey(bounds) {
if (!bounds) return "";
return `${bounds.minLat}:${bounds.maxLat}:${bounds.minLng}:${bounds.maxLng}`;
}

View file

@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess()
};

View file

@ -0,0 +1,27 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{svelte,js,ts,jsx,tsx}"],
darkMode: "class",
theme: {
extend: {
colors: {
// Palette for drone paths
// Light theme
"p-light-1": "#16a34a", // green-600
"p-light-2": "#ca8a04", // yellow-600
"p-light-3": "#0ea5e9", // sky-500
"p-light-4": "#db2777", // pink-600
"p-light-5": "#7c3aed", // violet-600
"p-light-6": "#dc2626", // red-600
"p-dark-1": "#00ff00", // Lime
"p-dark-2": "#ffff00", // Yellow
"p-dark-3": "#00ffff", // Cyan
"p-dark-4": "#ff00ff", // Magenta
"p-dark-5": "#bd00ff", // Violet
"p-dark-6": "#ff4d4d", // Red-ish
},
},
},
plugins: [],
};

View file

@ -0,0 +1,9 @@
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default {
plugins: [svelte()],
server: {
port: 4173,
strictPort: true
}
};