this repo has no description
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+11538
+10
.dockerignore
··· 1 + api/target/ 2 + web/node_modules/ 3 + web/dist/ 4 + .git/ 5 + scripts/ 6 + *.log 7 + .DS_Store 8 + __pycache__/ 9 + *.pyc 10 + .env
+14
.gitignore
··· 1 + # Rust 2 + api/target/ 3 + api/.env 4 + 5 + # Node 6 + web/node_modules/ 7 + web/dist/ 8 + 9 + # IDE 10 + .vscode/ 11 + .idea/ 12 + *.swp 13 + *.swo 14 + .DS_Store
+47
Dockerfile
··· 1 + # -- Stage 1: Build frontend -- 2 + FROM node:22-alpine AS web-build 3 + 4 + WORKDIR /app 5 + 6 + COPY web/package.json web/package-lock.json ./ 7 + RUN npm ci 8 + 9 + COPY web/ . 10 + COPY content/ ../content/ 11 + RUN npm run build 12 + 13 + # -- Stage 2: Build server -- 14 + FROM rust:1-alpine AS server-build 15 + 16 + RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig 17 + 18 + WORKDIR /app 19 + 20 + # Cache dependencies: copy manifests first, build a dummy, then swap in real source 21 + COPY api/Cargo.toml api/Cargo.lock ./ 22 + RUN mkdir src && echo 'fn main() {}' > src/main.rs && cargo build --release && rm -rf src target/release/deps/ayos* 23 + 24 + COPY api/src ./src 25 + COPY api/migrations ./migrations 26 + RUN cargo build --release 27 + 28 + # -- Stage 3: Runtime -- 29 + FROM alpine:3.21 30 + 31 + RUN apk add --no-cache ca-certificates 32 + 33 + WORKDIR /app 34 + 35 + COPY --from=server-build /app/target/release/ayos-api ./server 36 + COPY --from=web-build /app/dist ./dist 37 + 38 + VOLUME /app/data 39 + 40 + ENV DATABASE_URL=sqlite:/app/data/ayos.db 41 + ENV JWT_SECRET=change-me-in-production 42 + ENV STATIC_DIR=/app/dist 43 + ENV PORT=3000 44 + 45 + EXPOSE 3000 46 + 47 + CMD ["./server"]
+12
Makefile
··· 1 + IMAGE := pierrelf/ayos 2 + TAG := latest 3 + 4 + .PHONY: default build push 5 + 6 + default: push 7 + 8 + build: 9 + docker build --platform linux/arm64 -t $(IMAGE):$(TAG) . 10 + 11 + push: build 12 + docker push $(IMAGE):$(TAG)
+2
api/.env.example
··· 1 + DATABASE_URL=sqlite:ayos.db 2 + JWT_SECRET=change-me-in-production
+2551
api/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "allocator-api2" 7 + version = "0.2.21" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 10 + 11 + [[package]] 12 + name = "android_system_properties" 13 + version = "0.1.5" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 + dependencies = [ 17 + "libc", 18 + ] 19 + 20 + [[package]] 21 + name = "anyhow" 22 + version = "1.0.102" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 25 + 26 + [[package]] 27 + name = "argon2" 28 + version = "0.5.3" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" 31 + dependencies = [ 32 + "base64ct", 33 + "blake2", 34 + "cpufeatures", 35 + "password-hash", 36 + ] 37 + 38 + [[package]] 39 + name = "atoi" 40 + version = "2.0.0" 41 + source = "registry+https://github.com/rust-lang/crates.io-index" 42 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 43 + dependencies = [ 44 + "num-traits", 45 + ] 46 + 47 + [[package]] 48 + name = "atomic-waker" 49 + version = "1.1.2" 50 + source = "registry+https://github.com/rust-lang/crates.io-index" 51 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 52 + 53 + [[package]] 54 + name = "autocfg" 55 + version = "1.5.0" 56 + source = "registry+https://github.com/rust-lang/crates.io-index" 57 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 58 + 59 + [[package]] 60 + name = "axum" 61 + version = "0.8.8" 62 + source = "registry+https://github.com/rust-lang/crates.io-index" 63 + checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 64 + dependencies = [ 65 + "axum-core", 66 + "bytes", 67 + "form_urlencoded", 68 + "futures-util", 69 + "http", 70 + "http-body", 71 + "http-body-util", 72 + "hyper", 73 + "hyper-util", 74 + "itoa", 75 + "matchit", 76 + "memchr", 77 + "mime", 78 + "percent-encoding", 79 + "pin-project-lite", 80 + "serde_core", 81 + "serde_json", 82 + "serde_path_to_error", 83 + "serde_urlencoded", 84 + "sync_wrapper", 85 + "tokio", 86 + "tower", 87 + "tower-layer", 88 + "tower-service", 89 + "tracing", 90 + ] 91 + 92 + [[package]] 93 + name = "axum-core" 94 + version = "0.5.6" 95 + source = "registry+https://github.com/rust-lang/crates.io-index" 96 + checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" 97 + dependencies = [ 98 + "bytes", 99 + "futures-core", 100 + "http", 101 + "http-body", 102 + "http-body-util", 103 + "mime", 104 + "pin-project-lite", 105 + "sync_wrapper", 106 + "tower-layer", 107 + "tower-service", 108 + "tracing", 109 + ] 110 + 111 + [[package]] 112 + name = "ayos-api" 113 + version = "0.1.0" 114 + dependencies = [ 115 + "argon2", 116 + "axum", 117 + "chrono", 118 + "dotenvy", 119 + "jsonwebtoken", 120 + "serde", 121 + "serde_json", 122 + "sqlx", 123 + "tokio", 124 + "tower-http", 125 + "uuid", 126 + ] 127 + 128 + [[package]] 129 + name = "base64" 130 + version = "0.22.1" 131 + source = "registry+https://github.com/rust-lang/crates.io-index" 132 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 133 + 134 + [[package]] 135 + name = "base64ct" 136 + version = "1.8.3" 137 + source = "registry+https://github.com/rust-lang/crates.io-index" 138 + checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 139 + 140 + [[package]] 141 + name = "bitflags" 142 + version = "2.11.0" 143 + source = "registry+https://github.com/rust-lang/crates.io-index" 144 + checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 145 + dependencies = [ 146 + "serde_core", 147 + ] 148 + 149 + [[package]] 150 + name = "blake2" 151 + version = "0.10.6" 152 + source = "registry+https://github.com/rust-lang/crates.io-index" 153 + checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" 154 + dependencies = [ 155 + "digest", 156 + ] 157 + 158 + [[package]] 159 + name = "block-buffer" 160 + version = "0.10.4" 161 + source = "registry+https://github.com/rust-lang/crates.io-index" 162 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 163 + dependencies = [ 164 + "generic-array", 165 + ] 166 + 167 + [[package]] 168 + name = "bumpalo" 169 + version = "3.20.2" 170 + source = "registry+https://github.com/rust-lang/crates.io-index" 171 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 172 + 173 + [[package]] 174 + name = "byteorder" 175 + version = "1.5.0" 176 + source = "registry+https://github.com/rust-lang/crates.io-index" 177 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 178 + 179 + [[package]] 180 + name = "bytes" 181 + version = "1.11.1" 182 + source = "registry+https://github.com/rust-lang/crates.io-index" 183 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" 184 + 185 + [[package]] 186 + name = "cc" 187 + version = "1.2.56" 188 + source = "registry+https://github.com/rust-lang/crates.io-index" 189 + checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" 190 + dependencies = [ 191 + "find-msvc-tools", 192 + "shlex", 193 + ] 194 + 195 + [[package]] 196 + name = "cfg-if" 197 + version = "1.0.4" 198 + source = "registry+https://github.com/rust-lang/crates.io-index" 199 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 200 + 201 + [[package]] 202 + name = "chrono" 203 + version = "0.4.44" 204 + source = "registry+https://github.com/rust-lang/crates.io-index" 205 + checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" 206 + dependencies = [ 207 + "iana-time-zone", 208 + "js-sys", 209 + "num-traits", 210 + "serde", 211 + "wasm-bindgen", 212 + "windows-link", 213 + ] 214 + 215 + [[package]] 216 + name = "concurrent-queue" 217 + version = "2.5.0" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 220 + dependencies = [ 221 + "crossbeam-utils", 222 + ] 223 + 224 + [[package]] 225 + name = "const-oid" 226 + version = "0.9.6" 227 + source = "registry+https://github.com/rust-lang/crates.io-index" 228 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 229 + 230 + [[package]] 231 + name = "core-foundation-sys" 232 + version = "0.8.7" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 235 + 236 + [[package]] 237 + name = "cpufeatures" 238 + version = "0.2.17" 239 + source = "registry+https://github.com/rust-lang/crates.io-index" 240 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 241 + dependencies = [ 242 + "libc", 243 + ] 244 + 245 + [[package]] 246 + name = "crc" 247 + version = "3.4.0" 248 + source = "registry+https://github.com/rust-lang/crates.io-index" 249 + checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" 250 + dependencies = [ 251 + "crc-catalog", 252 + ] 253 + 254 + [[package]] 255 + name = "crc-catalog" 256 + version = "2.4.0" 257 + source = "registry+https://github.com/rust-lang/crates.io-index" 258 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 259 + 260 + [[package]] 261 + name = "crossbeam-queue" 262 + version = "0.3.12" 263 + source = "registry+https://github.com/rust-lang/crates.io-index" 264 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 265 + dependencies = [ 266 + "crossbeam-utils", 267 + ] 268 + 269 + [[package]] 270 + name = "crossbeam-utils" 271 + version = "0.8.21" 272 + source = "registry+https://github.com/rust-lang/crates.io-index" 273 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 274 + 275 + [[package]] 276 + name = "crypto-common" 277 + version = "0.1.7" 278 + source = "registry+https://github.com/rust-lang/crates.io-index" 279 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 280 + dependencies = [ 281 + "generic-array", 282 + "typenum", 283 + ] 284 + 285 + [[package]] 286 + name = "der" 287 + version = "0.7.10" 288 + source = "registry+https://github.com/rust-lang/crates.io-index" 289 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 290 + dependencies = [ 291 + "const-oid", 292 + "pem-rfc7468", 293 + "zeroize", 294 + ] 295 + 296 + [[package]] 297 + name = "deranged" 298 + version = "0.5.8" 299 + source = "registry+https://github.com/rust-lang/crates.io-index" 300 + checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" 301 + dependencies = [ 302 + "powerfmt", 303 + ] 304 + 305 + [[package]] 306 + name = "digest" 307 + version = "0.10.7" 308 + source = "registry+https://github.com/rust-lang/crates.io-index" 309 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 310 + dependencies = [ 311 + "block-buffer", 312 + "const-oid", 313 + "crypto-common", 314 + "subtle", 315 + ] 316 + 317 + [[package]] 318 + name = "displaydoc" 319 + version = "0.2.5" 320 + source = "registry+https://github.com/rust-lang/crates.io-index" 321 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 322 + dependencies = [ 323 + "proc-macro2", 324 + "quote", 325 + "syn", 326 + ] 327 + 328 + [[package]] 329 + name = "dotenvy" 330 + version = "0.15.7" 331 + source = "registry+https://github.com/rust-lang/crates.io-index" 332 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 333 + 334 + [[package]] 335 + name = "either" 336 + version = "1.15.0" 337 + source = "registry+https://github.com/rust-lang/crates.io-index" 338 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 339 + dependencies = [ 340 + "serde", 341 + ] 342 + 343 + [[package]] 344 + name = "equivalent" 345 + version = "1.0.2" 346 + source = "registry+https://github.com/rust-lang/crates.io-index" 347 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 348 + 349 + [[package]] 350 + name = "errno" 351 + version = "0.3.14" 352 + source = "registry+https://github.com/rust-lang/crates.io-index" 353 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 354 + dependencies = [ 355 + "libc", 356 + "windows-sys 0.61.2", 357 + ] 358 + 359 + [[package]] 360 + name = "etcetera" 361 + version = "0.8.0" 362 + source = "registry+https://github.com/rust-lang/crates.io-index" 363 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 364 + dependencies = [ 365 + "cfg-if", 366 + "home", 367 + "windows-sys 0.48.0", 368 + ] 369 + 370 + [[package]] 371 + name = "event-listener" 372 + version = "5.4.1" 373 + source = "registry+https://github.com/rust-lang/crates.io-index" 374 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 375 + dependencies = [ 376 + "concurrent-queue", 377 + "parking", 378 + "pin-project-lite", 379 + ] 380 + 381 + [[package]] 382 + name = "find-msvc-tools" 383 + version = "0.1.9" 384 + source = "registry+https://github.com/rust-lang/crates.io-index" 385 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" 386 + 387 + [[package]] 388 + name = "flume" 389 + version = "0.11.1" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 392 + dependencies = [ 393 + "futures-core", 394 + "futures-sink", 395 + "spin", 396 + ] 397 + 398 + [[package]] 399 + name = "foldhash" 400 + version = "0.1.5" 401 + source = "registry+https://github.com/rust-lang/crates.io-index" 402 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 403 + 404 + [[package]] 405 + name = "form_urlencoded" 406 + version = "1.2.2" 407 + source = "registry+https://github.com/rust-lang/crates.io-index" 408 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 409 + dependencies = [ 410 + "percent-encoding", 411 + ] 412 + 413 + [[package]] 414 + name = "futures-channel" 415 + version = "0.3.32" 416 + source = "registry+https://github.com/rust-lang/crates.io-index" 417 + checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" 418 + dependencies = [ 419 + "futures-core", 420 + "futures-sink", 421 + ] 422 + 423 + [[package]] 424 + name = "futures-core" 425 + version = "0.3.32" 426 + source = "registry+https://github.com/rust-lang/crates.io-index" 427 + checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 428 + 429 + [[package]] 430 + name = "futures-executor" 431 + version = "0.3.32" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" 434 + dependencies = [ 435 + "futures-core", 436 + "futures-task", 437 + "futures-util", 438 + ] 439 + 440 + [[package]] 441 + name = "futures-intrusive" 442 + version = "0.5.0" 443 + source = "registry+https://github.com/rust-lang/crates.io-index" 444 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 445 + dependencies = [ 446 + "futures-core", 447 + "lock_api", 448 + "parking_lot", 449 + ] 450 + 451 + [[package]] 452 + name = "futures-io" 453 + version = "0.3.32" 454 + source = "registry+https://github.com/rust-lang/crates.io-index" 455 + checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" 456 + 457 + [[package]] 458 + name = "futures-sink" 459 + version = "0.3.32" 460 + source = "registry+https://github.com/rust-lang/crates.io-index" 461 + checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" 462 + 463 + [[package]] 464 + name = "futures-task" 465 + version = "0.3.32" 466 + source = "registry+https://github.com/rust-lang/crates.io-index" 467 + checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" 468 + 469 + [[package]] 470 + name = "futures-util" 471 + version = "0.3.32" 472 + source = "registry+https://github.com/rust-lang/crates.io-index" 473 + checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" 474 + dependencies = [ 475 + "futures-core", 476 + "futures-io", 477 + "futures-sink", 478 + "futures-task", 479 + "memchr", 480 + "pin-project-lite", 481 + "slab", 482 + ] 483 + 484 + [[package]] 485 + name = "generic-array" 486 + version = "0.14.7" 487 + source = "registry+https://github.com/rust-lang/crates.io-index" 488 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 489 + dependencies = [ 490 + "typenum", 491 + "version_check", 492 + ] 493 + 494 + [[package]] 495 + name = "getrandom" 496 + version = "0.2.17" 497 + source = "registry+https://github.com/rust-lang/crates.io-index" 498 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 499 + dependencies = [ 500 + "cfg-if", 501 + "js-sys", 502 + "libc", 503 + "wasi", 504 + "wasm-bindgen", 505 + ] 506 + 507 + [[package]] 508 + name = "getrandom" 509 + version = "0.4.2" 510 + source = "registry+https://github.com/rust-lang/crates.io-index" 511 + checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" 512 + dependencies = [ 513 + "cfg-if", 514 + "libc", 515 + "r-efi", 516 + "wasip2", 517 + "wasip3", 518 + ] 519 + 520 + [[package]] 521 + name = "hashbrown" 522 + version = "0.15.5" 523 + source = "registry+https://github.com/rust-lang/crates.io-index" 524 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 525 + dependencies = [ 526 + "allocator-api2", 527 + "equivalent", 528 + "foldhash", 529 + ] 530 + 531 + [[package]] 532 + name = "hashbrown" 533 + version = "0.16.1" 534 + source = "registry+https://github.com/rust-lang/crates.io-index" 535 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 536 + 537 + [[package]] 538 + name = "hashlink" 539 + version = "0.10.0" 540 + source = "registry+https://github.com/rust-lang/crates.io-index" 541 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 542 + dependencies = [ 543 + "hashbrown 0.15.5", 544 + ] 545 + 546 + [[package]] 547 + name = "heck" 548 + version = "0.5.0" 549 + source = "registry+https://github.com/rust-lang/crates.io-index" 550 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 551 + 552 + [[package]] 553 + name = "hex" 554 + version = "0.4.3" 555 + source = "registry+https://github.com/rust-lang/crates.io-index" 556 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 557 + 558 + [[package]] 559 + name = "hkdf" 560 + version = "0.12.4" 561 + source = "registry+https://github.com/rust-lang/crates.io-index" 562 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 563 + dependencies = [ 564 + "hmac", 565 + ] 566 + 567 + [[package]] 568 + name = "hmac" 569 + version = "0.12.1" 570 + source = "registry+https://github.com/rust-lang/crates.io-index" 571 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 572 + dependencies = [ 573 + "digest", 574 + ] 575 + 576 + [[package]] 577 + name = "home" 578 + version = "0.5.12" 579 + source = "registry+https://github.com/rust-lang/crates.io-index" 580 + checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" 581 + dependencies = [ 582 + "windows-sys 0.61.2", 583 + ] 584 + 585 + [[package]] 586 + name = "http" 587 + version = "1.4.0" 588 + source = "registry+https://github.com/rust-lang/crates.io-index" 589 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 590 + dependencies = [ 591 + "bytes", 592 + "itoa", 593 + ] 594 + 595 + [[package]] 596 + name = "http-body" 597 + version = "1.0.1" 598 + source = "registry+https://github.com/rust-lang/crates.io-index" 599 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 600 + dependencies = [ 601 + "bytes", 602 + "http", 603 + ] 604 + 605 + [[package]] 606 + name = "http-body-util" 607 + version = "0.1.3" 608 + source = "registry+https://github.com/rust-lang/crates.io-index" 609 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 610 + dependencies = [ 611 + "bytes", 612 + "futures-core", 613 + "http", 614 + "http-body", 615 + "pin-project-lite", 616 + ] 617 + 618 + [[package]] 619 + name = "http-range-header" 620 + version = "0.4.2" 621 + source = "registry+https://github.com/rust-lang/crates.io-index" 622 + checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 623 + 624 + [[package]] 625 + name = "httparse" 626 + version = "1.10.1" 627 + source = "registry+https://github.com/rust-lang/crates.io-index" 628 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 629 + 630 + [[package]] 631 + name = "httpdate" 632 + version = "1.0.3" 633 + source = "registry+https://github.com/rust-lang/crates.io-index" 634 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 635 + 636 + [[package]] 637 + name = "hyper" 638 + version = "1.8.1" 639 + source = "registry+https://github.com/rust-lang/crates.io-index" 640 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 641 + dependencies = [ 642 + "atomic-waker", 643 + "bytes", 644 + "futures-channel", 645 + "futures-core", 646 + "http", 647 + "http-body", 648 + "httparse", 649 + "httpdate", 650 + "itoa", 651 + "pin-project-lite", 652 + "pin-utils", 653 + "smallvec", 654 + "tokio", 655 + ] 656 + 657 + [[package]] 658 + name = "hyper-util" 659 + version = "0.1.20" 660 + source = "registry+https://github.com/rust-lang/crates.io-index" 661 + checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 662 + dependencies = [ 663 + "bytes", 664 + "http", 665 + "http-body", 666 + "hyper", 667 + "pin-project-lite", 668 + "tokio", 669 + "tower-service", 670 + ] 671 + 672 + [[package]] 673 + name = "iana-time-zone" 674 + version = "0.1.65" 675 + source = "registry+https://github.com/rust-lang/crates.io-index" 676 + checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" 677 + dependencies = [ 678 + "android_system_properties", 679 + "core-foundation-sys", 680 + "iana-time-zone-haiku", 681 + "js-sys", 682 + "log", 683 + "wasm-bindgen", 684 + "windows-core", 685 + ] 686 + 687 + [[package]] 688 + name = "iana-time-zone-haiku" 689 + version = "0.1.2" 690 + source = "registry+https://github.com/rust-lang/crates.io-index" 691 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 692 + dependencies = [ 693 + "cc", 694 + ] 695 + 696 + [[package]] 697 + name = "icu_collections" 698 + version = "2.1.1" 699 + source = "registry+https://github.com/rust-lang/crates.io-index" 700 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 701 + dependencies = [ 702 + "displaydoc", 703 + "potential_utf", 704 + "yoke", 705 + "zerofrom", 706 + "zerovec", 707 + ] 708 + 709 + [[package]] 710 + name = "icu_locale_core" 711 + version = "2.1.1" 712 + source = "registry+https://github.com/rust-lang/crates.io-index" 713 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 714 + dependencies = [ 715 + "displaydoc", 716 + "litemap", 717 + "tinystr", 718 + "writeable", 719 + "zerovec", 720 + ] 721 + 722 + [[package]] 723 + name = "icu_normalizer" 724 + version = "2.1.1" 725 + source = "registry+https://github.com/rust-lang/crates.io-index" 726 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 727 + dependencies = [ 728 + "icu_collections", 729 + "icu_normalizer_data", 730 + "icu_properties", 731 + "icu_provider", 732 + "smallvec", 733 + "zerovec", 734 + ] 735 + 736 + [[package]] 737 + name = "icu_normalizer_data" 738 + version = "2.1.1" 739 + source = "registry+https://github.com/rust-lang/crates.io-index" 740 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 741 + 742 + [[package]] 743 + name = "icu_properties" 744 + version = "2.1.2" 745 + source = "registry+https://github.com/rust-lang/crates.io-index" 746 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 747 + dependencies = [ 748 + "icu_collections", 749 + "icu_locale_core", 750 + "icu_properties_data", 751 + "icu_provider", 752 + "zerotrie", 753 + "zerovec", 754 + ] 755 + 756 + [[package]] 757 + name = "icu_properties_data" 758 + version = "2.1.2" 759 + source = "registry+https://github.com/rust-lang/crates.io-index" 760 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 761 + 762 + [[package]] 763 + name = "icu_provider" 764 + version = "2.1.1" 765 + source = "registry+https://github.com/rust-lang/crates.io-index" 766 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 767 + dependencies = [ 768 + "displaydoc", 769 + "icu_locale_core", 770 + "writeable", 771 + "yoke", 772 + "zerofrom", 773 + "zerotrie", 774 + "zerovec", 775 + ] 776 + 777 + [[package]] 778 + name = "id-arena" 779 + version = "2.3.0" 780 + source = "registry+https://github.com/rust-lang/crates.io-index" 781 + checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 782 + 783 + [[package]] 784 + name = "idna" 785 + version = "1.1.0" 786 + source = "registry+https://github.com/rust-lang/crates.io-index" 787 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 788 + dependencies = [ 789 + "idna_adapter", 790 + "smallvec", 791 + "utf8_iter", 792 + ] 793 + 794 + [[package]] 795 + name = "idna_adapter" 796 + version = "1.2.1" 797 + source = "registry+https://github.com/rust-lang/crates.io-index" 798 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 799 + dependencies = [ 800 + "icu_normalizer", 801 + "icu_properties", 802 + ] 803 + 804 + [[package]] 805 + name = "indexmap" 806 + version = "2.13.0" 807 + source = "registry+https://github.com/rust-lang/crates.io-index" 808 + checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" 809 + dependencies = [ 810 + "equivalent", 811 + "hashbrown 0.16.1", 812 + "serde", 813 + "serde_core", 814 + ] 815 + 816 + [[package]] 817 + name = "itoa" 818 + version = "1.0.17" 819 + source = "registry+https://github.com/rust-lang/crates.io-index" 820 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 821 + 822 + [[package]] 823 + name = "js-sys" 824 + version = "0.3.91" 825 + source = "registry+https://github.com/rust-lang/crates.io-index" 826 + checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" 827 + dependencies = [ 828 + "once_cell", 829 + "wasm-bindgen", 830 + ] 831 + 832 + [[package]] 833 + name = "jsonwebtoken" 834 + version = "9.3.1" 835 + source = "registry+https://github.com/rust-lang/crates.io-index" 836 + checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" 837 + dependencies = [ 838 + "base64", 839 + "js-sys", 840 + "pem", 841 + "ring", 842 + "serde", 843 + "serde_json", 844 + "simple_asn1", 845 + ] 846 + 847 + [[package]] 848 + name = "lazy_static" 849 + version = "1.5.0" 850 + source = "registry+https://github.com/rust-lang/crates.io-index" 851 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 852 + dependencies = [ 853 + "spin", 854 + ] 855 + 856 + [[package]] 857 + name = "leb128fmt" 858 + version = "0.1.0" 859 + source = "registry+https://github.com/rust-lang/crates.io-index" 860 + checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 861 + 862 + [[package]] 863 + name = "libc" 864 + version = "0.2.183" 865 + source = "registry+https://github.com/rust-lang/crates.io-index" 866 + checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" 867 + 868 + [[package]] 869 + name = "libm" 870 + version = "0.2.16" 871 + source = "registry+https://github.com/rust-lang/crates.io-index" 872 + checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" 873 + 874 + [[package]] 875 + name = "libredox" 876 + version = "0.1.14" 877 + source = "registry+https://github.com/rust-lang/crates.io-index" 878 + checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" 879 + dependencies = [ 880 + "bitflags", 881 + "libc", 882 + "plain", 883 + "redox_syscall 0.7.3", 884 + ] 885 + 886 + [[package]] 887 + name = "libsqlite3-sys" 888 + version = "0.30.1" 889 + source = "registry+https://github.com/rust-lang/crates.io-index" 890 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 891 + dependencies = [ 892 + "cc", 893 + "pkg-config", 894 + "vcpkg", 895 + ] 896 + 897 + [[package]] 898 + name = "litemap" 899 + version = "0.8.1" 900 + source = "registry+https://github.com/rust-lang/crates.io-index" 901 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 902 + 903 + [[package]] 904 + name = "lock_api" 905 + version = "0.4.14" 906 + source = "registry+https://github.com/rust-lang/crates.io-index" 907 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 908 + dependencies = [ 909 + "scopeguard", 910 + ] 911 + 912 + [[package]] 913 + name = "log" 914 + version = "0.4.29" 915 + source = "registry+https://github.com/rust-lang/crates.io-index" 916 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 917 + 918 + [[package]] 919 + name = "matchit" 920 + version = "0.8.4" 921 + source = "registry+https://github.com/rust-lang/crates.io-index" 922 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 923 + 924 + [[package]] 925 + name = "md-5" 926 + version = "0.10.6" 927 + source = "registry+https://github.com/rust-lang/crates.io-index" 928 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 929 + dependencies = [ 930 + "cfg-if", 931 + "digest", 932 + ] 933 + 934 + [[package]] 935 + name = "memchr" 936 + version = "2.8.0" 937 + source = "registry+https://github.com/rust-lang/crates.io-index" 938 + checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 939 + 940 + [[package]] 941 + name = "mime" 942 + version = "0.3.17" 943 + source = "registry+https://github.com/rust-lang/crates.io-index" 944 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 945 + 946 + [[package]] 947 + name = "mime_guess" 948 + version = "2.0.5" 949 + source = "registry+https://github.com/rust-lang/crates.io-index" 950 + checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 951 + dependencies = [ 952 + "mime", 953 + "unicase", 954 + ] 955 + 956 + [[package]] 957 + name = "mio" 958 + version = "1.1.1" 959 + source = "registry+https://github.com/rust-lang/crates.io-index" 960 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 961 + dependencies = [ 962 + "libc", 963 + "wasi", 964 + "windows-sys 0.61.2", 965 + ] 966 + 967 + [[package]] 968 + name = "num-bigint" 969 + version = "0.4.6" 970 + source = "registry+https://github.com/rust-lang/crates.io-index" 971 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 972 + dependencies = [ 973 + "num-integer", 974 + "num-traits", 975 + ] 976 + 977 + [[package]] 978 + name = "num-bigint-dig" 979 + version = "0.8.6" 980 + source = "registry+https://github.com/rust-lang/crates.io-index" 981 + checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 982 + dependencies = [ 983 + "lazy_static", 984 + "libm", 985 + "num-integer", 986 + "num-iter", 987 + "num-traits", 988 + "rand", 989 + "smallvec", 990 + "zeroize", 991 + ] 992 + 993 + [[package]] 994 + name = "num-conv" 995 + version = "0.2.0" 996 + source = "registry+https://github.com/rust-lang/crates.io-index" 997 + checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" 998 + 999 + [[package]] 1000 + name = "num-integer" 1001 + version = "0.1.46" 1002 + source = "registry+https://github.com/rust-lang/crates.io-index" 1003 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1004 + dependencies = [ 1005 + "num-traits", 1006 + ] 1007 + 1008 + [[package]] 1009 + name = "num-iter" 1010 + version = "0.1.45" 1011 + source = "registry+https://github.com/rust-lang/crates.io-index" 1012 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1013 + dependencies = [ 1014 + "autocfg", 1015 + "num-integer", 1016 + "num-traits", 1017 + ] 1018 + 1019 + [[package]] 1020 + name = "num-traits" 1021 + version = "0.2.19" 1022 + source = "registry+https://github.com/rust-lang/crates.io-index" 1023 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1024 + dependencies = [ 1025 + "autocfg", 1026 + "libm", 1027 + ] 1028 + 1029 + [[package]] 1030 + name = "once_cell" 1031 + version = "1.21.4" 1032 + source = "registry+https://github.com/rust-lang/crates.io-index" 1033 + checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" 1034 + 1035 + [[package]] 1036 + name = "parking" 1037 + version = "2.2.1" 1038 + source = "registry+https://github.com/rust-lang/crates.io-index" 1039 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1040 + 1041 + [[package]] 1042 + name = "parking_lot" 1043 + version = "0.12.5" 1044 + source = "registry+https://github.com/rust-lang/crates.io-index" 1045 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 1046 + dependencies = [ 1047 + "lock_api", 1048 + "parking_lot_core", 1049 + ] 1050 + 1051 + [[package]] 1052 + name = "parking_lot_core" 1053 + version = "0.9.12" 1054 + source = "registry+https://github.com/rust-lang/crates.io-index" 1055 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 1056 + dependencies = [ 1057 + "cfg-if", 1058 + "libc", 1059 + "redox_syscall 0.5.18", 1060 + "smallvec", 1061 + "windows-link", 1062 + ] 1063 + 1064 + [[package]] 1065 + name = "password-hash" 1066 + version = "0.5.0" 1067 + source = "registry+https://github.com/rust-lang/crates.io-index" 1068 + checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" 1069 + dependencies = [ 1070 + "base64ct", 1071 + "rand_core", 1072 + "subtle", 1073 + ] 1074 + 1075 + [[package]] 1076 + name = "pem" 1077 + version = "3.0.6" 1078 + source = "registry+https://github.com/rust-lang/crates.io-index" 1079 + checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" 1080 + dependencies = [ 1081 + "base64", 1082 + "serde_core", 1083 + ] 1084 + 1085 + [[package]] 1086 + name = "pem-rfc7468" 1087 + version = "0.7.0" 1088 + source = "registry+https://github.com/rust-lang/crates.io-index" 1089 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1090 + dependencies = [ 1091 + "base64ct", 1092 + ] 1093 + 1094 + [[package]] 1095 + name = "percent-encoding" 1096 + version = "2.3.2" 1097 + source = "registry+https://github.com/rust-lang/crates.io-index" 1098 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1099 + 1100 + [[package]] 1101 + name = "pin-project-lite" 1102 + version = "0.2.17" 1103 + source = "registry+https://github.com/rust-lang/crates.io-index" 1104 + checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 1105 + 1106 + [[package]] 1107 + name = "pin-utils" 1108 + version = "0.1.0" 1109 + source = "registry+https://github.com/rust-lang/crates.io-index" 1110 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1111 + 1112 + [[package]] 1113 + name = "pkcs1" 1114 + version = "0.7.5" 1115 + source = "registry+https://github.com/rust-lang/crates.io-index" 1116 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1117 + dependencies = [ 1118 + "der", 1119 + "pkcs8", 1120 + "spki", 1121 + ] 1122 + 1123 + [[package]] 1124 + name = "pkcs8" 1125 + version = "0.10.2" 1126 + source = "registry+https://github.com/rust-lang/crates.io-index" 1127 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1128 + dependencies = [ 1129 + "der", 1130 + "spki", 1131 + ] 1132 + 1133 + [[package]] 1134 + name = "pkg-config" 1135 + version = "0.3.32" 1136 + source = "registry+https://github.com/rust-lang/crates.io-index" 1137 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1138 + 1139 + [[package]] 1140 + name = "plain" 1141 + version = "0.2.3" 1142 + source = "registry+https://github.com/rust-lang/crates.io-index" 1143 + checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 1144 + 1145 + [[package]] 1146 + name = "potential_utf" 1147 + version = "0.1.4" 1148 + source = "registry+https://github.com/rust-lang/crates.io-index" 1149 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 1150 + dependencies = [ 1151 + "zerovec", 1152 + ] 1153 + 1154 + [[package]] 1155 + name = "powerfmt" 1156 + version = "0.2.0" 1157 + source = "registry+https://github.com/rust-lang/crates.io-index" 1158 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1159 + 1160 + [[package]] 1161 + name = "ppv-lite86" 1162 + version = "0.2.21" 1163 + source = "registry+https://github.com/rust-lang/crates.io-index" 1164 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1165 + dependencies = [ 1166 + "zerocopy", 1167 + ] 1168 + 1169 + [[package]] 1170 + name = "prettyplease" 1171 + version = "0.2.37" 1172 + source = "registry+https://github.com/rust-lang/crates.io-index" 1173 + checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 1174 + dependencies = [ 1175 + "proc-macro2", 1176 + "syn", 1177 + ] 1178 + 1179 + [[package]] 1180 + name = "proc-macro2" 1181 + version = "1.0.106" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 1184 + dependencies = [ 1185 + "unicode-ident", 1186 + ] 1187 + 1188 + [[package]] 1189 + name = "quote" 1190 + version = "1.0.45" 1191 + source = "registry+https://github.com/rust-lang/crates.io-index" 1192 + checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" 1193 + dependencies = [ 1194 + "proc-macro2", 1195 + ] 1196 + 1197 + [[package]] 1198 + name = "r-efi" 1199 + version = "6.0.0" 1200 + source = "registry+https://github.com/rust-lang/crates.io-index" 1201 + checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 1202 + 1203 + [[package]] 1204 + name = "rand" 1205 + version = "0.8.5" 1206 + source = "registry+https://github.com/rust-lang/crates.io-index" 1207 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1208 + dependencies = [ 1209 + "libc", 1210 + "rand_chacha", 1211 + "rand_core", 1212 + ] 1213 + 1214 + [[package]] 1215 + name = "rand_chacha" 1216 + version = "0.3.1" 1217 + source = "registry+https://github.com/rust-lang/crates.io-index" 1218 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1219 + dependencies = [ 1220 + "ppv-lite86", 1221 + "rand_core", 1222 + ] 1223 + 1224 + [[package]] 1225 + name = "rand_core" 1226 + version = "0.6.4" 1227 + source = "registry+https://github.com/rust-lang/crates.io-index" 1228 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1229 + dependencies = [ 1230 + "getrandom 0.2.17", 1231 + ] 1232 + 1233 + [[package]] 1234 + name = "redox_syscall" 1235 + version = "0.5.18" 1236 + source = "registry+https://github.com/rust-lang/crates.io-index" 1237 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1238 + dependencies = [ 1239 + "bitflags", 1240 + ] 1241 + 1242 + [[package]] 1243 + name = "redox_syscall" 1244 + version = "0.7.3" 1245 + source = "registry+https://github.com/rust-lang/crates.io-index" 1246 + checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" 1247 + dependencies = [ 1248 + "bitflags", 1249 + ] 1250 + 1251 + [[package]] 1252 + name = "ring" 1253 + version = "0.17.14" 1254 + source = "registry+https://github.com/rust-lang/crates.io-index" 1255 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1256 + dependencies = [ 1257 + "cc", 1258 + "cfg-if", 1259 + "getrandom 0.2.17", 1260 + "libc", 1261 + "untrusted", 1262 + "windows-sys 0.52.0", 1263 + ] 1264 + 1265 + [[package]] 1266 + name = "rsa" 1267 + version = "0.9.10" 1268 + source = "registry+https://github.com/rust-lang/crates.io-index" 1269 + checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" 1270 + dependencies = [ 1271 + "const-oid", 1272 + "digest", 1273 + "num-bigint-dig", 1274 + "num-integer", 1275 + "num-traits", 1276 + "pkcs1", 1277 + "pkcs8", 1278 + "rand_core", 1279 + "signature", 1280 + "spki", 1281 + "subtle", 1282 + "zeroize", 1283 + ] 1284 + 1285 + [[package]] 1286 + name = "rustversion" 1287 + version = "1.0.22" 1288 + source = "registry+https://github.com/rust-lang/crates.io-index" 1289 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1290 + 1291 + [[package]] 1292 + name = "ryu" 1293 + version = "1.0.23" 1294 + source = "registry+https://github.com/rust-lang/crates.io-index" 1295 + checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 1296 + 1297 + [[package]] 1298 + name = "scopeguard" 1299 + version = "1.2.0" 1300 + source = "registry+https://github.com/rust-lang/crates.io-index" 1301 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1302 + 1303 + [[package]] 1304 + name = "semver" 1305 + version = "1.0.27" 1306 + source = "registry+https://github.com/rust-lang/crates.io-index" 1307 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 1308 + 1309 + [[package]] 1310 + name = "serde" 1311 + version = "1.0.228" 1312 + source = "registry+https://github.com/rust-lang/crates.io-index" 1313 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 1314 + dependencies = [ 1315 + "serde_core", 1316 + "serde_derive", 1317 + ] 1318 + 1319 + [[package]] 1320 + name = "serde_core" 1321 + version = "1.0.228" 1322 + source = "registry+https://github.com/rust-lang/crates.io-index" 1323 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1324 + dependencies = [ 1325 + "serde_derive", 1326 + ] 1327 + 1328 + [[package]] 1329 + name = "serde_derive" 1330 + version = "1.0.228" 1331 + source = "registry+https://github.com/rust-lang/crates.io-index" 1332 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1333 + dependencies = [ 1334 + "proc-macro2", 1335 + "quote", 1336 + "syn", 1337 + ] 1338 + 1339 + [[package]] 1340 + name = "serde_json" 1341 + version = "1.0.149" 1342 + source = "registry+https://github.com/rust-lang/crates.io-index" 1343 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 1344 + dependencies = [ 1345 + "itoa", 1346 + "memchr", 1347 + "serde", 1348 + "serde_core", 1349 + "zmij", 1350 + ] 1351 + 1352 + [[package]] 1353 + name = "serde_path_to_error" 1354 + version = "0.1.20" 1355 + source = "registry+https://github.com/rust-lang/crates.io-index" 1356 + checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" 1357 + dependencies = [ 1358 + "itoa", 1359 + "serde", 1360 + "serde_core", 1361 + ] 1362 + 1363 + [[package]] 1364 + name = "serde_urlencoded" 1365 + version = "0.7.1" 1366 + source = "registry+https://github.com/rust-lang/crates.io-index" 1367 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1368 + dependencies = [ 1369 + "form_urlencoded", 1370 + "itoa", 1371 + "ryu", 1372 + "serde", 1373 + ] 1374 + 1375 + [[package]] 1376 + name = "sha1" 1377 + version = "0.10.6" 1378 + source = "registry+https://github.com/rust-lang/crates.io-index" 1379 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1380 + dependencies = [ 1381 + "cfg-if", 1382 + "cpufeatures", 1383 + "digest", 1384 + ] 1385 + 1386 + [[package]] 1387 + name = "sha2" 1388 + version = "0.10.9" 1389 + source = "registry+https://github.com/rust-lang/crates.io-index" 1390 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1391 + dependencies = [ 1392 + "cfg-if", 1393 + "cpufeatures", 1394 + "digest", 1395 + ] 1396 + 1397 + [[package]] 1398 + name = "shlex" 1399 + version = "1.3.0" 1400 + source = "registry+https://github.com/rust-lang/crates.io-index" 1401 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1402 + 1403 + [[package]] 1404 + name = "signal-hook-registry" 1405 + version = "1.4.8" 1406 + source = "registry+https://github.com/rust-lang/crates.io-index" 1407 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 1408 + dependencies = [ 1409 + "errno", 1410 + "libc", 1411 + ] 1412 + 1413 + [[package]] 1414 + name = "signature" 1415 + version = "2.2.0" 1416 + source = "registry+https://github.com/rust-lang/crates.io-index" 1417 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1418 + dependencies = [ 1419 + "digest", 1420 + "rand_core", 1421 + ] 1422 + 1423 + [[package]] 1424 + name = "simple_asn1" 1425 + version = "0.6.4" 1426 + source = "registry+https://github.com/rust-lang/crates.io-index" 1427 + checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" 1428 + dependencies = [ 1429 + "num-bigint", 1430 + "num-traits", 1431 + "thiserror", 1432 + "time", 1433 + ] 1434 + 1435 + [[package]] 1436 + name = "slab" 1437 + version = "0.4.12" 1438 + source = "registry+https://github.com/rust-lang/crates.io-index" 1439 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 1440 + 1441 + [[package]] 1442 + name = "smallvec" 1443 + version = "1.15.1" 1444 + source = "registry+https://github.com/rust-lang/crates.io-index" 1445 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1446 + dependencies = [ 1447 + "serde", 1448 + ] 1449 + 1450 + [[package]] 1451 + name = "socket2" 1452 + version = "0.6.3" 1453 + source = "registry+https://github.com/rust-lang/crates.io-index" 1454 + checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" 1455 + dependencies = [ 1456 + "libc", 1457 + "windows-sys 0.61.2", 1458 + ] 1459 + 1460 + [[package]] 1461 + name = "spin" 1462 + version = "0.9.8" 1463 + source = "registry+https://github.com/rust-lang/crates.io-index" 1464 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1465 + dependencies = [ 1466 + "lock_api", 1467 + ] 1468 + 1469 + [[package]] 1470 + name = "spki" 1471 + version = "0.7.3" 1472 + source = "registry+https://github.com/rust-lang/crates.io-index" 1473 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1474 + dependencies = [ 1475 + "base64ct", 1476 + "der", 1477 + ] 1478 + 1479 + [[package]] 1480 + name = "sqlx" 1481 + version = "0.8.6" 1482 + source = "registry+https://github.com/rust-lang/crates.io-index" 1483 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 1484 + dependencies = [ 1485 + "sqlx-core", 1486 + "sqlx-macros", 1487 + "sqlx-mysql", 1488 + "sqlx-postgres", 1489 + "sqlx-sqlite", 1490 + ] 1491 + 1492 + [[package]] 1493 + name = "sqlx-core" 1494 + version = "0.8.6" 1495 + source = "registry+https://github.com/rust-lang/crates.io-index" 1496 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 1497 + dependencies = [ 1498 + "base64", 1499 + "bytes", 1500 + "crc", 1501 + "crossbeam-queue", 1502 + "either", 1503 + "event-listener", 1504 + "futures-core", 1505 + "futures-intrusive", 1506 + "futures-io", 1507 + "futures-util", 1508 + "hashbrown 0.15.5", 1509 + "hashlink", 1510 + "indexmap", 1511 + "log", 1512 + "memchr", 1513 + "once_cell", 1514 + "percent-encoding", 1515 + "serde", 1516 + "serde_json", 1517 + "sha2", 1518 + "smallvec", 1519 + "thiserror", 1520 + "tokio", 1521 + "tokio-stream", 1522 + "tracing", 1523 + "url", 1524 + ] 1525 + 1526 + [[package]] 1527 + name = "sqlx-macros" 1528 + version = "0.8.6" 1529 + source = "registry+https://github.com/rust-lang/crates.io-index" 1530 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 1531 + dependencies = [ 1532 + "proc-macro2", 1533 + "quote", 1534 + "sqlx-core", 1535 + "sqlx-macros-core", 1536 + "syn", 1537 + ] 1538 + 1539 + [[package]] 1540 + name = "sqlx-macros-core" 1541 + version = "0.8.6" 1542 + source = "registry+https://github.com/rust-lang/crates.io-index" 1543 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 1544 + dependencies = [ 1545 + "dotenvy", 1546 + "either", 1547 + "heck", 1548 + "hex", 1549 + "once_cell", 1550 + "proc-macro2", 1551 + "quote", 1552 + "serde", 1553 + "serde_json", 1554 + "sha2", 1555 + "sqlx-core", 1556 + "sqlx-mysql", 1557 + "sqlx-postgres", 1558 + "sqlx-sqlite", 1559 + "syn", 1560 + "tokio", 1561 + "url", 1562 + ] 1563 + 1564 + [[package]] 1565 + name = "sqlx-mysql" 1566 + version = "0.8.6" 1567 + source = "registry+https://github.com/rust-lang/crates.io-index" 1568 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 1569 + dependencies = [ 1570 + "atoi", 1571 + "base64", 1572 + "bitflags", 1573 + "byteorder", 1574 + "bytes", 1575 + "crc", 1576 + "digest", 1577 + "dotenvy", 1578 + "either", 1579 + "futures-channel", 1580 + "futures-core", 1581 + "futures-io", 1582 + "futures-util", 1583 + "generic-array", 1584 + "hex", 1585 + "hkdf", 1586 + "hmac", 1587 + "itoa", 1588 + "log", 1589 + "md-5", 1590 + "memchr", 1591 + "once_cell", 1592 + "percent-encoding", 1593 + "rand", 1594 + "rsa", 1595 + "serde", 1596 + "sha1", 1597 + "sha2", 1598 + "smallvec", 1599 + "sqlx-core", 1600 + "stringprep", 1601 + "thiserror", 1602 + "tracing", 1603 + "whoami", 1604 + ] 1605 + 1606 + [[package]] 1607 + name = "sqlx-postgres" 1608 + version = "0.8.6" 1609 + source = "registry+https://github.com/rust-lang/crates.io-index" 1610 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 1611 + dependencies = [ 1612 + "atoi", 1613 + "base64", 1614 + "bitflags", 1615 + "byteorder", 1616 + "crc", 1617 + "dotenvy", 1618 + "etcetera", 1619 + "futures-channel", 1620 + "futures-core", 1621 + "futures-util", 1622 + "hex", 1623 + "hkdf", 1624 + "hmac", 1625 + "home", 1626 + "itoa", 1627 + "log", 1628 + "md-5", 1629 + "memchr", 1630 + "once_cell", 1631 + "rand", 1632 + "serde", 1633 + "serde_json", 1634 + "sha2", 1635 + "smallvec", 1636 + "sqlx-core", 1637 + "stringprep", 1638 + "thiserror", 1639 + "tracing", 1640 + "whoami", 1641 + ] 1642 + 1643 + [[package]] 1644 + name = "sqlx-sqlite" 1645 + version = "0.8.6" 1646 + source = "registry+https://github.com/rust-lang/crates.io-index" 1647 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 1648 + dependencies = [ 1649 + "atoi", 1650 + "flume", 1651 + "futures-channel", 1652 + "futures-core", 1653 + "futures-executor", 1654 + "futures-intrusive", 1655 + "futures-util", 1656 + "libsqlite3-sys", 1657 + "log", 1658 + "percent-encoding", 1659 + "serde", 1660 + "serde_urlencoded", 1661 + "sqlx-core", 1662 + "thiserror", 1663 + "tracing", 1664 + "url", 1665 + ] 1666 + 1667 + [[package]] 1668 + name = "stable_deref_trait" 1669 + version = "1.2.1" 1670 + source = "registry+https://github.com/rust-lang/crates.io-index" 1671 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1672 + 1673 + [[package]] 1674 + name = "stringprep" 1675 + version = "0.1.5" 1676 + source = "registry+https://github.com/rust-lang/crates.io-index" 1677 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 1678 + dependencies = [ 1679 + "unicode-bidi", 1680 + "unicode-normalization", 1681 + "unicode-properties", 1682 + ] 1683 + 1684 + [[package]] 1685 + name = "subtle" 1686 + version = "2.6.1" 1687 + source = "registry+https://github.com/rust-lang/crates.io-index" 1688 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1689 + 1690 + [[package]] 1691 + name = "syn" 1692 + version = "2.0.117" 1693 + source = "registry+https://github.com/rust-lang/crates.io-index" 1694 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 1695 + dependencies = [ 1696 + "proc-macro2", 1697 + "quote", 1698 + "unicode-ident", 1699 + ] 1700 + 1701 + [[package]] 1702 + name = "sync_wrapper" 1703 + version = "1.0.2" 1704 + source = "registry+https://github.com/rust-lang/crates.io-index" 1705 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1706 + 1707 + [[package]] 1708 + name = "synstructure" 1709 + version = "0.13.2" 1710 + source = "registry+https://github.com/rust-lang/crates.io-index" 1711 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1712 + dependencies = [ 1713 + "proc-macro2", 1714 + "quote", 1715 + "syn", 1716 + ] 1717 + 1718 + [[package]] 1719 + name = "thiserror" 1720 + version = "2.0.18" 1721 + source = "registry+https://github.com/rust-lang/crates.io-index" 1722 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 1723 + dependencies = [ 1724 + "thiserror-impl", 1725 + ] 1726 + 1727 + [[package]] 1728 + name = "thiserror-impl" 1729 + version = "2.0.18" 1730 + source = "registry+https://github.com/rust-lang/crates.io-index" 1731 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 1732 + dependencies = [ 1733 + "proc-macro2", 1734 + "quote", 1735 + "syn", 1736 + ] 1737 + 1738 + [[package]] 1739 + name = "time" 1740 + version = "0.3.47" 1741 + source = "registry+https://github.com/rust-lang/crates.io-index" 1742 + checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" 1743 + dependencies = [ 1744 + "deranged", 1745 + "itoa", 1746 + "num-conv", 1747 + "powerfmt", 1748 + "serde_core", 1749 + "time-core", 1750 + "time-macros", 1751 + ] 1752 + 1753 + [[package]] 1754 + name = "time-core" 1755 + version = "0.1.8" 1756 + source = "registry+https://github.com/rust-lang/crates.io-index" 1757 + checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" 1758 + 1759 + [[package]] 1760 + name = "time-macros" 1761 + version = "0.2.27" 1762 + source = "registry+https://github.com/rust-lang/crates.io-index" 1763 + checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" 1764 + dependencies = [ 1765 + "num-conv", 1766 + "time-core", 1767 + ] 1768 + 1769 + [[package]] 1770 + name = "tinystr" 1771 + version = "0.8.2" 1772 + source = "registry+https://github.com/rust-lang/crates.io-index" 1773 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 1774 + dependencies = [ 1775 + "displaydoc", 1776 + "zerovec", 1777 + ] 1778 + 1779 + [[package]] 1780 + name = "tinyvec" 1781 + version = "1.10.0" 1782 + source = "registry+https://github.com/rust-lang/crates.io-index" 1783 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 1784 + dependencies = [ 1785 + "tinyvec_macros", 1786 + ] 1787 + 1788 + [[package]] 1789 + name = "tinyvec_macros" 1790 + version = "0.1.1" 1791 + source = "registry+https://github.com/rust-lang/crates.io-index" 1792 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1793 + 1794 + [[package]] 1795 + name = "tokio" 1796 + version = "1.50.0" 1797 + source = "registry+https://github.com/rust-lang/crates.io-index" 1798 + checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" 1799 + dependencies = [ 1800 + "bytes", 1801 + "libc", 1802 + "mio", 1803 + "parking_lot", 1804 + "pin-project-lite", 1805 + "signal-hook-registry", 1806 + "socket2", 1807 + "tokio-macros", 1808 + "windows-sys 0.61.2", 1809 + ] 1810 + 1811 + [[package]] 1812 + name = "tokio-macros" 1813 + version = "2.6.1" 1814 + source = "registry+https://github.com/rust-lang/crates.io-index" 1815 + checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" 1816 + dependencies = [ 1817 + "proc-macro2", 1818 + "quote", 1819 + "syn", 1820 + ] 1821 + 1822 + [[package]] 1823 + name = "tokio-stream" 1824 + version = "0.1.18" 1825 + source = "registry+https://github.com/rust-lang/crates.io-index" 1826 + checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" 1827 + dependencies = [ 1828 + "futures-core", 1829 + "pin-project-lite", 1830 + "tokio", 1831 + ] 1832 + 1833 + [[package]] 1834 + name = "tokio-util" 1835 + version = "0.7.18" 1836 + source = "registry+https://github.com/rust-lang/crates.io-index" 1837 + checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" 1838 + dependencies = [ 1839 + "bytes", 1840 + "futures-core", 1841 + "futures-sink", 1842 + "pin-project-lite", 1843 + "tokio", 1844 + ] 1845 + 1846 + [[package]] 1847 + name = "tower" 1848 + version = "0.5.3" 1849 + source = "registry+https://github.com/rust-lang/crates.io-index" 1850 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 1851 + dependencies = [ 1852 + "futures-core", 1853 + "futures-util", 1854 + "pin-project-lite", 1855 + "sync_wrapper", 1856 + "tokio", 1857 + "tower-layer", 1858 + "tower-service", 1859 + "tracing", 1860 + ] 1861 + 1862 + [[package]] 1863 + name = "tower-http" 1864 + version = "0.6.8" 1865 + source = "registry+https://github.com/rust-lang/crates.io-index" 1866 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 1867 + dependencies = [ 1868 + "bitflags", 1869 + "bytes", 1870 + "futures-core", 1871 + "futures-util", 1872 + "http", 1873 + "http-body", 1874 + "http-body-util", 1875 + "http-range-header", 1876 + "httpdate", 1877 + "mime", 1878 + "mime_guess", 1879 + "percent-encoding", 1880 + "pin-project-lite", 1881 + "tokio", 1882 + "tokio-util", 1883 + "tower-layer", 1884 + "tower-service", 1885 + "tracing", 1886 + ] 1887 + 1888 + [[package]] 1889 + name = "tower-layer" 1890 + version = "0.3.3" 1891 + source = "registry+https://github.com/rust-lang/crates.io-index" 1892 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1893 + 1894 + [[package]] 1895 + name = "tower-service" 1896 + version = "0.3.3" 1897 + source = "registry+https://github.com/rust-lang/crates.io-index" 1898 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1899 + 1900 + [[package]] 1901 + name = "tracing" 1902 + version = "0.1.44" 1903 + source = "registry+https://github.com/rust-lang/crates.io-index" 1904 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 1905 + dependencies = [ 1906 + "log", 1907 + "pin-project-lite", 1908 + "tracing-attributes", 1909 + "tracing-core", 1910 + ] 1911 + 1912 + [[package]] 1913 + name = "tracing-attributes" 1914 + version = "0.1.31" 1915 + source = "registry+https://github.com/rust-lang/crates.io-index" 1916 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 1917 + dependencies = [ 1918 + "proc-macro2", 1919 + "quote", 1920 + "syn", 1921 + ] 1922 + 1923 + [[package]] 1924 + name = "tracing-core" 1925 + version = "0.1.36" 1926 + source = "registry+https://github.com/rust-lang/crates.io-index" 1927 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 1928 + dependencies = [ 1929 + "once_cell", 1930 + ] 1931 + 1932 + [[package]] 1933 + name = "typenum" 1934 + version = "1.19.0" 1935 + source = "registry+https://github.com/rust-lang/crates.io-index" 1936 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 1937 + 1938 + [[package]] 1939 + name = "unicase" 1940 + version = "2.9.0" 1941 + source = "registry+https://github.com/rust-lang/crates.io-index" 1942 + checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" 1943 + 1944 + [[package]] 1945 + name = "unicode-bidi" 1946 + version = "0.3.18" 1947 + source = "registry+https://github.com/rust-lang/crates.io-index" 1948 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 1949 + 1950 + [[package]] 1951 + name = "unicode-ident" 1952 + version = "1.0.24" 1953 + source = "registry+https://github.com/rust-lang/crates.io-index" 1954 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 1955 + 1956 + [[package]] 1957 + name = "unicode-normalization" 1958 + version = "0.1.25" 1959 + source = "registry+https://github.com/rust-lang/crates.io-index" 1960 + checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" 1961 + dependencies = [ 1962 + "tinyvec", 1963 + ] 1964 + 1965 + [[package]] 1966 + name = "unicode-properties" 1967 + version = "0.1.4" 1968 + source = "registry+https://github.com/rust-lang/crates.io-index" 1969 + checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 1970 + 1971 + [[package]] 1972 + name = "unicode-xid" 1973 + version = "0.2.6" 1974 + source = "registry+https://github.com/rust-lang/crates.io-index" 1975 + checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1976 + 1977 + [[package]] 1978 + name = "untrusted" 1979 + version = "0.9.0" 1980 + source = "registry+https://github.com/rust-lang/crates.io-index" 1981 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1982 + 1983 + [[package]] 1984 + name = "url" 1985 + version = "2.5.8" 1986 + source = "registry+https://github.com/rust-lang/crates.io-index" 1987 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 1988 + dependencies = [ 1989 + "form_urlencoded", 1990 + "idna", 1991 + "percent-encoding", 1992 + "serde", 1993 + ] 1994 + 1995 + [[package]] 1996 + name = "utf8_iter" 1997 + version = "1.0.4" 1998 + source = "registry+https://github.com/rust-lang/crates.io-index" 1999 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2000 + 2001 + [[package]] 2002 + name = "uuid" 2003 + version = "1.22.0" 2004 + source = "registry+https://github.com/rust-lang/crates.io-index" 2005 + checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" 2006 + dependencies = [ 2007 + "getrandom 0.4.2", 2008 + "js-sys", 2009 + "wasm-bindgen", 2010 + ] 2011 + 2012 + [[package]] 2013 + name = "vcpkg" 2014 + version = "0.2.15" 2015 + source = "registry+https://github.com/rust-lang/crates.io-index" 2016 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2017 + 2018 + [[package]] 2019 + name = "version_check" 2020 + version = "0.9.5" 2021 + source = "registry+https://github.com/rust-lang/crates.io-index" 2022 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2023 + 2024 + [[package]] 2025 + name = "wasi" 2026 + version = "0.11.1+wasi-snapshot-preview1" 2027 + source = "registry+https://github.com/rust-lang/crates.io-index" 2028 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2029 + 2030 + [[package]] 2031 + name = "wasip2" 2032 + version = "1.0.2+wasi-0.2.9" 2033 + source = "registry+https://github.com/rust-lang/crates.io-index" 2034 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 2035 + dependencies = [ 2036 + "wit-bindgen", 2037 + ] 2038 + 2039 + [[package]] 2040 + name = "wasip3" 2041 + version = "0.4.0+wasi-0.3.0-rc-2026-01-06" 2042 + source = "registry+https://github.com/rust-lang/crates.io-index" 2043 + checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" 2044 + dependencies = [ 2045 + "wit-bindgen", 2046 + ] 2047 + 2048 + [[package]] 2049 + name = "wasite" 2050 + version = "0.1.0" 2051 + source = "registry+https://github.com/rust-lang/crates.io-index" 2052 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 2053 + 2054 + [[package]] 2055 + name = "wasm-bindgen" 2056 + version = "0.2.114" 2057 + source = "registry+https://github.com/rust-lang/crates.io-index" 2058 + checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" 2059 + dependencies = [ 2060 + "cfg-if", 2061 + "once_cell", 2062 + "rustversion", 2063 + "wasm-bindgen-macro", 2064 + "wasm-bindgen-shared", 2065 + ] 2066 + 2067 + [[package]] 2068 + name = "wasm-bindgen-macro" 2069 + version = "0.2.114" 2070 + source = "registry+https://github.com/rust-lang/crates.io-index" 2071 + checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" 2072 + dependencies = [ 2073 + "quote", 2074 + "wasm-bindgen-macro-support", 2075 + ] 2076 + 2077 + [[package]] 2078 + name = "wasm-bindgen-macro-support" 2079 + version = "0.2.114" 2080 + source = "registry+https://github.com/rust-lang/crates.io-index" 2081 + checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" 2082 + dependencies = [ 2083 + "bumpalo", 2084 + "proc-macro2", 2085 + "quote", 2086 + "syn", 2087 + "wasm-bindgen-shared", 2088 + ] 2089 + 2090 + [[package]] 2091 + name = "wasm-bindgen-shared" 2092 + version = "0.2.114" 2093 + source = "registry+https://github.com/rust-lang/crates.io-index" 2094 + checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" 2095 + dependencies = [ 2096 + "unicode-ident", 2097 + ] 2098 + 2099 + [[package]] 2100 + name = "wasm-encoder" 2101 + version = "0.244.0" 2102 + source = "registry+https://github.com/rust-lang/crates.io-index" 2103 + checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" 2104 + dependencies = [ 2105 + "leb128fmt", 2106 + "wasmparser", 2107 + ] 2108 + 2109 + [[package]] 2110 + name = "wasm-metadata" 2111 + version = "0.244.0" 2112 + source = "registry+https://github.com/rust-lang/crates.io-index" 2113 + checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" 2114 + dependencies = [ 2115 + "anyhow", 2116 + "indexmap", 2117 + "wasm-encoder", 2118 + "wasmparser", 2119 + ] 2120 + 2121 + [[package]] 2122 + name = "wasmparser" 2123 + version = "0.244.0" 2124 + source = "registry+https://github.com/rust-lang/crates.io-index" 2125 + checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" 2126 + dependencies = [ 2127 + "bitflags", 2128 + "hashbrown 0.15.5", 2129 + "indexmap", 2130 + "semver", 2131 + ] 2132 + 2133 + [[package]] 2134 + name = "whoami" 2135 + version = "1.6.1" 2136 + source = "registry+https://github.com/rust-lang/crates.io-index" 2137 + checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" 2138 + dependencies = [ 2139 + "libredox", 2140 + "wasite", 2141 + ] 2142 + 2143 + [[package]] 2144 + name = "windows-core" 2145 + version = "0.62.2" 2146 + source = "registry+https://github.com/rust-lang/crates.io-index" 2147 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 2148 + dependencies = [ 2149 + "windows-implement", 2150 + "windows-interface", 2151 + "windows-link", 2152 + "windows-result", 2153 + "windows-strings", 2154 + ] 2155 + 2156 + [[package]] 2157 + name = "windows-implement" 2158 + version = "0.60.2" 2159 + source = "registry+https://github.com/rust-lang/crates.io-index" 2160 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 2161 + dependencies = [ 2162 + "proc-macro2", 2163 + "quote", 2164 + "syn", 2165 + ] 2166 + 2167 + [[package]] 2168 + name = "windows-interface" 2169 + version = "0.59.3" 2170 + source = "registry+https://github.com/rust-lang/crates.io-index" 2171 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 2172 + dependencies = [ 2173 + "proc-macro2", 2174 + "quote", 2175 + "syn", 2176 + ] 2177 + 2178 + [[package]] 2179 + name = "windows-link" 2180 + version = "0.2.1" 2181 + source = "registry+https://github.com/rust-lang/crates.io-index" 2182 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 2183 + 2184 + [[package]] 2185 + name = "windows-result" 2186 + version = "0.4.1" 2187 + source = "registry+https://github.com/rust-lang/crates.io-index" 2188 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 2189 + dependencies = [ 2190 + "windows-link", 2191 + ] 2192 + 2193 + [[package]] 2194 + name = "windows-strings" 2195 + version = "0.5.1" 2196 + source = "registry+https://github.com/rust-lang/crates.io-index" 2197 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 2198 + dependencies = [ 2199 + "windows-link", 2200 + ] 2201 + 2202 + [[package]] 2203 + name = "windows-sys" 2204 + version = "0.48.0" 2205 + source = "registry+https://github.com/rust-lang/crates.io-index" 2206 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2207 + dependencies = [ 2208 + "windows-targets 0.48.5", 2209 + ] 2210 + 2211 + [[package]] 2212 + name = "windows-sys" 2213 + version = "0.52.0" 2214 + source = "registry+https://github.com/rust-lang/crates.io-index" 2215 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2216 + dependencies = [ 2217 + "windows-targets 0.52.6", 2218 + ] 2219 + 2220 + [[package]] 2221 + name = "windows-sys" 2222 + version = "0.61.2" 2223 + source = "registry+https://github.com/rust-lang/crates.io-index" 2224 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 2225 + dependencies = [ 2226 + "windows-link", 2227 + ] 2228 + 2229 + [[package]] 2230 + name = "windows-targets" 2231 + version = "0.48.5" 2232 + source = "registry+https://github.com/rust-lang/crates.io-index" 2233 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2234 + dependencies = [ 2235 + "windows_aarch64_gnullvm 0.48.5", 2236 + "windows_aarch64_msvc 0.48.5", 2237 + "windows_i686_gnu 0.48.5", 2238 + "windows_i686_msvc 0.48.5", 2239 + "windows_x86_64_gnu 0.48.5", 2240 + "windows_x86_64_gnullvm 0.48.5", 2241 + "windows_x86_64_msvc 0.48.5", 2242 + ] 2243 + 2244 + [[package]] 2245 + name = "windows-targets" 2246 + version = "0.52.6" 2247 + source = "registry+https://github.com/rust-lang/crates.io-index" 2248 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2249 + dependencies = [ 2250 + "windows_aarch64_gnullvm 0.52.6", 2251 + "windows_aarch64_msvc 0.52.6", 2252 + "windows_i686_gnu 0.52.6", 2253 + "windows_i686_gnullvm", 2254 + "windows_i686_msvc 0.52.6", 2255 + "windows_x86_64_gnu 0.52.6", 2256 + "windows_x86_64_gnullvm 0.52.6", 2257 + "windows_x86_64_msvc 0.52.6", 2258 + ] 2259 + 2260 + [[package]] 2261 + name = "windows_aarch64_gnullvm" 2262 + version = "0.48.5" 2263 + source = "registry+https://github.com/rust-lang/crates.io-index" 2264 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2265 + 2266 + [[package]] 2267 + name = "windows_aarch64_gnullvm" 2268 + version = "0.52.6" 2269 + source = "registry+https://github.com/rust-lang/crates.io-index" 2270 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2271 + 2272 + [[package]] 2273 + name = "windows_aarch64_msvc" 2274 + version = "0.48.5" 2275 + source = "registry+https://github.com/rust-lang/crates.io-index" 2276 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2277 + 2278 + [[package]] 2279 + name = "windows_aarch64_msvc" 2280 + version = "0.52.6" 2281 + source = "registry+https://github.com/rust-lang/crates.io-index" 2282 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2283 + 2284 + [[package]] 2285 + name = "windows_i686_gnu" 2286 + version = "0.48.5" 2287 + source = "registry+https://github.com/rust-lang/crates.io-index" 2288 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2289 + 2290 + [[package]] 2291 + name = "windows_i686_gnu" 2292 + version = "0.52.6" 2293 + source = "registry+https://github.com/rust-lang/crates.io-index" 2294 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2295 + 2296 + [[package]] 2297 + name = "windows_i686_gnullvm" 2298 + version = "0.52.6" 2299 + source = "registry+https://github.com/rust-lang/crates.io-index" 2300 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2301 + 2302 + [[package]] 2303 + name = "windows_i686_msvc" 2304 + version = "0.48.5" 2305 + source = "registry+https://github.com/rust-lang/crates.io-index" 2306 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2307 + 2308 + [[package]] 2309 + name = "windows_i686_msvc" 2310 + version = "0.52.6" 2311 + source = "registry+https://github.com/rust-lang/crates.io-index" 2312 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2313 + 2314 + [[package]] 2315 + name = "windows_x86_64_gnu" 2316 + version = "0.48.5" 2317 + source = "registry+https://github.com/rust-lang/crates.io-index" 2318 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2319 + 2320 + [[package]] 2321 + name = "windows_x86_64_gnu" 2322 + version = "0.52.6" 2323 + source = "registry+https://github.com/rust-lang/crates.io-index" 2324 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2325 + 2326 + [[package]] 2327 + name = "windows_x86_64_gnullvm" 2328 + version = "0.48.5" 2329 + source = "registry+https://github.com/rust-lang/crates.io-index" 2330 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2331 + 2332 + [[package]] 2333 + name = "windows_x86_64_gnullvm" 2334 + version = "0.52.6" 2335 + source = "registry+https://github.com/rust-lang/crates.io-index" 2336 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2337 + 2338 + [[package]] 2339 + name = "windows_x86_64_msvc" 2340 + version = "0.48.5" 2341 + source = "registry+https://github.com/rust-lang/crates.io-index" 2342 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2343 + 2344 + [[package]] 2345 + name = "windows_x86_64_msvc" 2346 + version = "0.52.6" 2347 + source = "registry+https://github.com/rust-lang/crates.io-index" 2348 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2349 + 2350 + [[package]] 2351 + name = "wit-bindgen" 2352 + version = "0.51.0" 2353 + source = "registry+https://github.com/rust-lang/crates.io-index" 2354 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 2355 + dependencies = [ 2356 + "wit-bindgen-rust-macro", 2357 + ] 2358 + 2359 + [[package]] 2360 + name = "wit-bindgen-core" 2361 + version = "0.51.0" 2362 + source = "registry+https://github.com/rust-lang/crates.io-index" 2363 + checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" 2364 + dependencies = [ 2365 + "anyhow", 2366 + "heck", 2367 + "wit-parser", 2368 + ] 2369 + 2370 + [[package]] 2371 + name = "wit-bindgen-rust" 2372 + version = "0.51.0" 2373 + source = "registry+https://github.com/rust-lang/crates.io-index" 2374 + checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" 2375 + dependencies = [ 2376 + "anyhow", 2377 + "heck", 2378 + "indexmap", 2379 + "prettyplease", 2380 + "syn", 2381 + "wasm-metadata", 2382 + "wit-bindgen-core", 2383 + "wit-component", 2384 + ] 2385 + 2386 + [[package]] 2387 + name = "wit-bindgen-rust-macro" 2388 + version = "0.51.0" 2389 + source = "registry+https://github.com/rust-lang/crates.io-index" 2390 + checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" 2391 + dependencies = [ 2392 + "anyhow", 2393 + "prettyplease", 2394 + "proc-macro2", 2395 + "quote", 2396 + "syn", 2397 + "wit-bindgen-core", 2398 + "wit-bindgen-rust", 2399 + ] 2400 + 2401 + [[package]] 2402 + name = "wit-component" 2403 + version = "0.244.0" 2404 + source = "registry+https://github.com/rust-lang/crates.io-index" 2405 + checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" 2406 + dependencies = [ 2407 + "anyhow", 2408 + "bitflags", 2409 + "indexmap", 2410 + "log", 2411 + "serde", 2412 + "serde_derive", 2413 + "serde_json", 2414 + "wasm-encoder", 2415 + "wasm-metadata", 2416 + "wasmparser", 2417 + "wit-parser", 2418 + ] 2419 + 2420 + [[package]] 2421 + name = "wit-parser" 2422 + version = "0.244.0" 2423 + source = "registry+https://github.com/rust-lang/crates.io-index" 2424 + checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" 2425 + dependencies = [ 2426 + "anyhow", 2427 + "id-arena", 2428 + "indexmap", 2429 + "log", 2430 + "semver", 2431 + "serde", 2432 + "serde_derive", 2433 + "serde_json", 2434 + "unicode-xid", 2435 + "wasmparser", 2436 + ] 2437 + 2438 + [[package]] 2439 + name = "writeable" 2440 + version = "0.6.2" 2441 + source = "registry+https://github.com/rust-lang/crates.io-index" 2442 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 2443 + 2444 + [[package]] 2445 + name = "yoke" 2446 + version = "0.8.1" 2447 + source = "registry+https://github.com/rust-lang/crates.io-index" 2448 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 2449 + dependencies = [ 2450 + "stable_deref_trait", 2451 + "yoke-derive", 2452 + "zerofrom", 2453 + ] 2454 + 2455 + [[package]] 2456 + name = "yoke-derive" 2457 + version = "0.8.1" 2458 + source = "registry+https://github.com/rust-lang/crates.io-index" 2459 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 2460 + dependencies = [ 2461 + "proc-macro2", 2462 + "quote", 2463 + "syn", 2464 + "synstructure", 2465 + ] 2466 + 2467 + [[package]] 2468 + name = "zerocopy" 2469 + version = "0.8.42" 2470 + source = "registry+https://github.com/rust-lang/crates.io-index" 2471 + checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" 2472 + dependencies = [ 2473 + "zerocopy-derive", 2474 + ] 2475 + 2476 + [[package]] 2477 + name = "zerocopy-derive" 2478 + version = "0.8.42" 2479 + source = "registry+https://github.com/rust-lang/crates.io-index" 2480 + checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" 2481 + dependencies = [ 2482 + "proc-macro2", 2483 + "quote", 2484 + "syn", 2485 + ] 2486 + 2487 + [[package]] 2488 + name = "zerofrom" 2489 + version = "0.1.6" 2490 + source = "registry+https://github.com/rust-lang/crates.io-index" 2491 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2492 + dependencies = [ 2493 + "zerofrom-derive", 2494 + ] 2495 + 2496 + [[package]] 2497 + name = "zerofrom-derive" 2498 + version = "0.1.6" 2499 + source = "registry+https://github.com/rust-lang/crates.io-index" 2500 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2501 + dependencies = [ 2502 + "proc-macro2", 2503 + "quote", 2504 + "syn", 2505 + "synstructure", 2506 + ] 2507 + 2508 + [[package]] 2509 + name = "zeroize" 2510 + version = "1.8.2" 2511 + source = "registry+https://github.com/rust-lang/crates.io-index" 2512 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 2513 + 2514 + [[package]] 2515 + name = "zerotrie" 2516 + version = "0.2.3" 2517 + source = "registry+https://github.com/rust-lang/crates.io-index" 2518 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 2519 + dependencies = [ 2520 + "displaydoc", 2521 + "yoke", 2522 + "zerofrom", 2523 + ] 2524 + 2525 + [[package]] 2526 + name = "zerovec" 2527 + version = "0.11.5" 2528 + source = "registry+https://github.com/rust-lang/crates.io-index" 2529 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 2530 + dependencies = [ 2531 + "yoke", 2532 + "zerofrom", 2533 + "zerovec-derive", 2534 + ] 2535 + 2536 + [[package]] 2537 + name = "zerovec-derive" 2538 + version = "0.11.2" 2539 + source = "registry+https://github.com/rust-lang/crates.io-index" 2540 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 2541 + dependencies = [ 2542 + "proc-macro2", 2543 + "quote", 2544 + "syn", 2545 + ] 2546 + 2547 + [[package]] 2548 + name = "zmij" 2549 + version = "1.0.21" 2550 + source = "registry+https://github.com/rust-lang/crates.io-index" 2551 + checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+17
api/Cargo.toml
··· 1 + [package] 2 + name = "ayos-api" 3 + version = "0.1.0" 4 + edition = "2021" 5 + 6 + [dependencies] 7 + axum = "0.8" 8 + tokio = { version = "1", features = ["full"] } 9 + sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } 10 + serde = { version = "1", features = ["derive"] } 11 + serde_json = "1" 12 + jsonwebtoken = "9" 13 + argon2 = "0.5" 14 + uuid = { version = "1", features = ["v4"] } 15 + chrono = { version = "0.4", features = ["serde"] } 16 + tower-http = { version = "0.6", features = ["cors", "fs"] } 17 + dotenvy = "0.15"
api/ayos.db

This is a binary file and will not be displayed.

+7
api/migrations/001_create_users.sql
··· 1 + CREATE TABLE IF NOT EXISTS users ( 2 + id TEXT PRIMARY KEY, 3 + username TEXT UNIQUE NOT NULL, 4 + email TEXT UNIQUE NOT NULL, 5 + password_hash TEXT NOT NULL, 6 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 7 + );
+17
api/migrations/002_create_progress.sql
··· 1 + CREATE TABLE IF NOT EXISTS progress ( 2 + user_id TEXT NOT NULL REFERENCES users(id), 3 + topic_id TEXT NOT NULL, 4 + lesson_id TEXT NOT NULL, 5 + completed INTEGER NOT NULL DEFAULT 0, 6 + best_score INTEGER NOT NULL DEFAULT 0, 7 + completed_at TEXT, 8 + PRIMARY KEY (user_id, topic_id, lesson_id) 9 + ); 10 + 11 + CREATE TABLE IF NOT EXISTS user_stats ( 12 + user_id TEXT PRIMARY KEY REFERENCES users(id), 13 + xp INTEGER NOT NULL DEFAULT 0, 14 + streak_days INTEGER NOT NULL DEFAULT 0, 15 + last_active_date TEXT, 16 + hearts INTEGER NOT NULL DEFAULT 5 17 + );
+103
api/src/auth.rs
··· 1 + use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; 2 + use argon2::Argon2; 3 + use axum::extract::FromRequestParts; 4 + use axum::http::request::Parts; 5 + use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 6 + use serde::{Deserialize, Serialize}; 7 + 8 + use crate::config::AppState; 9 + use crate::errors::AppError; 10 + 11 + // --------------------------------------------------------------------------- 12 + // Password hashing 13 + // --------------------------------------------------------------------------- 14 + 15 + pub fn hash_password(password: &str) -> String { 16 + // Use UUID v4 random bytes as salt material (uuid is a direct dependency). 17 + let salt_bytes = uuid::Uuid::new_v4().into_bytes(); 18 + let salt = SaltString::encode_b64(&salt_bytes).expect("Failed to encode salt"); 19 + let argon2 = Argon2::default(); 20 + argon2 21 + .hash_password(password.as_bytes(), &salt) 22 + .expect("Failed to hash password") 23 + .to_string() 24 + } 25 + 26 + pub fn verify_password(password: &str, hash: &str) -> bool { 27 + let parsed = match PasswordHash::new(hash) { 28 + Ok(h) => h, 29 + Err(_) => return false, 30 + }; 31 + Argon2::default() 32 + .verify_password(password.as_bytes(), &parsed) 33 + .is_ok() 34 + } 35 + 36 + // --------------------------------------------------------------------------- 37 + // JWT 38 + // --------------------------------------------------------------------------- 39 + 40 + #[derive(Debug, Serialize, Deserialize)] 41 + struct Claims { 42 + sub: String, 43 + exp: usize, 44 + } 45 + 46 + pub fn create_token(user_id: &str, secret: &str) -> String { 47 + let expiration = chrono::Utc::now() 48 + .checked_add_signed(chrono::Duration::days(30)) 49 + .expect("valid timestamp") 50 + .timestamp() as usize; 51 + 52 + let claims = Claims { 53 + sub: user_id.to_string(), 54 + exp: expiration, 55 + }; 56 + 57 + encode( 58 + &Header::default(), 59 + &claims, 60 + &EncodingKey::from_secret(secret.as_bytes()), 61 + ) 62 + .expect("Failed to create JWT token") 63 + } 64 + 65 + fn decode_token(token: &str, secret: &str) -> Result<String, AppError> { 66 + let data = decode::<Claims>( 67 + token, 68 + &DecodingKey::from_secret(secret.as_bytes()), 69 + &Validation::default(), 70 + ) 71 + .map_err(|e| AppError::Unauthorized(format!("Invalid token: {e}")))?; 72 + 73 + Ok(data.claims.sub) 74 + } 75 + 76 + // --------------------------------------------------------------------------- 77 + // AuthUser extractor 78 + // --------------------------------------------------------------------------- 79 + 80 + /// Extracts the authenticated user's ID from the Authorization header. 81 + pub struct AuthUser(pub String); 82 + 83 + impl FromRequestParts<AppState> for AuthUser { 84 + type Rejection = AppError; 85 + 86 + async fn from_request_parts( 87 + parts: &mut Parts, 88 + state: &AppState, 89 + ) -> Result<Self, Self::Rejection> { 90 + let header = parts 91 + .headers 92 + .get("Authorization") 93 + .and_then(|v| v.to_str().ok()) 94 + .ok_or_else(|| AppError::Unauthorized("Missing Authorization header".to_string()))?; 95 + 96 + let token = header 97 + .strip_prefix("Bearer ") 98 + .ok_or_else(|| AppError::Unauthorized("Invalid Authorization format".to_string()))?; 99 + 100 + let user_id = decode_token(token, &state.jwt_secret)?; 101 + Ok(AuthUser(user_id)) 102 + } 103 + }
+30
api/src/config.rs
··· 1 + use sqlx::SqlitePool; 2 + 3 + #[derive(Clone)] 4 + pub struct AppState { 5 + pub db: SqlitePool, 6 + pub jwt_secret: String, 7 + } 8 + 9 + impl AppState { 10 + pub fn from_env(db: SqlitePool) -> Self { 11 + let jwt_secret = 12 + std::env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret-change-me".to_string()); 13 + Self { db, jwt_secret } 14 + } 15 + } 16 + 17 + pub fn database_url() -> String { 18 + std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:ayos.db".to_string()) 19 + } 20 + 21 + pub fn static_dir() -> Option<String> { 22 + std::env::var("STATIC_DIR").ok() 23 + } 24 + 25 + pub fn port() -> u16 { 26 + std::env::var("PORT") 27 + .ok() 28 + .and_then(|p| p.parse().ok()) 29 + .unwrap_or(3001) 30 + }
+36
api/src/db.rs
··· 1 + use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; 2 + use sqlx::SqlitePool; 3 + use std::str::FromStr; 4 + 5 + pub async fn init_pool(database_url: &str) -> SqlitePool { 6 + let options = SqliteConnectOptions::from_str(database_url) 7 + .expect("Invalid DATABASE_URL") 8 + .create_if_missing(true); 9 + 10 + SqlitePoolOptions::new() 11 + .max_connections(5) 12 + .connect_with(options) 13 + .await 14 + .expect("Failed to create database pool") 15 + } 16 + 17 + pub async fn run_migrations(pool: &SqlitePool) { 18 + let migrations = [ 19 + include_str!("../migrations/001_create_users.sql"), 20 + include_str!("../migrations/002_create_progress.sql"), 21 + ]; 22 + 23 + for sql in &migrations { 24 + // Each migration file may contain multiple statements separated by semicolons. 25 + for statement in sql.split(';') { 26 + let trimmed = statement.trim(); 27 + if trimmed.is_empty() { 28 + continue; 29 + } 30 + sqlx::query(trimmed) 31 + .execute(pool) 32 + .await 33 + .expect("Failed to run migration"); 34 + } 35 + } 36 + }
+42
api/src/errors.rs
··· 1 + use axum::http::StatusCode; 2 + use axum::response::{IntoResponse, Response}; 3 + use serde_json::json; 4 + 5 + #[derive(Debug)] 6 + pub enum AppError { 7 + BadRequest(String), 8 + Unauthorized(String), 9 + NotFound(String), 10 + Internal(String), 11 + } 12 + 13 + impl std::fmt::Display for AppError { 14 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 + match self { 16 + AppError::BadRequest(msg) => write!(f, "Bad request: {msg}"), 17 + AppError::Unauthorized(msg) => write!(f, "Unauthorized: {msg}"), 18 + AppError::NotFound(msg) => write!(f, "Not found: {msg}"), 19 + AppError::Internal(msg) => write!(f, "Internal error: {msg}"), 20 + } 21 + } 22 + } 23 + 24 + impl IntoResponse for AppError { 25 + fn into_response(self) -> Response { 26 + let (status, message) = match &self { 27 + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), 28 + AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()), 29 + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), 30 + AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()), 31 + }; 32 + 33 + let body = axum::Json(json!({ "error": message })); 34 + (status, body).into_response() 35 + } 36 + } 37 + 38 + impl From<sqlx::Error> for AppError { 39 + fn from(err: sqlx::Error) -> Self { 40 + AppError::Internal(err.to_string()) 41 + } 42 + }
+67
api/src/main.rs
··· 1 + mod auth; 2 + mod config; 3 + mod db; 4 + mod errors; 5 + mod models; 6 + mod routes; 7 + 8 + use axum::routing::{get, post}; 9 + use axum::Router; 10 + use tower_http::cors::{Any, CorsLayer}; 11 + use tower_http::services::{ServeDir, ServeFile}; 12 + 13 + #[tokio::main] 14 + async fn main() { 15 + // Load .env file (silently ignore if missing). 16 + let _ = dotenvy::dotenv(); 17 + 18 + let database_url = config::database_url(); 19 + let pool = db::init_pool(&database_url).await; 20 + db::run_migrations(&pool).await; 21 + 22 + let state = config::AppState::from_env(pool); 23 + let port = config::port(); 24 + 25 + let cors = CorsLayer::new() 26 + .allow_origin(Any) 27 + .allow_methods([ 28 + axum::http::Method::GET, 29 + axum::http::Method::POST, 30 + axum::http::Method::PUT, 31 + ]) 32 + .allow_headers([ 33 + axum::http::header::AUTHORIZATION, 34 + axum::http::header::CONTENT_TYPE, 35 + ]); 36 + 37 + let mut app = Router::new() 38 + .route("/api/register", post(routes::auth::register)) 39 + .route("/api/login", post(routes::auth::login)) 40 + .route("/api/me", get(routes::auth::me)) 41 + .route( 42 + "/api/progress", 43 + get(routes::progress::get_progress).put(routes::progress::update_progress), 44 + ) 45 + .layer(cors) 46 + .with_state(state); 47 + 48 + // In production, serve the frontend build as static files with SPA fallback 49 + if let Some(static_dir) = config::static_dir() { 50 + let index = format!("{}/index.html", static_dir); 51 + app = app.fallback_service( 52 + ServeDir::new(&static_dir).fallback(ServeFile::new(index)), 53 + ); 54 + println!("Serving static files from {static_dir}"); 55 + } 56 + 57 + let addr = format!("0.0.0.0:{port}"); 58 + let listener = tokio::net::TcpListener::bind(&addr) 59 + .await 60 + .expect("Failed to bind"); 61 + 62 + println!("Ayos API listening on http://{addr}"); 63 + 64 + axum::serve(listener, app) 65 + .await 66 + .expect("Server error"); 67 + }
+107
api/src/models.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use sqlx::FromRow; 3 + 4 + // --------------------------------------------------------------------------- 5 + // Database models 6 + // --------------------------------------------------------------------------- 7 + 8 + #[derive(Debug, FromRow)] 9 + #[allow(dead_code)] 10 + pub struct User { 11 + pub id: String, 12 + pub username: String, 13 + pub email: String, 14 + pub password_hash: String, 15 + pub created_at: String, 16 + } 17 + 18 + #[derive(Debug, FromRow)] 19 + #[allow(dead_code)] 20 + pub struct Progress { 21 + pub user_id: String, 22 + pub topic_id: String, 23 + pub lesson_id: String, 24 + pub completed: i32, 25 + pub best_score: i32, 26 + pub completed_at: Option<String>, 27 + } 28 + 29 + #[derive(Debug, FromRow)] 30 + #[allow(dead_code)] 31 + pub struct UserStats { 32 + pub user_id: String, 33 + pub xp: i32, 34 + pub streak_days: i32, 35 + pub last_active_date: Option<String>, 36 + pub hearts: i32, 37 + } 38 + 39 + // --------------------------------------------------------------------------- 40 + // Request DTOs 41 + // --------------------------------------------------------------------------- 42 + 43 + #[derive(Debug, Deserialize)] 44 + pub struct RegisterRequest { 45 + pub username: String, 46 + pub email: String, 47 + pub password: String, 48 + } 49 + 50 + #[derive(Debug, Deserialize)] 51 + pub struct LoginRequest { 52 + pub email: String, 53 + pub password: String, 54 + } 55 + 56 + #[derive(Debug, Deserialize)] 57 + pub struct ProgressUpdateRequest { 58 + pub topic_id: String, 59 + pub lesson_id: String, 60 + pub score: i32, 61 + pub xp_earned: i32, 62 + } 63 + 64 + // --------------------------------------------------------------------------- 65 + // Response DTOs 66 + // --------------------------------------------------------------------------- 67 + 68 + #[derive(Debug, Serialize)] 69 + pub struct AuthResponse { 70 + pub token: String, 71 + pub user: UserResponse, 72 + } 73 + 74 + #[derive(Debug, Serialize)] 75 + pub struct UserResponse { 76 + pub id: String, 77 + pub username: String, 78 + pub email: String, 79 + } 80 + 81 + #[derive(Debug, Serialize)] 82 + pub struct MeResponse { 83 + pub user: UserResponse, 84 + pub stats: StatsResponse, 85 + } 86 + 87 + #[derive(Debug, Serialize, Clone)] 88 + pub struct StatsResponse { 89 + pub xp: i32, 90 + pub streak_days: i32, 91 + pub hearts: i32, 92 + } 93 + 94 + #[derive(Debug, Serialize)] 95 + pub struct ProgressResponse { 96 + pub lessons: Vec<LessonProgress>, 97 + pub stats: StatsResponse, 98 + } 99 + 100 + #[derive(Debug, Serialize)] 101 + pub struct LessonProgress { 102 + pub topic_id: String, 103 + pub lesson_id: String, 104 + pub completed: bool, 105 + pub best_score: i32, 106 + pub completed_at: Option<String>, 107 + }
+136
api/src/routes/auth.rs
··· 1 + use axum::extract::State; 2 + use axum::Json; 3 + 4 + use crate::auth::{create_token, hash_password, verify_password, AuthUser}; 5 + use crate::config::AppState; 6 + use crate::errors::AppError; 7 + use crate::models::{ 8 + AuthResponse, LoginRequest, MeResponse, RegisterRequest, StatsResponse, UserResponse, 9 + }; 10 + 11 + // --------------------------------------------------------------------------- 12 + // POST /api/register 13 + // --------------------------------------------------------------------------- 14 + 15 + pub async fn register( 16 + State(state): State<AppState>, 17 + Json(body): Json<RegisterRequest>, 18 + ) -> Result<Json<AuthResponse>, AppError> { 19 + if body.username.is_empty() || body.email.is_empty() || body.password.is_empty() { 20 + return Err(AppError::BadRequest( 21 + "username, email, and password are required".to_string(), 22 + )); 23 + } 24 + 25 + let id = uuid::Uuid::new_v4().to_string(); 26 + let password_hash = hash_password(&body.password); 27 + 28 + // Insert user 29 + sqlx::query( 30 + "INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?)", 31 + ) 32 + .bind(&id) 33 + .bind(&body.username) 34 + .bind(&body.email) 35 + .bind(&password_hash) 36 + .execute(&state.db) 37 + .await 38 + .map_err(|e| { 39 + if e.to_string().contains("UNIQUE") { 40 + AppError::BadRequest("Username or email already taken".to_string()) 41 + } else { 42 + AppError::Internal(e.to_string()) 43 + } 44 + })?; 45 + 46 + // Insert default user_stats row 47 + sqlx::query("INSERT INTO user_stats (user_id) VALUES (?)") 48 + .bind(&id) 49 + .execute(&state.db) 50 + .await?; 51 + 52 + let token = create_token(&id, &state.jwt_secret); 53 + 54 + Ok(Json(AuthResponse { 55 + token, 56 + user: UserResponse { 57 + id, 58 + username: body.username, 59 + email: body.email, 60 + }, 61 + })) 62 + } 63 + 64 + // --------------------------------------------------------------------------- 65 + // POST /api/login 66 + // --------------------------------------------------------------------------- 67 + 68 + pub async fn login( 69 + State(state): State<AppState>, 70 + Json(body): Json<LoginRequest>, 71 + ) -> Result<Json<AuthResponse>, AppError> { 72 + let row = sqlx::query_as::<_, (String, String, String, String)>( 73 + "SELECT id, username, email, password_hash FROM users WHERE email = ?", 74 + ) 75 + .bind(&body.email) 76 + .fetch_optional(&state.db) 77 + .await? 78 + .ok_or_else(|| AppError::Unauthorized("Invalid email or password".to_string()))?; 79 + 80 + let (id, username, email, password_hash) = row; 81 + 82 + if !verify_password(&body.password, &password_hash) { 83 + return Err(AppError::Unauthorized( 84 + "Invalid email or password".to_string(), 85 + )); 86 + } 87 + 88 + let token = create_token(&id, &state.jwt_secret); 89 + 90 + Ok(Json(AuthResponse { 91 + token, 92 + user: UserResponse { 93 + id, 94 + username, 95 + email, 96 + }, 97 + })) 98 + } 99 + 100 + // --------------------------------------------------------------------------- 101 + // GET /api/me 102 + // --------------------------------------------------------------------------- 103 + 104 + pub async fn me( 105 + State(state): State<AppState>, 106 + AuthUser(user_id): AuthUser, 107 + ) -> Result<Json<MeResponse>, AppError> { 108 + let user = sqlx::query_as::<_, (String, String, String)>( 109 + "SELECT id, username, email FROM users WHERE id = ?", 110 + ) 111 + .bind(&user_id) 112 + .fetch_optional(&state.db) 113 + .await? 114 + .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; 115 + 116 + let stats = sqlx::query_as::<_, (i32, i32, i32)>( 117 + "SELECT xp, streak_days, hearts FROM user_stats WHERE user_id = ?", 118 + ) 119 + .bind(&user_id) 120 + .fetch_optional(&state.db) 121 + .await? 122 + .ok_or_else(|| AppError::NotFound("User stats not found".to_string()))?; 123 + 124 + Ok(Json(MeResponse { 125 + user: UserResponse { 126 + id: user.0, 127 + username: user.1, 128 + email: user.2, 129 + }, 130 + stats: StatsResponse { 131 + xp: stats.0, 132 + streak_days: stats.1, 133 + hearts: stats.2, 134 + }, 135 + })) 136 + }
+2
api/src/routes/mod.rs
··· 1 + pub mod auth; 2 + pub mod progress;
+153
api/src/routes/progress.rs
··· 1 + use axum::extract::State; 2 + use axum::Json; 3 + use chrono::Utc; 4 + 5 + use crate::auth::AuthUser; 6 + use crate::config::AppState; 7 + use crate::errors::AppError; 8 + use crate::models::{ 9 + LessonProgress, Progress, ProgressResponse, ProgressUpdateRequest, StatsResponse, UserStats, 10 + }; 11 + 12 + // --------------------------------------------------------------------------- 13 + // GET /api/progress 14 + // --------------------------------------------------------------------------- 15 + 16 + pub async fn get_progress( 17 + State(state): State<AppState>, 18 + AuthUser(user_id): AuthUser, 19 + ) -> Result<Json<ProgressResponse>, AppError> { 20 + let rows = sqlx::query_as::<_, Progress>( 21 + "SELECT user_id, topic_id, lesson_id, completed, best_score, completed_at \ 22 + FROM progress WHERE user_id = ?", 23 + ) 24 + .bind(&user_id) 25 + .fetch_all(&state.db) 26 + .await?; 27 + 28 + let stats = sqlx::query_as::<_, UserStats>( 29 + "SELECT user_id, xp, streak_days, last_active_date, hearts \ 30 + FROM user_stats WHERE user_id = ?", 31 + ) 32 + .bind(&user_id) 33 + .fetch_optional(&state.db) 34 + .await? 35 + .ok_or_else(|| AppError::NotFound("User stats not found".to_string()))?; 36 + 37 + let lessons: Vec<LessonProgress> = rows 38 + .into_iter() 39 + .map(|p| LessonProgress { 40 + topic_id: p.topic_id, 41 + lesson_id: p.lesson_id, 42 + completed: p.completed != 0, 43 + best_score: p.best_score, 44 + completed_at: p.completed_at, 45 + }) 46 + .collect(); 47 + 48 + Ok(Json(ProgressResponse { 49 + lessons, 50 + stats: StatsResponse { 51 + xp: stats.xp, 52 + streak_days: stats.streak_days, 53 + hearts: stats.hearts, 54 + }, 55 + })) 56 + } 57 + 58 + // --------------------------------------------------------------------------- 59 + // PUT /api/progress 60 + // --------------------------------------------------------------------------- 61 + 62 + pub async fn update_progress( 63 + State(state): State<AppState>, 64 + AuthUser(user_id): AuthUser, 65 + Json(body): Json<ProgressUpdateRequest>, 66 + ) -> Result<Json<StatsResponse>, AppError> { 67 + let today = Utc::now().format("%Y-%m-%d").to_string(); 68 + let now_iso = Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string(); 69 + 70 + // Upsert progress row. 71 + // If the row already exists, only update best_score when the new score is higher. 72 + let existing = sqlx::query_as::<_, Progress>( 73 + "SELECT user_id, topic_id, lesson_id, completed, best_score, completed_at \ 74 + FROM progress WHERE user_id = ? AND topic_id = ? AND lesson_id = ?", 75 + ) 76 + .bind(&user_id) 77 + .bind(&body.topic_id) 78 + .bind(&body.lesson_id) 79 + .fetch_optional(&state.db) 80 + .await?; 81 + 82 + match existing { 83 + Some(prev) => { 84 + let new_best = if body.score > prev.best_score { 85 + body.score 86 + } else { 87 + prev.best_score 88 + }; 89 + sqlx::query( 90 + "UPDATE progress SET best_score = ?, completed = 1, completed_at = ? \ 91 + WHERE user_id = ? AND topic_id = ? AND lesson_id = ?", 92 + ) 93 + .bind(new_best) 94 + .bind(&now_iso) 95 + .bind(&user_id) 96 + .bind(&body.topic_id) 97 + .bind(&body.lesson_id) 98 + .execute(&state.db) 99 + .await?; 100 + } 101 + None => { 102 + sqlx::query( 103 + "INSERT INTO progress (user_id, topic_id, lesson_id, completed, best_score, completed_at) \ 104 + VALUES (?, ?, ?, 1, ?, ?)", 105 + ) 106 + .bind(&user_id) 107 + .bind(&body.topic_id) 108 + .bind(&body.lesson_id) 109 + .bind(body.score) 110 + .bind(&now_iso) 111 + .execute(&state.db) 112 + .await?; 113 + } 114 + } 115 + 116 + // Fetch current stats for streak calculation. 117 + let stats = sqlx::query_as::<_, UserStats>( 118 + "SELECT user_id, xp, streak_days, last_active_date, hearts \ 119 + FROM user_stats WHERE user_id = ?", 120 + ) 121 + .bind(&user_id) 122 + .fetch_one(&state.db) 123 + .await?; 124 + 125 + // Streak logic. 126 + let yesterday = (Utc::now() - chrono::Duration::days(1)) 127 + .format("%Y-%m-%d") 128 + .to_string(); 129 + 130 + let new_streak = match stats.last_active_date.as_deref() { 131 + Some(d) if d == today => stats.streak_days, // already active today, no-op 132 + Some(d) if d == yesterday => stats.streak_days + 1, 133 + _ => 1, // first time or gap in activity 134 + }; 135 + 136 + let new_xp = stats.xp + body.xp_earned; 137 + 138 + sqlx::query( 139 + "UPDATE user_stats SET xp = ?, streak_days = ?, last_active_date = ? WHERE user_id = ?", 140 + ) 141 + .bind(new_xp) 142 + .bind(new_streak) 143 + .bind(&today) 144 + .bind(&user_id) 145 + .execute(&state.db) 146 + .await?; 147 + 148 + Ok(Json(StatsResponse { 149 + xp: new_xp, 150 + streak_days: new_streak, 151 + hearts: stats.hearts, 152 + })) 153 + }
+25
compose.yml
··· 1 + services: 2 + ayos: 3 + image: pierrelf/ayos:latest 4 + container_name: ayos 5 + ports: 6 + - "8882:3000" 7 + restart: unless-stopped 8 + volumes: 9 + - ~/ayos/data:/app/data 10 + environment: 11 + - JWT_SECRET=${JWT_SECRET:-change-me-in-production} 12 + labels: 13 + - wud.watch=true 14 + 15 + wud: 16 + image: getwud/wud 17 + container_name: wud-ayos 18 + restart: unless-stopped 19 + volumes: 20 + - /var/run/docker.sock:/var/run/docker.sock 21 + environment: 22 + - WUD_WATCHER_LOCAL_WATCHBYDEFAULT=false 23 + - WUD_WATCHER_LOCAL_CRON=0 */5 * * * * 24 + - WUD_TRIGGER_DOCKER_LOCAL_AUTO=true 25 + - WUD_TRIGGER_DOCKER_LOCAL_PRUNE=true
+61
content/basics/lesson-01.json
··· 1 + { 2 + "id": "lesson-01", 3 + "topicId": "basics", 4 + "title": "Pronouns & Questions", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Ako' mean?", 10 + "promptAudio": true, 11 + "choices": ["I / Me", "You", "He / She", "We"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "matching-pairs", 16 + "prompt": "Match the Tagalog pronouns with their English translations", 17 + "pairs": [ 18 + { "left": "Ako", "right": "I / Me" }, 19 + { "left": "Ikaw", "right": "You" }, 20 + { "left": "Siya", "right": "He / She" }, 21 + { "left": "Tayo", "right": "We (inclusive)" } 22 + ] 23 + }, 24 + { 25 + "type": "translation", 26 + "prompt": "Translate to English: 'Ano ang pangalan mo?'", 27 + "promptAudio": true, 28 + "acceptedAnswers": ["What is your name?", "what is your name?", "What's your name?", "what's your name?"], 29 + "hint": "'Ano' means 'what' and 'pangalan' means 'name'" 30 + }, 31 + { 32 + "type": "fill-in-the-blank", 33 + "prompt": "Complete the sentence: asking 'what'", 34 + "sentence": "___ ito?", 35 + "blank": "Ano", 36 + "hint": "A question word meaning 'what'", 37 + "wordBank": ["Ano", "Saan", "Kailan", "Bakit"] 38 + }, 39 + { 40 + "type": "multiple-choice", 41 + "prompt": "How do you say 'Where?' in Tagalog?", 42 + "promptAudio": false, 43 + "choices": ["Saan", "Ano", "Kailan", "Sino"], 44 + "correctIndex": 0 45 + }, 46 + { 47 + "type": "speak", 48 + "prompt": "Say this question in Tagalog", 49 + "phrase": "Ano ang pangalan mo?", 50 + "promptAudio": true, 51 + "acceptedAnswers": ["ano ang pangalan mo"] 52 + }, 53 + { 54 + "type": "translation", 55 + "prompt": "Translate to Tagalog: 'Who is that?'", 56 + "promptAudio": false, 57 + "acceptedAnswers": ["Sino iyan?", "sino iyan?", "Sino 'yan?", "sino 'yan?"], 58 + "hint": "'Who' is 'Sino' and 'that' is 'iyan'" 59 + } 60 + ] 61 + }
+68
content/basics/lesson-02.json
··· 1 + { 2 + "id": "lesson-02", 3 + "topicId": "basics", 4 + "title": "Descriptions & Adjectives", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Mabuti' mean?", 10 + "promptAudio": true, 11 + "choices": ["Good", "Bad", "Big", "Small"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "matching-pairs", 16 + "prompt": "Match the Tagalog adjectives with their English translations", 17 + "pairs": [ 18 + { "left": "Mabuti", "right": "Good" }, 19 + { "left": "Masama", "right": "Bad" }, 20 + { "left": "Malaki", "right": "Big" }, 21 + { "left": "Maliit", "right": "Small" } 22 + ] 23 + }, 24 + { 25 + "type": "translation", 26 + "prompt": "Translate to English: 'Ito ay maliit'", 27 + "promptAudio": true, 28 + "acceptedAnswers": ["This is small", "this is small", "It is small", "it is small"], 29 + "hint": "'Ito' means 'this' and 'maliit' is an adjective for size" 30 + }, 31 + { 32 + "type": "fill-in-the-blank", 33 + "prompt": "Complete the sentence describing something big", 34 + "sentence": "___ ang bahay.", 35 + "blank": "Malaki", 36 + "hint": "The opposite of 'maliit' (small)", 37 + "wordBank": ["Malaki", "Maliit", "Mabuti", "Masama"] 38 + }, 39 + { 40 + "type": "multiple-choice", 41 + "prompt": "What is the Tagalog word for 'this'?", 42 + "promptAudio": false, 43 + "choices": ["Ito", "Iyan", "Siya", "Ano"], 44 + "correctIndex": 0 45 + }, 46 + { 47 + "type": "speak", 48 + "prompt": "Say this phrase in Tagalog", 49 + "phrase": "Mabuti naman, salamat.", 50 + "promptAudio": true, 51 + "acceptedAnswers": ["mabuti naman salamat", "mabuti naman, salamat"] 52 + }, 53 + { 54 + "type": "translation", 55 + "prompt": "Translate to Tagalog: 'That is bad'", 56 + "promptAudio": false, 57 + "acceptedAnswers": ["Iyan ay masama", "iyan ay masama", "Masama iyan", "masama iyan"], 58 + "hint": "'That' is 'iyan' and 'bad' starts with 'ma-'" 59 + }, 60 + { 61 + "type": "multiple-choice", 62 + "prompt": "What does 'Bakit?' mean?", 63 + "promptAudio": true, 64 + "choices": ["Why?", "What?", "When?", "Where?"], 65 + "correctIndex": 0 66 + } 67 + ] 68 + }
+71
content/family/lesson-01.json
··· 1 + { 2 + "id": "lesson-01", 3 + "topicId": "family", 4 + "title": "Family Members", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Nanay' mean?", 10 + "promptAudio": true, 11 + "choices": ["Mother", "Father", "Grandmother", "Sister"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "matching-pairs", 16 + "prompt": "Match the Tagalog family words with their English translations", 17 + "pairs": [ 18 + { "left": "Nanay", "right": "Mother" }, 19 + { "left": "Tatay", "right": "Father" }, 20 + { "left": "Lolo", "right": "Grandfather" }, 21 + { "left": "Lola", "right": "Grandmother" } 22 + ] 23 + }, 24 + { 25 + "type": "translation", 26 + "prompt": "Translate to English: 'Siya ang kuya ko'", 27 + "promptAudio": true, 28 + "acceptedAnswers": ["He is my older brother", "he is my older brother", "He's my older brother", "he's my older brother"], 29 + "hint": "'Kuya' is what you call an older brother" 30 + }, 31 + { 32 + "type": "fill-in-the-blank", 33 + "prompt": "Complete the sentence about the youngest child", 34 + "sentence": "Siya ang ___ ng pamilya.", 35 + "blank": "bunso", 36 + "hint": "The word for the youngest sibling", 37 + "wordBank": ["bunso", "kuya", "ate", "anak"] 38 + }, 39 + { 40 + "type": "multiple-choice", 41 + "prompt": "What do you call your older sister in Tagalog?", 42 + "promptAudio": false, 43 + "choices": ["Ate", "Kuya", "Nanay", "Bunso"], 44 + "correctIndex": 0 45 + }, 46 + { 47 + "type": "speak", 48 + "prompt": "Say this phrase in Tagalog", 49 + "phrase": "Mahal ko ang pamilya ko.", 50 + "promptAudio": true, 51 + "acceptedAnswers": ["mahal ko ang pamilya ko"] 52 + }, 53 + { 54 + "type": "matching-pairs", 55 + "prompt": "Match these family relationship words", 56 + "pairs": [ 57 + { "left": "Kuya", "right": "Older brother" }, 58 + { "left": "Ate", "right": "Older sister" }, 59 + { "left": "Kapatid", "right": "Sibling" }, 60 + { "left": "Anak", "right": "Child" } 61 + ] 62 + }, 63 + { 64 + "type": "translation", 65 + "prompt": "Translate to Tagalog: 'My grandmother is kind'", 66 + "promptAudio": false, 67 + "acceptedAnswers": ["Mabait ang lola ko", "mabait ang lola ko", "Ang lola ko ay mabait", "ang lola ko ay mabait"], 68 + "hint": "'Grandmother' is 'Lola' and 'kind' is 'mabait'" 69 + } 70 + ] 71 + }
+71
content/food/lesson-01.json
··· 1 + { 2 + "id": "lesson-01", 3 + "topicId": "food", 4 + "title": "Food & Drinks", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Kanin' mean?", 10 + "promptAudio": true, 11 + "choices": ["Rice", "Bread", "Water", "Coffee"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "matching-pairs", 16 + "prompt": "Match the Tagalog food words with their English translations", 17 + "pairs": [ 18 + { "left": "Kanin", "right": "Rice" }, 19 + { "left": "Tubig", "right": "Water" }, 20 + { "left": "Kape", "right": "Coffee" }, 21 + { "left": "Tinapay", "right": "Bread" } 22 + ] 23 + }, 24 + { 25 + "type": "translation", 26 + "prompt": "Translate to English: 'Gutom na ako'", 27 + "promptAudio": true, 28 + "acceptedAnswers": ["I am hungry", "I'm hungry", "i am hungry", "i'm hungry", "I am already hungry", "I'm already hungry"], 29 + "hint": "'Gutom' means 'hungry' and 'ako' means 'I'" 30 + }, 31 + { 32 + "type": "fill-in-the-blank", 33 + "prompt": "Complete the sentence about a delicious meal", 34 + "sentence": "___ ang ulam!", 35 + "blank": "Masarap", 36 + "hint": "An adjective meaning 'delicious'", 37 + "wordBank": ["Masarap", "Masama", "Malaki", "Maliit"] 38 + }, 39 + { 40 + "type": "multiple-choice", 41 + "prompt": "What is the Tagalog word for 'chicken'?", 42 + "promptAudio": false, 43 + "choices": ["Manok", "Isda", "Gulay", "Kanin"], 44 + "correctIndex": 0 45 + }, 46 + { 47 + "type": "speak", 48 + "prompt": "Say this phrase in Tagalog", 49 + "phrase": "Masarap ang pagkain!", 50 + "promptAudio": true, 51 + "acceptedAnswers": ["masarap ang pagkain"] 52 + }, 53 + { 54 + "type": "matching-pairs", 55 + "prompt": "Match these food-related words", 56 + "pairs": [ 57 + { "left": "Isda", "right": "Fish" }, 58 + { "left": "Manok", "right": "Chicken" }, 59 + { "left": "Gulay", "right": "Vegetables" }, 60 + { "left": "Uhaw", "right": "Thirsty" } 61 + ] 62 + }, 63 + { 64 + "type": "translation", 65 + "prompt": "Translate to Tagalog: 'I am thirsty'", 66 + "promptAudio": false, 67 + "acceptedAnswers": ["Uhaw na ako", "uhaw na ako", "Uhaw ako", "uhaw ako"], 68 + "hint": "'Thirsty' in Tagalog starts with 'U'" 69 + } 70 + ] 71 + }
+61
content/greetings/lesson-01.json
··· 1 + { 2 + "id": "lesson-01", 3 + "topicId": "greetings", 4 + "title": "Hello & Goodbye", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Kamusta' mean?", 10 + "promptAudio": true, 11 + "choices": ["How are you", "Goodbye", "Thank you", "Good night"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "translation", 16 + "prompt": "Translate to English: 'Magandang umaga'", 17 + "promptAudio": true, 18 + "acceptedAnswers": ["Good morning", "good morning"], 19 + "hint": "A greeting you say when the sun has just risen" 20 + }, 21 + { 22 + "type": "matching-pairs", 23 + "prompt": "Match the Tagalog greetings with their English translations", 24 + "pairs": [ 25 + { "left": "Kamusta", "right": "How are you" }, 26 + { "left": "Paalam", "right": "Goodbye" }, 27 + { "left": "Magandang umaga", "right": "Good morning" }, 28 + { "left": "Magandang gabi", "right": "Good evening" } 29 + ] 30 + }, 31 + { 32 + "type": "fill-in-the-blank", 33 + "prompt": "Complete the evening greeting", 34 + "sentence": "Magandang ___ po!", 35 + "blank": "gabi", 36 + "hint": "evening / night", 37 + "wordBank": ["umaga", "gabi", "hapon", "araw"] 38 + }, 39 + { 40 + "type": "multiple-choice", 41 + "prompt": "How do you say 'Good afternoon' in Tagalog?", 42 + "promptAudio": false, 43 + "choices": ["Magandang hapon", "Magandang umaga", "Magandang gabi", "Magandang araw"], 44 + "correctIndex": 0 45 + }, 46 + { 47 + "type": "speak", 48 + "prompt": "Say this greeting in Tagalog", 49 + "phrase": "Kamusta ka?", 50 + "promptAudio": true, 51 + "acceptedAnswers": ["kamusta ka", "kumusta ka"] 52 + }, 53 + { 54 + "type": "translation", 55 + "prompt": "Translate to Tagalog: 'Goodbye'", 56 + "promptAudio": false, 57 + "acceptedAnswers": ["Paalam", "paalam"], 58 + "hint": "This is a farewell word" 59 + } 60 + ] 61 + }
+68
content/greetings/lesson-02.json
··· 1 + { 2 + "id": "lesson-02", 3 + "topicId": "greetings", 4 + "title": "Polite Expressions", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Salamat' mean?", 10 + "promptAudio": true, 11 + "choices": ["Thank you", "Please", "Sorry", "Excuse me"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "translation", 16 + "prompt": "Translate to English: 'Oo'", 17 + "promptAudio": true, 18 + "acceptedAnswers": ["Yes", "yes"], 19 + "hint": "The opposite of 'no'" 20 + }, 21 + { 22 + "type": "matching-pairs", 23 + "prompt": "Match the Tagalog words with their English translations", 24 + "pairs": [ 25 + { "left": "Oo", "right": "Yes" }, 26 + { "left": "Hindi", "right": "No" }, 27 + { "left": "Salamat", "right": "Thank you" }, 28 + { "left": "Opo", "right": "Yes (respectful)" } 29 + ] 30 + }, 31 + { 32 + "type": "multiple-choice", 33 + "prompt": "'Po' and 'Opo' are used to show what?", 34 + "promptAudio": false, 35 + "choices": ["Respect to elders", "Anger", "Excitement", "Sadness"], 36 + "correctIndex": 0 37 + }, 38 + { 39 + "type": "fill-in-the-blank", 40 + "prompt": "Complete the polite response", 41 + "sentence": "Salamat ___!", 42 + "blank": "po", 43 + "hint": "A word added for politeness and respect", 44 + "wordBank": ["po", "ko", "mo", "ba"] 45 + }, 46 + { 47 + "type": "speak", 48 + "prompt": "Say this polite phrase in Tagalog", 49 + "phrase": "Salamat po", 50 + "promptAudio": true, 51 + "acceptedAnswers": ["salamat po"] 52 + }, 53 + { 54 + "type": "translation", 55 + "prompt": "Translate to English: 'Kumain ka na ba?'", 56 + "promptAudio": true, 57 + "acceptedAnswers": ["Have you eaten?", "Have you eaten already?", "have you eaten?", "have you eaten already?", "Did you eat already?", "did you eat already?"], 58 + "hint": "A common Filipino greeting that asks about a meal" 59 + }, 60 + { 61 + "type": "multiple-choice", 62 + "prompt": "Which phrase is a common Filipino greeting that literally asks if you've eaten?", 63 + "promptAudio": false, 64 + "choices": ["Kumain ka na ba?", "Kamusta ka?", "Saan ka pupunta?", "Magandang araw!"], 65 + "correctIndex": 0 66 + } 67 + ] 68 + }
+72
content/numbers/lesson-01.json
··· 1 + { 2 + "id": "lesson-01", 3 + "topicId": "numbers", 4 + "title": "Counting 1-10", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Isa' mean?", 10 + "promptAudio": true, 11 + "choices": ["One", "Two", "Three", "Ten"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "matching-pairs", 16 + "prompt": "Match the Tagalog numbers with their values", 17 + "pairs": [ 18 + { "left": "Isa", "right": "1" }, 19 + { "left": "Dalawa", "right": "2" }, 20 + { "left": "Tatlo", "right": "3" }, 21 + { "left": "Apat", "right": "4" } 22 + ] 23 + }, 24 + { 25 + "type": "fill-in-the-blank", 26 + "prompt": "Complete the counting sequence: Isa, Dalawa, ___", 27 + "sentence": "Isa, Dalawa, ___", 28 + "blank": "Tatlo", 29 + "hint": "The number 3", 30 + "wordBank": ["Tatlo", "Apat", "Lima", "Anim"] 31 + }, 32 + { 33 + "type": "matching-pairs", 34 + "prompt": "Match the higher Tagalog numbers with their values", 35 + "pairs": [ 36 + { "left": "Lima", "right": "5" }, 37 + { "left": "Anim", "right": "6" }, 38 + { "left": "Pito", "right": "7" }, 39 + { "left": "Sampu", "right": "10" } 40 + ] 41 + }, 42 + { 43 + "type": "translation", 44 + "prompt": "Translate to English: 'Magkano ito?'", 45 + "promptAudio": true, 46 + "acceptedAnswers": ["How much is this?", "how much is this?", "How much?", "how much?"], 47 + "hint": "'Magkano' is used when asking about price" 48 + }, 49 + { 50 + "type": "multiple-choice", 51 + "prompt": "What number is 'Walo'?", 52 + "promptAudio": true, 53 + "choices": ["8", "5", "9", "7"], 54 + "correctIndex": 0 55 + }, 56 + { 57 + "type": "speak", 58 + "prompt": "Count from one to five in Tagalog", 59 + "phrase": "Isa, dalawa, tatlo, apat, lima", 60 + "promptAudio": true, 61 + "acceptedAnswers": ["isa dalawa tatlo apat lima", "isa, dalawa, tatlo, apat, lima"] 62 + }, 63 + { 64 + "type": "fill-in-the-blank", 65 + "prompt": "Complete the sequence: Anim, Pito, Walo, ___", 66 + "sentence": "Anim, Pito, Walo, ___", 67 + "blank": "Siyam", 68 + "hint": "The number 9", 69 + "wordBank": ["Siyam", "Sampu", "Lima", "Tatlo"] 70 + } 71 + ] 72 + }
+52
content/topics.json
··· 1 + { 2 + "topics": [ 3 + { 4 + "id": "greetings", 5 + "name": "Greetings", 6 + "icon": "👋", 7 + "description": "Learn basic Tagalog greetings and introductions", 8 + "prerequisites": [], 9 + "lessons": ["lesson-01", "lesson-02"] 10 + }, 11 + { 12 + "id": "basics", 13 + "name": "Basics", 14 + "icon": "📚", 15 + "description": "Essential words and phrases", 16 + "prerequisites": ["greetings"], 17 + "lessons": ["lesson-01", "lesson-02"] 18 + }, 19 + { 20 + "id": "food", 21 + "name": "Food", 22 + "icon": "🍜", 23 + "description": "Food, drinks, and dining vocabulary", 24 + "prerequisites": ["basics"], 25 + "lessons": ["lesson-01"] 26 + }, 27 + { 28 + "id": "family", 29 + "name": "Family", 30 + "icon": "👨‍👩‍👧‍👦", 31 + "description": "Family members and relationships", 32 + "prerequisites": ["basics"], 33 + "lessons": ["lesson-01"] 34 + }, 35 + { 36 + "id": "numbers", 37 + "name": "Numbers", 38 + "icon": "🔢", 39 + "description": "Counting and numbers in Tagalog", 40 + "prerequisites": ["basics"], 41 + "lessons": ["lesson-01"] 42 + }, 43 + { 44 + "id": "travel", 45 + "name": "Travel", 46 + "icon": "✈️", 47 + "description": "Travel phrases and directions", 48 + "prerequisites": ["food", "numbers"], 49 + "lessons": ["lesson-01"] 50 + } 51 + ] 52 + }
+71
content/travel/lesson-01.json
··· 1 + { 2 + "id": "lesson-01", 3 + "topicId": "travel", 4 + "title": "Getting Around", 5 + "xpReward": 10, 6 + "exercises": [ 7 + { 8 + "type": "multiple-choice", 9 + "prompt": "What does 'Saan' mean?", 10 + "promptAudio": true, 11 + "choices": ["Where", "When", "What", "Who"], 12 + "correctIndex": 0 13 + }, 14 + { 15 + "type": "matching-pairs", 16 + "prompt": "Match the Tagalog direction words with their English translations", 17 + "pairs": [ 18 + { "left": "Kanan", "right": "Right" }, 19 + { "left": "Kaliwa", "right": "Left" }, 20 + { "left": "Deretso", "right": "Straight" }, 21 + { "left": "Malapit", "right": "Near" } 22 + ] 23 + }, 24 + { 25 + "type": "translation", 26 + "prompt": "Translate to English: 'Saan ang istasyon?'", 27 + "promptAudio": true, 28 + "acceptedAnswers": ["Where is the station?", "where is the station?"], 29 + "hint": "'Saan' means 'where' and 'istasyon' sounds like its English equivalent" 30 + }, 31 + { 32 + "type": "fill-in-the-blank", 33 + "prompt": "Ask where something is located", 34 + "sentence": "___ ang palengke?", 35 + "blank": "Saan", 36 + "hint": "The question word for 'where'", 37 + "wordBank": ["Saan", "Ano", "Kailan", "Bakit"] 38 + }, 39 + { 40 + "type": "multiple-choice", 41 + "prompt": "What is the Tagalog word for 'far'?", 42 + "promptAudio": false, 43 + "choices": ["Malayo", "Malapit", "Malaki", "Maliit"], 44 + "correctIndex": 0 45 + }, 46 + { 47 + "type": "speak", 48 + "prompt": "Say this direction in Tagalog", 49 + "phrase": "Kumaliwa ka, tapos deretso.", 50 + "promptAudio": true, 51 + "acceptedAnswers": ["kumaliwa ka tapos deretso", "kumaliwa ka, tapos deretso"] 52 + }, 53 + { 54 + "type": "matching-pairs", 55 + "prompt": "Match these travel-related words", 56 + "pairs": [ 57 + { "left": "Sasakyan", "right": "Vehicle" }, 58 + { "left": "Eroplano", "right": "Airplane" }, 59 + { "left": "Dito", "right": "Here" }, 60 + { "left": "Doon", "right": "There" } 61 + ] 62 + }, 63 + { 64 + "type": "translation", 65 + "prompt": "Translate to Tagalog: 'The station is near'", 66 + "promptAudio": false, 67 + "acceptedAnswers": ["Malapit ang istasyon", "malapit ang istasyon", "Ang istasyon ay malapit", "ang istasyon ay malapit"], 68 + "hint": "'Near' is 'malapit' and 'station' is 'istasyon'" 69 + } 70 + ] 71 + }
+238
implementor
··· 1 + #!/usr/bin/env -S uv run --script 2 + # /// script 3 + # requires-python = ">=3.11" 4 + # /// 5 + """ 6 + Autonomous implementation loop for Ayos. 7 + 8 + Runs `claude --dangerously-skip-permissions` in a loop, each iteration picking 9 + up and completing one task (PR review, issue implementation, or deploy cycle). 10 + Sleeps when idle, never exits on its own. 11 + """ 12 + 13 + import subprocess 14 + import time 15 + 16 + REPO = "pierrelf.com/ayos" 17 + GIT_ROOT = "/Users/piefev/misc/ayos" 18 + 19 + PROMPT = r""" 20 + You are an autonomous implementation agent for the Ayos project (a Duolingo-style Tagalog learning app). 21 + The git root is at /Users/piefev/misc/ayos. Always start by reading /Users/piefev/misc/ayos/CLAUDE.md if it exists. 22 + 23 + The project is hosted on Tangled (not GitHub). Use the `tangled` CLI for all issue/PR operations. 24 + Repo identifier: pierrelf.com/ayos 25 + 26 + Your job is to pick ONE task, complete it fully, and then exit. Follow this priority order: 27 + 28 + --- 29 + 30 + ## Priority 1: Open Pull Requests 31 + 32 + Check for open PRs: 33 + ``` 34 + tangled pr list --repo pierrelf.com/ayos 35 + ``` 36 + 37 + If there are open PRs, pick the first one and: 38 + 39 + 1. Read the PR details: `tangled pr show <rkey>` 40 + 2. Read the diff: `tangled pr show <rkey> --diff` 41 + 3. Read comments: `tangled pr show <rkey> --comments` 42 + 4. Fetch and create a local worktree to review: 43 + ``` 44 + cd /Users/piefev/misc/ayos 45 + git fetch origin 46 + ``` 47 + Find the PR's branch from the PR details, then: 48 + ``` 49 + git worktree add ../ayos-pr-review <branch> 50 + ``` 51 + 5. Review the pull request meticulously. Read every changed file. Check for: 52 + - Correctness and logic errors 53 + - Style and convention violations 54 + - Missing error handling 55 + - Dead code, unused imports 56 + - Type safety issues 57 + - Any regressions 58 + 6. Fix any issues you find by editing files in the worktree. 59 + 7. Run the completion checklist: 60 + ``` 61 + cd ../ayos-pr-review 62 + cd api && cargo fmt && cargo clippy -- -D warnings && cd .. 63 + cd web && npx tsc --noEmit && cd .. 64 + ``` 65 + 8. Commit and push fixes if any. 66 + 9. If everything passes and the code is good, merge it: 67 + ``` 68 + tangled pr merge <rkey> 69 + ``` 70 + 10. Clean up: 71 + ``` 72 + cd /Users/piefev/misc/ayos 73 + git worktree remove ../ayos-pr-review 2>/dev/null 74 + git pull 75 + ``` 76 + 77 + After completing the PR review, exit. 78 + 79 + --- 80 + 81 + ## Priority 2: Open Issues 82 + 83 + Check for open issues: 84 + ``` 85 + tangled issue list --repo pierrelf.com/ayos 86 + ``` 87 + 88 + If there are open issues, pick the FIRST one listed (oldest) and implement it: 89 + 90 + 1. Read the issue details: 91 + ``` 92 + tangled issue show <rkey> 93 + tangled issue show <rkey> --comments 94 + ``` 95 + 2. Understand the task described in the issue. 96 + 3. Derive a short branch name from the issue title (e.g., "Add dark mode toggle" -> "dark-mode-toggle"). 97 + 4. Create a worktree: 98 + ``` 99 + cd /Users/piefev/misc/ayos 100 + git checkout main && git pull 101 + git worktree add ../ayos-<branch> -b <branch> 102 + ``` 103 + 5. Implement the issue in the worktree. 104 + 6. Run the full completion checklist: 105 + ``` 106 + cd ../ayos-<branch> 107 + cd api && cargo fmt && cargo clippy -- -D warnings && cd .. 108 + cd web && npx tsc --noEmit && cd .. 109 + ``` 110 + 7. Commit all changes with a descriptive message. 111 + 8. Push the branch: 112 + ``` 113 + git push -u origin <branch> 114 + ``` 115 + 9. Create a PR referencing the issue: 116 + ``` 117 + tangled pr create --repo pierrelf.com/ayos --head <branch> --base main --title "<issue title>" --body "Implements issue <rkey>" 118 + ``` 119 + 10. Clean up the worktree: 120 + ``` 121 + cd /Users/piefev/misc/ayos 122 + git worktree remove ../ayos-<branch> 123 + ``` 124 + 125 + After creating the PR, exit. 126 + 127 + --- 128 + 129 + ## Priority 3: Deploy Cycle 130 + 131 + If there are no open PRs AND no open issues, run a deploy cycle: 132 + 133 + 1. Pull main: 134 + ``` 135 + cd /Users/piefev/misc/ayos 136 + git checkout main && git pull 137 + ``` 138 + 2. Build and push the Docker image: 139 + ``` 140 + cd /Users/piefev/misc/ayos 141 + make 142 + ``` 143 + 3. Wait 5 minutes for the production server to pull the new image: 144 + ``` 145 + sleep 300 146 + ``` 147 + 4. Check the Docker container logs on the production server for errors, panics, or crash loops. 148 + 5. If there are issues, create new issues on Tangled describing how to fix them: 149 + ``` 150 + tangled issue create --repo pierrelf.com/ayos --title "..." --body "..." 151 + ``` 152 + 153 + If new issues were created, exit (next iteration will pick them up). 154 + 155 + If no new issues were created (deploy is healthy, no issues found), and there are truly no more tasks, print exactly this on its own line: 156 + 157 + OUT_OF_JOB 158 + 159 + Then exit. 160 + 161 + --- 162 + 163 + ## Important Rules 164 + 165 + - Only do ONE task per invocation (one PR review, one issue implementation, or one deploy cycle), then exit. 166 + - Always work in git worktrees, never directly on main (except for moving plan files). 167 + - Always run the completion checklist before pushing. 168 + - If something fails and you cannot fix it, create a new issue describing the problem: 169 + ``` 170 + tangled issue create --repo pierrelf.com/ayos --title "..." --body "..." 171 + ``` 172 + Then exit. 173 + - Be thorough but focused. Don't gold-plate or add scope beyond what the issue requires. 174 + - Do NOT close issues manually. They will be resolved when the PR is merged. 175 + """.strip() 176 + 177 + 178 + def run_iteration(iteration: int) -> tuple[str, float]: 179 + """Run one claude invocation. Returns (output, elapsed_seconds).""" 180 + cmd = [ 181 + "claude", 182 + "--dangerously-skip-permissions", 183 + "-p", 184 + PROMPT, 185 + "--output-format", 186 + "text", 187 + "--max-turns", 188 + "200", 189 + "--verbose", 190 + ] 191 + 192 + start = time.monotonic() 193 + try: 194 + result = subprocess.run( 195 + cmd, 196 + capture_output=True, 197 + text=True, 198 + cwd=GIT_ROOT, 199 + ) 200 + elapsed = time.monotonic() - start 201 + output = result.stdout + result.stderr 202 + print(f"\n{'='*60}") 203 + print(f"Iteration {iteration} completed in {elapsed:.1f}s") 204 + print(f"{'='*60}") 205 + lines = output.strip().split("\n") 206 + tail = lines[-40:] if len(lines) > 40 else lines 207 + for line in tail: 208 + print(f" {line}") 209 + print() 210 + return output, elapsed 211 + except Exception as e: 212 + elapsed = time.monotonic() - start 213 + print(f"\nIteration {iteration} failed after {elapsed:.1f}s: {e}") 214 + return str(e), elapsed 215 + 216 + 217 + def main() -> None: 218 + print("Ayos — autonomous implementation loop") 219 + print("Press Ctrl+C to stop\n") 220 + 221 + iteration = 0 222 + 223 + try: 224 + while True: 225 + iteration += 1 226 + print(f"\n--- Starting iteration {iteration} ---") 227 + output, elapsed = run_iteration(iteration) 228 + 229 + if "OUT_OF_JOB" in output: 230 + print("Nothing to do. Sleeping 60s...") 231 + time.sleep(60) 232 + 233 + except KeyboardInterrupt: 234 + print(f"\n\nStopped after {iteration} iteration(s).") 235 + 236 + 237 + if __name__ == "__main__": 238 + main()
+33
scripts/dev.sh
··· 1 + #!/usr/bin/env bash 2 + set -e 3 + 4 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 5 + ROOT_DIR="$(dirname "$SCRIPT_DIR")" 6 + 7 + cleanup() { 8 + echo "Shutting down..." 9 + kill $API_PID $WEB_PID 2>/dev/null || true 10 + wait $API_PID $WEB_PID 2>/dev/null || true 11 + } 12 + trap cleanup EXIT INT TERM 13 + 14 + # Start API 15 + echo "Starting API server on :3001..." 16 + cd "$ROOT_DIR/api" 17 + cp -n .env.example .env 2>/dev/null || true 18 + cargo run & 19 + API_PID=$! 20 + 21 + # Start frontend 22 + echo "Starting frontend on :5173..." 23 + cd "$ROOT_DIR/web" 24 + npm run dev & 25 + WEB_PID=$! 26 + 27 + echo "Ayos dev servers starting..." 28 + echo " API: http://localhost:3001" 29 + echo " Frontend: http://localhost:5173" 30 + echo "" 31 + echo "Press Ctrl+C to stop." 32 + 33 + wait
+24
web/.gitignore
··· 1 + # Logs 2 + logs 3 + *.log 4 + npm-debug.log* 5 + yarn-debug.log* 6 + yarn-error.log* 7 + pnpm-debug.log* 8 + lerna-debug.log* 9 + 10 + node_modules 11 + dist 12 + dist-ssr 13 + *.local 14 + 15 + # Editor directories and files 16 + .vscode/* 17 + !.vscode/extensions.json 18 + .idea 19 + .DS_Store 20 + *.suo 21 + *.ntvs* 22 + *.njsproj 23 + *.sln 24 + *.sw?
+73
web/README.md
··· 1 + # React + TypeScript + Vite 2 + 3 + This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 + 5 + Currently, two official plugins are available: 6 + 7 + - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) 8 + - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) 9 + 10 + ## React Compiler 11 + 12 + The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). 13 + 14 + ## Expanding the ESLint configuration 15 + 16 + If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 17 + 18 + ```js 19 + export default defineConfig([ 20 + globalIgnores(['dist']), 21 + { 22 + files: ['**/*.{ts,tsx}'], 23 + extends: [ 24 + // Other configs... 25 + 26 + // Remove tseslint.configs.recommended and replace with this 27 + tseslint.configs.recommendedTypeChecked, 28 + // Alternatively, use this for stricter rules 29 + tseslint.configs.strictTypeChecked, 30 + // Optionally, add this for stylistic rules 31 + tseslint.configs.stylisticTypeChecked, 32 + 33 + // Other configs... 34 + ], 35 + languageOptions: { 36 + parserOptions: { 37 + project: ['./tsconfig.node.json', './tsconfig.app.json'], 38 + tsconfigRootDir: import.meta.dirname, 39 + }, 40 + // other options... 41 + }, 42 + }, 43 + ]) 44 + ``` 45 + 46 + You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 47 + 48 + ```js 49 + // eslint.config.js 50 + import reactX from 'eslint-plugin-react-x' 51 + import reactDom from 'eslint-plugin-react-dom' 52 + 53 + export default defineConfig([ 54 + globalIgnores(['dist']), 55 + { 56 + files: ['**/*.{ts,tsx}'], 57 + extends: [ 58 + // Other configs... 59 + // Enable lint rules for React 60 + reactX.configs['recommended-typescript'], 61 + // Enable lint rules for React DOM 62 + reactDom.configs.recommended, 63 + ], 64 + languageOptions: { 65 + parserOptions: { 66 + project: ['./tsconfig.node.json', './tsconfig.app.json'], 67 + tsconfigRootDir: import.meta.dirname, 68 + }, 69 + // other options... 70 + }, 71 + }, 72 + ]) 73 + ```
+23
web/eslint.config.js
··· 1 + import js from '@eslint/js' 2 + import globals from 'globals' 3 + import reactHooks from 'eslint-plugin-react-hooks' 4 + import reactRefresh from 'eslint-plugin-react-refresh' 5 + import tseslint from 'typescript-eslint' 6 + import { defineConfig, globalIgnores } from 'eslint/config' 7 + 8 + export default defineConfig([ 9 + globalIgnores(['dist']), 10 + { 11 + files: ['**/*.{ts,tsx}'], 12 + extends: [ 13 + js.configs.recommended, 14 + tseslint.configs.recommended, 15 + reactHooks.configs.flat.recommended, 16 + reactRefresh.configs.vite, 17 + ], 18 + languageOptions: { 19 + ecmaVersion: 2020, 20 + globals: globals.browser, 21 + }, 22 + }, 23 + ])
+13
web/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>web</title> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="/src/main.tsx"></script> 12 + </body> 13 + </html>
+3763
web/package-lock.json
··· 1 + { 2 + "name": "web", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "web", 9 + "version": "0.0.0", 10 + "dependencies": { 11 + "@emotion/react": "^11.14.0", 12 + "@emotion/styled": "^11.14.1", 13 + "@fontsource/nunito": "^5.2.7", 14 + "@mui/icons-material": "^7.3.9", 15 + "@mui/material": "^7.3.9", 16 + "react": "^19.2.4", 17 + "react-dom": "^19.2.4", 18 + "react-router-dom": "^7.13.1" 19 + }, 20 + "devDependencies": { 21 + "@eslint/js": "^9.39.4", 22 + "@types/node": "^24.12.0", 23 + "@types/react": "^19.2.14", 24 + "@types/react-dom": "^19.2.3", 25 + "@vitejs/plugin-react": "^6.0.0", 26 + "eslint": "^9.39.4", 27 + "eslint-plugin-react-hooks": "^7.0.1", 28 + "eslint-plugin-react-refresh": "^0.5.2", 29 + "globals": "^17.4.0", 30 + "typescript": "~5.9.3", 31 + "typescript-eslint": "^8.56.1", 32 + "vite": "^8.0.0" 33 + } 34 + }, 35 + "node_modules/@babel/code-frame": { 36 + "version": "7.29.0", 37 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", 38 + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", 39 + "license": "MIT", 40 + "dependencies": { 41 + "@babel/helper-validator-identifier": "^7.28.5", 42 + "js-tokens": "^4.0.0", 43 + "picocolors": "^1.1.1" 44 + }, 45 + "engines": { 46 + "node": ">=6.9.0" 47 + } 48 + }, 49 + "node_modules/@babel/compat-data": { 50 + "version": "7.29.0", 51 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", 52 + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", 53 + "dev": true, 54 + "license": "MIT", 55 + "engines": { 56 + "node": ">=6.9.0" 57 + } 58 + }, 59 + "node_modules/@babel/core": { 60 + "version": "7.29.0", 61 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", 62 + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", 63 + "dev": true, 64 + "license": "MIT", 65 + "dependencies": { 66 + "@babel/code-frame": "^7.29.0", 67 + "@babel/generator": "^7.29.0", 68 + "@babel/helper-compilation-targets": "^7.28.6", 69 + "@babel/helper-module-transforms": "^7.28.6", 70 + "@babel/helpers": "^7.28.6", 71 + "@babel/parser": "^7.29.0", 72 + "@babel/template": "^7.28.6", 73 + "@babel/traverse": "^7.29.0", 74 + "@babel/types": "^7.29.0", 75 + "@jridgewell/remapping": "^2.3.5", 76 + "convert-source-map": "^2.0.0", 77 + "debug": "^4.1.0", 78 + "gensync": "^1.0.0-beta.2", 79 + "json5": "^2.2.3", 80 + "semver": "^6.3.1" 81 + }, 82 + "engines": { 83 + "node": ">=6.9.0" 84 + }, 85 + "funding": { 86 + "type": "opencollective", 87 + "url": "https://opencollective.com/babel" 88 + } 89 + }, 90 + "node_modules/@babel/core/node_modules/convert-source-map": { 91 + "version": "2.0.0", 92 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 93 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 94 + "dev": true, 95 + "license": "MIT" 96 + }, 97 + "node_modules/@babel/generator": { 98 + "version": "7.29.1", 99 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", 100 + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", 101 + "license": "MIT", 102 + "dependencies": { 103 + "@babel/parser": "^7.29.0", 104 + "@babel/types": "^7.29.0", 105 + "@jridgewell/gen-mapping": "^0.3.12", 106 + "@jridgewell/trace-mapping": "^0.3.28", 107 + "jsesc": "^3.0.2" 108 + }, 109 + "engines": { 110 + "node": ">=6.9.0" 111 + } 112 + }, 113 + "node_modules/@babel/helper-compilation-targets": { 114 + "version": "7.28.6", 115 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", 116 + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", 117 + "dev": true, 118 + "license": "MIT", 119 + "dependencies": { 120 + "@babel/compat-data": "^7.28.6", 121 + "@babel/helper-validator-option": "^7.27.1", 122 + "browserslist": "^4.24.0", 123 + "lru-cache": "^5.1.1", 124 + "semver": "^6.3.1" 125 + }, 126 + "engines": { 127 + "node": ">=6.9.0" 128 + } 129 + }, 130 + "node_modules/@babel/helper-globals": { 131 + "version": "7.28.0", 132 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", 133 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", 134 + "license": "MIT", 135 + "engines": { 136 + "node": ">=6.9.0" 137 + } 138 + }, 139 + "node_modules/@babel/helper-module-imports": { 140 + "version": "7.28.6", 141 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", 142 + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", 143 + "license": "MIT", 144 + "dependencies": { 145 + "@babel/traverse": "^7.28.6", 146 + "@babel/types": "^7.28.6" 147 + }, 148 + "engines": { 149 + "node": ">=6.9.0" 150 + } 151 + }, 152 + "node_modules/@babel/helper-module-transforms": { 153 + "version": "7.28.6", 154 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", 155 + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", 156 + "dev": true, 157 + "license": "MIT", 158 + "dependencies": { 159 + "@babel/helper-module-imports": "^7.28.6", 160 + "@babel/helper-validator-identifier": "^7.28.5", 161 + "@babel/traverse": "^7.28.6" 162 + }, 163 + "engines": { 164 + "node": ">=6.9.0" 165 + }, 166 + "peerDependencies": { 167 + "@babel/core": "^7.0.0" 168 + } 169 + }, 170 + "node_modules/@babel/helper-string-parser": { 171 + "version": "7.27.1", 172 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 173 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 174 + "license": "MIT", 175 + "engines": { 176 + "node": ">=6.9.0" 177 + } 178 + }, 179 + "node_modules/@babel/helper-validator-identifier": { 180 + "version": "7.28.5", 181 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", 182 + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", 183 + "license": "MIT", 184 + "engines": { 185 + "node": ">=6.9.0" 186 + } 187 + }, 188 + "node_modules/@babel/helper-validator-option": { 189 + "version": "7.27.1", 190 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", 191 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", 192 + "dev": true, 193 + "license": "MIT", 194 + "engines": { 195 + "node": ">=6.9.0" 196 + } 197 + }, 198 + "node_modules/@babel/helpers": { 199 + "version": "7.28.6", 200 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", 201 + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", 202 + "dev": true, 203 + "license": "MIT", 204 + "dependencies": { 205 + "@babel/template": "^7.28.6", 206 + "@babel/types": "^7.28.6" 207 + }, 208 + "engines": { 209 + "node": ">=6.9.0" 210 + } 211 + }, 212 + "node_modules/@babel/parser": { 213 + "version": "7.29.0", 214 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", 215 + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", 216 + "license": "MIT", 217 + "dependencies": { 218 + "@babel/types": "^7.29.0" 219 + }, 220 + "bin": { 221 + "parser": "bin/babel-parser.js" 222 + }, 223 + "engines": { 224 + "node": ">=6.0.0" 225 + } 226 + }, 227 + "node_modules/@babel/runtime": { 228 + "version": "7.28.6", 229 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", 230 + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", 231 + "license": "MIT", 232 + "engines": { 233 + "node": ">=6.9.0" 234 + } 235 + }, 236 + "node_modules/@babel/template": { 237 + "version": "7.28.6", 238 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", 239 + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", 240 + "license": "MIT", 241 + "dependencies": { 242 + "@babel/code-frame": "^7.28.6", 243 + "@babel/parser": "^7.28.6", 244 + "@babel/types": "^7.28.6" 245 + }, 246 + "engines": { 247 + "node": ">=6.9.0" 248 + } 249 + }, 250 + "node_modules/@babel/traverse": { 251 + "version": "7.29.0", 252 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", 253 + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", 254 + "license": "MIT", 255 + "dependencies": { 256 + "@babel/code-frame": "^7.29.0", 257 + "@babel/generator": "^7.29.0", 258 + "@babel/helper-globals": "^7.28.0", 259 + "@babel/parser": "^7.29.0", 260 + "@babel/template": "^7.28.6", 261 + "@babel/types": "^7.29.0", 262 + "debug": "^4.3.1" 263 + }, 264 + "engines": { 265 + "node": ">=6.9.0" 266 + } 267 + }, 268 + "node_modules/@babel/types": { 269 + "version": "7.29.0", 270 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", 271 + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", 272 + "license": "MIT", 273 + "dependencies": { 274 + "@babel/helper-string-parser": "^7.27.1", 275 + "@babel/helper-validator-identifier": "^7.28.5" 276 + }, 277 + "engines": { 278 + "node": ">=6.9.0" 279 + } 280 + }, 281 + "node_modules/@emnapi/core": { 282 + "version": "1.9.0", 283 + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", 284 + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", 285 + "dev": true, 286 + "license": "MIT", 287 + "optional": true, 288 + "dependencies": { 289 + "@emnapi/wasi-threads": "1.2.0", 290 + "tslib": "^2.4.0" 291 + } 292 + }, 293 + "node_modules/@emnapi/runtime": { 294 + "version": "1.9.0", 295 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", 296 + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", 297 + "dev": true, 298 + "license": "MIT", 299 + "optional": true, 300 + "dependencies": { 301 + "tslib": "^2.4.0" 302 + } 303 + }, 304 + "node_modules/@emnapi/wasi-threads": { 305 + "version": "1.2.0", 306 + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", 307 + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", 308 + "dev": true, 309 + "license": "MIT", 310 + "optional": true, 311 + "dependencies": { 312 + "tslib": "^2.4.0" 313 + } 314 + }, 315 + "node_modules/@emotion/babel-plugin": { 316 + "version": "11.13.5", 317 + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", 318 + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", 319 + "license": "MIT", 320 + "dependencies": { 321 + "@babel/helper-module-imports": "^7.16.7", 322 + "@babel/runtime": "^7.18.3", 323 + "@emotion/hash": "^0.9.2", 324 + "@emotion/memoize": "^0.9.0", 325 + "@emotion/serialize": "^1.3.3", 326 + "babel-plugin-macros": "^3.1.0", 327 + "convert-source-map": "^1.5.0", 328 + "escape-string-regexp": "^4.0.0", 329 + "find-root": "^1.1.0", 330 + "source-map": "^0.5.7", 331 + "stylis": "4.2.0" 332 + } 333 + }, 334 + "node_modules/@emotion/cache": { 335 + "version": "11.14.0", 336 + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", 337 + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", 338 + "license": "MIT", 339 + "dependencies": { 340 + "@emotion/memoize": "^0.9.0", 341 + "@emotion/sheet": "^1.4.0", 342 + "@emotion/utils": "^1.4.2", 343 + "@emotion/weak-memoize": "^0.4.0", 344 + "stylis": "4.2.0" 345 + } 346 + }, 347 + "node_modules/@emotion/hash": { 348 + "version": "0.9.2", 349 + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", 350 + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", 351 + "license": "MIT" 352 + }, 353 + "node_modules/@emotion/is-prop-valid": { 354 + "version": "1.4.0", 355 + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", 356 + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", 357 + "license": "MIT", 358 + "dependencies": { 359 + "@emotion/memoize": "^0.9.0" 360 + } 361 + }, 362 + "node_modules/@emotion/memoize": { 363 + "version": "0.9.0", 364 + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", 365 + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", 366 + "license": "MIT" 367 + }, 368 + "node_modules/@emotion/react": { 369 + "version": "11.14.0", 370 + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", 371 + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", 372 + "license": "MIT", 373 + "peer": true, 374 + "dependencies": { 375 + "@babel/runtime": "^7.18.3", 376 + "@emotion/babel-plugin": "^11.13.5", 377 + "@emotion/cache": "^11.14.0", 378 + "@emotion/serialize": "^1.3.3", 379 + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", 380 + "@emotion/utils": "^1.4.2", 381 + "@emotion/weak-memoize": "^0.4.0", 382 + "hoist-non-react-statics": "^3.3.1" 383 + }, 384 + "peerDependencies": { 385 + "react": ">=16.8.0" 386 + }, 387 + "peerDependenciesMeta": { 388 + "@types/react": { 389 + "optional": true 390 + } 391 + } 392 + }, 393 + "node_modules/@emotion/serialize": { 394 + "version": "1.3.3", 395 + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", 396 + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", 397 + "license": "MIT", 398 + "dependencies": { 399 + "@emotion/hash": "^0.9.2", 400 + "@emotion/memoize": "^0.9.0", 401 + "@emotion/unitless": "^0.10.0", 402 + "@emotion/utils": "^1.4.2", 403 + "csstype": "^3.0.2" 404 + } 405 + }, 406 + "node_modules/@emotion/sheet": { 407 + "version": "1.4.0", 408 + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", 409 + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", 410 + "license": "MIT" 411 + }, 412 + "node_modules/@emotion/styled": { 413 + "version": "11.14.1", 414 + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", 415 + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", 416 + "license": "MIT", 417 + "peer": true, 418 + "dependencies": { 419 + "@babel/runtime": "^7.18.3", 420 + "@emotion/babel-plugin": "^11.13.5", 421 + "@emotion/is-prop-valid": "^1.3.0", 422 + "@emotion/serialize": "^1.3.3", 423 + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", 424 + "@emotion/utils": "^1.4.2" 425 + }, 426 + "peerDependencies": { 427 + "@emotion/react": "^11.0.0-rc.0", 428 + "react": ">=16.8.0" 429 + }, 430 + "peerDependenciesMeta": { 431 + "@types/react": { 432 + "optional": true 433 + } 434 + } 435 + }, 436 + "node_modules/@emotion/unitless": { 437 + "version": "0.10.0", 438 + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", 439 + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", 440 + "license": "MIT" 441 + }, 442 + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { 443 + "version": "1.2.0", 444 + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", 445 + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", 446 + "license": "MIT", 447 + "peerDependencies": { 448 + "react": ">=16.8.0" 449 + } 450 + }, 451 + "node_modules/@emotion/utils": { 452 + "version": "1.4.2", 453 + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", 454 + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", 455 + "license": "MIT" 456 + }, 457 + "node_modules/@emotion/weak-memoize": { 458 + "version": "0.4.0", 459 + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", 460 + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", 461 + "license": "MIT" 462 + }, 463 + "node_modules/@eslint-community/eslint-utils": { 464 + "version": "4.9.1", 465 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", 466 + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", 467 + "dev": true, 468 + "license": "MIT", 469 + "dependencies": { 470 + "eslint-visitor-keys": "^3.4.3" 471 + }, 472 + "engines": { 473 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 474 + }, 475 + "funding": { 476 + "url": "https://opencollective.com/eslint" 477 + }, 478 + "peerDependencies": { 479 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 480 + } 481 + }, 482 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 483 + "version": "3.4.3", 484 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 485 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 486 + "dev": true, 487 + "license": "Apache-2.0", 488 + "engines": { 489 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 490 + }, 491 + "funding": { 492 + "url": "https://opencollective.com/eslint" 493 + } 494 + }, 495 + "node_modules/@eslint-community/regexpp": { 496 + "version": "4.12.2", 497 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", 498 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", 499 + "dev": true, 500 + "license": "MIT", 501 + "engines": { 502 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 503 + } 504 + }, 505 + "node_modules/@eslint/config-array": { 506 + "version": "0.21.2", 507 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", 508 + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", 509 + "dev": true, 510 + "license": "Apache-2.0", 511 + "dependencies": { 512 + "@eslint/object-schema": "^2.1.7", 513 + "debug": "^4.3.1", 514 + "minimatch": "^3.1.5" 515 + }, 516 + "engines": { 517 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 518 + } 519 + }, 520 + "node_modules/@eslint/config-helpers": { 521 + "version": "0.4.2", 522 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", 523 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", 524 + "dev": true, 525 + "license": "Apache-2.0", 526 + "dependencies": { 527 + "@eslint/core": "^0.17.0" 528 + }, 529 + "engines": { 530 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 531 + } 532 + }, 533 + "node_modules/@eslint/core": { 534 + "version": "0.17.0", 535 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", 536 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", 537 + "dev": true, 538 + "license": "Apache-2.0", 539 + "dependencies": { 540 + "@types/json-schema": "^7.0.15" 541 + }, 542 + "engines": { 543 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 544 + } 545 + }, 546 + "node_modules/@eslint/eslintrc": { 547 + "version": "3.3.5", 548 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", 549 + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", 550 + "dev": true, 551 + "license": "MIT", 552 + "dependencies": { 553 + "ajv": "^6.14.0", 554 + "debug": "^4.3.2", 555 + "espree": "^10.0.1", 556 + "globals": "^14.0.0", 557 + "ignore": "^5.2.0", 558 + "import-fresh": "^3.2.1", 559 + "js-yaml": "^4.1.1", 560 + "minimatch": "^3.1.5", 561 + "strip-json-comments": "^3.1.1" 562 + }, 563 + "engines": { 564 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 565 + }, 566 + "funding": { 567 + "url": "https://opencollective.com/eslint" 568 + } 569 + }, 570 + "node_modules/@eslint/eslintrc/node_modules/globals": { 571 + "version": "14.0.0", 572 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 573 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 574 + "dev": true, 575 + "license": "MIT", 576 + "engines": { 577 + "node": ">=18" 578 + }, 579 + "funding": { 580 + "url": "https://github.com/sponsors/sindresorhus" 581 + } 582 + }, 583 + "node_modules/@eslint/js": { 584 + "version": "9.39.4", 585 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", 586 + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", 587 + "dev": true, 588 + "license": "MIT", 589 + "engines": { 590 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 591 + }, 592 + "funding": { 593 + "url": "https://eslint.org/donate" 594 + } 595 + }, 596 + "node_modules/@eslint/object-schema": { 597 + "version": "2.1.7", 598 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 599 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 600 + "dev": true, 601 + "license": "Apache-2.0", 602 + "engines": { 603 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 604 + } 605 + }, 606 + "node_modules/@eslint/plugin-kit": { 607 + "version": "0.4.1", 608 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", 609 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", 610 + "dev": true, 611 + "license": "Apache-2.0", 612 + "dependencies": { 613 + "@eslint/core": "^0.17.0", 614 + "levn": "^0.4.1" 615 + }, 616 + "engines": { 617 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 618 + } 619 + }, 620 + "node_modules/@fontsource/nunito": { 621 + "version": "5.2.7", 622 + "resolved": "https://registry.npmjs.org/@fontsource/nunito/-/nunito-5.2.7.tgz", 623 + "integrity": "sha512-pmtBq0H9ex9nk+RtJYEJOD9pag393iHETnl/PVKleF4i06cd0ttngK5ZCTgYb5eOqR3Xdlrjtev8m7bmgYprew==", 624 + "license": "OFL-1.1", 625 + "funding": { 626 + "url": "https://github.com/sponsors/ayuhito" 627 + } 628 + }, 629 + "node_modules/@humanfs/core": { 630 + "version": "0.19.1", 631 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 632 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 633 + "dev": true, 634 + "license": "Apache-2.0", 635 + "engines": { 636 + "node": ">=18.18.0" 637 + } 638 + }, 639 + "node_modules/@humanfs/node": { 640 + "version": "0.16.7", 641 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 642 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 643 + "dev": true, 644 + "license": "Apache-2.0", 645 + "dependencies": { 646 + "@humanfs/core": "^0.19.1", 647 + "@humanwhocodes/retry": "^0.4.0" 648 + }, 649 + "engines": { 650 + "node": ">=18.18.0" 651 + } 652 + }, 653 + "node_modules/@humanwhocodes/module-importer": { 654 + "version": "1.0.1", 655 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 656 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 657 + "dev": true, 658 + "license": "Apache-2.0", 659 + "engines": { 660 + "node": ">=12.22" 661 + }, 662 + "funding": { 663 + "type": "github", 664 + "url": "https://github.com/sponsors/nzakas" 665 + } 666 + }, 667 + "node_modules/@humanwhocodes/retry": { 668 + "version": "0.4.3", 669 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 670 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 671 + "dev": true, 672 + "license": "Apache-2.0", 673 + "engines": { 674 + "node": ">=18.18" 675 + }, 676 + "funding": { 677 + "type": "github", 678 + "url": "https://github.com/sponsors/nzakas" 679 + } 680 + }, 681 + "node_modules/@jridgewell/gen-mapping": { 682 + "version": "0.3.13", 683 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 684 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 685 + "license": "MIT", 686 + "dependencies": { 687 + "@jridgewell/sourcemap-codec": "^1.5.0", 688 + "@jridgewell/trace-mapping": "^0.3.24" 689 + } 690 + }, 691 + "node_modules/@jridgewell/remapping": { 692 + "version": "2.3.5", 693 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 694 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 695 + "dev": true, 696 + "license": "MIT", 697 + "dependencies": { 698 + "@jridgewell/gen-mapping": "^0.3.5", 699 + "@jridgewell/trace-mapping": "^0.3.24" 700 + } 701 + }, 702 + "node_modules/@jridgewell/resolve-uri": { 703 + "version": "3.1.2", 704 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 705 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 706 + "license": "MIT", 707 + "engines": { 708 + "node": ">=6.0.0" 709 + } 710 + }, 711 + "node_modules/@jridgewell/sourcemap-codec": { 712 + "version": "1.5.5", 713 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 714 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 715 + "license": "MIT" 716 + }, 717 + "node_modules/@jridgewell/trace-mapping": { 718 + "version": "0.3.31", 719 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 720 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 721 + "license": "MIT", 722 + "dependencies": { 723 + "@jridgewell/resolve-uri": "^3.1.0", 724 + "@jridgewell/sourcemap-codec": "^1.4.14" 725 + } 726 + }, 727 + "node_modules/@mui/core-downloads-tracker": { 728 + "version": "7.3.9", 729 + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz", 730 + "integrity": "sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==", 731 + "license": "MIT", 732 + "funding": { 733 + "type": "opencollective", 734 + "url": "https://opencollective.com/mui-org" 735 + } 736 + }, 737 + "node_modules/@mui/icons-material": { 738 + "version": "7.3.9", 739 + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.9.tgz", 740 + "integrity": "sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==", 741 + "license": "MIT", 742 + "dependencies": { 743 + "@babel/runtime": "^7.28.6" 744 + }, 745 + "engines": { 746 + "node": ">=14.0.0" 747 + }, 748 + "funding": { 749 + "type": "opencollective", 750 + "url": "https://opencollective.com/mui-org" 751 + }, 752 + "peerDependencies": { 753 + "@mui/material": "^7.3.9", 754 + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", 755 + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 756 + }, 757 + "peerDependenciesMeta": { 758 + "@types/react": { 759 + "optional": true 760 + } 761 + } 762 + }, 763 + "node_modules/@mui/material": { 764 + "version": "7.3.9", 765 + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", 766 + "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", 767 + "license": "MIT", 768 + "peer": true, 769 + "dependencies": { 770 + "@babel/runtime": "^7.28.6", 771 + "@mui/core-downloads-tracker": "^7.3.9", 772 + "@mui/system": "^7.3.9", 773 + "@mui/types": "^7.4.12", 774 + "@mui/utils": "^7.3.9", 775 + "@popperjs/core": "^2.11.8", 776 + "@types/react-transition-group": "^4.4.12", 777 + "clsx": "^2.1.1", 778 + "csstype": "^3.2.3", 779 + "prop-types": "^15.8.1", 780 + "react-is": "^19.2.3", 781 + "react-transition-group": "^4.4.5" 782 + }, 783 + "engines": { 784 + "node": ">=14.0.0" 785 + }, 786 + "funding": { 787 + "type": "opencollective", 788 + "url": "https://opencollective.com/mui-org" 789 + }, 790 + "peerDependencies": { 791 + "@emotion/react": "^11.5.0", 792 + "@emotion/styled": "^11.3.0", 793 + "@mui/material-pigment-css": "^7.3.9", 794 + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", 795 + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", 796 + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" 797 + }, 798 + "peerDependenciesMeta": { 799 + "@emotion/react": { 800 + "optional": true 801 + }, 802 + "@emotion/styled": { 803 + "optional": true 804 + }, 805 + "@mui/material-pigment-css": { 806 + "optional": true 807 + }, 808 + "@types/react": { 809 + "optional": true 810 + } 811 + } 812 + }, 813 + "node_modules/@mui/private-theming": { 814 + "version": "7.3.9", 815 + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.9.tgz", 816 + "integrity": "sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==", 817 + "license": "MIT", 818 + "dependencies": { 819 + "@babel/runtime": "^7.28.6", 820 + "@mui/utils": "^7.3.9", 821 + "prop-types": "^15.8.1" 822 + }, 823 + "engines": { 824 + "node": ">=14.0.0" 825 + }, 826 + "funding": { 827 + "type": "opencollective", 828 + "url": "https://opencollective.com/mui-org" 829 + }, 830 + "peerDependencies": { 831 + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", 832 + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 833 + }, 834 + "peerDependenciesMeta": { 835 + "@types/react": { 836 + "optional": true 837 + } 838 + } 839 + }, 840 + "node_modules/@mui/styled-engine": { 841 + "version": "7.3.9", 842 + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.9.tgz", 843 + "integrity": "sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==", 844 + "license": "MIT", 845 + "dependencies": { 846 + "@babel/runtime": "^7.28.6", 847 + "@emotion/cache": "^11.14.0", 848 + "@emotion/serialize": "^1.3.3", 849 + "@emotion/sheet": "^1.4.0", 850 + "csstype": "^3.2.3", 851 + "prop-types": "^15.8.1" 852 + }, 853 + "engines": { 854 + "node": ">=14.0.0" 855 + }, 856 + "funding": { 857 + "type": "opencollective", 858 + "url": "https://opencollective.com/mui-org" 859 + }, 860 + "peerDependencies": { 861 + "@emotion/react": "^11.4.1", 862 + "@emotion/styled": "^11.3.0", 863 + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 864 + }, 865 + "peerDependenciesMeta": { 866 + "@emotion/react": { 867 + "optional": true 868 + }, 869 + "@emotion/styled": { 870 + "optional": true 871 + } 872 + } 873 + }, 874 + "node_modules/@mui/system": { 875 + "version": "7.3.9", 876 + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.9.tgz", 877 + "integrity": "sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==", 878 + "license": "MIT", 879 + "dependencies": { 880 + "@babel/runtime": "^7.28.6", 881 + "@mui/private-theming": "^7.3.9", 882 + "@mui/styled-engine": "^7.3.9", 883 + "@mui/types": "^7.4.12", 884 + "@mui/utils": "^7.3.9", 885 + "clsx": "^2.1.1", 886 + "csstype": "^3.2.3", 887 + "prop-types": "^15.8.1" 888 + }, 889 + "engines": { 890 + "node": ">=14.0.0" 891 + }, 892 + "funding": { 893 + "type": "opencollective", 894 + "url": "https://opencollective.com/mui-org" 895 + }, 896 + "peerDependencies": { 897 + "@emotion/react": "^11.5.0", 898 + "@emotion/styled": "^11.3.0", 899 + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", 900 + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 901 + }, 902 + "peerDependenciesMeta": { 903 + "@emotion/react": { 904 + "optional": true 905 + }, 906 + "@emotion/styled": { 907 + "optional": true 908 + }, 909 + "@types/react": { 910 + "optional": true 911 + } 912 + } 913 + }, 914 + "node_modules/@mui/types": { 915 + "version": "7.4.12", 916 + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", 917 + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", 918 + "license": "MIT", 919 + "dependencies": { 920 + "@babel/runtime": "^7.28.6" 921 + }, 922 + "peerDependencies": { 923 + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" 924 + }, 925 + "peerDependenciesMeta": { 926 + "@types/react": { 927 + "optional": true 928 + } 929 + } 930 + }, 931 + "node_modules/@mui/utils": { 932 + "version": "7.3.9", 933 + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.9.tgz", 934 + "integrity": "sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==", 935 + "license": "MIT", 936 + "dependencies": { 937 + "@babel/runtime": "^7.28.6", 938 + "@mui/types": "^7.4.12", 939 + "@types/prop-types": "^15.7.15", 940 + "clsx": "^2.1.1", 941 + "prop-types": "^15.8.1", 942 + "react-is": "^19.2.3" 943 + }, 944 + "engines": { 945 + "node": ">=14.0.0" 946 + }, 947 + "funding": { 948 + "type": "opencollective", 949 + "url": "https://opencollective.com/mui-org" 950 + }, 951 + "peerDependencies": { 952 + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", 953 + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 954 + }, 955 + "peerDependenciesMeta": { 956 + "@types/react": { 957 + "optional": true 958 + } 959 + } 960 + }, 961 + "node_modules/@napi-rs/wasm-runtime": { 962 + "version": "1.1.1", 963 + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", 964 + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", 965 + "dev": true, 966 + "license": "MIT", 967 + "optional": true, 968 + "dependencies": { 969 + "@emnapi/core": "^1.7.1", 970 + "@emnapi/runtime": "^1.7.1", 971 + "@tybys/wasm-util": "^0.10.1" 972 + }, 973 + "funding": { 974 + "type": "github", 975 + "url": "https://github.com/sponsors/Brooooooklyn" 976 + } 977 + }, 978 + "node_modules/@oxc-project/runtime": { 979 + "version": "0.115.0", 980 + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", 981 + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", 982 + "dev": true, 983 + "license": "MIT", 984 + "engines": { 985 + "node": "^20.19.0 || >=22.12.0" 986 + } 987 + }, 988 + "node_modules/@oxc-project/types": { 989 + "version": "0.115.0", 990 + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", 991 + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", 992 + "dev": true, 993 + "license": "MIT", 994 + "funding": { 995 + "url": "https://github.com/sponsors/Boshen" 996 + } 997 + }, 998 + "node_modules/@popperjs/core": { 999 + "version": "2.11.8", 1000 + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", 1001 + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", 1002 + "license": "MIT", 1003 + "funding": { 1004 + "type": "opencollective", 1005 + "url": "https://opencollective.com/popperjs" 1006 + } 1007 + }, 1008 + "node_modules/@rolldown/binding-android-arm64": { 1009 + "version": "1.0.0-rc.9", 1010 + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", 1011 + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", 1012 + "cpu": [ 1013 + "arm64" 1014 + ], 1015 + "dev": true, 1016 + "license": "MIT", 1017 + "optional": true, 1018 + "os": [ 1019 + "android" 1020 + ], 1021 + "engines": { 1022 + "node": "^20.19.0 || >=22.12.0" 1023 + } 1024 + }, 1025 + "node_modules/@rolldown/binding-darwin-arm64": { 1026 + "version": "1.0.0-rc.9", 1027 + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", 1028 + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", 1029 + "cpu": [ 1030 + "arm64" 1031 + ], 1032 + "dev": true, 1033 + "license": "MIT", 1034 + "optional": true, 1035 + "os": [ 1036 + "darwin" 1037 + ], 1038 + "engines": { 1039 + "node": "^20.19.0 || >=22.12.0" 1040 + } 1041 + }, 1042 + "node_modules/@rolldown/binding-darwin-x64": { 1043 + "version": "1.0.0-rc.9", 1044 + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", 1045 + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", 1046 + "cpu": [ 1047 + "x64" 1048 + ], 1049 + "dev": true, 1050 + "license": "MIT", 1051 + "optional": true, 1052 + "os": [ 1053 + "darwin" 1054 + ], 1055 + "engines": { 1056 + "node": "^20.19.0 || >=22.12.0" 1057 + } 1058 + }, 1059 + "node_modules/@rolldown/binding-freebsd-x64": { 1060 + "version": "1.0.0-rc.9", 1061 + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", 1062 + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", 1063 + "cpu": [ 1064 + "x64" 1065 + ], 1066 + "dev": true, 1067 + "license": "MIT", 1068 + "optional": true, 1069 + "os": [ 1070 + "freebsd" 1071 + ], 1072 + "engines": { 1073 + "node": "^20.19.0 || >=22.12.0" 1074 + } 1075 + }, 1076 + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { 1077 + "version": "1.0.0-rc.9", 1078 + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", 1079 + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", 1080 + "cpu": [ 1081 + "arm" 1082 + ], 1083 + "dev": true, 1084 + "license": "MIT", 1085 + "optional": true, 1086 + "os": [ 1087 + "linux" 1088 + ], 1089 + "engines": { 1090 + "node": "^20.19.0 || >=22.12.0" 1091 + } 1092 + }, 1093 + "node_modules/@rolldown/binding-linux-arm64-gnu": { 1094 + "version": "1.0.0-rc.9", 1095 + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", 1096 + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", 1097 + "cpu": [ 1098 + "arm64" 1099 + ], 1100 + "dev": true, 1101 + "license": "MIT", 1102 + "optional": true, 1103 + "os": [ 1104 + "linux" 1105 + ], 1106 + "engines": { 1107 + "node": "^20.19.0 || >=22.12.0" 1108 + } 1109 + }, 1110 + "node_modules/@rolldown/binding-linux-arm64-musl": { 1111 + "version": "1.0.0-rc.9", 1112 + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", 1113 + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", 1114 + "cpu": [ 1115 + "arm64" 1116 + ], 1117 + "dev": true, 1118 + "license": "MIT", 1119 + "optional": true, 1120 + "os": [ 1121 + "linux" 1122 + ], 1123 + "engines": { 1124 + "node": "^20.19.0 || >=22.12.0" 1125 + } 1126 + }, 1127 + "node_modules/@rolldown/binding-linux-ppc64-gnu": { 1128 + "version": "1.0.0-rc.9", 1129 + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", 1130 + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", 1131 + "cpu": [ 1132 + "ppc64" 1133 + ], 1134 + "dev": true, 1135 + "license": "MIT", 1136 + "optional": true, 1137 + "os": [ 1138 + "linux" 1139 + ], 1140 + "engines": { 1141 + "node": "^20.19.0 || >=22.12.0" 1142 + } 1143 + }, 1144 + "node_modules/@rolldown/binding-linux-s390x-gnu": { 1145 + "version": "1.0.0-rc.9", 1146 + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", 1147 + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", 1148 + "cpu": [ 1149 + "s390x" 1150 + ], 1151 + "dev": true, 1152 + "license": "MIT", 1153 + "optional": true, 1154 + "os": [ 1155 + "linux" 1156 + ], 1157 + "engines": { 1158 + "node": "^20.19.0 || >=22.12.0" 1159 + } 1160 + }, 1161 + "node_modules/@rolldown/binding-linux-x64-gnu": { 1162 + "version": "1.0.0-rc.9", 1163 + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", 1164 + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", 1165 + "cpu": [ 1166 + "x64" 1167 + ], 1168 + "dev": true, 1169 + "license": "MIT", 1170 + "optional": true, 1171 + "os": [ 1172 + "linux" 1173 + ], 1174 + "engines": { 1175 + "node": "^20.19.0 || >=22.12.0" 1176 + } 1177 + }, 1178 + "node_modules/@rolldown/binding-linux-x64-musl": { 1179 + "version": "1.0.0-rc.9", 1180 + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", 1181 + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", 1182 + "cpu": [ 1183 + "x64" 1184 + ], 1185 + "dev": true, 1186 + "license": "MIT", 1187 + "optional": true, 1188 + "os": [ 1189 + "linux" 1190 + ], 1191 + "engines": { 1192 + "node": "^20.19.0 || >=22.12.0" 1193 + } 1194 + }, 1195 + "node_modules/@rolldown/binding-openharmony-arm64": { 1196 + "version": "1.0.0-rc.9", 1197 + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", 1198 + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", 1199 + "cpu": [ 1200 + "arm64" 1201 + ], 1202 + "dev": true, 1203 + "license": "MIT", 1204 + "optional": true, 1205 + "os": [ 1206 + "openharmony" 1207 + ], 1208 + "engines": { 1209 + "node": "^20.19.0 || >=22.12.0" 1210 + } 1211 + }, 1212 + "node_modules/@rolldown/binding-wasm32-wasi": { 1213 + "version": "1.0.0-rc.9", 1214 + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", 1215 + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", 1216 + "cpu": [ 1217 + "wasm32" 1218 + ], 1219 + "dev": true, 1220 + "license": "MIT", 1221 + "optional": true, 1222 + "dependencies": { 1223 + "@napi-rs/wasm-runtime": "^1.1.1" 1224 + }, 1225 + "engines": { 1226 + "node": ">=14.0.0" 1227 + } 1228 + }, 1229 + "node_modules/@rolldown/binding-win32-arm64-msvc": { 1230 + "version": "1.0.0-rc.9", 1231 + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", 1232 + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", 1233 + "cpu": [ 1234 + "arm64" 1235 + ], 1236 + "dev": true, 1237 + "license": "MIT", 1238 + "optional": true, 1239 + "os": [ 1240 + "win32" 1241 + ], 1242 + "engines": { 1243 + "node": "^20.19.0 || >=22.12.0" 1244 + } 1245 + }, 1246 + "node_modules/@rolldown/binding-win32-x64-msvc": { 1247 + "version": "1.0.0-rc.9", 1248 + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", 1249 + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", 1250 + "cpu": [ 1251 + "x64" 1252 + ], 1253 + "dev": true, 1254 + "license": "MIT", 1255 + "optional": true, 1256 + "os": [ 1257 + "win32" 1258 + ], 1259 + "engines": { 1260 + "node": "^20.19.0 || >=22.12.0" 1261 + } 1262 + }, 1263 + "node_modules/@rolldown/pluginutils": { 1264 + "version": "1.0.0-rc.7", 1265 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", 1266 + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", 1267 + "dev": true, 1268 + "license": "MIT" 1269 + }, 1270 + "node_modules/@tybys/wasm-util": { 1271 + "version": "0.10.1", 1272 + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", 1273 + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", 1274 + "dev": true, 1275 + "license": "MIT", 1276 + "optional": true, 1277 + "dependencies": { 1278 + "tslib": "^2.4.0" 1279 + } 1280 + }, 1281 + "node_modules/@types/estree": { 1282 + "version": "1.0.8", 1283 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1284 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1285 + "dev": true, 1286 + "license": "MIT" 1287 + }, 1288 + "node_modules/@types/json-schema": { 1289 + "version": "7.0.15", 1290 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 1291 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 1292 + "dev": true, 1293 + "license": "MIT" 1294 + }, 1295 + "node_modules/@types/node": { 1296 + "version": "24.12.0", 1297 + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", 1298 + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", 1299 + "dev": true, 1300 + "license": "MIT", 1301 + "peer": true, 1302 + "dependencies": { 1303 + "undici-types": "~7.16.0" 1304 + } 1305 + }, 1306 + "node_modules/@types/parse-json": { 1307 + "version": "4.0.2", 1308 + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", 1309 + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", 1310 + "license": "MIT" 1311 + }, 1312 + "node_modules/@types/prop-types": { 1313 + "version": "15.7.15", 1314 + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", 1315 + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", 1316 + "license": "MIT" 1317 + }, 1318 + "node_modules/@types/react": { 1319 + "version": "19.2.14", 1320 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", 1321 + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", 1322 + "license": "MIT", 1323 + "peer": true, 1324 + "dependencies": { 1325 + "csstype": "^3.2.2" 1326 + } 1327 + }, 1328 + "node_modules/@types/react-dom": { 1329 + "version": "19.2.3", 1330 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", 1331 + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", 1332 + "dev": true, 1333 + "license": "MIT", 1334 + "peerDependencies": { 1335 + "@types/react": "^19.2.0" 1336 + } 1337 + }, 1338 + "node_modules/@types/react-transition-group": { 1339 + "version": "4.4.12", 1340 + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", 1341 + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", 1342 + "license": "MIT", 1343 + "peerDependencies": { 1344 + "@types/react": "*" 1345 + } 1346 + }, 1347 + "node_modules/@typescript-eslint/eslint-plugin": { 1348 + "version": "8.57.0", 1349 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", 1350 + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", 1351 + "dev": true, 1352 + "license": "MIT", 1353 + "dependencies": { 1354 + "@eslint-community/regexpp": "^4.12.2", 1355 + "@typescript-eslint/scope-manager": "8.57.0", 1356 + "@typescript-eslint/type-utils": "8.57.0", 1357 + "@typescript-eslint/utils": "8.57.0", 1358 + "@typescript-eslint/visitor-keys": "8.57.0", 1359 + "ignore": "^7.0.5", 1360 + "natural-compare": "^1.4.0", 1361 + "ts-api-utils": "^2.4.0" 1362 + }, 1363 + "engines": { 1364 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1365 + }, 1366 + "funding": { 1367 + "type": "opencollective", 1368 + "url": "https://opencollective.com/typescript-eslint" 1369 + }, 1370 + "peerDependencies": { 1371 + "@typescript-eslint/parser": "^8.57.0", 1372 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 1373 + "typescript": ">=4.8.4 <6.0.0" 1374 + } 1375 + }, 1376 + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { 1377 + "version": "7.0.5", 1378 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", 1379 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", 1380 + "dev": true, 1381 + "license": "MIT", 1382 + "engines": { 1383 + "node": ">= 4" 1384 + } 1385 + }, 1386 + "node_modules/@typescript-eslint/parser": { 1387 + "version": "8.57.0", 1388 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", 1389 + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", 1390 + "dev": true, 1391 + "license": "MIT", 1392 + "peer": true, 1393 + "dependencies": { 1394 + "@typescript-eslint/scope-manager": "8.57.0", 1395 + "@typescript-eslint/types": "8.57.0", 1396 + "@typescript-eslint/typescript-estree": "8.57.0", 1397 + "@typescript-eslint/visitor-keys": "8.57.0", 1398 + "debug": "^4.4.3" 1399 + }, 1400 + "engines": { 1401 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1402 + }, 1403 + "funding": { 1404 + "type": "opencollective", 1405 + "url": "https://opencollective.com/typescript-eslint" 1406 + }, 1407 + "peerDependencies": { 1408 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 1409 + "typescript": ">=4.8.4 <6.0.0" 1410 + } 1411 + }, 1412 + "node_modules/@typescript-eslint/project-service": { 1413 + "version": "8.57.0", 1414 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", 1415 + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", 1416 + "dev": true, 1417 + "license": "MIT", 1418 + "dependencies": { 1419 + "@typescript-eslint/tsconfig-utils": "^8.57.0", 1420 + "@typescript-eslint/types": "^8.57.0", 1421 + "debug": "^4.4.3" 1422 + }, 1423 + "engines": { 1424 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1425 + }, 1426 + "funding": { 1427 + "type": "opencollective", 1428 + "url": "https://opencollective.com/typescript-eslint" 1429 + }, 1430 + "peerDependencies": { 1431 + "typescript": ">=4.8.4 <6.0.0" 1432 + } 1433 + }, 1434 + "node_modules/@typescript-eslint/scope-manager": { 1435 + "version": "8.57.0", 1436 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", 1437 + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", 1438 + "dev": true, 1439 + "license": "MIT", 1440 + "dependencies": { 1441 + "@typescript-eslint/types": "8.57.0", 1442 + "@typescript-eslint/visitor-keys": "8.57.0" 1443 + }, 1444 + "engines": { 1445 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1446 + }, 1447 + "funding": { 1448 + "type": "opencollective", 1449 + "url": "https://opencollective.com/typescript-eslint" 1450 + } 1451 + }, 1452 + "node_modules/@typescript-eslint/tsconfig-utils": { 1453 + "version": "8.57.0", 1454 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", 1455 + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", 1456 + "dev": true, 1457 + "license": "MIT", 1458 + "engines": { 1459 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1460 + }, 1461 + "funding": { 1462 + "type": "opencollective", 1463 + "url": "https://opencollective.com/typescript-eslint" 1464 + }, 1465 + "peerDependencies": { 1466 + "typescript": ">=4.8.4 <6.0.0" 1467 + } 1468 + }, 1469 + "node_modules/@typescript-eslint/type-utils": { 1470 + "version": "8.57.0", 1471 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", 1472 + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", 1473 + "dev": true, 1474 + "license": "MIT", 1475 + "dependencies": { 1476 + "@typescript-eslint/types": "8.57.0", 1477 + "@typescript-eslint/typescript-estree": "8.57.0", 1478 + "@typescript-eslint/utils": "8.57.0", 1479 + "debug": "^4.4.3", 1480 + "ts-api-utils": "^2.4.0" 1481 + }, 1482 + "engines": { 1483 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1484 + }, 1485 + "funding": { 1486 + "type": "opencollective", 1487 + "url": "https://opencollective.com/typescript-eslint" 1488 + }, 1489 + "peerDependencies": { 1490 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 1491 + "typescript": ">=4.8.4 <6.0.0" 1492 + } 1493 + }, 1494 + "node_modules/@typescript-eslint/types": { 1495 + "version": "8.57.0", 1496 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", 1497 + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", 1498 + "dev": true, 1499 + "license": "MIT", 1500 + "engines": { 1501 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1502 + }, 1503 + "funding": { 1504 + "type": "opencollective", 1505 + "url": "https://opencollective.com/typescript-eslint" 1506 + } 1507 + }, 1508 + "node_modules/@typescript-eslint/typescript-estree": { 1509 + "version": "8.57.0", 1510 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", 1511 + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", 1512 + "dev": true, 1513 + "license": "MIT", 1514 + "dependencies": { 1515 + "@typescript-eslint/project-service": "8.57.0", 1516 + "@typescript-eslint/tsconfig-utils": "8.57.0", 1517 + "@typescript-eslint/types": "8.57.0", 1518 + "@typescript-eslint/visitor-keys": "8.57.0", 1519 + "debug": "^4.4.3", 1520 + "minimatch": "^10.2.2", 1521 + "semver": "^7.7.3", 1522 + "tinyglobby": "^0.2.15", 1523 + "ts-api-utils": "^2.4.0" 1524 + }, 1525 + "engines": { 1526 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1527 + }, 1528 + "funding": { 1529 + "type": "opencollective", 1530 + "url": "https://opencollective.com/typescript-eslint" 1531 + }, 1532 + "peerDependencies": { 1533 + "typescript": ">=4.8.4 <6.0.0" 1534 + } 1535 + }, 1536 + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { 1537 + "version": "4.0.4", 1538 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", 1539 + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", 1540 + "dev": true, 1541 + "license": "MIT", 1542 + "engines": { 1543 + "node": "18 || 20 || >=22" 1544 + } 1545 + }, 1546 + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { 1547 + "version": "5.0.4", 1548 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", 1549 + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", 1550 + "dev": true, 1551 + "license": "MIT", 1552 + "dependencies": { 1553 + "balanced-match": "^4.0.2" 1554 + }, 1555 + "engines": { 1556 + "node": "18 || 20 || >=22" 1557 + } 1558 + }, 1559 + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { 1560 + "version": "10.2.4", 1561 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", 1562 + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", 1563 + "dev": true, 1564 + "license": "BlueOak-1.0.0", 1565 + "dependencies": { 1566 + "brace-expansion": "^5.0.2" 1567 + }, 1568 + "engines": { 1569 + "node": "18 || 20 || >=22" 1570 + }, 1571 + "funding": { 1572 + "url": "https://github.com/sponsors/isaacs" 1573 + } 1574 + }, 1575 + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { 1576 + "version": "7.7.4", 1577 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", 1578 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", 1579 + "dev": true, 1580 + "license": "ISC", 1581 + "bin": { 1582 + "semver": "bin/semver.js" 1583 + }, 1584 + "engines": { 1585 + "node": ">=10" 1586 + } 1587 + }, 1588 + "node_modules/@typescript-eslint/utils": { 1589 + "version": "8.57.0", 1590 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", 1591 + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", 1592 + "dev": true, 1593 + "license": "MIT", 1594 + "dependencies": { 1595 + "@eslint-community/eslint-utils": "^4.9.1", 1596 + "@typescript-eslint/scope-manager": "8.57.0", 1597 + "@typescript-eslint/types": "8.57.0", 1598 + "@typescript-eslint/typescript-estree": "8.57.0" 1599 + }, 1600 + "engines": { 1601 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1602 + }, 1603 + "funding": { 1604 + "type": "opencollective", 1605 + "url": "https://opencollective.com/typescript-eslint" 1606 + }, 1607 + "peerDependencies": { 1608 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 1609 + "typescript": ">=4.8.4 <6.0.0" 1610 + } 1611 + }, 1612 + "node_modules/@typescript-eslint/visitor-keys": { 1613 + "version": "8.57.0", 1614 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", 1615 + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", 1616 + "dev": true, 1617 + "license": "MIT", 1618 + "dependencies": { 1619 + "@typescript-eslint/types": "8.57.0", 1620 + "eslint-visitor-keys": "^5.0.0" 1621 + }, 1622 + "engines": { 1623 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1624 + }, 1625 + "funding": { 1626 + "type": "opencollective", 1627 + "url": "https://opencollective.com/typescript-eslint" 1628 + } 1629 + }, 1630 + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { 1631 + "version": "5.0.1", 1632 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", 1633 + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", 1634 + "dev": true, 1635 + "license": "Apache-2.0", 1636 + "engines": { 1637 + "node": "^20.19.0 || ^22.13.0 || >=24" 1638 + }, 1639 + "funding": { 1640 + "url": "https://opencollective.com/eslint" 1641 + } 1642 + }, 1643 + "node_modules/@vitejs/plugin-react": { 1644 + "version": "6.0.1", 1645 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", 1646 + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", 1647 + "dev": true, 1648 + "license": "MIT", 1649 + "dependencies": { 1650 + "@rolldown/pluginutils": "1.0.0-rc.7" 1651 + }, 1652 + "engines": { 1653 + "node": "^20.19.0 || >=22.12.0" 1654 + }, 1655 + "peerDependencies": { 1656 + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", 1657 + "babel-plugin-react-compiler": "^1.0.0", 1658 + "vite": "^8.0.0" 1659 + }, 1660 + "peerDependenciesMeta": { 1661 + "@rolldown/plugin-babel": { 1662 + "optional": true 1663 + }, 1664 + "babel-plugin-react-compiler": { 1665 + "optional": true 1666 + } 1667 + } 1668 + }, 1669 + "node_modules/acorn": { 1670 + "version": "8.16.0", 1671 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", 1672 + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", 1673 + "dev": true, 1674 + "license": "MIT", 1675 + "peer": true, 1676 + "bin": { 1677 + "acorn": "bin/acorn" 1678 + }, 1679 + "engines": { 1680 + "node": ">=0.4.0" 1681 + } 1682 + }, 1683 + "node_modules/acorn-jsx": { 1684 + "version": "5.3.2", 1685 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 1686 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 1687 + "dev": true, 1688 + "license": "MIT", 1689 + "peerDependencies": { 1690 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 1691 + } 1692 + }, 1693 + "node_modules/ajv": { 1694 + "version": "6.14.0", 1695 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", 1696 + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", 1697 + "dev": true, 1698 + "license": "MIT", 1699 + "dependencies": { 1700 + "fast-deep-equal": "^3.1.1", 1701 + "fast-json-stable-stringify": "^2.0.0", 1702 + "json-schema-traverse": "^0.4.1", 1703 + "uri-js": "^4.2.2" 1704 + }, 1705 + "funding": { 1706 + "type": "github", 1707 + "url": "https://github.com/sponsors/epoberezkin" 1708 + } 1709 + }, 1710 + "node_modules/ansi-styles": { 1711 + "version": "4.3.0", 1712 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1713 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1714 + "dev": true, 1715 + "license": "MIT", 1716 + "dependencies": { 1717 + "color-convert": "^2.0.1" 1718 + }, 1719 + "engines": { 1720 + "node": ">=8" 1721 + }, 1722 + "funding": { 1723 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1724 + } 1725 + }, 1726 + "node_modules/argparse": { 1727 + "version": "2.0.1", 1728 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1729 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1730 + "dev": true, 1731 + "license": "Python-2.0" 1732 + }, 1733 + "node_modules/babel-plugin-macros": { 1734 + "version": "3.1.0", 1735 + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", 1736 + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", 1737 + "license": "MIT", 1738 + "dependencies": { 1739 + "@babel/runtime": "^7.12.5", 1740 + "cosmiconfig": "^7.0.0", 1741 + "resolve": "^1.19.0" 1742 + }, 1743 + "engines": { 1744 + "node": ">=10", 1745 + "npm": ">=6" 1746 + } 1747 + }, 1748 + "node_modules/balanced-match": { 1749 + "version": "1.0.2", 1750 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1751 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1752 + "dev": true, 1753 + "license": "MIT" 1754 + }, 1755 + "node_modules/baseline-browser-mapping": { 1756 + "version": "2.10.7", 1757 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", 1758 + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", 1759 + "dev": true, 1760 + "license": "Apache-2.0", 1761 + "bin": { 1762 + "baseline-browser-mapping": "dist/cli.cjs" 1763 + }, 1764 + "engines": { 1765 + "node": ">=6.0.0" 1766 + } 1767 + }, 1768 + "node_modules/brace-expansion": { 1769 + "version": "1.1.12", 1770 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 1771 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 1772 + "dev": true, 1773 + "license": "MIT", 1774 + "dependencies": { 1775 + "balanced-match": "^1.0.0", 1776 + "concat-map": "0.0.1" 1777 + } 1778 + }, 1779 + "node_modules/browserslist": { 1780 + "version": "4.28.1", 1781 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", 1782 + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", 1783 + "dev": true, 1784 + "funding": [ 1785 + { 1786 + "type": "opencollective", 1787 + "url": "https://opencollective.com/browserslist" 1788 + }, 1789 + { 1790 + "type": "tidelift", 1791 + "url": "https://tidelift.com/funding/github/npm/browserslist" 1792 + }, 1793 + { 1794 + "type": "github", 1795 + "url": "https://github.com/sponsors/ai" 1796 + } 1797 + ], 1798 + "license": "MIT", 1799 + "peer": true, 1800 + "dependencies": { 1801 + "baseline-browser-mapping": "^2.9.0", 1802 + "caniuse-lite": "^1.0.30001759", 1803 + "electron-to-chromium": "^1.5.263", 1804 + "node-releases": "^2.0.27", 1805 + "update-browserslist-db": "^1.2.0" 1806 + }, 1807 + "bin": { 1808 + "browserslist": "cli.js" 1809 + }, 1810 + "engines": { 1811 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 1812 + } 1813 + }, 1814 + "node_modules/callsites": { 1815 + "version": "3.1.0", 1816 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 1817 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 1818 + "license": "MIT", 1819 + "engines": { 1820 + "node": ">=6" 1821 + } 1822 + }, 1823 + "node_modules/caniuse-lite": { 1824 + "version": "1.0.30001778", 1825 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", 1826 + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", 1827 + "dev": true, 1828 + "funding": [ 1829 + { 1830 + "type": "opencollective", 1831 + "url": "https://opencollective.com/browserslist" 1832 + }, 1833 + { 1834 + "type": "tidelift", 1835 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 1836 + }, 1837 + { 1838 + "type": "github", 1839 + "url": "https://github.com/sponsors/ai" 1840 + } 1841 + ], 1842 + "license": "CC-BY-4.0" 1843 + }, 1844 + "node_modules/chalk": { 1845 + "version": "4.1.2", 1846 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1847 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1848 + "dev": true, 1849 + "license": "MIT", 1850 + "dependencies": { 1851 + "ansi-styles": "^4.1.0", 1852 + "supports-color": "^7.1.0" 1853 + }, 1854 + "engines": { 1855 + "node": ">=10" 1856 + }, 1857 + "funding": { 1858 + "url": "https://github.com/chalk/chalk?sponsor=1" 1859 + } 1860 + }, 1861 + "node_modules/clsx": { 1862 + "version": "2.1.1", 1863 + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", 1864 + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", 1865 + "license": "MIT", 1866 + "engines": { 1867 + "node": ">=6" 1868 + } 1869 + }, 1870 + "node_modules/color-convert": { 1871 + "version": "2.0.1", 1872 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1873 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1874 + "dev": true, 1875 + "license": "MIT", 1876 + "dependencies": { 1877 + "color-name": "~1.1.4" 1878 + }, 1879 + "engines": { 1880 + "node": ">=7.0.0" 1881 + } 1882 + }, 1883 + "node_modules/color-name": { 1884 + "version": "1.1.4", 1885 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1886 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1887 + "dev": true, 1888 + "license": "MIT" 1889 + }, 1890 + "node_modules/concat-map": { 1891 + "version": "0.0.1", 1892 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1893 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1894 + "dev": true, 1895 + "license": "MIT" 1896 + }, 1897 + "node_modules/convert-source-map": { 1898 + "version": "1.9.0", 1899 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", 1900 + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", 1901 + "license": "MIT" 1902 + }, 1903 + "node_modules/cookie": { 1904 + "version": "1.1.1", 1905 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", 1906 + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", 1907 + "license": "MIT", 1908 + "engines": { 1909 + "node": ">=18" 1910 + }, 1911 + "funding": { 1912 + "type": "opencollective", 1913 + "url": "https://opencollective.com/express" 1914 + } 1915 + }, 1916 + "node_modules/cosmiconfig": { 1917 + "version": "7.1.0", 1918 + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", 1919 + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", 1920 + "license": "MIT", 1921 + "dependencies": { 1922 + "@types/parse-json": "^4.0.0", 1923 + "import-fresh": "^3.2.1", 1924 + "parse-json": "^5.0.0", 1925 + "path-type": "^4.0.0", 1926 + "yaml": "^1.10.0" 1927 + }, 1928 + "engines": { 1929 + "node": ">=10" 1930 + } 1931 + }, 1932 + "node_modules/cosmiconfig/node_modules/yaml": { 1933 + "version": "1.10.2", 1934 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", 1935 + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", 1936 + "license": "ISC", 1937 + "engines": { 1938 + "node": ">= 6" 1939 + } 1940 + }, 1941 + "node_modules/cross-spawn": { 1942 + "version": "7.0.6", 1943 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 1944 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 1945 + "dev": true, 1946 + "license": "MIT", 1947 + "dependencies": { 1948 + "path-key": "^3.1.0", 1949 + "shebang-command": "^2.0.0", 1950 + "which": "^2.0.1" 1951 + }, 1952 + "engines": { 1953 + "node": ">= 8" 1954 + } 1955 + }, 1956 + "node_modules/csstype": { 1957 + "version": "3.2.3", 1958 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", 1959 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", 1960 + "license": "MIT" 1961 + }, 1962 + "node_modules/debug": { 1963 + "version": "4.4.3", 1964 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1965 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1966 + "license": "MIT", 1967 + "dependencies": { 1968 + "ms": "^2.1.3" 1969 + }, 1970 + "engines": { 1971 + "node": ">=6.0" 1972 + }, 1973 + "peerDependenciesMeta": { 1974 + "supports-color": { 1975 + "optional": true 1976 + } 1977 + } 1978 + }, 1979 + "node_modules/deep-is": { 1980 + "version": "0.1.4", 1981 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 1982 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 1983 + "dev": true, 1984 + "license": "MIT" 1985 + }, 1986 + "node_modules/detect-libc": { 1987 + "version": "2.1.2", 1988 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 1989 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 1990 + "dev": true, 1991 + "license": "Apache-2.0", 1992 + "engines": { 1993 + "node": ">=8" 1994 + } 1995 + }, 1996 + "node_modules/dom-helpers": { 1997 + "version": "5.2.1", 1998 + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", 1999 + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", 2000 + "license": "MIT", 2001 + "dependencies": { 2002 + "@babel/runtime": "^7.8.7", 2003 + "csstype": "^3.0.2" 2004 + } 2005 + }, 2006 + "node_modules/electron-to-chromium": { 2007 + "version": "1.5.313", 2008 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", 2009 + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", 2010 + "dev": true, 2011 + "license": "ISC" 2012 + }, 2013 + "node_modules/error-ex": { 2014 + "version": "1.3.4", 2015 + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", 2016 + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", 2017 + "license": "MIT", 2018 + "dependencies": { 2019 + "is-arrayish": "^0.2.1" 2020 + } 2021 + }, 2022 + "node_modules/escalade": { 2023 + "version": "3.2.0", 2024 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 2025 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 2026 + "dev": true, 2027 + "license": "MIT", 2028 + "engines": { 2029 + "node": ">=6" 2030 + } 2031 + }, 2032 + "node_modules/escape-string-regexp": { 2033 + "version": "4.0.0", 2034 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 2035 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 2036 + "license": "MIT", 2037 + "engines": { 2038 + "node": ">=10" 2039 + }, 2040 + "funding": { 2041 + "url": "https://github.com/sponsors/sindresorhus" 2042 + } 2043 + }, 2044 + "node_modules/eslint": { 2045 + "version": "9.39.4", 2046 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", 2047 + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", 2048 + "dev": true, 2049 + "license": "MIT", 2050 + "peer": true, 2051 + "dependencies": { 2052 + "@eslint-community/eslint-utils": "^4.8.0", 2053 + "@eslint-community/regexpp": "^4.12.1", 2054 + "@eslint/config-array": "^0.21.2", 2055 + "@eslint/config-helpers": "^0.4.2", 2056 + "@eslint/core": "^0.17.0", 2057 + "@eslint/eslintrc": "^3.3.5", 2058 + "@eslint/js": "9.39.4", 2059 + "@eslint/plugin-kit": "^0.4.1", 2060 + "@humanfs/node": "^0.16.6", 2061 + "@humanwhocodes/module-importer": "^1.0.1", 2062 + "@humanwhocodes/retry": "^0.4.2", 2063 + "@types/estree": "^1.0.6", 2064 + "ajv": "^6.14.0", 2065 + "chalk": "^4.0.0", 2066 + "cross-spawn": "^7.0.6", 2067 + "debug": "^4.3.2", 2068 + "escape-string-regexp": "^4.0.0", 2069 + "eslint-scope": "^8.4.0", 2070 + "eslint-visitor-keys": "^4.2.1", 2071 + "espree": "^10.4.0", 2072 + "esquery": "^1.5.0", 2073 + "esutils": "^2.0.2", 2074 + "fast-deep-equal": "^3.1.3", 2075 + "file-entry-cache": "^8.0.0", 2076 + "find-up": "^5.0.0", 2077 + "glob-parent": "^6.0.2", 2078 + "ignore": "^5.2.0", 2079 + "imurmurhash": "^0.1.4", 2080 + "is-glob": "^4.0.0", 2081 + "json-stable-stringify-without-jsonify": "^1.0.1", 2082 + "lodash.merge": "^4.6.2", 2083 + "minimatch": "^3.1.5", 2084 + "natural-compare": "^1.4.0", 2085 + "optionator": "^0.9.3" 2086 + }, 2087 + "bin": { 2088 + "eslint": "bin/eslint.js" 2089 + }, 2090 + "engines": { 2091 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2092 + }, 2093 + "funding": { 2094 + "url": "https://eslint.org/donate" 2095 + }, 2096 + "peerDependencies": { 2097 + "jiti": "*" 2098 + }, 2099 + "peerDependenciesMeta": { 2100 + "jiti": { 2101 + "optional": true 2102 + } 2103 + } 2104 + }, 2105 + "node_modules/eslint-plugin-react-hooks": { 2106 + "version": "7.0.1", 2107 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", 2108 + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", 2109 + "dev": true, 2110 + "license": "MIT", 2111 + "dependencies": { 2112 + "@babel/core": "^7.24.4", 2113 + "@babel/parser": "^7.24.4", 2114 + "hermes-parser": "^0.25.1", 2115 + "zod": "^3.25.0 || ^4.0.0", 2116 + "zod-validation-error": "^3.5.0 || ^4.0.0" 2117 + }, 2118 + "engines": { 2119 + "node": ">=18" 2120 + }, 2121 + "peerDependencies": { 2122 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 2123 + } 2124 + }, 2125 + "node_modules/eslint-plugin-react-refresh": { 2126 + "version": "0.5.2", 2127 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", 2128 + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", 2129 + "dev": true, 2130 + "license": "MIT", 2131 + "peerDependencies": { 2132 + "eslint": "^9 || ^10" 2133 + } 2134 + }, 2135 + "node_modules/eslint-scope": { 2136 + "version": "8.4.0", 2137 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 2138 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 2139 + "dev": true, 2140 + "license": "BSD-2-Clause", 2141 + "dependencies": { 2142 + "esrecurse": "^4.3.0", 2143 + "estraverse": "^5.2.0" 2144 + }, 2145 + "engines": { 2146 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2147 + }, 2148 + "funding": { 2149 + "url": "https://opencollective.com/eslint" 2150 + } 2151 + }, 2152 + "node_modules/eslint-visitor-keys": { 2153 + "version": "4.2.1", 2154 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 2155 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 2156 + "dev": true, 2157 + "license": "Apache-2.0", 2158 + "engines": { 2159 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2160 + }, 2161 + "funding": { 2162 + "url": "https://opencollective.com/eslint" 2163 + } 2164 + }, 2165 + "node_modules/espree": { 2166 + "version": "10.4.0", 2167 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 2168 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 2169 + "dev": true, 2170 + "license": "BSD-2-Clause", 2171 + "dependencies": { 2172 + "acorn": "^8.15.0", 2173 + "acorn-jsx": "^5.3.2", 2174 + "eslint-visitor-keys": "^4.2.1" 2175 + }, 2176 + "engines": { 2177 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2178 + }, 2179 + "funding": { 2180 + "url": "https://opencollective.com/eslint" 2181 + } 2182 + }, 2183 + "node_modules/esquery": { 2184 + "version": "1.7.0", 2185 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", 2186 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", 2187 + "dev": true, 2188 + "license": "BSD-3-Clause", 2189 + "dependencies": { 2190 + "estraverse": "^5.1.0" 2191 + }, 2192 + "engines": { 2193 + "node": ">=0.10" 2194 + } 2195 + }, 2196 + "node_modules/esrecurse": { 2197 + "version": "4.3.0", 2198 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 2199 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 2200 + "dev": true, 2201 + "license": "BSD-2-Clause", 2202 + "dependencies": { 2203 + "estraverse": "^5.2.0" 2204 + }, 2205 + "engines": { 2206 + "node": ">=4.0" 2207 + } 2208 + }, 2209 + "node_modules/estraverse": { 2210 + "version": "5.3.0", 2211 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2212 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2213 + "dev": true, 2214 + "license": "BSD-2-Clause", 2215 + "engines": { 2216 + "node": ">=4.0" 2217 + } 2218 + }, 2219 + "node_modules/esutils": { 2220 + "version": "2.0.3", 2221 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 2222 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 2223 + "dev": true, 2224 + "license": "BSD-2-Clause", 2225 + "engines": { 2226 + "node": ">=0.10.0" 2227 + } 2228 + }, 2229 + "node_modules/fast-deep-equal": { 2230 + "version": "3.1.3", 2231 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 2232 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2233 + "dev": true, 2234 + "license": "MIT" 2235 + }, 2236 + "node_modules/fast-json-stable-stringify": { 2237 + "version": "2.1.0", 2238 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 2239 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 2240 + "dev": true, 2241 + "license": "MIT" 2242 + }, 2243 + "node_modules/fast-levenshtein": { 2244 + "version": "2.0.6", 2245 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 2246 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 2247 + "dev": true, 2248 + "license": "MIT" 2249 + }, 2250 + "node_modules/fdir": { 2251 + "version": "6.5.0", 2252 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 2253 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 2254 + "dev": true, 2255 + "license": "MIT", 2256 + "engines": { 2257 + "node": ">=12.0.0" 2258 + }, 2259 + "peerDependencies": { 2260 + "picomatch": "^3 || ^4" 2261 + }, 2262 + "peerDependenciesMeta": { 2263 + "picomatch": { 2264 + "optional": true 2265 + } 2266 + } 2267 + }, 2268 + "node_modules/file-entry-cache": { 2269 + "version": "8.0.0", 2270 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 2271 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 2272 + "dev": true, 2273 + "license": "MIT", 2274 + "dependencies": { 2275 + "flat-cache": "^4.0.0" 2276 + }, 2277 + "engines": { 2278 + "node": ">=16.0.0" 2279 + } 2280 + }, 2281 + "node_modules/find-root": { 2282 + "version": "1.1.0", 2283 + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", 2284 + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", 2285 + "license": "MIT" 2286 + }, 2287 + "node_modules/find-up": { 2288 + "version": "5.0.0", 2289 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 2290 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 2291 + "dev": true, 2292 + "license": "MIT", 2293 + "dependencies": { 2294 + "locate-path": "^6.0.0", 2295 + "path-exists": "^4.0.0" 2296 + }, 2297 + "engines": { 2298 + "node": ">=10" 2299 + }, 2300 + "funding": { 2301 + "url": "https://github.com/sponsors/sindresorhus" 2302 + } 2303 + }, 2304 + "node_modules/flat-cache": { 2305 + "version": "4.0.1", 2306 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 2307 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 2308 + "dev": true, 2309 + "license": "MIT", 2310 + "dependencies": { 2311 + "flatted": "^3.2.9", 2312 + "keyv": "^4.5.4" 2313 + }, 2314 + "engines": { 2315 + "node": ">=16" 2316 + } 2317 + }, 2318 + "node_modules/flatted": { 2319 + "version": "3.4.1", 2320 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", 2321 + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", 2322 + "dev": true, 2323 + "license": "ISC" 2324 + }, 2325 + "node_modules/fsevents": { 2326 + "version": "2.3.3", 2327 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 2328 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 2329 + "dev": true, 2330 + "hasInstallScript": true, 2331 + "license": "MIT", 2332 + "optional": true, 2333 + "os": [ 2334 + "darwin" 2335 + ], 2336 + "engines": { 2337 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 2338 + } 2339 + }, 2340 + "node_modules/function-bind": { 2341 + "version": "1.1.2", 2342 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 2343 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 2344 + "license": "MIT", 2345 + "funding": { 2346 + "url": "https://github.com/sponsors/ljharb" 2347 + } 2348 + }, 2349 + "node_modules/gensync": { 2350 + "version": "1.0.0-beta.2", 2351 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 2352 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 2353 + "dev": true, 2354 + "license": "MIT", 2355 + "engines": { 2356 + "node": ">=6.9.0" 2357 + } 2358 + }, 2359 + "node_modules/glob-parent": { 2360 + "version": "6.0.2", 2361 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 2362 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 2363 + "dev": true, 2364 + "license": "ISC", 2365 + "dependencies": { 2366 + "is-glob": "^4.0.3" 2367 + }, 2368 + "engines": { 2369 + "node": ">=10.13.0" 2370 + } 2371 + }, 2372 + "node_modules/globals": { 2373 + "version": "17.4.0", 2374 + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", 2375 + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", 2376 + "dev": true, 2377 + "license": "MIT", 2378 + "engines": { 2379 + "node": ">=18" 2380 + }, 2381 + "funding": { 2382 + "url": "https://github.com/sponsors/sindresorhus" 2383 + } 2384 + }, 2385 + "node_modules/has-flag": { 2386 + "version": "4.0.0", 2387 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2388 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2389 + "dev": true, 2390 + "license": "MIT", 2391 + "engines": { 2392 + "node": ">=8" 2393 + } 2394 + }, 2395 + "node_modules/hasown": { 2396 + "version": "2.0.2", 2397 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 2398 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 2399 + "license": "MIT", 2400 + "dependencies": { 2401 + "function-bind": "^1.1.2" 2402 + }, 2403 + "engines": { 2404 + "node": ">= 0.4" 2405 + } 2406 + }, 2407 + "node_modules/hermes-estree": { 2408 + "version": "0.25.1", 2409 + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", 2410 + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", 2411 + "dev": true, 2412 + "license": "MIT" 2413 + }, 2414 + "node_modules/hermes-parser": { 2415 + "version": "0.25.1", 2416 + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", 2417 + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", 2418 + "dev": true, 2419 + "license": "MIT", 2420 + "dependencies": { 2421 + "hermes-estree": "0.25.1" 2422 + } 2423 + }, 2424 + "node_modules/hoist-non-react-statics": { 2425 + "version": "3.3.2", 2426 + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", 2427 + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", 2428 + "license": "BSD-3-Clause", 2429 + "dependencies": { 2430 + "react-is": "^16.7.0" 2431 + } 2432 + }, 2433 + "node_modules/hoist-non-react-statics/node_modules/react-is": { 2434 + "version": "16.13.1", 2435 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 2436 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 2437 + "license": "MIT" 2438 + }, 2439 + "node_modules/ignore": { 2440 + "version": "5.3.2", 2441 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 2442 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 2443 + "dev": true, 2444 + "license": "MIT", 2445 + "engines": { 2446 + "node": ">= 4" 2447 + } 2448 + }, 2449 + "node_modules/import-fresh": { 2450 + "version": "3.3.1", 2451 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 2452 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 2453 + "license": "MIT", 2454 + "dependencies": { 2455 + "parent-module": "^1.0.0", 2456 + "resolve-from": "^4.0.0" 2457 + }, 2458 + "engines": { 2459 + "node": ">=6" 2460 + }, 2461 + "funding": { 2462 + "url": "https://github.com/sponsors/sindresorhus" 2463 + } 2464 + }, 2465 + "node_modules/imurmurhash": { 2466 + "version": "0.1.4", 2467 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 2468 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 2469 + "dev": true, 2470 + "license": "MIT", 2471 + "engines": { 2472 + "node": ">=0.8.19" 2473 + } 2474 + }, 2475 + "node_modules/is-arrayish": { 2476 + "version": "0.2.1", 2477 + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 2478 + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", 2479 + "license": "MIT" 2480 + }, 2481 + "node_modules/is-core-module": { 2482 + "version": "2.16.1", 2483 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", 2484 + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", 2485 + "license": "MIT", 2486 + "dependencies": { 2487 + "hasown": "^2.0.2" 2488 + }, 2489 + "engines": { 2490 + "node": ">= 0.4" 2491 + }, 2492 + "funding": { 2493 + "url": "https://github.com/sponsors/ljharb" 2494 + } 2495 + }, 2496 + "node_modules/is-extglob": { 2497 + "version": "2.1.1", 2498 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 2499 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 2500 + "dev": true, 2501 + "license": "MIT", 2502 + "engines": { 2503 + "node": ">=0.10.0" 2504 + } 2505 + }, 2506 + "node_modules/is-glob": { 2507 + "version": "4.0.3", 2508 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 2509 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 2510 + "dev": true, 2511 + "license": "MIT", 2512 + "dependencies": { 2513 + "is-extglob": "^2.1.1" 2514 + }, 2515 + "engines": { 2516 + "node": ">=0.10.0" 2517 + } 2518 + }, 2519 + "node_modules/isexe": { 2520 + "version": "2.0.0", 2521 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 2522 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 2523 + "dev": true, 2524 + "license": "ISC" 2525 + }, 2526 + "node_modules/js-tokens": { 2527 + "version": "4.0.0", 2528 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 2529 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 2530 + "license": "MIT" 2531 + }, 2532 + "node_modules/js-yaml": { 2533 + "version": "4.1.1", 2534 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 2535 + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 2536 + "dev": true, 2537 + "license": "MIT", 2538 + "dependencies": { 2539 + "argparse": "^2.0.1" 2540 + }, 2541 + "bin": { 2542 + "js-yaml": "bin/js-yaml.js" 2543 + } 2544 + }, 2545 + "node_modules/jsesc": { 2546 + "version": "3.1.0", 2547 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 2548 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 2549 + "license": "MIT", 2550 + "bin": { 2551 + "jsesc": "bin/jsesc" 2552 + }, 2553 + "engines": { 2554 + "node": ">=6" 2555 + } 2556 + }, 2557 + "node_modules/json-buffer": { 2558 + "version": "3.0.1", 2559 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 2560 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 2561 + "dev": true, 2562 + "license": "MIT" 2563 + }, 2564 + "node_modules/json-parse-even-better-errors": { 2565 + "version": "2.3.1", 2566 + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", 2567 + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", 2568 + "license": "MIT" 2569 + }, 2570 + "node_modules/json-schema-traverse": { 2571 + "version": "0.4.1", 2572 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 2573 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 2574 + "dev": true, 2575 + "license": "MIT" 2576 + }, 2577 + "node_modules/json-stable-stringify-without-jsonify": { 2578 + "version": "1.0.1", 2579 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 2580 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 2581 + "dev": true, 2582 + "license": "MIT" 2583 + }, 2584 + "node_modules/json5": { 2585 + "version": "2.2.3", 2586 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 2587 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 2588 + "dev": true, 2589 + "license": "MIT", 2590 + "bin": { 2591 + "json5": "lib/cli.js" 2592 + }, 2593 + "engines": { 2594 + "node": ">=6" 2595 + } 2596 + }, 2597 + "node_modules/keyv": { 2598 + "version": "4.5.4", 2599 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 2600 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 2601 + "dev": true, 2602 + "license": "MIT", 2603 + "dependencies": { 2604 + "json-buffer": "3.0.1" 2605 + } 2606 + }, 2607 + "node_modules/levn": { 2608 + "version": "0.4.1", 2609 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 2610 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 2611 + "dev": true, 2612 + "license": "MIT", 2613 + "dependencies": { 2614 + "prelude-ls": "^1.2.1", 2615 + "type-check": "~0.4.0" 2616 + }, 2617 + "engines": { 2618 + "node": ">= 0.8.0" 2619 + } 2620 + }, 2621 + "node_modules/lightningcss": { 2622 + "version": "1.32.0", 2623 + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", 2624 + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", 2625 + "dev": true, 2626 + "license": "MPL-2.0", 2627 + "dependencies": { 2628 + "detect-libc": "^2.0.3" 2629 + }, 2630 + "engines": { 2631 + "node": ">= 12.0.0" 2632 + }, 2633 + "funding": { 2634 + "type": "opencollective", 2635 + "url": "https://opencollective.com/parcel" 2636 + }, 2637 + "optionalDependencies": { 2638 + "lightningcss-android-arm64": "1.32.0", 2639 + "lightningcss-darwin-arm64": "1.32.0", 2640 + "lightningcss-darwin-x64": "1.32.0", 2641 + "lightningcss-freebsd-x64": "1.32.0", 2642 + "lightningcss-linux-arm-gnueabihf": "1.32.0", 2643 + "lightningcss-linux-arm64-gnu": "1.32.0", 2644 + "lightningcss-linux-arm64-musl": "1.32.0", 2645 + "lightningcss-linux-x64-gnu": "1.32.0", 2646 + "lightningcss-linux-x64-musl": "1.32.0", 2647 + "lightningcss-win32-arm64-msvc": "1.32.0", 2648 + "lightningcss-win32-x64-msvc": "1.32.0" 2649 + } 2650 + }, 2651 + "node_modules/lightningcss-android-arm64": { 2652 + "version": "1.32.0", 2653 + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", 2654 + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", 2655 + "cpu": [ 2656 + "arm64" 2657 + ], 2658 + "dev": true, 2659 + "license": "MPL-2.0", 2660 + "optional": true, 2661 + "os": [ 2662 + "android" 2663 + ], 2664 + "engines": { 2665 + "node": ">= 12.0.0" 2666 + }, 2667 + "funding": { 2668 + "type": "opencollective", 2669 + "url": "https://opencollective.com/parcel" 2670 + } 2671 + }, 2672 + "node_modules/lightningcss-darwin-arm64": { 2673 + "version": "1.32.0", 2674 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", 2675 + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", 2676 + "cpu": [ 2677 + "arm64" 2678 + ], 2679 + "dev": true, 2680 + "license": "MPL-2.0", 2681 + "optional": true, 2682 + "os": [ 2683 + "darwin" 2684 + ], 2685 + "engines": { 2686 + "node": ">= 12.0.0" 2687 + }, 2688 + "funding": { 2689 + "type": "opencollective", 2690 + "url": "https://opencollective.com/parcel" 2691 + } 2692 + }, 2693 + "node_modules/lightningcss-darwin-x64": { 2694 + "version": "1.32.0", 2695 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", 2696 + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", 2697 + "cpu": [ 2698 + "x64" 2699 + ], 2700 + "dev": true, 2701 + "license": "MPL-2.0", 2702 + "optional": true, 2703 + "os": [ 2704 + "darwin" 2705 + ], 2706 + "engines": { 2707 + "node": ">= 12.0.0" 2708 + }, 2709 + "funding": { 2710 + "type": "opencollective", 2711 + "url": "https://opencollective.com/parcel" 2712 + } 2713 + }, 2714 + "node_modules/lightningcss-freebsd-x64": { 2715 + "version": "1.32.0", 2716 + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", 2717 + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", 2718 + "cpu": [ 2719 + "x64" 2720 + ], 2721 + "dev": true, 2722 + "license": "MPL-2.0", 2723 + "optional": true, 2724 + "os": [ 2725 + "freebsd" 2726 + ], 2727 + "engines": { 2728 + "node": ">= 12.0.0" 2729 + }, 2730 + "funding": { 2731 + "type": "opencollective", 2732 + "url": "https://opencollective.com/parcel" 2733 + } 2734 + }, 2735 + "node_modules/lightningcss-linux-arm-gnueabihf": { 2736 + "version": "1.32.0", 2737 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", 2738 + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", 2739 + "cpu": [ 2740 + "arm" 2741 + ], 2742 + "dev": true, 2743 + "license": "MPL-2.0", 2744 + "optional": true, 2745 + "os": [ 2746 + "linux" 2747 + ], 2748 + "engines": { 2749 + "node": ">= 12.0.0" 2750 + }, 2751 + "funding": { 2752 + "type": "opencollective", 2753 + "url": "https://opencollective.com/parcel" 2754 + } 2755 + }, 2756 + "node_modules/lightningcss-linux-arm64-gnu": { 2757 + "version": "1.32.0", 2758 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", 2759 + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", 2760 + "cpu": [ 2761 + "arm64" 2762 + ], 2763 + "dev": true, 2764 + "license": "MPL-2.0", 2765 + "optional": true, 2766 + "os": [ 2767 + "linux" 2768 + ], 2769 + "engines": { 2770 + "node": ">= 12.0.0" 2771 + }, 2772 + "funding": { 2773 + "type": "opencollective", 2774 + "url": "https://opencollective.com/parcel" 2775 + } 2776 + }, 2777 + "node_modules/lightningcss-linux-arm64-musl": { 2778 + "version": "1.32.0", 2779 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", 2780 + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", 2781 + "cpu": [ 2782 + "arm64" 2783 + ], 2784 + "dev": true, 2785 + "license": "MPL-2.0", 2786 + "optional": true, 2787 + "os": [ 2788 + "linux" 2789 + ], 2790 + "engines": { 2791 + "node": ">= 12.0.0" 2792 + }, 2793 + "funding": { 2794 + "type": "opencollective", 2795 + "url": "https://opencollective.com/parcel" 2796 + } 2797 + }, 2798 + "node_modules/lightningcss-linux-x64-gnu": { 2799 + "version": "1.32.0", 2800 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", 2801 + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", 2802 + "cpu": [ 2803 + "x64" 2804 + ], 2805 + "dev": true, 2806 + "license": "MPL-2.0", 2807 + "optional": true, 2808 + "os": [ 2809 + "linux" 2810 + ], 2811 + "engines": { 2812 + "node": ">= 12.0.0" 2813 + }, 2814 + "funding": { 2815 + "type": "opencollective", 2816 + "url": "https://opencollective.com/parcel" 2817 + } 2818 + }, 2819 + "node_modules/lightningcss-linux-x64-musl": { 2820 + "version": "1.32.0", 2821 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", 2822 + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", 2823 + "cpu": [ 2824 + "x64" 2825 + ], 2826 + "dev": true, 2827 + "license": "MPL-2.0", 2828 + "optional": true, 2829 + "os": [ 2830 + "linux" 2831 + ], 2832 + "engines": { 2833 + "node": ">= 12.0.0" 2834 + }, 2835 + "funding": { 2836 + "type": "opencollective", 2837 + "url": "https://opencollective.com/parcel" 2838 + } 2839 + }, 2840 + "node_modules/lightningcss-win32-arm64-msvc": { 2841 + "version": "1.32.0", 2842 + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", 2843 + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", 2844 + "cpu": [ 2845 + "arm64" 2846 + ], 2847 + "dev": true, 2848 + "license": "MPL-2.0", 2849 + "optional": true, 2850 + "os": [ 2851 + "win32" 2852 + ], 2853 + "engines": { 2854 + "node": ">= 12.0.0" 2855 + }, 2856 + "funding": { 2857 + "type": "opencollective", 2858 + "url": "https://opencollective.com/parcel" 2859 + } 2860 + }, 2861 + "node_modules/lightningcss-win32-x64-msvc": { 2862 + "version": "1.32.0", 2863 + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", 2864 + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", 2865 + "cpu": [ 2866 + "x64" 2867 + ], 2868 + "dev": true, 2869 + "license": "MPL-2.0", 2870 + "optional": true, 2871 + "os": [ 2872 + "win32" 2873 + ], 2874 + "engines": { 2875 + "node": ">= 12.0.0" 2876 + }, 2877 + "funding": { 2878 + "type": "opencollective", 2879 + "url": "https://opencollective.com/parcel" 2880 + } 2881 + }, 2882 + "node_modules/lines-and-columns": { 2883 + "version": "1.2.4", 2884 + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 2885 + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", 2886 + "license": "MIT" 2887 + }, 2888 + "node_modules/locate-path": { 2889 + "version": "6.0.0", 2890 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 2891 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 2892 + "dev": true, 2893 + "license": "MIT", 2894 + "dependencies": { 2895 + "p-locate": "^5.0.0" 2896 + }, 2897 + "engines": { 2898 + "node": ">=10" 2899 + }, 2900 + "funding": { 2901 + "url": "https://github.com/sponsors/sindresorhus" 2902 + } 2903 + }, 2904 + "node_modules/lodash.merge": { 2905 + "version": "4.6.2", 2906 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 2907 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 2908 + "dev": true, 2909 + "license": "MIT" 2910 + }, 2911 + "node_modules/loose-envify": { 2912 + "version": "1.4.0", 2913 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 2914 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 2915 + "license": "MIT", 2916 + "dependencies": { 2917 + "js-tokens": "^3.0.0 || ^4.0.0" 2918 + }, 2919 + "bin": { 2920 + "loose-envify": "cli.js" 2921 + } 2922 + }, 2923 + "node_modules/lru-cache": { 2924 + "version": "5.1.1", 2925 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 2926 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 2927 + "dev": true, 2928 + "license": "ISC", 2929 + "dependencies": { 2930 + "yallist": "^3.0.2" 2931 + } 2932 + }, 2933 + "node_modules/minimatch": { 2934 + "version": "3.1.5", 2935 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", 2936 + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", 2937 + "dev": true, 2938 + "license": "ISC", 2939 + "dependencies": { 2940 + "brace-expansion": "^1.1.7" 2941 + }, 2942 + "engines": { 2943 + "node": "*" 2944 + } 2945 + }, 2946 + "node_modules/ms": { 2947 + "version": "2.1.3", 2948 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 2949 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 2950 + "license": "MIT" 2951 + }, 2952 + "node_modules/nanoid": { 2953 + "version": "3.3.11", 2954 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 2955 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 2956 + "dev": true, 2957 + "funding": [ 2958 + { 2959 + "type": "github", 2960 + "url": "https://github.com/sponsors/ai" 2961 + } 2962 + ], 2963 + "license": "MIT", 2964 + "bin": { 2965 + "nanoid": "bin/nanoid.cjs" 2966 + }, 2967 + "engines": { 2968 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2969 + } 2970 + }, 2971 + "node_modules/natural-compare": { 2972 + "version": "1.4.0", 2973 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 2974 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 2975 + "dev": true, 2976 + "license": "MIT" 2977 + }, 2978 + "node_modules/node-releases": { 2979 + "version": "2.0.36", 2980 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", 2981 + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", 2982 + "dev": true, 2983 + "license": "MIT" 2984 + }, 2985 + "node_modules/object-assign": { 2986 + "version": "4.1.1", 2987 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 2988 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 2989 + "license": "MIT", 2990 + "engines": { 2991 + "node": ">=0.10.0" 2992 + } 2993 + }, 2994 + "node_modules/optionator": { 2995 + "version": "0.9.4", 2996 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 2997 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 2998 + "dev": true, 2999 + "license": "MIT", 3000 + "dependencies": { 3001 + "deep-is": "^0.1.3", 3002 + "fast-levenshtein": "^2.0.6", 3003 + "levn": "^0.4.1", 3004 + "prelude-ls": "^1.2.1", 3005 + "type-check": "^0.4.0", 3006 + "word-wrap": "^1.2.5" 3007 + }, 3008 + "engines": { 3009 + "node": ">= 0.8.0" 3010 + } 3011 + }, 3012 + "node_modules/p-limit": { 3013 + "version": "3.1.0", 3014 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 3015 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 3016 + "dev": true, 3017 + "license": "MIT", 3018 + "dependencies": { 3019 + "yocto-queue": "^0.1.0" 3020 + }, 3021 + "engines": { 3022 + "node": ">=10" 3023 + }, 3024 + "funding": { 3025 + "url": "https://github.com/sponsors/sindresorhus" 3026 + } 3027 + }, 3028 + "node_modules/p-locate": { 3029 + "version": "5.0.0", 3030 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 3031 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 3032 + "dev": true, 3033 + "license": "MIT", 3034 + "dependencies": { 3035 + "p-limit": "^3.0.2" 3036 + }, 3037 + "engines": { 3038 + "node": ">=10" 3039 + }, 3040 + "funding": { 3041 + "url": "https://github.com/sponsors/sindresorhus" 3042 + } 3043 + }, 3044 + "node_modules/parent-module": { 3045 + "version": "1.0.1", 3046 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 3047 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 3048 + "license": "MIT", 3049 + "dependencies": { 3050 + "callsites": "^3.0.0" 3051 + }, 3052 + "engines": { 3053 + "node": ">=6" 3054 + } 3055 + }, 3056 + "node_modules/parse-json": { 3057 + "version": "5.2.0", 3058 + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", 3059 + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", 3060 + "license": "MIT", 3061 + "dependencies": { 3062 + "@babel/code-frame": "^7.0.0", 3063 + "error-ex": "^1.3.1", 3064 + "json-parse-even-better-errors": "^2.3.0", 3065 + "lines-and-columns": "^1.1.6" 3066 + }, 3067 + "engines": { 3068 + "node": ">=8" 3069 + }, 3070 + "funding": { 3071 + "url": "https://github.com/sponsors/sindresorhus" 3072 + } 3073 + }, 3074 + "node_modules/path-exists": { 3075 + "version": "4.0.0", 3076 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 3077 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 3078 + "dev": true, 3079 + "license": "MIT", 3080 + "engines": { 3081 + "node": ">=8" 3082 + } 3083 + }, 3084 + "node_modules/path-key": { 3085 + "version": "3.1.1", 3086 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 3087 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 3088 + "dev": true, 3089 + "license": "MIT", 3090 + "engines": { 3091 + "node": ">=8" 3092 + } 3093 + }, 3094 + "node_modules/path-parse": { 3095 + "version": "1.0.7", 3096 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 3097 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 3098 + "license": "MIT" 3099 + }, 3100 + "node_modules/path-type": { 3101 + "version": "4.0.0", 3102 + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", 3103 + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", 3104 + "license": "MIT", 3105 + "engines": { 3106 + "node": ">=8" 3107 + } 3108 + }, 3109 + "node_modules/picocolors": { 3110 + "version": "1.1.1", 3111 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 3112 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 3113 + "license": "ISC" 3114 + }, 3115 + "node_modules/picomatch": { 3116 + "version": "4.0.3", 3117 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 3118 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 3119 + "dev": true, 3120 + "license": "MIT", 3121 + "peer": true, 3122 + "engines": { 3123 + "node": ">=12" 3124 + }, 3125 + "funding": { 3126 + "url": "https://github.com/sponsors/jonschlinkert" 3127 + } 3128 + }, 3129 + "node_modules/postcss": { 3130 + "version": "8.5.8", 3131 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", 3132 + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", 3133 + "dev": true, 3134 + "funding": [ 3135 + { 3136 + "type": "opencollective", 3137 + "url": "https://opencollective.com/postcss/" 3138 + }, 3139 + { 3140 + "type": "tidelift", 3141 + "url": "https://tidelift.com/funding/github/npm/postcss" 3142 + }, 3143 + { 3144 + "type": "github", 3145 + "url": "https://github.com/sponsors/ai" 3146 + } 3147 + ], 3148 + "license": "MIT", 3149 + "dependencies": { 3150 + "nanoid": "^3.3.11", 3151 + "picocolors": "^1.1.1", 3152 + "source-map-js": "^1.2.1" 3153 + }, 3154 + "engines": { 3155 + "node": "^10 || ^12 || >=14" 3156 + } 3157 + }, 3158 + "node_modules/prelude-ls": { 3159 + "version": "1.2.1", 3160 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 3161 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 3162 + "dev": true, 3163 + "license": "MIT", 3164 + "engines": { 3165 + "node": ">= 0.8.0" 3166 + } 3167 + }, 3168 + "node_modules/prop-types": { 3169 + "version": "15.8.1", 3170 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 3171 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 3172 + "license": "MIT", 3173 + "dependencies": { 3174 + "loose-envify": "^1.4.0", 3175 + "object-assign": "^4.1.1", 3176 + "react-is": "^16.13.1" 3177 + } 3178 + }, 3179 + "node_modules/prop-types/node_modules/react-is": { 3180 + "version": "16.13.1", 3181 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 3182 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 3183 + "license": "MIT" 3184 + }, 3185 + "node_modules/punycode": { 3186 + "version": "2.3.1", 3187 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 3188 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 3189 + "dev": true, 3190 + "license": "MIT", 3191 + "engines": { 3192 + "node": ">=6" 3193 + } 3194 + }, 3195 + "node_modules/react": { 3196 + "version": "19.2.4", 3197 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", 3198 + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", 3199 + "license": "MIT", 3200 + "peer": true, 3201 + "engines": { 3202 + "node": ">=0.10.0" 3203 + } 3204 + }, 3205 + "node_modules/react-dom": { 3206 + "version": "19.2.4", 3207 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", 3208 + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", 3209 + "license": "MIT", 3210 + "peer": true, 3211 + "dependencies": { 3212 + "scheduler": "^0.27.0" 3213 + }, 3214 + "peerDependencies": { 3215 + "react": "^19.2.4" 3216 + } 3217 + }, 3218 + "node_modules/react-is": { 3219 + "version": "19.2.4", 3220 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", 3221 + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", 3222 + "license": "MIT" 3223 + }, 3224 + "node_modules/react-router": { 3225 + "version": "7.13.1", 3226 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", 3227 + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", 3228 + "license": "MIT", 3229 + "dependencies": { 3230 + "cookie": "^1.0.1", 3231 + "set-cookie-parser": "^2.6.0" 3232 + }, 3233 + "engines": { 3234 + "node": ">=20.0.0" 3235 + }, 3236 + "peerDependencies": { 3237 + "react": ">=18", 3238 + "react-dom": ">=18" 3239 + }, 3240 + "peerDependenciesMeta": { 3241 + "react-dom": { 3242 + "optional": true 3243 + } 3244 + } 3245 + }, 3246 + "node_modules/react-router-dom": { 3247 + "version": "7.13.1", 3248 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", 3249 + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", 3250 + "license": "MIT", 3251 + "dependencies": { 3252 + "react-router": "7.13.1" 3253 + }, 3254 + "engines": { 3255 + "node": ">=20.0.0" 3256 + }, 3257 + "peerDependencies": { 3258 + "react": ">=18", 3259 + "react-dom": ">=18" 3260 + } 3261 + }, 3262 + "node_modules/react-transition-group": { 3263 + "version": "4.4.5", 3264 + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", 3265 + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", 3266 + "license": "BSD-3-Clause", 3267 + "dependencies": { 3268 + "@babel/runtime": "^7.5.5", 3269 + "dom-helpers": "^5.0.1", 3270 + "loose-envify": "^1.4.0", 3271 + "prop-types": "^15.6.2" 3272 + }, 3273 + "peerDependencies": { 3274 + "react": ">=16.6.0", 3275 + "react-dom": ">=16.6.0" 3276 + } 3277 + }, 3278 + "node_modules/resolve": { 3279 + "version": "1.22.11", 3280 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", 3281 + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", 3282 + "license": "MIT", 3283 + "dependencies": { 3284 + "is-core-module": "^2.16.1", 3285 + "path-parse": "^1.0.7", 3286 + "supports-preserve-symlinks-flag": "^1.0.0" 3287 + }, 3288 + "bin": { 3289 + "resolve": "bin/resolve" 3290 + }, 3291 + "engines": { 3292 + "node": ">= 0.4" 3293 + }, 3294 + "funding": { 3295 + "url": "https://github.com/sponsors/ljharb" 3296 + } 3297 + }, 3298 + "node_modules/resolve-from": { 3299 + "version": "4.0.0", 3300 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 3301 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 3302 + "license": "MIT", 3303 + "engines": { 3304 + "node": ">=4" 3305 + } 3306 + }, 3307 + "node_modules/rolldown": { 3308 + "version": "1.0.0-rc.9", 3309 + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", 3310 + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", 3311 + "dev": true, 3312 + "license": "MIT", 3313 + "dependencies": { 3314 + "@oxc-project/types": "=0.115.0", 3315 + "@rolldown/pluginutils": "1.0.0-rc.9" 3316 + }, 3317 + "bin": { 3318 + "rolldown": "bin/cli.mjs" 3319 + }, 3320 + "engines": { 3321 + "node": "^20.19.0 || >=22.12.0" 3322 + }, 3323 + "optionalDependencies": { 3324 + "@rolldown/binding-android-arm64": "1.0.0-rc.9", 3325 + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", 3326 + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", 3327 + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", 3328 + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", 3329 + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", 3330 + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", 3331 + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", 3332 + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", 3333 + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", 3334 + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", 3335 + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", 3336 + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", 3337 + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", 3338 + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" 3339 + } 3340 + }, 3341 + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { 3342 + "version": "1.0.0-rc.9", 3343 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", 3344 + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", 3345 + "dev": true, 3346 + "license": "MIT" 3347 + }, 3348 + "node_modules/scheduler": { 3349 + "version": "0.27.0", 3350 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", 3351 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", 3352 + "license": "MIT" 3353 + }, 3354 + "node_modules/semver": { 3355 + "version": "6.3.1", 3356 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 3357 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 3358 + "dev": true, 3359 + "license": "ISC", 3360 + "bin": { 3361 + "semver": "bin/semver.js" 3362 + } 3363 + }, 3364 + "node_modules/set-cookie-parser": { 3365 + "version": "2.7.2", 3366 + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", 3367 + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", 3368 + "license": "MIT" 3369 + }, 3370 + "node_modules/shebang-command": { 3371 + "version": "2.0.0", 3372 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 3373 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 3374 + "dev": true, 3375 + "license": "MIT", 3376 + "dependencies": { 3377 + "shebang-regex": "^3.0.0" 3378 + }, 3379 + "engines": { 3380 + "node": ">=8" 3381 + } 3382 + }, 3383 + "node_modules/shebang-regex": { 3384 + "version": "3.0.0", 3385 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 3386 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 3387 + "dev": true, 3388 + "license": "MIT", 3389 + "engines": { 3390 + "node": ">=8" 3391 + } 3392 + }, 3393 + "node_modules/source-map": { 3394 + "version": "0.5.7", 3395 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 3396 + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", 3397 + "license": "BSD-3-Clause", 3398 + "engines": { 3399 + "node": ">=0.10.0" 3400 + } 3401 + }, 3402 + "node_modules/source-map-js": { 3403 + "version": "1.2.1", 3404 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 3405 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 3406 + "dev": true, 3407 + "license": "BSD-3-Clause", 3408 + "engines": { 3409 + "node": ">=0.10.0" 3410 + } 3411 + }, 3412 + "node_modules/strip-json-comments": { 3413 + "version": "3.1.1", 3414 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 3415 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 3416 + "dev": true, 3417 + "license": "MIT", 3418 + "engines": { 3419 + "node": ">=8" 3420 + }, 3421 + "funding": { 3422 + "url": "https://github.com/sponsors/sindresorhus" 3423 + } 3424 + }, 3425 + "node_modules/stylis": { 3426 + "version": "4.2.0", 3427 + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", 3428 + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", 3429 + "license": "MIT" 3430 + }, 3431 + "node_modules/supports-color": { 3432 + "version": "7.2.0", 3433 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 3434 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 3435 + "dev": true, 3436 + "license": "MIT", 3437 + "dependencies": { 3438 + "has-flag": "^4.0.0" 3439 + }, 3440 + "engines": { 3441 + "node": ">=8" 3442 + } 3443 + }, 3444 + "node_modules/supports-preserve-symlinks-flag": { 3445 + "version": "1.0.0", 3446 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 3447 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 3448 + "license": "MIT", 3449 + "engines": { 3450 + "node": ">= 0.4" 3451 + }, 3452 + "funding": { 3453 + "url": "https://github.com/sponsors/ljharb" 3454 + } 3455 + }, 3456 + "node_modules/tinyglobby": { 3457 + "version": "0.2.15", 3458 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 3459 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 3460 + "dev": true, 3461 + "license": "MIT", 3462 + "dependencies": { 3463 + "fdir": "^6.5.0", 3464 + "picomatch": "^4.0.3" 3465 + }, 3466 + "engines": { 3467 + "node": ">=12.0.0" 3468 + }, 3469 + "funding": { 3470 + "url": "https://github.com/sponsors/SuperchupuDev" 3471 + } 3472 + }, 3473 + "node_modules/ts-api-utils": { 3474 + "version": "2.4.0", 3475 + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", 3476 + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", 3477 + "dev": true, 3478 + "license": "MIT", 3479 + "engines": { 3480 + "node": ">=18.12" 3481 + }, 3482 + "peerDependencies": { 3483 + "typescript": ">=4.8.4" 3484 + } 3485 + }, 3486 + "node_modules/tslib": { 3487 + "version": "2.8.1", 3488 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 3489 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 3490 + "dev": true, 3491 + "license": "0BSD", 3492 + "optional": true 3493 + }, 3494 + "node_modules/type-check": { 3495 + "version": "0.4.0", 3496 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 3497 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 3498 + "dev": true, 3499 + "license": "MIT", 3500 + "dependencies": { 3501 + "prelude-ls": "^1.2.1" 3502 + }, 3503 + "engines": { 3504 + "node": ">= 0.8.0" 3505 + } 3506 + }, 3507 + "node_modules/typescript": { 3508 + "version": "5.9.3", 3509 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 3510 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 3511 + "dev": true, 3512 + "license": "Apache-2.0", 3513 + "peer": true, 3514 + "bin": { 3515 + "tsc": "bin/tsc", 3516 + "tsserver": "bin/tsserver" 3517 + }, 3518 + "engines": { 3519 + "node": ">=14.17" 3520 + } 3521 + }, 3522 + "node_modules/typescript-eslint": { 3523 + "version": "8.57.0", 3524 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", 3525 + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", 3526 + "dev": true, 3527 + "license": "MIT", 3528 + "dependencies": { 3529 + "@typescript-eslint/eslint-plugin": "8.57.0", 3530 + "@typescript-eslint/parser": "8.57.0", 3531 + "@typescript-eslint/typescript-estree": "8.57.0", 3532 + "@typescript-eslint/utils": "8.57.0" 3533 + }, 3534 + "engines": { 3535 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 3536 + }, 3537 + "funding": { 3538 + "type": "opencollective", 3539 + "url": "https://opencollective.com/typescript-eslint" 3540 + }, 3541 + "peerDependencies": { 3542 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 3543 + "typescript": ">=4.8.4 <6.0.0" 3544 + } 3545 + }, 3546 + "node_modules/undici-types": { 3547 + "version": "7.16.0", 3548 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", 3549 + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", 3550 + "dev": true, 3551 + "license": "MIT" 3552 + }, 3553 + "node_modules/update-browserslist-db": { 3554 + "version": "1.2.3", 3555 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", 3556 + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", 3557 + "dev": true, 3558 + "funding": [ 3559 + { 3560 + "type": "opencollective", 3561 + "url": "https://opencollective.com/browserslist" 3562 + }, 3563 + { 3564 + "type": "tidelift", 3565 + "url": "https://tidelift.com/funding/github/npm/browserslist" 3566 + }, 3567 + { 3568 + "type": "github", 3569 + "url": "https://github.com/sponsors/ai" 3570 + } 3571 + ], 3572 + "license": "MIT", 3573 + "dependencies": { 3574 + "escalade": "^3.2.0", 3575 + "picocolors": "^1.1.1" 3576 + }, 3577 + "bin": { 3578 + "update-browserslist-db": "cli.js" 3579 + }, 3580 + "peerDependencies": { 3581 + "browserslist": ">= 4.21.0" 3582 + } 3583 + }, 3584 + "node_modules/uri-js": { 3585 + "version": "4.4.1", 3586 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 3587 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 3588 + "dev": true, 3589 + "license": "BSD-2-Clause", 3590 + "dependencies": { 3591 + "punycode": "^2.1.0" 3592 + } 3593 + }, 3594 + "node_modules/vite": { 3595 + "version": "8.0.0", 3596 + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", 3597 + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", 3598 + "dev": true, 3599 + "license": "MIT", 3600 + "peer": true, 3601 + "dependencies": { 3602 + "@oxc-project/runtime": "0.115.0", 3603 + "lightningcss": "^1.32.0", 3604 + "picomatch": "^4.0.3", 3605 + "postcss": "^8.5.8", 3606 + "rolldown": "1.0.0-rc.9", 3607 + "tinyglobby": "^0.2.15" 3608 + }, 3609 + "bin": { 3610 + "vite": "bin/vite.js" 3611 + }, 3612 + "engines": { 3613 + "node": "^20.19.0 || >=22.12.0" 3614 + }, 3615 + "funding": { 3616 + "url": "https://github.com/vitejs/vite?sponsor=1" 3617 + }, 3618 + "optionalDependencies": { 3619 + "fsevents": "~2.3.3" 3620 + }, 3621 + "peerDependencies": { 3622 + "@types/node": "^20.19.0 || >=22.12.0", 3623 + "@vitejs/devtools": "^0.0.0-alpha.31", 3624 + "esbuild": "^0.27.0", 3625 + "jiti": ">=1.21.0", 3626 + "less": "^4.0.0", 3627 + "sass": "^1.70.0", 3628 + "sass-embedded": "^1.70.0", 3629 + "stylus": ">=0.54.8", 3630 + "sugarss": "^5.0.0", 3631 + "terser": "^5.16.0", 3632 + "tsx": "^4.8.1", 3633 + "yaml": "^2.4.2" 3634 + }, 3635 + "peerDependenciesMeta": { 3636 + "@types/node": { 3637 + "optional": true 3638 + }, 3639 + "@vitejs/devtools": { 3640 + "optional": true 3641 + }, 3642 + "esbuild": { 3643 + "optional": true 3644 + }, 3645 + "jiti": { 3646 + "optional": true 3647 + }, 3648 + "less": { 3649 + "optional": true 3650 + }, 3651 + "sass": { 3652 + "optional": true 3653 + }, 3654 + "sass-embedded": { 3655 + "optional": true 3656 + }, 3657 + "stylus": { 3658 + "optional": true 3659 + }, 3660 + "sugarss": { 3661 + "optional": true 3662 + }, 3663 + "terser": { 3664 + "optional": true 3665 + }, 3666 + "tsx": { 3667 + "optional": true 3668 + }, 3669 + "yaml": { 3670 + "optional": true 3671 + } 3672 + } 3673 + }, 3674 + "node_modules/which": { 3675 + "version": "2.0.2", 3676 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 3677 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 3678 + "dev": true, 3679 + "license": "ISC", 3680 + "dependencies": { 3681 + "isexe": "^2.0.0" 3682 + }, 3683 + "bin": { 3684 + "node-which": "bin/node-which" 3685 + }, 3686 + "engines": { 3687 + "node": ">= 8" 3688 + } 3689 + }, 3690 + "node_modules/word-wrap": { 3691 + "version": "1.2.5", 3692 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 3693 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 3694 + "dev": true, 3695 + "license": "MIT", 3696 + "engines": { 3697 + "node": ">=0.10.0" 3698 + } 3699 + }, 3700 + "node_modules/yallist": { 3701 + "version": "3.1.1", 3702 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 3703 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 3704 + "dev": true, 3705 + "license": "ISC" 3706 + }, 3707 + "node_modules/yaml": { 3708 + "version": "2.8.2", 3709 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", 3710 + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", 3711 + "dev": true, 3712 + "license": "ISC", 3713 + "optional": true, 3714 + "peer": true, 3715 + "bin": { 3716 + "yaml": "bin.mjs" 3717 + }, 3718 + "engines": { 3719 + "node": ">= 14.6" 3720 + }, 3721 + "funding": { 3722 + "url": "https://github.com/sponsors/eemeli" 3723 + } 3724 + }, 3725 + "node_modules/yocto-queue": { 3726 + "version": "0.1.0", 3727 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 3728 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 3729 + "dev": true, 3730 + "license": "MIT", 3731 + "engines": { 3732 + "node": ">=10" 3733 + }, 3734 + "funding": { 3735 + "url": "https://github.com/sponsors/sindresorhus" 3736 + } 3737 + }, 3738 + "node_modules/zod": { 3739 + "version": "4.3.6", 3740 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", 3741 + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", 3742 + "dev": true, 3743 + "license": "MIT", 3744 + "peer": true, 3745 + "funding": { 3746 + "url": "https://github.com/sponsors/colinhacks" 3747 + } 3748 + }, 3749 + "node_modules/zod-validation-error": { 3750 + "version": "4.0.2", 3751 + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", 3752 + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", 3753 + "dev": true, 3754 + "license": "MIT", 3755 + "engines": { 3756 + "node": ">=18.0.0" 3757 + }, 3758 + "peerDependencies": { 3759 + "zod": "^3.25.0 || ^4.0.0" 3760 + } 3761 + } 3762 + } 3763 + }
+36
web/package.json
··· 1 + { 2 + "name": "web", 3 + "private": true, 4 + "version": "0.0.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc -b && vite build", 9 + "lint": "eslint .", 10 + "preview": "vite preview" 11 + }, 12 + "dependencies": { 13 + "@emotion/react": "^11.14.0", 14 + "@emotion/styled": "^11.14.1", 15 + "@fontsource/nunito": "^5.2.7", 16 + "@mui/icons-material": "^7.3.9", 17 + "@mui/material": "^7.3.9", 18 + "react": "^19.2.4", 19 + "react-dom": "^19.2.4", 20 + "react-router-dom": "^7.13.1" 21 + }, 22 + "devDependencies": { 23 + "@eslint/js": "^9.39.4", 24 + "@types/node": "^24.12.0", 25 + "@types/react": "^19.2.14", 26 + "@types/react-dom": "^19.2.3", 27 + "@vitejs/plugin-react": "^6.0.0", 28 + "eslint": "^9.39.4", 29 + "eslint-plugin-react-hooks": "^7.0.1", 30 + "eslint-plugin-react-refresh": "^0.5.2", 31 + "globals": "^17.4.0", 32 + "typescript": "~5.9.3", 33 + "typescript-eslint": "^8.56.1", 34 + "vite": "^8.0.0" 35 + } 36 + }
+1
web/public/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
+24
web/public/icons.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg"> 2 + <symbol id="bluesky-icon" viewBox="0 0 16 17"> 3 + <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g> 4 + <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs> 5 + </symbol> 6 + <symbol id="discord-icon" viewBox="0 0 20 19"> 7 + <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/> 8 + </symbol> 9 + <symbol id="documentation-icon" viewBox="0 0 21 20"> 10 + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/> 11 + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/> 12 + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/> 13 + </symbol> 14 + <symbol id="github-icon" viewBox="0 0 19 19"> 15 + <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/> 16 + </symbol> 17 + <symbol id="social-icon" viewBox="0 0 20 20"> 18 + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/> 19 + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/> 20 + </symbol> 21 + <symbol id="x-icon" viewBox="0 0 19 19"> 22 + <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/> 23 + </symbol> 24 + </svg>
+89
web/src/App.tsx
··· 1 + import { Routes, Route, Navigate } from 'react-router-dom'; 2 + import { ThemeProvider } from '@mui/material/styles'; 3 + import CssBaseline from '@mui/material/CssBaseline'; 4 + import CircularProgress from '@mui/material/CircularProgress'; 5 + import Box from '@mui/material/Box'; 6 + import theme from './theme'; 7 + import { useAuth } from './hooks/useAuth'; 8 + import { AppShell } from './components/layout/AppShell'; 9 + import { LoginPage } from './pages/LoginPage'; 10 + import { RegisterPage } from './pages/RegisterPage'; 11 + import { HomePage } from './pages/HomePage'; 12 + import { LessonPage } from './pages/LessonPage'; 13 + import { ReviewPage } from './pages/ReviewPage'; 14 + import type { ReactNode } from 'react'; 15 + 16 + function ProtectedRoute({ children }: { children: ReactNode }) { 17 + const { user, loading } = useAuth(); 18 + 19 + if (loading) { 20 + return ( 21 + <Box 22 + sx={{ 23 + display: 'flex', 24 + justifyContent: 'center', 25 + alignItems: 'center', 26 + minHeight: '100vh', 27 + }} 28 + > 29 + <CircularProgress color="primary" /> 30 + </Box> 31 + ); 32 + } 33 + 34 + if (!user) { 35 + return <Navigate to="/login" replace />; 36 + } 37 + 38 + return <>{children}</>; 39 + } 40 + 41 + function RootRedirect() { 42 + const { user, loading } = useAuth(); 43 + 44 + if (loading) { 45 + return ( 46 + <Box 47 + sx={{ 48 + display: 'flex', 49 + justifyContent: 'center', 50 + alignItems: 'center', 51 + minHeight: '100vh', 52 + }} 53 + > 54 + <CircularProgress color="primary" /> 55 + </Box> 56 + ); 57 + } 58 + 59 + return <Navigate to={user ? '/home' : '/login'} replace />; 60 + } 61 + 62 + function App() { 63 + return ( 64 + <ThemeProvider theme={theme}> 65 + <CssBaseline /> 66 + <Routes> 67 + <Route path="/" element={<RootRedirect />} /> 68 + <Route path="/login" element={<LoginPage />} /> 69 + <Route path="/register" element={<RegisterPage />} /> 70 + <Route 71 + element={ 72 + <ProtectedRoute> 73 + <AppShell /> 74 + </ProtectedRoute> 75 + } 76 + > 77 + <Route path="/home" element={<HomePage />} /> 78 + <Route 79 + path="/lesson/:topicId/:lessonId" 80 + element={<LessonPage />} 81 + /> 82 + <Route path="/review" element={<ReviewPage />} /> 83 + </Route> 84 + </Routes> 85 + </ThemeProvider> 86 + ); 87 + } 88 + 89 + export default App;
+61
web/src/api/client.ts
··· 1 + const TOKEN_KEY = 'ayos_token'; 2 + 3 + function getToken(): string | null { 4 + return localStorage.getItem(TOKEN_KEY); 5 + } 6 + 7 + async function request<T>( 8 + method: string, 9 + path: string, 10 + body?: unknown, 11 + ): Promise<T> { 12 + const headers: Record<string, string> = { 13 + 'Content-Type': 'application/json', 14 + }; 15 + 16 + const token = getToken(); 17 + if (token) { 18 + headers['Authorization'] = `Bearer ${token}`; 19 + } 20 + 21 + const response = await fetch(path, { 22 + method, 23 + headers, 24 + body: body != null ? JSON.stringify(body) : undefined, 25 + }); 26 + 27 + if (!response.ok) { 28 + let message = `Request failed: ${response.status}`; 29 + try { 30 + const errorData = await response.json(); 31 + if (errorData.error) { 32 + message = errorData.error; 33 + } else if (errorData.message) { 34 + message = errorData.message; 35 + } 36 + } catch { 37 + // ignore JSON parse errors 38 + } 39 + throw new Error(message); 40 + } 41 + 42 + if (response.status === 204) { 43 + return undefined as T; 44 + } 45 + 46 + return response.json() as Promise<T>; 47 + } 48 + 49 + export const apiClient = { 50 + get<T>(path: string): Promise<T> { 51 + return request<T>('GET', path); 52 + }, 53 + 54 + post<T>(path: string, body?: unknown): Promise<T> { 55 + return request<T>('POST', path, body); 56 + }, 57 + 58 + put<T>(path: string, body?: unknown): Promise<T> { 59 + return request<T>('PUT', path, body); 60 + }, 61 + };
web/src/assets/hero.png

This is a binary file and will not be displayed.

+1
web/src/assets/react.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
+1
web/src/assets/vite.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
+28
web/src/components/common/HeartsDisplay.tsx
··· 1 + import Box from '@mui/material/Box'; 2 + import FavoriteIcon from '@mui/icons-material/Favorite'; 3 + import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; 4 + 5 + interface HeartsDisplayProps { 6 + hearts: number; 7 + maxHearts?: number; 8 + } 9 + 10 + export function HeartsDisplay({ hearts, maxHearts = 5 }: HeartsDisplayProps) { 11 + return ( 12 + <Box sx={{ display: 'flex', gap: 0.25, alignItems: 'center' }}> 13 + {Array.from({ length: maxHearts }, (_, i) => 14 + i < hearts ? ( 15 + <FavoriteIcon 16 + key={i} 17 + sx={{ color: '#FF4B4B', fontSize: 20 }} 18 + /> 19 + ) : ( 20 + <FavoriteBorderIcon 21 + key={i} 22 + sx={{ color: 'rgba(255,75,75,0.3)', fontSize: 20 }} 23 + /> 24 + ), 25 + )} 26 + </Box> 27 + ); 28 + }
+30
web/src/components/common/SpeakButton.tsx
··· 1 + import IconButton from '@mui/material/IconButton'; 2 + import VolumeUpIcon from '@mui/icons-material/VolumeUp'; 3 + import { useSpeech } from '../../hooks/useSpeech'; 4 + 5 + interface SpeakButtonProps { 6 + text: string; 7 + lang?: string; 8 + size?: 'small' | 'medium' | 'large'; 9 + } 10 + 11 + export function SpeakButton({ text, lang = 'fil', size = 'medium' }: SpeakButtonProps) { 12 + const { speak, isSpeaking, isSupported } = useSpeech(); 13 + 14 + if (!isSupported.tts) return null; 15 + 16 + return ( 17 + <IconButton 18 + onClick={() => speak(text, lang)} 19 + disabled={isSpeaking} 20 + color="primary" 21 + size={size} 22 + sx={{ 23 + bgcolor: 'rgba(88, 204, 2, 0.1)', 24 + '&:hover': { bgcolor: 'rgba(88, 204, 2, 0.2)' }, 25 + }} 26 + > 27 + <VolumeUpIcon /> 28 + </IconButton> 29 + ); 30 + }
+21
web/src/components/common/XPDisplay.tsx
··· 1 + import Box from '@mui/material/Box'; 2 + import Typography from '@mui/material/Typography'; 3 + import BoltIcon from '@mui/icons-material/Bolt'; 4 + 5 + interface XPDisplayProps { 6 + xp: number; 7 + } 8 + 9 + export function XPDisplay({ xp }: XPDisplayProps) { 10 + return ( 11 + <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> 12 + <BoltIcon sx={{ color: '#FFC800', fontSize: 22 }} /> 13 + <Typography 14 + variant="body2" 15 + sx={{ color: '#FFC800', fontWeight: 700, fontSize: '0.95rem' }} 16 + > 17 + {xp} 18 + </Typography> 19 + </Box> 20 + ); 21 + }
+163
web/src/components/exercises/FillInTheBlank.tsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import Box from '@mui/material/Box'; 3 + import Typography from '@mui/material/Typography'; 4 + import Chip from '@mui/material/Chip'; 5 + import TextField from '@mui/material/TextField'; 6 + import { SpeakButton } from '../common/SpeakButton'; 7 + import type { FillInTheBlankExercise } from '../../types/lesson'; 8 + 9 + interface FillInTheBlankProps { 10 + exercise: FillInTheBlankExercise; 11 + selectedAnswer: string | null; 12 + isChecked: boolean; 13 + onSelect: (answer: string) => void; 14 + } 15 + 16 + export function FillInTheBlank({ 17 + exercise, 18 + selectedAnswer, 19 + isChecked, 20 + onSelect, 21 + }: FillInTheBlankProps) { 22 + const [inputValue, setInputValue] = useState(''); 23 + 24 + useEffect(() => { 25 + setInputValue(selectedAnswer ?? ''); 26 + }, [selectedAnswer]); 27 + 28 + // Split sentence around the blank marker "___" 29 + const parts = exercise.sentence.split('___'); 30 + 31 + function handleWordBankClick(word: string) { 32 + if (isChecked) return; 33 + setInputValue(word); 34 + onSelect(word); 35 + } 36 + 37 + function handleInputChange(value: string) { 38 + setInputValue(value); 39 + onSelect(value); 40 + } 41 + 42 + return ( 43 + <Box sx={{ width: '100%', maxWidth: 500, mx: 'auto' }}> 44 + <Box 45 + sx={{ 46 + display: 'flex', 47 + alignItems: 'center', 48 + justifyContent: 'center', 49 + gap: 1.5, 50 + mb: 3, 51 + }} 52 + > 53 + {exercise.promptAudio && <SpeakButton text={exercise.prompt} />} 54 + <Typography variant="h5" sx={{ textAlign: 'center' }}> 55 + {exercise.prompt} 56 + </Typography> 57 + </Box> 58 + 59 + {/* Sentence with blank */} 60 + <Box 61 + sx={{ 62 + display: 'flex', 63 + flexWrap: 'wrap', 64 + alignItems: 'center', 65 + justifyContent: 'center', 66 + gap: 0.5, 67 + mb: 3, 68 + p: 2, 69 + bgcolor: 'rgba(255,255,255,0.05)', 70 + borderRadius: 2, 71 + }} 72 + > 73 + {parts.map((part, i) => ( 74 + <Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> 75 + <Typography variant="h6" sx={{ fontSize: '1.2rem' }}> 76 + {part} 77 + </Typography> 78 + {i < parts.length - 1 && ( 79 + <Box 80 + sx={{ 81 + display: 'inline-flex', 82 + minWidth: 80, 83 + borderBottom: '3px solid', 84 + borderColor: inputValue 85 + ? isChecked 86 + ? selectedAnswer === exercise.blank 87 + ? '#4CAF50' 88 + : '#FF4B4B' 89 + : '#CE82FF' 90 + : 'rgba(255,255,255,0.3)', 91 + px: 1, 92 + justifyContent: 'center', 93 + }} 94 + > 95 + <Typography 96 + variant="h6" 97 + sx={{ 98 + fontSize: '1.2rem', 99 + color: inputValue ? '#CE82FF' : 'transparent', 100 + fontWeight: 700, 101 + }} 102 + > 103 + {inputValue || '\u00A0'} 104 + </Typography> 105 + </Box> 106 + )} 107 + </Box> 108 + ))} 109 + </Box> 110 + 111 + {exercise.hint && ( 112 + <Typography 113 + variant="body2" 114 + color="text.secondary" 115 + sx={{ textAlign: 'center', mb: 2 }} 116 + > 117 + Hint: {exercise.hint} 118 + </Typography> 119 + )} 120 + 121 + {/* Word bank or text input */} 122 + {exercise.wordBank && exercise.wordBank.length > 0 ? ( 123 + <Box 124 + sx={{ 125 + display: 'flex', 126 + flexWrap: 'wrap', 127 + gap: 1, 128 + justifyContent: 'center', 129 + }} 130 + > 131 + {exercise.wordBank.map((word) => ( 132 + <Chip 133 + key={word} 134 + label={word} 135 + onClick={() => handleWordBankClick(word)} 136 + variant={inputValue === word ? 'filled' : 'outlined'} 137 + color={inputValue === word ? 'secondary' : 'default'} 138 + disabled={isChecked} 139 + sx={{ 140 + fontSize: '1rem', 141 + py: 2, 142 + px: 1, 143 + cursor: 'pointer', 144 + '&.MuiChip-outlined': { 145 + borderColor: 'rgba(255,255,255,0.3)', 146 + }, 147 + }} 148 + /> 149 + ))} 150 + </Box> 151 + ) : ( 152 + <TextField 153 + fullWidth 154 + value={inputValue} 155 + onChange={(e) => handleInputChange(e.target.value)} 156 + disabled={isChecked} 157 + placeholder="Type the missing word..." 158 + size="small" 159 + /> 160 + )} 161 + </Box> 162 + ); 163 + }
+163
web/src/components/exercises/MatchingPairs.tsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import Box from '@mui/material/Box'; 3 + import Chip from '@mui/material/Chip'; 4 + import Typography from '@mui/material/Typography'; 5 + import { keyframes } from '@emotion/react'; 6 + import type { MatchingPairsExercise } from '../../types/lesson'; 7 + 8 + const fadeOut = keyframes` 9 + from { opacity: 1; transform: scale(1); } 10 + to { opacity: 0; transform: scale(0.8); } 11 + `; 12 + 13 + interface MatchingPairsProps { 14 + exercise: MatchingPairsExercise; 15 + selectedAnswer: Array<[string, string]> | null; 16 + isChecked: boolean; 17 + onSelect: (pairs: Array<[string, string]>) => void; 18 + } 19 + 20 + export function MatchingPairs({ 21 + exercise, 22 + selectedAnswer, 23 + isChecked, 24 + onSelect, 25 + }: MatchingPairsProps) { 26 + const [selectedLeft, setSelectedLeft] = useState<string | null>(null); 27 + const [selectedRight, setSelectedRight] = useState<string | null>(null); 28 + const [matchedPairs, setMatchedPairs] = useState<Array<[string, string]>>([]); 29 + const [fadingPairs, setFadingPairs] = useState<Set<string>>(new Set()); 30 + 31 + // Sync with external state 32 + useEffect(() => { 33 + if (selectedAnswer) { 34 + setMatchedPairs(selectedAnswer); 35 + } 36 + }, [selectedAnswer]); 37 + 38 + // Shuffled lists (stable within exercise) 39 + const leftItems = exercise.pairs.map((p) => p.left); 40 + const rightItems = exercise.pairs.map((p) => p.right); 41 + 42 + const matchedLefts = new Set(matchedPairs.map(([l]) => l)); 43 + const matchedRights = new Set(matchedPairs.map(([, r]) => r)); 44 + 45 + function handleLeftClick(item: string) { 46 + if (isChecked || matchedLefts.has(item)) return; 47 + setSelectedLeft(item); 48 + 49 + if (selectedRight) { 50 + attemptMatch(item, selectedRight); 51 + } 52 + } 53 + 54 + function handleRightClick(item: string) { 55 + if (isChecked || matchedRights.has(item)) return; 56 + setSelectedRight(item); 57 + 58 + if (selectedLeft) { 59 + attemptMatch(selectedLeft, item); 60 + } 61 + } 62 + 63 + function attemptMatch(left: string, right: string) { 64 + const isCorrectPair = exercise.pairs.some( 65 + (p) => p.left === left && p.right === right, 66 + ); 67 + 68 + if (isCorrectPair) { 69 + const fadingKey = `${left}::${right}`; 70 + setFadingPairs((prev) => new Set(prev).add(fadingKey)); 71 + 72 + setTimeout(() => { 73 + const newPairs = [...matchedPairs, [left, right] as [string, string]]; 74 + setMatchedPairs(newPairs); 75 + setFadingPairs((prev) => { 76 + const next = new Set(prev); 77 + next.delete(fadingKey); 78 + return next; 79 + }); 80 + onSelect(newPairs); 81 + }, 400); 82 + } 83 + 84 + setSelectedLeft(null); 85 + setSelectedRight(null); 86 + } 87 + 88 + return ( 89 + <Box sx={{ width: '100%', maxWidth: 500, mx: 'auto' }}> 90 + <Typography variant="h5" sx={{ textAlign: 'center', mb: 4 }}> 91 + {exercise.prompt} 92 + </Typography> 93 + 94 + <Box sx={{ display: 'flex', gap: 4, justifyContent: 'center' }}> 95 + {/* Left column */} 96 + <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> 97 + {leftItems.map((item) => { 98 + const isMatched = matchedLefts.has(item); 99 + const isFading = [...fadingPairs].some((fp) => 100 + fp.startsWith(item + '::'), 101 + ); 102 + return ( 103 + <Chip 104 + key={item} 105 + label={item} 106 + onClick={() => handleLeftClick(item)} 107 + variant={selectedLeft === item ? 'filled' : 'outlined'} 108 + color={selectedLeft === item ? 'secondary' : 'default'} 109 + disabled={isChecked} 110 + sx={{ 111 + fontSize: '1rem', 112 + py: 2.5, 113 + px: 1, 114 + visibility: isMatched ? 'hidden' : 'visible', 115 + animation: isFading 116 + ? `${fadeOut} 0.4s ease-out forwards` 117 + : 'none', 118 + cursor: isMatched ? 'default' : 'pointer', 119 + '&.MuiChip-outlined': { 120 + borderColor: 'rgba(255,255,255,0.3)', 121 + }, 122 + }} 123 + /> 124 + ); 125 + })} 126 + </Box> 127 + 128 + {/* Right column */} 129 + <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> 130 + {rightItems.map((item) => { 131 + const isMatched = matchedRights.has(item); 132 + const isFading = [...fadingPairs].some((fp) => 133 + fp.endsWith('::' + item), 134 + ); 135 + return ( 136 + <Chip 137 + key={item} 138 + label={item} 139 + onClick={() => handleRightClick(item)} 140 + variant={selectedRight === item ? 'filled' : 'outlined'} 141 + color={selectedRight === item ? 'secondary' : 'default'} 142 + disabled={isChecked} 143 + sx={{ 144 + fontSize: '1rem', 145 + py: 2.5, 146 + px: 1, 147 + visibility: isMatched ? 'hidden' : 'visible', 148 + animation: isFading 149 + ? `${fadeOut} 0.4s ease-out forwards` 150 + : 'none', 151 + cursor: isMatched ? 'default' : 'pointer', 152 + '&.MuiChip-outlined': { 153 + borderColor: 'rgba(255,255,255,0.3)', 154 + }, 155 + }} 156 + /> 157 + ); 158 + })} 159 + </Box> 160 + </Box> 161 + </Box> 162 + ); 163 + }
+88
web/src/components/exercises/MultipleChoice.tsx
··· 1 + import Grid from '@mui/material/Grid'; 2 + import Button from '@mui/material/Button'; 3 + import Typography from '@mui/material/Typography'; 4 + import Box from '@mui/material/Box'; 5 + import { SpeakButton } from '../common/SpeakButton'; 6 + import type { MultipleChoiceExercise } from '../../types/lesson'; 7 + 8 + interface MultipleChoiceProps { 9 + exercise: MultipleChoiceExercise; 10 + selectedAnswer: number | null; 11 + isChecked: boolean; 12 + isCorrect: boolean | null; 13 + onSelect: (index: number) => void; 14 + } 15 + 16 + export function MultipleChoice({ 17 + exercise, 18 + selectedAnswer, 19 + isChecked, 20 + isCorrect, 21 + onSelect, 22 + }: MultipleChoiceProps) { 23 + function getButtonVariant(index: number) { 24 + if (!isChecked) { 25 + return index === selectedAnswer ? 'contained' : 'outlined'; 26 + } 27 + if (index === exercise.correctIndex) return 'contained'; 28 + if (index === selectedAnswer && !isCorrect) return 'contained'; 29 + return 'outlined'; 30 + } 31 + 32 + function getButtonColor(index: number) { 33 + if (!isChecked) { 34 + return index === selectedAnswer ? 'secondary' : 'inherit'; 35 + } 36 + if (index === exercise.correctIndex) return 'primary'; 37 + if (index === selectedAnswer && !isCorrect) return 'error'; 38 + return 'inherit'; 39 + } 40 + 41 + return ( 42 + <Box sx={{ width: '100%', maxWidth: 500, mx: 'auto' }}> 43 + <Box 44 + sx={{ 45 + display: 'flex', 46 + alignItems: 'center', 47 + justifyContent: 'center', 48 + gap: 1.5, 49 + mb: 4, 50 + }} 51 + > 52 + {exercise.promptAudio && <SpeakButton text={exercise.prompt} />} 53 + <Typography variant="h5" sx={{ textAlign: 'center' }}> 54 + {exercise.prompt} 55 + </Typography> 56 + </Box> 57 + 58 + <Grid container spacing={2}> 59 + {exercise.choices.map((choice, index) => ( 60 + <Grid size={{ xs: 6 }} key={index}> 61 + <Button 62 + fullWidth 63 + variant={getButtonVariant(index)} 64 + color={getButtonColor(index) as 'primary' | 'secondary' | 'error' | 'inherit'} 65 + onClick={() => onSelect(index)} 66 + disabled={isChecked} 67 + sx={{ 68 + py: 2, 69 + fontSize: '1rem', 70 + textTransform: 'none', 71 + borderColor: 'rgba(255,255,255,0.2)', 72 + '&.MuiButton-outlined': { 73 + boxShadow: '0 4px 0 rgba(255,255,255,0.1)', 74 + '&:active': { 75 + boxShadow: '0 1px 0 rgba(255,255,255,0.1)', 76 + transform: 'translateY(3px)', 77 + }, 78 + }, 79 + }} 80 + > 81 + {choice} 82 + </Button> 83 + </Grid> 84 + ))} 85 + </Grid> 86 + </Box> 87 + ); 88 + }
+117
web/src/components/exercises/SpeakExercise.tsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import Box from '@mui/material/Box'; 3 + import Typography from '@mui/material/Typography'; 4 + import Button from '@mui/material/Button'; 5 + import TextField from '@mui/material/TextField'; 6 + import MicIcon from '@mui/icons-material/Mic'; 7 + import MicOffIcon from '@mui/icons-material/MicOff'; 8 + import { SpeakButton } from '../common/SpeakButton'; 9 + import { useSpeech } from '../../hooks/useSpeech'; 10 + import type { SpeakExercise as SpeakExerciseType } from '../../types/lesson'; 11 + 12 + interface SpeakExerciseProps { 13 + exercise: SpeakExerciseType; 14 + selectedAnswer: string | null; 15 + isChecked: boolean; 16 + onSelect: (answer: string) => void; 17 + } 18 + 19 + export function SpeakExercise({ 20 + exercise, 21 + selectedAnswer, 22 + isChecked, 23 + onSelect, 24 + }: SpeakExerciseProps) { 25 + const { listen, isListening, isSupported, stopListening } = useSpeech(); 26 + const [recognizedText, setRecognizedText] = useState(''); 27 + const [textFallback, setTextFallback] = useState(''); 28 + 29 + useEffect(() => { 30 + setRecognizedText(selectedAnswer ?? ''); 31 + setTextFallback(selectedAnswer ?? ''); 32 + }, [selectedAnswer]); 33 + 34 + async function handleListen() { 35 + if (isListening) { 36 + stopListening(); 37 + return; 38 + } 39 + 40 + try { 41 + const result = await listen('fil'); 42 + setRecognizedText(result); 43 + onSelect(result); 44 + } catch { 45 + // STT failed silently 46 + } 47 + } 48 + 49 + function handleTextChange(value: string) { 50 + setTextFallback(value); 51 + onSelect(value); 52 + } 53 + 54 + const showTextFallback = !isSupported.stt; 55 + 56 + return ( 57 + <Box sx={{ width: '100%', maxWidth: 500, mx: 'auto' }}> 58 + <Typography variant="h5" sx={{ textAlign: 'center', mb: 2 }}> 59 + {exercise.prompt} 60 + </Typography> 61 + 62 + {/* Play the phrase */} 63 + <Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}> 64 + <SpeakButton text={exercise.phrase} size="large" /> 65 + </Box> 66 + 67 + <Typography 68 + variant="body1" 69 + color="text.secondary" 70 + sx={{ textAlign: 'center', mb: 3 }} 71 + > 72 + {exercise.phrase} 73 + </Typography> 74 + 75 + {showTextFallback ? ( 76 + /* Text fallback when STT is not available */ 77 + <TextField 78 + fullWidth 79 + value={textFallback} 80 + onChange={(e) => handleTextChange(e.target.value)} 81 + disabled={isChecked} 82 + placeholder="Type what you hear..." 83 + sx={{ mb: 2 }} 84 + /> 85 + ) : ( 86 + /* Mic recording */ 87 + <Box sx={{ textAlign: 'center' }}> 88 + <Button 89 + variant="contained" 90 + color={isListening ? 'error' : 'secondary'} 91 + onClick={handleListen} 92 + disabled={isChecked} 93 + startIcon={isListening ? <MicOffIcon /> : <MicIcon />} 94 + sx={{ mb: 2, px: 4 }} 95 + > 96 + {isListening ? 'Stop' : 'Speak'} 97 + </Button> 98 + 99 + {recognizedText && ( 100 + <Typography 101 + variant="body1" 102 + sx={{ 103 + mt: 1, 104 + p: 2, 105 + bgcolor: 'rgba(255,255,255,0.05)', 106 + borderRadius: 2, 107 + textAlign: 'center', 108 + }} 109 + > 110 + You said: &ldquo;{recognizedText}&rdquo; 111 + </Typography> 112 + )} 113 + </Box> 114 + )} 115 + </Box> 116 + ); 117 + }
+111
web/src/components/exercises/Translation.tsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import TextField from '@mui/material/TextField'; 3 + import Typography from '@mui/material/Typography'; 4 + import Box from '@mui/material/Box'; 5 + import { SpeakButton } from '../common/SpeakButton'; 6 + import type { TranslationExercise } from '../../types/lesson'; 7 + 8 + interface TranslationProps { 9 + exercise: TranslationExercise; 10 + selectedAnswer: string | null; 11 + isChecked: boolean; 12 + onSelect: (answer: string) => void; 13 + } 14 + 15 + function levenshteinDistance(a: string, b: string): number { 16 + const matrix: number[][] = []; 17 + 18 + for (let i = 0; i <= b.length; i++) { 19 + matrix[i] = [i]; 20 + } 21 + for (let j = 0; j <= a.length; j++) { 22 + matrix[0][j] = j; 23 + } 24 + 25 + for (let i = 1; i <= b.length; i++) { 26 + for (let j = 1; j <= a.length; j++) { 27 + const cost = b.charAt(i - 1) === a.charAt(j - 1) ? 0 : 1; 28 + matrix[i][j] = Math.min( 29 + matrix[i - 1][j] + 1, 30 + matrix[i][j - 1] + 1, 31 + matrix[i - 1][j - 1] + cost, 32 + ); 33 + } 34 + } 35 + 36 + return matrix[b.length][a.length]; 37 + } 38 + 39 + export function checkTranslation( 40 + input: string, 41 + acceptedAnswers: string[], 42 + ): boolean { 43 + const normalized = input.trim().toLowerCase(); 44 + return acceptedAnswers.some((answer) => { 45 + const normalizedAnswer = answer.trim().toLowerCase(); 46 + if (normalized === normalizedAnswer) return true; 47 + return levenshteinDistance(normalized, normalizedAnswer) <= 2; 48 + }); 49 + } 50 + 51 + export function Translation({ 52 + exercise, 53 + selectedAnswer, 54 + isChecked, 55 + onSelect, 56 + }: TranslationProps) { 57 + const [inputValue, setInputValue] = useState(''); 58 + 59 + useEffect(() => { 60 + setInputValue(selectedAnswer ?? ''); 61 + }, [selectedAnswer]); 62 + 63 + function handleChange(value: string) { 64 + setInputValue(value); 65 + onSelect(value); 66 + } 67 + 68 + return ( 69 + <Box sx={{ width: '100%', maxWidth: 500, mx: 'auto' }}> 70 + <Box 71 + sx={{ 72 + display: 'flex', 73 + alignItems: 'center', 74 + justifyContent: 'center', 75 + gap: 1.5, 76 + mb: 3, 77 + }} 78 + > 79 + {exercise.promptAudio && <SpeakButton text={exercise.prompt} />} 80 + <Typography variant="h5" sx={{ textAlign: 'center' }}> 81 + {exercise.prompt} 82 + </Typography> 83 + </Box> 84 + 85 + {exercise.hint && ( 86 + <Typography 87 + variant="body2" 88 + color="text.secondary" 89 + sx={{ textAlign: 'center', mb: 2 }} 90 + > 91 + Hint: {exercise.hint} 92 + </Typography> 93 + )} 94 + 95 + <TextField 96 + fullWidth 97 + multiline 98 + minRows={3} 99 + value={inputValue} 100 + onChange={(e) => handleChange(e.target.value)} 101 + disabled={isChecked} 102 + placeholder="Type your answer..." 103 + sx={{ 104 + '& .MuiOutlinedInput-root': { 105 + fontSize: '1.1rem', 106 + }, 107 + }} 108 + /> 109 + </Box> 110 + ); 111 + }
+33
web/src/components/feedback/CorrectBanner.tsx
··· 1 + import { keyframes } from '@emotion/react'; 2 + import Box from '@mui/material/Box'; 3 + import Typography from '@mui/material/Typography'; 4 + 5 + const slideUp = keyframes` 6 + from { transform: translateY(100%); opacity: 0; } 7 + to { transform: translateY(0); opacity: 1; } 8 + `; 9 + 10 + export function CorrectBanner() { 11 + return ( 12 + <Box 13 + sx={{ 14 + position: 'fixed', 15 + bottom: 0, 16 + left: 0, 17 + right: 0, 18 + bgcolor: '#4CAF50', 19 + py: 2, 20 + px: 3, 21 + animation: `${slideUp} 0.3s ease-out`, 22 + zIndex: 1300, 23 + }} 24 + > 25 + <Typography 26 + variant="h5" 27 + sx={{ color: '#FFFFFF', fontWeight: 700, textAlign: 'center' }} 28 + > 29 + Correct! 30 + </Typography> 31 + </Box> 32 + ); 33 + }
+48
web/src/components/feedback/IncorrectBanner.tsx
··· 1 + import { keyframes } from '@emotion/react'; 2 + import Box from '@mui/material/Box'; 3 + import Typography from '@mui/material/Typography'; 4 + 5 + const slideUp = keyframes` 6 + from { transform: translateY(100%); opacity: 0; } 7 + to { transform: translateY(0); opacity: 1; } 8 + `; 9 + 10 + interface IncorrectBannerProps { 11 + correctAnswer: string; 12 + } 13 + 14 + export function IncorrectBanner({ correctAnswer }: IncorrectBannerProps) { 15 + return ( 16 + <Box 17 + sx={{ 18 + position: 'fixed', 19 + bottom: 0, 20 + left: 0, 21 + right: 0, 22 + bgcolor: '#FF4B4B', 23 + py: 2, 24 + px: 3, 25 + animation: `${slideUp} 0.3s ease-out`, 26 + zIndex: 1300, 27 + }} 28 + > 29 + <Typography 30 + variant="h6" 31 + sx={{ color: '#FFFFFF', fontWeight: 700, textAlign: 'center' }} 32 + > 33 + Correct answer: 34 + </Typography> 35 + <Typography 36 + variant="body1" 37 + sx={{ 38 + color: '#FFFFFF', 39 + textAlign: 'center', 40 + fontWeight: 600, 41 + mt: 0.5, 42 + }} 43 + > 44 + {correctAnswer} 45 + </Typography> 46 + </Box> 47 + ); 48 + }
+157
web/src/components/home/SkillNode.tsx
··· 1 + import { keyframes } from '@emotion/react'; 2 + import Box from '@mui/material/Box'; 3 + import ButtonBase from '@mui/material/ButtonBase'; 4 + import Typography from '@mui/material/Typography'; 5 + import LockIcon from '@mui/icons-material/Lock'; 6 + import CheckCircleIcon from '@mui/icons-material/CheckCircle'; 7 + import type { Topic } from '../../types/lesson'; 8 + 9 + interface SkillNodeProps { 10 + topic: Topic; 11 + completedLessons: number; 12 + totalLessons: number; 13 + locked: boolean; 14 + active: boolean; 15 + onClick: () => void; 16 + } 17 + 18 + const pulse = keyframes` 19 + 0%, 100% { box-shadow: 0 0 0 0 rgba(232, 69, 60, 0.4); } 20 + 50% { box-shadow: 0 0 0 12px rgba(232, 69, 60, 0); } 21 + `; 22 + 23 + const NODE_SIZE = 80; 24 + const RING_SIZE = NODE_SIZE + 10; 25 + 26 + export function SkillNode({ 27 + topic, 28 + completedLessons, 29 + totalLessons, 30 + locked, 31 + active, 32 + onClick, 33 + }: SkillNodeProps) { 34 + const progress = totalLessons > 0 ? completedLessons / totalLessons : 0; 35 + const isComplete = completedLessons >= totalLessons && totalLessons > 0; 36 + 37 + // SVG circular progress ring 38 + const radius = RING_SIZE / 2 - 3; 39 + const circumference = 2 * Math.PI * radius; 40 + const strokeDashoffset = circumference * (1 - progress); 41 + 42 + return ( 43 + <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}> 44 + <Box sx={{ position: 'relative', width: RING_SIZE, height: RING_SIZE }}> 45 + {/* Progress ring */} 46 + <svg 47 + width={RING_SIZE} 48 + height={RING_SIZE} 49 + style={{ position: 'absolute', top: 0, left: 0, transform: 'rotate(-90deg)' }} 50 + > 51 + <circle 52 + cx={RING_SIZE / 2} 53 + cy={RING_SIZE / 2} 54 + r={radius} 55 + fill="none" 56 + stroke="rgba(255,255,255,0.08)" 57 + strokeWidth={4} 58 + /> 59 + {progress > 0 && ( 60 + <circle 61 + cx={RING_SIZE / 2} 62 + cy={RING_SIZE / 2} 63 + r={radius} 64 + fill="none" 65 + stroke={isComplete ? '#4CAF50' : '#CE82FF'} 66 + strokeWidth={4} 67 + strokeDasharray={circumference} 68 + strokeDashoffset={strokeDashoffset} 69 + strokeLinecap="round" 70 + style={{ transition: 'stroke-dashoffset 0.5s ease' }} 71 + /> 72 + )} 73 + </svg> 74 + 75 + {/* Node bubble */} 76 + <ButtonBase 77 + onClick={onClick} 78 + disabled={locked} 79 + sx={{ 80 + position: 'absolute', 81 + top: (RING_SIZE - NODE_SIZE) / 2, 82 + left: (RING_SIZE - NODE_SIZE) / 2, 83 + width: NODE_SIZE, 84 + height: NODE_SIZE, 85 + borderRadius: '50%', 86 + bgcolor: locked 87 + ? 'rgba(255,255,255,0.06)' 88 + : isComplete 89 + ? '#4CAF50' 90 + : active 91 + ? '#E8453C' 92 + : 'background.paper', 93 + border: locked 94 + ? '3px solid rgba(255,255,255,0.1)' 95 + : isComplete 96 + ? '3px solid #388E3C' 97 + : active 98 + ? '3px solid #C02E26' 99 + : '3px solid rgba(255,255,255,0.15)', 100 + transition: 'transform 0.2s, box-shadow 0.2s', 101 + animation: active ? `${pulse} 2s ease-in-out infinite` : 'none', 102 + '&:hover': locked 103 + ? {} 104 + : { 105 + transform: 'scale(1.08)', 106 + }, 107 + }} 108 + > 109 + {locked ? ( 110 + <LockIcon sx={{ color: 'rgba(255,255,255,0.3)', fontSize: 32 }} /> 111 + ) : isComplete ? ( 112 + <Box sx={{ position: 'relative' }}> 113 + <Typography sx={{ fontSize: 36, lineHeight: 1, filter: 'none' }}> 114 + {topic.icon} 115 + </Typography> 116 + <CheckCircleIcon 117 + sx={{ 118 + position: 'absolute', 119 + bottom: -6, 120 + right: -10, 121 + fontSize: 20, 122 + color: '#fff', 123 + bgcolor: '#388E3C', 124 + borderRadius: '50%', 125 + }} 126 + /> 127 + </Box> 128 + ) : ( 129 + <Typography 130 + sx={{ 131 + fontSize: 36, 132 + lineHeight: 1, 133 + filter: locked ? 'grayscale(1)' : 'none', 134 + }} 135 + > 136 + {topic.icon} 137 + </Typography> 138 + )} 139 + </ButtonBase> 140 + </Box> 141 + 142 + {/* Label */} 143 + <Typography 144 + variant="body2" 145 + sx={{ 146 + fontWeight: 700, 147 + color: locked ? 'rgba(255,255,255,0.3)' : 'text.primary', 148 + textAlign: 'center', 149 + fontSize: '0.8rem', 150 + maxWidth: 100, 151 + }} 152 + > 153 + {topic.name} 154 + </Typography> 155 + </Box> 156 + ); 157 + }
+42
web/src/components/home/StreakBanner.tsx
··· 1 + import Box from '@mui/material/Box'; 2 + import Typography from '@mui/material/Typography'; 3 + import LocalFireDepartmentIcon from '@mui/icons-material/LocalFireDepartment'; 4 + 5 + interface StreakBannerProps { 6 + streak: number; 7 + } 8 + 9 + export function StreakBanner({ streak }: StreakBannerProps) { 10 + if (streak <= 0) return null; 11 + 12 + return ( 13 + <Box 14 + sx={{ 15 + display: 'flex', 16 + alignItems: 'center', 17 + gap: 1, 18 + bgcolor: 'rgba(255, 200, 0, 0.1)', 19 + border: '1px solid rgba(255, 200, 0, 0.3)', 20 + borderRadius: 3, 21 + px: 2, 22 + py: 1, 23 + }} 24 + > 25 + <LocalFireDepartmentIcon sx={{ color: '#FF9600', fontSize: 28 }} /> 26 + <Box> 27 + <Typography 28 + variant="body2" 29 + sx={{ color: '#FFC800', fontWeight: 700, lineHeight: 1.2 }} 30 + > 31 + {streak} day streak! 32 + </Typography> 33 + <Typography 34 + variant="caption" 35 + sx={{ color: 'text.secondary', lineHeight: 1.2 }} 36 + > 37 + Keep it up! 38 + </Typography> 39 + </Box> 40 + </Box> 41 + ); 42 + }
+84
web/src/components/layout/AppShell.tsx
··· 1 + import AppBar from '@mui/material/AppBar'; 2 + import Toolbar from '@mui/material/Toolbar'; 3 + import Typography from '@mui/material/Typography'; 4 + import Box from '@mui/material/Box'; 5 + import IconButton from '@mui/material/IconButton'; 6 + import LogoutIcon from '@mui/icons-material/Logout'; 7 + import { Outlet } from 'react-router-dom'; 8 + import { useAuth } from '../../hooks/useAuth'; 9 + import { useProgress } from '../../hooks/useProgress'; 10 + import { XPDisplay } from '../common/XPDisplay'; 11 + import { HeartsDisplay } from '../common/HeartsDisplay'; 12 + import LocalFireDepartmentIcon from '@mui/icons-material/LocalFireDepartment'; 13 + 14 + export function AppShell() { 15 + const { logout } = useAuth(); 16 + const { progress } = useProgress(); 17 + 18 + const stats = progress?.stats; 19 + 20 + return ( 21 + <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}> 22 + <AppBar 23 + position="sticky" 24 + sx={{ 25 + bgcolor: 'background.paper', 26 + backgroundImage: 'none', 27 + borderBottom: '1px solid rgba(255,255,255,0.08)', 28 + }} 29 + elevation={0} 30 + > 31 + <Toolbar sx={{ justifyContent: 'space-between' }}> 32 + <Typography 33 + variant="h6" 34 + sx={{ 35 + fontWeight: 700, 36 + color: '#E8453C', 37 + letterSpacing: '0.05em', 38 + }} 39 + > 40 + AYOS 41 + </Typography> 42 + 43 + <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> 44 + {stats && ( 45 + <> 46 + <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> 47 + <LocalFireDepartmentIcon 48 + sx={{ color: '#FF9600', fontSize: 20 }} 49 + /> 50 + <Typography 51 + variant="body2" 52 + sx={{ color: '#FF9600', fontWeight: 700 }} 53 + > 54 + {stats.streak_days} 55 + </Typography> 56 + </Box> 57 + <XPDisplay xp={stats.xp} /> 58 + <HeartsDisplay hearts={stats.hearts} /> 59 + </> 60 + )} 61 + <IconButton 62 + onClick={logout} 63 + size="small" 64 + sx={{ color: 'text.secondary' }} 65 + > 66 + <LogoutIcon fontSize="small" /> 67 + </IconButton> 68 + </Box> 69 + </Toolbar> 70 + </AppBar> 71 + 72 + <Box 73 + component="main" 74 + sx={{ 75 + flex: 1, 76 + display: 'flex', 77 + flexDirection: 'column', 78 + }} 79 + > 80 + <Outlet /> 81 + </Box> 82 + </Box> 83 + ); 84 + }
+29
web/src/components/layout/ProgressBar.tsx
··· 1 + import Box from '@mui/material/Box'; 2 + import LinearProgress from '@mui/material/LinearProgress'; 3 + 4 + interface ProgressBarProps { 5 + current: number; 6 + total: number; 7 + } 8 + 9 + export function ProgressBar({ current, total }: ProgressBarProps) { 10 + const progress = total > 0 ? (current / total) * 100 : 0; 11 + 12 + return ( 13 + <Box sx={{ width: '100%' }}> 14 + <LinearProgress 15 + variant="determinate" 16 + value={progress} 17 + sx={{ 18 + height: 12, 19 + borderRadius: 6, 20 + bgcolor: 'rgba(255,255,255,0.1)', 21 + '& .MuiLinearProgress-bar': { 22 + borderRadius: 6, 23 + bgcolor: '#E8453C', 24 + }, 25 + }} 26 + /> 27 + </Box> 28 + ); 29 + }
+360
web/src/components/mascot/Rambutan.tsx
··· 1 + import { keyframes } from '@emotion/react'; 2 + import Box from '@mui/material/Box'; 3 + 4 + type Mood = 'idle' | 'happy' | 'sad' | 'celebrate' | 'peek' | 'wave'; 5 + 6 + interface RambutanProps { 7 + mood?: Mood; 8 + size?: number; 9 + } 10 + 11 + const bounce = keyframes` 12 + 0%, 100% { transform: translateY(0); } 13 + 50% { transform: translateY(-6px); } 14 + `; 15 + 16 + const jump = keyframes` 17 + 0%, 100% { transform: translateY(0) scale(1); } 18 + 30% { transform: translateY(-18px) scale(1.05); } 19 + 50% { transform: translateY(-20px) scale(1.05); } 20 + 70% { transform: translateY(-10px) scale(1); } 21 + `; 22 + 23 + const droop = keyframes` 24 + 0%, 100% { transform: translateY(0) rotate(0deg); } 25 + 50% { transform: translateY(4px) rotate(-3deg); } 26 + `; 27 + 28 + const celebrate = keyframes` 29 + 0% { transform: rotate(0deg) scale(1); } 30 + 25% { transform: rotate(10deg) scale(1.1); } 31 + 50% { transform: rotate(-10deg) scale(1.1); } 32 + 75% { transform: rotate(5deg) scale(1.05); } 33 + 100% { transform: rotate(0deg) scale(1); } 34 + `; 35 + 36 + const peekUp = keyframes` 37 + 0% { transform: translateY(60%); opacity: 0; } 38 + 100% { transform: translateY(0); opacity: 1; } 39 + `; 40 + 41 + const waveArm = keyframes` 42 + 0%, 100% { transform: rotate(0deg); } 43 + 25% { transform: rotate(-20deg); } 44 + 50% { transform: rotate(20deg); } 45 + 75% { transform: rotate(-10deg); } 46 + `; 47 + 48 + function getAnimation(mood: Mood) { 49 + switch (mood) { 50 + case 'idle': 51 + return `${bounce} 2s ease-in-out infinite`; 52 + case 'happy': 53 + return `${jump} 0.8s ease-in-out infinite`; 54 + case 'sad': 55 + return `${droop} 2s ease-in-out infinite`; 56 + case 'celebrate': 57 + return `${celebrate} 0.6s ease-in-out infinite`; 58 + case 'peek': 59 + return `${peekUp} 0.5s ease-out forwards`; 60 + case 'wave': 61 + return `${bounce} 2s ease-in-out infinite`; 62 + default: 63 + return 'none'; 64 + } 65 + } 66 + 67 + function getEyes(mood: Mood): [string, string] { 68 + switch (mood) { 69 + case 'happy': 70 + case 'celebrate': 71 + return ['\u2303', '\u2303']; // ^ ^ 72 + case 'sad': 73 + return [';', ';']; 74 + case 'peek': 75 + return ['\u25CF', '\u25CF']; // big round eyes 76 + case 'wave': 77 + return ['\u25CF', '\u2012']; // wink 78 + default: 79 + return ['\u25CF', '\u25CF']; 80 + } 81 + } 82 + 83 + function getMouth(mood: Mood): string { 84 + switch (mood) { 85 + case 'happy': 86 + case 'celebrate': 87 + case 'wave': 88 + return 'D'; 89 + case 'sad': 90 + return '\u2323'; // frown 91 + default: 92 + return '\u25E1'; // smile 93 + } 94 + } 95 + 96 + export function Rambutan({ mood = 'idle', size = 120 }: RambutanProps) { 97 + const [leftEye, rightEye] = getEyes(mood); 98 + const mouth = getMouth(mood); 99 + const s = size; 100 + const cx = s / 2; 101 + const cy = s / 2; 102 + const r = s * 0.3; 103 + 104 + // Spike positions around the circle 105 + const spikeCount = 12; 106 + const spikes = Array.from({ length: spikeCount }, (_, i) => { 107 + const angle = (i / spikeCount) * Math.PI * 2 - Math.PI / 2; 108 + const innerR = r + 2; 109 + const outerR = r + s * 0.13; 110 + const x1 = cx + Math.cos(angle - 0.15) * innerR; 111 + const y1 = cy + Math.sin(angle - 0.15) * innerR; 112 + const x2 = cx + Math.cos(angle) * outerR; 113 + const y2 = cy + Math.sin(angle) * outerR; 114 + const x3 = cx + Math.cos(angle + 0.15) * innerR; 115 + const y3 = cy + Math.sin(angle + 0.15) * innerR; 116 + return `M${x1},${y1} Q${x2},${y2} ${x3},${y3}`; 117 + }); 118 + 119 + // Arm positions 120 + const armLength = s * 0.15; 121 + const leftArmStart = { x: cx - r * 0.8, y: cy + r * 0.4 }; 122 + const rightArmStart = { x: cx + r * 0.8, y: cy + r * 0.4 }; 123 + 124 + // Leg positions 125 + const legLength = s * 0.13; 126 + const leftLegStart = { x: cx - r * 0.35, y: cy + r }; 127 + const rightLegStart = { x: cx + r * 0.35, y: cy + r }; 128 + 129 + const isWaving = mood === 'wave'; 130 + 131 + return ( 132 + <Box 133 + sx={{ 134 + display: 'inline-flex', 135 + animation: getAnimation(mood), 136 + }} 137 + > 138 + <svg 139 + width={s} 140 + height={s} 141 + viewBox={`0 0 ${s} ${s}`} 142 + xmlns="http://www.w3.org/2000/svg" 143 + > 144 + {/* Spikes / hair */} 145 + {spikes.map((d, i) => ( 146 + <path 147 + key={i} 148 + d={d} 149 + fill="none" 150 + stroke={i % 2 === 0 ? '#E03030' : '#FF6B35'} 151 + strokeWidth={s * 0.035} 152 + strokeLinecap="round" 153 + /> 154 + ))} 155 + 156 + {/* Body */} 157 + <circle cx={cx} cy={cy} r={r} fill="#E03030" /> 158 + 159 + {/* Highlight */} 160 + <circle 161 + cx={cx - r * 0.25} 162 + cy={cy - r * 0.3} 163 + r={r * 0.15} 164 + fill="rgba(255,255,255,0.25)" 165 + /> 166 + 167 + {/* Eyes */} 168 + <text 169 + x={cx - r * 0.3} 170 + y={cy - r * 0.05} 171 + textAnchor="middle" 172 + fontSize={s * 0.1} 173 + fill="white" 174 + fontFamily="sans-serif" 175 + > 176 + {leftEye} 177 + </text> 178 + <text 179 + x={cx + r * 0.3} 180 + y={cy - r * 0.05} 181 + textAnchor="middle" 182 + fontSize={s * 0.1} 183 + fill="white" 184 + fontFamily="sans-serif" 185 + > 186 + {rightEye} 187 + </text> 188 + 189 + {/* Eye pupils (for non-kaomoji eyes) */} 190 + {mood !== 'happy' && mood !== 'celebrate' && mood !== 'sad' && ( 191 + <> 192 + <circle 193 + cx={cx - r * 0.3} 194 + cy={cy - r * 0.1} 195 + r={s * 0.025} 196 + fill="white" 197 + /> 198 + <circle 199 + cx={cx + r * 0.3} 200 + cy={cy - r * 0.1} 201 + r={s * 0.025} 202 + fill="white" 203 + /> 204 + </> 205 + )} 206 + 207 + {/* Mouth */} 208 + <text 209 + x={cx} 210 + y={cy + r * 0.4} 211 + textAnchor="middle" 212 + fontSize={s * 0.12} 213 + fill="white" 214 + fontFamily="sans-serif" 215 + > 216 + {mouth} 217 + </text> 218 + 219 + {/* Cheeks (blush) */} 220 + <circle 221 + cx={cx - r * 0.6} 222 + cy={cy + r * 0.15} 223 + r={s * 0.04} 224 + fill="rgba(255,150,150,0.5)" 225 + /> 226 + <circle 227 + cx={cx + r * 0.6} 228 + cy={cy + r * 0.15} 229 + r={s * 0.04} 230 + fill="rgba(255,150,150,0.5)" 231 + /> 232 + 233 + {/* Left arm */} 234 + <line 235 + x1={leftArmStart.x} 236 + y1={leftArmStart.y} 237 + x2={leftArmStart.x - armLength} 238 + y2={leftArmStart.y + armLength * 0.6} 239 + stroke="#E03030" 240 + strokeWidth={s * 0.03} 241 + strokeLinecap="round" 242 + /> 243 + 244 + {/* Right arm (waves when mood is 'wave') */} 245 + <line 246 + x1={rightArmStart.x} 247 + y1={rightArmStart.y} 248 + x2={rightArmStart.x + armLength} 249 + y2={ 250 + isWaving 251 + ? rightArmStart.y - armLength 252 + : rightArmStart.y + armLength * 0.6 253 + } 254 + stroke="#E03030" 255 + strokeWidth={s * 0.03} 256 + strokeLinecap="round" 257 + style={ 258 + isWaving 259 + ? { 260 + transformOrigin: `${rightArmStart.x}px ${rightArmStart.y}px`, 261 + animation: `${waveArm} 0.6s ease-in-out infinite`, 262 + } 263 + : undefined 264 + } 265 + /> 266 + 267 + {/* Left leg */} 268 + <line 269 + x1={leftLegStart.x} 270 + y1={leftLegStart.y} 271 + x2={leftLegStart.x - s * 0.03} 272 + y2={leftLegStart.y + legLength} 273 + stroke="#E03030" 274 + strokeWidth={s * 0.03} 275 + strokeLinecap="round" 276 + /> 277 + 278 + {/* Right leg */} 279 + <line 280 + x1={rightLegStart.x} 281 + y1={rightLegStart.y} 282 + x2={rightLegStart.x + s * 0.03} 283 + y2={rightLegStart.y + legLength} 284 + stroke="#E03030" 285 + strokeWidth={s * 0.03} 286 + strokeLinecap="round" 287 + /> 288 + 289 + {/* Confetti for celebrate mood */} 290 + {mood === 'celebrate' && ( 291 + <> 292 + <circle cx={cx - r * 1.2} cy={cy - r * 0.8} r={3} fill="#FFC800"> 293 + <animate 294 + attributeName="cy" 295 + values={`${cy - r * 0.8};${cy + r * 1.2}`} 296 + dur="1s" 297 + repeatCount="indefinite" 298 + /> 299 + <animate 300 + attributeName="opacity" 301 + values="1;0" 302 + dur="1s" 303 + repeatCount="indefinite" 304 + /> 305 + </circle> 306 + <circle cx={cx + r * 1.1} cy={cy - r} r={3} fill="#CE82FF"> 307 + <animate 308 + attributeName="cy" 309 + values={`${cy - r};${cy + r * 1.3}`} 310 + dur="1.2s" 311 + repeatCount="indefinite" 312 + /> 313 + <animate 314 + attributeName="opacity" 315 + values="1;0" 316 + dur="1.2s" 317 + repeatCount="indefinite" 318 + /> 319 + </circle> 320 + <circle cx={cx} cy={cy - r * 1.3} r={2.5} fill="#4CAF50"> 321 + <animate 322 + attributeName="cy" 323 + values={`${cy - r * 1.3};${cy + r}`} 324 + dur="0.9s" 325 + repeatCount="indefinite" 326 + /> 327 + <animate 328 + attributeName="opacity" 329 + values="1;0" 330 + dur="0.9s" 331 + repeatCount="indefinite" 332 + /> 333 + </circle> 334 + <rect 335 + x={cx - r * 0.9} 336 + y={cy - r * 1.1} 337 + width={4} 338 + height={4} 339 + fill="#FF4B4B" 340 + transform={`rotate(45 ${cx - r * 0.9} ${cy - r * 1.1})`} 341 + > 342 + <animate 343 + attributeName="y" 344 + values={`${cy - r * 1.1};${cy + r * 1.2}`} 345 + dur="1.1s" 346 + repeatCount="indefinite" 347 + /> 348 + <animate 349 + attributeName="opacity" 350 + values="1;0" 351 + dur="1.1s" 352 + repeatCount="indefinite" 353 + /> 354 + </rect> 355 + </> 356 + )} 357 + </svg> 358 + </Box> 359 + ); 360 + }
+57
web/src/content/loader.ts
··· 1 + import type { Lesson, TopicsConfig, Topic } from '../types/lesson'; 2 + 3 + const modules = import.meta.glob('../../../content/**/*.json', { eager: true }); 4 + 5 + function getModule<T>(path: string): T | null { 6 + const entry = modules[path]; 7 + if (!entry) return null; 8 + // Vite eager imports have a `default` property for JSON files 9 + const mod = entry as { default?: T } & T; 10 + return mod.default ?? (entry as T); 11 + } 12 + 13 + export function getTopics(): Topic[] { 14 + const config = getModule<TopicsConfig>('../../../content/topics.json'); 15 + return config?.topics ?? []; 16 + } 17 + 18 + export function getLesson(topicId: string, lessonId: string): Lesson | null { 19 + // Try common path patterns 20 + const paths = [ 21 + `../../../content/${topicId}/${lessonId}.json`, 22 + `../../../content/lessons/${topicId}/${lessonId}.json`, 23 + `../../../content/${topicId}/lessons/${lessonId}.json`, 24 + ]; 25 + 26 + for (const path of paths) { 27 + const lesson = getModule<Lesson>(path); 28 + if (lesson) return lesson; 29 + } 30 + 31 + // Fallback: search through all modules for matching lesson 32 + for (const [path, mod] of Object.entries(modules)) { 33 + if (path.endsWith('.json') && !path.endsWith('topics.json')) { 34 + const data = (mod as { default?: Lesson }).default ?? (mod as Lesson); 35 + if (data.id === lessonId && data.topicId === topicId) { 36 + return data; 37 + } 38 + } 39 + } 40 + 41 + return null; 42 + } 43 + 44 + export function getLessonsForTopic(topicId: string): Lesson[] { 45 + const lessons: Lesson[] = []; 46 + 47 + for (const [path, mod] of Object.entries(modules)) { 48 + if (path.endsWith('.json') && !path.endsWith('topics.json')) { 49 + const data = (mod as { default?: Lesson }).default ?? (mod as Lesson); 50 + if (data.topicId === topicId) { 51 + lessons.push(data); 52 + } 53 + } 54 + } 55 + 56 + return lessons; 57 + }
+102
web/src/context/AuthContext.tsx
··· 1 + import { createContext, useState, useEffect, useCallback, type ReactNode } from 'react'; 2 + import { apiClient } from '../api/client'; 3 + 4 + const TOKEN_KEY = 'ayos_token'; 5 + 6 + export interface UserResponse { 7 + id: string; 8 + username: string; 9 + email: string; 10 + } 11 + 12 + interface AuthState { 13 + user: UserResponse | null; 14 + token: string | null; 15 + loading: boolean; 16 + } 17 + 18 + export interface AuthContextValue extends AuthState { 19 + login: (email: string, password: string) => Promise<void>; 20 + register: (username: string, email: string, password: string) => Promise<void>; 21 + logout: () => void; 22 + } 23 + 24 + export const AuthContext = createContext<AuthContextValue | null>(null); 25 + 26 + interface LoginResponse { 27 + token: string; 28 + user: UserResponse; 29 + } 30 + 31 + interface RegisterResponse { 32 + token: string; 33 + user: UserResponse; 34 + } 35 + 36 + export function AuthProvider({ children }: { children: ReactNode }) { 37 + const [state, setState] = useState<AuthState>({ 38 + user: null, 39 + token: localStorage.getItem(TOKEN_KEY), 40 + loading: true, 41 + }); 42 + 43 + useEffect(() => { 44 + const token = localStorage.getItem(TOKEN_KEY); 45 + if (!token) { 46 + setState({ user: null, token: null, loading: false }); 47 + return; 48 + } 49 + 50 + apiClient 51 + .get<UserResponse>('/api/me') 52 + .then((user) => { 53 + setState({ user, token, loading: false }); 54 + }) 55 + .catch(() => { 56 + localStorage.removeItem(TOKEN_KEY); 57 + setState({ user: null, token: null, loading: false }); 58 + }); 59 + }, []); 60 + 61 + const login = useCallback(async (email: string, password: string) => { 62 + const data = await apiClient.post<LoginResponse>('/api/login', { 63 + email, 64 + password, 65 + }); 66 + localStorage.setItem(TOKEN_KEY, data.token); 67 + setState({ user: data.user, token: data.token, loading: false }); 68 + }, []); 69 + 70 + const register = useCallback( 71 + async (username: string, email: string, password: string) => { 72 + const data = await apiClient.post<RegisterResponse>('/api/register', { 73 + username, 74 + email, 75 + password, 76 + }); 77 + localStorage.setItem(TOKEN_KEY, data.token); 78 + setState({ user: data.user, token: data.token, loading: false }); 79 + }, 80 + [], 81 + ); 82 + 83 + const logout = useCallback(() => { 84 + localStorage.removeItem(TOKEN_KEY); 85 + setState({ user: null, token: null, loading: false }); 86 + }, []); 87 + 88 + return ( 89 + <AuthContext.Provider 90 + value={{ 91 + user: state.user, 92 + token: state.token, 93 + loading: state.loading, 94 + login, 95 + register, 96 + logout, 97 + }} 98 + > 99 + {children} 100 + </AuthContext.Provider> 101 + ); 102 + }
+11
web/src/hooks/useAuth.ts
··· 1 + import { useContext } from 'react'; 2 + import { AuthContext } from '../context/AuthContext'; 3 + import type { AuthContextValue } from '../context/AuthContext'; 4 + 5 + export function useAuth(): AuthContextValue { 6 + const context = useContext(AuthContext); 7 + if (!context) { 8 + throw new Error('useAuth must be used within an AuthProvider'); 9 + } 10 + return context; 11 + }
+121
web/src/hooks/useLesson.ts
··· 1 + import { useReducer, useCallback } from 'react'; 2 + import type { Exercise } from '../types/lesson'; 3 + 4 + export interface LessonState { 5 + exercises: Exercise[]; 6 + currentIndex: number; 7 + hearts: number; 8 + correctCount: number; 9 + selectedAnswer: unknown; 10 + isCorrect: boolean | null; 11 + isChecked: boolean; 12 + isFinished: boolean; 13 + } 14 + 15 + type LessonAction = 16 + | { type: 'SET_EXERCISES'; exercises: Exercise[] } 17 + | { type: 'SELECT_ANSWER'; answer: unknown } 18 + | { type: 'CHECK_ANSWER'; correct: boolean } 19 + | { type: 'NEXT_EXERCISE' } 20 + | { type: 'FINISH' }; 21 + 22 + const initialState: LessonState = { 23 + exercises: [], 24 + currentIndex: 0, 25 + hearts: 5, 26 + correctCount: 0, 27 + selectedAnswer: null, 28 + isCorrect: null, 29 + isChecked: false, 30 + isFinished: false, 31 + }; 32 + 33 + function lessonReducer(state: LessonState, action: LessonAction): LessonState { 34 + switch (action.type) { 35 + case 'SET_EXERCISES': 36 + return { 37 + ...initialState, 38 + exercises: action.exercises, 39 + }; 40 + 41 + case 'SELECT_ANSWER': 42 + if (state.isChecked) return state; 43 + return { 44 + ...state, 45 + selectedAnswer: action.answer, 46 + }; 47 + 48 + case 'CHECK_ANSWER': { 49 + const newHearts = action.correct ? state.hearts : state.hearts - 1; 50 + const newCorrectCount = action.correct 51 + ? state.correctCount + 1 52 + : state.correctCount; 53 + const isFinished = newHearts === 0; 54 + return { 55 + ...state, 56 + isCorrect: action.correct, 57 + isChecked: true, 58 + hearts: newHearts, 59 + correctCount: newCorrectCount, 60 + isFinished, 61 + }; 62 + } 63 + 64 + case 'NEXT_EXERCISE': { 65 + const nextIndex = state.currentIndex + 1; 66 + const isFinished = nextIndex >= state.exercises.length; 67 + return { 68 + ...state, 69 + currentIndex: nextIndex, 70 + selectedAnswer: null, 71 + isCorrect: null, 72 + isChecked: false, 73 + isFinished, 74 + }; 75 + } 76 + 77 + case 'FINISH': 78 + return { 79 + ...state, 80 + isFinished: true, 81 + }; 82 + 83 + default: 84 + return state; 85 + } 86 + } 87 + 88 + export function useLesson() { 89 + const [state, dispatch] = useReducer(lessonReducer, initialState); 90 + 91 + const setExercises = useCallback( 92 + (exercises: Exercise[]) => dispatch({ type: 'SET_EXERCISES', exercises }), 93 + [], 94 + ); 95 + 96 + const selectAnswer = useCallback( 97 + (answer: unknown) => dispatch({ type: 'SELECT_ANSWER', answer }), 98 + [], 99 + ); 100 + 101 + const checkAnswer = useCallback( 102 + (correct: boolean) => dispatch({ type: 'CHECK_ANSWER', correct }), 103 + [], 104 + ); 105 + 106 + const nextExercise = useCallback( 107 + () => dispatch({ type: 'NEXT_EXERCISE' }), 108 + [], 109 + ); 110 + 111 + const finish = useCallback(() => dispatch({ type: 'FINISH' }), []); 112 + 113 + return { 114 + state, 115 + setExercises, 116 + selectAnswer, 117 + checkAnswer, 118 + nextExercise, 119 + finish, 120 + }; 121 + }
+38
web/src/hooks/useProgress.ts
··· 1 + import { useState, useEffect, useCallback } from 'react'; 2 + import { apiClient } from '../api/client'; 3 + import type { ProgressData } from '../types/progress'; 4 + 5 + interface UseProgressReturn { 6 + progress: ProgressData | null; 7 + loading: boolean; 8 + error: string | null; 9 + refetch: () => void; 10 + } 11 + 12 + export function useProgress(): UseProgressReturn { 13 + const [progress, setProgress] = useState<ProgressData | null>(null); 14 + const [loading, setLoading] = useState(true); 15 + const [error, setError] = useState<string | null>(null); 16 + 17 + const fetchProgress = useCallback(() => { 18 + setLoading(true); 19 + setError(null); 20 + apiClient 21 + .get<ProgressData>('/api/progress') 22 + .then((data) => { 23 + setProgress(data); 24 + }) 25 + .catch((err: Error) => { 26 + setError(err.message); 27 + }) 28 + .finally(() => { 29 + setLoading(false); 30 + }); 31 + }, []); 32 + 33 + useEffect(() => { 34 + fetchProgress(); 35 + }, [fetchProgress]); 36 + 37 + return { progress, loading, error, refetch: fetchProgress }; 38 + }
+107
web/src/hooks/useSpeech.ts
··· 1 + import { useState, useCallback, useRef, useMemo } from 'react'; 2 + 3 + interface SpeechSupport { 4 + tts: boolean; 5 + stt: boolean; 6 + } 7 + 8 + /* eslint-disable @typescript-eslint/no-explicit-any */ 9 + type SpeechRecognitionInstance = any; 10 + 11 + const SpeechRecognitionCtor: (new () => SpeechRecognitionInstance) | undefined = 12 + typeof window !== 'undefined' 13 + ? (window as any).SpeechRecognition ?? (window as any).webkitSpeechRecognition 14 + : undefined; 15 + /* eslint-enable @typescript-eslint/no-explicit-any */ 16 + 17 + export function useSpeech() { 18 + const [isSpeaking, setIsSpeaking] = useState(false); 19 + const [isListening, setIsListening] = useState(false); 20 + const recognitionRef = useRef<SpeechRecognitionInstance | null>(null); 21 + 22 + const isSupported: SpeechSupport = useMemo( 23 + () => ({ 24 + tts: typeof window !== 'undefined' && 'speechSynthesis' in window, 25 + stt: SpeechRecognitionCtor != null, 26 + }), 27 + [], 28 + ); 29 + 30 + const speak = useCallback( 31 + (text: string, lang: string = 'fil') => { 32 + if (!isSupported.tts) return; 33 + 34 + window.speechSynthesis.cancel(); 35 + const utterance = new SpeechSynthesisUtterance(text); 36 + utterance.lang = lang; 37 + utterance.rate = 0.9; 38 + 39 + utterance.onstart = () => setIsSpeaking(true); 40 + utterance.onend = () => setIsSpeaking(false); 41 + utterance.onerror = () => setIsSpeaking(false); 42 + 43 + window.speechSynthesis.speak(utterance); 44 + }, 45 + [isSupported.tts], 46 + ); 47 + 48 + const listen = useCallback( 49 + (lang: string = 'fil'): Promise<string> => { 50 + return new Promise((resolve, reject) => { 51 + if (!SpeechRecognitionCtor) { 52 + reject(new Error('Speech recognition not supported')); 53 + return; 54 + } 55 + 56 + if (recognitionRef.current) { 57 + recognitionRef.current.abort(); 58 + } 59 + 60 + const recognition = new SpeechRecognitionCtor(); 61 + recognitionRef.current = recognition; 62 + recognition.lang = lang; 63 + recognition.interimResults = false; 64 + recognition.maxAlternatives = 1; 65 + 66 + recognition.onstart = () => setIsListening(true); 67 + 68 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 69 + recognition.onresult = (event: any) => { 70 + setIsListening(false); 71 + const transcript: string = event.results[0][0].transcript; 72 + resolve(transcript); 73 + }; 74 + 75 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 76 + recognition.onerror = (event: any) => { 77 + setIsListening(false); 78 + reject(new Error(event.error)); 79 + }; 80 + 81 + recognition.onend = () => { 82 + setIsListening(false); 83 + }; 84 + 85 + recognition.start(); 86 + }); 87 + }, 88 + [], 89 + ); 90 + 91 + const stopListening = useCallback(() => { 92 + if (recognitionRef.current) { 93 + recognitionRef.current.stop(); 94 + recognitionRef.current = null; 95 + } 96 + setIsListening(false); 97 + }, []); 98 + 99 + return { 100 + speak, 101 + listen, 102 + stopListening, 103 + isSupported, 104 + isSpeaking, 105 + isListening, 106 + }; 107 + }
+17
web/src/main.tsx
··· 1 + import { StrictMode } from 'react'; 2 + import { createRoot } from 'react-dom/client'; 3 + import { BrowserRouter } from 'react-router-dom'; 4 + import '@fontsource/nunito/400.css'; 5 + import '@fontsource/nunito/700.css'; 6 + import { AuthProvider } from './context/AuthContext'; 7 + import App from './App'; 8 + 9 + createRoot(document.getElementById('root')!).render( 10 + <StrictMode> 11 + <BrowserRouter> 12 + <AuthProvider> 13 + <App /> 14 + </AuthProvider> 15 + </BrowserRouter> 16 + </StrictMode>, 17 + );
+209
web/src/pages/HomePage.tsx
··· 1 + import { useMemo } from 'react'; 2 + import { useNavigate } from 'react-router-dom'; 3 + import Box from '@mui/material/Box'; 4 + import Typography from '@mui/material/Typography'; 5 + import CircularProgress from '@mui/material/CircularProgress'; 6 + import { useProgress } from '../hooks/useProgress'; 7 + import { getTopics, getLessonsForTopic } from '../content/loader'; 8 + import { SkillNode } from '../components/home/SkillNode'; 9 + import { StreakBanner } from '../components/home/StreakBanner'; 10 + import { Rambutan } from '../components/mascot/Rambutan'; 11 + 12 + // Vertical spacing between nodes 13 + const NODE_GAP = 120; 14 + // Max horizontal offset from center for the sine wave 15 + const WAVE_AMPLITUDE = 80; 16 + // How wide the path container is 17 + const PATH_WIDTH = 300; 18 + 19 + export function HomePage() { 20 + const navigate = useNavigate(); 21 + const { progress, loading } = useProgress(); 22 + const topics = useMemo(() => getTopics(), []); 23 + 24 + const completedLessonIds = useMemo(() => { 25 + if (!progress) return new Set<string>(); 26 + return new Set( 27 + progress.lessons.filter((l) => l.completed).map((l) => l.lesson_id), 28 + ); 29 + }, [progress]); 30 + 31 + const completedTopicIds = useMemo(() => { 32 + const completed = new Set<string>(); 33 + for (const topic of topics) { 34 + const lessons = getLessonsForTopic(topic.id); 35 + if ( 36 + lessons.length > 0 && 37 + lessons.every((l) => completedLessonIds.has(l.id)) 38 + ) { 39 + completed.add(topic.id); 40 + } 41 + } 42 + return completed; 43 + }, [topics, completedLessonIds]); 44 + 45 + function isTopicLocked(topicId: string): boolean { 46 + const topic = topics.find((t) => t.id === topicId); 47 + if (!topic) return true; 48 + if (topic.prerequisites.length === 0) return false; 49 + return !topic.prerequisites.every((prereq) => 50 + completedTopicIds.has(prereq), 51 + ); 52 + } 53 + 54 + function isTopicActive(topicId: string): boolean { 55 + if (isTopicLocked(topicId)) return false; 56 + if (completedTopicIds.has(topicId)) return false; 57 + return true; 58 + } 59 + 60 + function getCompletedLessonsForTopic(topicId: string): number { 61 + const lessons = getLessonsForTopic(topicId); 62 + return lessons.filter((l) => completedLessonIds.has(l.id)).length; 63 + } 64 + 65 + function handleTopicClick(topicId: string) { 66 + const lessons = getLessonsForTopic(topicId); 67 + const nextLesson = lessons.find((l) => !completedLessonIds.has(l.id)); 68 + const lesson = nextLesson ?? lessons[0]; 69 + if (lesson) { 70 + navigate(`/lesson/${topicId}/${lesson.id}`); 71 + } 72 + } 73 + 74 + // Compute x offset for each node along a sine wave 75 + function getNodeX(index: number): number { 76 + return Math.sin(index * 0.9) * WAVE_AMPLITUDE; 77 + } 78 + 79 + // Node center positions for the SVG path 80 + const nodePositions = topics.map((_, i) => ({ 81 + x: PATH_WIDTH / 2 + getNodeX(i), 82 + y: 45 + i * NODE_GAP, // 45 = RING_SIZE/2 83 + })); 84 + 85 + // Build a smooth curve through all node positions 86 + function buildPath(): string { 87 + if (nodePositions.length < 2) return ''; 88 + const pts = nodePositions; 89 + let d = `M ${pts[0].x} ${pts[0].y}`; 90 + for (let i = 0; i < pts.length - 1; i++) { 91 + const curr = pts[i]; 92 + const next = pts[i + 1]; 93 + const midY = (curr.y + next.y) / 2; 94 + d += ` C ${curr.x} ${midY}, ${next.x} ${midY}, ${next.x} ${next.y}`; 95 + } 96 + return d; 97 + } 98 + 99 + const totalHeight = topics.length * NODE_GAP + 60; 100 + 101 + if (loading) { 102 + return ( 103 + <Box 104 + sx={{ 105 + display: 'flex', 106 + justifyContent: 'center', 107 + alignItems: 'center', 108 + flex: 1, 109 + py: 8, 110 + }} 111 + > 112 + <CircularProgress color="primary" /> 113 + </Box> 114 + ); 115 + } 116 + 117 + return ( 118 + <Box sx={{ maxWidth: 500, mx: 'auto', p: 3, width: '100%' }}> 119 + {/* Streak banner */} 120 + {progress?.stats && progress.stats.streak_days > 0 && ( 121 + <Box sx={{ mb: 3 }}> 122 + <StreakBanner streak={progress.stats.streak_days} /> 123 + </Box> 124 + )} 125 + 126 + {/* Mascot greeting */} 127 + <Box 128 + sx={{ 129 + display: 'flex', 130 + alignItems: 'center', 131 + gap: 2, 132 + mb: 4, 133 + }} 134 + > 135 + <Rambutan mood="idle" size={80} /> 136 + <Box> 137 + <Typography variant="h5">Kamusta!</Typography> 138 + <Typography variant="body2" color="text.secondary"> 139 + Choose a topic to practice 140 + </Typography> 141 + </Box> 142 + </Box> 143 + 144 + {/* Skill path */} 145 + {topics.length === 0 ? ( 146 + <Box sx={{ textAlign: 'center', py: 6 }}> 147 + <Typography color="text.secondary"> 148 + No topics available yet. Check back soon! 149 + </Typography> 150 + </Box> 151 + ) : ( 152 + <Box 153 + sx={{ 154 + position: 'relative', 155 + width: PATH_WIDTH, 156 + height: totalHeight, 157 + mx: 'auto', 158 + }} 159 + > 160 + {/* SVG connector path */} 161 + <svg 162 + width={PATH_WIDTH} 163 + height={totalHeight} 164 + style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} 165 + > 166 + <path 167 + d={buildPath()} 168 + fill="none" 169 + stroke="rgba(255,255,255,0.1)" 170 + strokeWidth={4} 171 + strokeDasharray="8 8" 172 + strokeLinecap="round" 173 + /> 174 + </svg> 175 + 176 + {/* Nodes */} 177 + {topics.map((topic, i) => { 178 + const totalLessons = getLessonsForTopic(topic.id).length; 179 + const completedCount = getCompletedLessonsForTopic(topic.id); 180 + const locked = isTopicLocked(topic.id); 181 + const active = isTopicActive(topic.id); 182 + const x = getNodeX(i); 183 + 184 + return ( 185 + <Box 186 + key={topic.id} 187 + sx={{ 188 + position: 'absolute', 189 + top: i * NODE_GAP, 190 + left: '50%', 191 + transform: `translateX(calc(-50% + ${x}px))`, 192 + }} 193 + > 194 + <SkillNode 195 + topic={topic} 196 + completedLessons={completedCount} 197 + totalLessons={totalLessons} 198 + locked={locked} 199 + active={active} 200 + onClick={() => handleTopicClick(topic.id)} 201 + /> 202 + </Box> 203 + ); 204 + })} 205 + </Box> 206 + )} 207 + </Box> 208 + ); 209 + }
+311
web/src/pages/LessonPage.tsx
··· 1 + import { useEffect, useCallback, useMemo } from 'react'; 2 + import { useParams, useNavigate } from 'react-router-dom'; 3 + import Box from '@mui/material/Box'; 4 + import Button from '@mui/material/Button'; 5 + import IconButton from '@mui/material/IconButton'; 6 + import Typography from '@mui/material/Typography'; 7 + import CircularProgress from '@mui/material/CircularProgress'; 8 + import CloseIcon from '@mui/icons-material/Close'; 9 + import { useLesson } from '../hooks/useLesson'; 10 + import { getLesson } from '../content/loader'; 11 + import { ProgressBar } from '../components/layout/ProgressBar'; 12 + import { HeartsDisplay } from '../components/common/HeartsDisplay'; 13 + import { MultipleChoice } from '../components/exercises/MultipleChoice'; 14 + import { Translation, checkTranslation } from '../components/exercises/Translation'; 15 + import { MatchingPairs } from '../components/exercises/MatchingPairs'; 16 + import { FillInTheBlank } from '../components/exercises/FillInTheBlank'; 17 + import { SpeakExercise } from '../components/exercises/SpeakExercise'; 18 + import { CorrectBanner } from '../components/feedback/CorrectBanner'; 19 + import { IncorrectBanner } from '../components/feedback/IncorrectBanner'; 20 + import type { Exercise } from '../types/lesson'; 21 + 22 + function getCorrectAnswer(exercise: Exercise): string { 23 + switch (exercise.type) { 24 + case 'multiple-choice': 25 + return exercise.choices[exercise.correctIndex]; 26 + case 'translation': 27 + return exercise.acceptedAnswers[0]; 28 + case 'matching-pairs': 29 + return exercise.pairs.map((p) => `${p.left} = ${p.right}`).join(', '); 30 + case 'fill-in-the-blank': 31 + return exercise.blank; 32 + case 'speak': 33 + return exercise.acceptedAnswers[0]; 34 + } 35 + } 36 + 37 + function isAnswerCorrect(exercise: Exercise, answer: unknown): boolean { 38 + switch (exercise.type) { 39 + case 'multiple-choice': 40 + return answer === exercise.correctIndex; 41 + 42 + case 'translation': 43 + return checkTranslation( 44 + (answer as string) || '', 45 + exercise.acceptedAnswers, 46 + ); 47 + 48 + case 'matching-pairs': { 49 + const pairs = answer as Array<[string, string]> | null; 50 + return pairs != null && pairs.length === exercise.pairs.length; 51 + } 52 + 53 + case 'fill-in-the-blank': { 54 + const input = ((answer as string) || '').trim().toLowerCase(); 55 + return input === exercise.blank.trim().toLowerCase(); 56 + } 57 + 58 + case 'speak': { 59 + const spoken = ((answer as string) || '').trim().toLowerCase(); 60 + return exercise.acceptedAnswers.some( 61 + (a) => a.trim().toLowerCase() === spoken, 62 + ); 63 + } 64 + } 65 + } 66 + 67 + export function LessonPage() { 68 + const { topicId, lessonId } = useParams<{ 69 + topicId: string; 70 + lessonId: string; 71 + }>(); 72 + const navigate = useNavigate(); 73 + const { 74 + state, 75 + setExercises, 76 + selectAnswer, 77 + checkAnswer, 78 + nextExercise, 79 + } = useLesson(); 80 + 81 + const lesson = useMemo( 82 + () => (topicId && lessonId ? getLesson(topicId, lessonId) : null), 83 + [topicId, lessonId], 84 + ); 85 + 86 + useEffect(() => { 87 + if (lesson) { 88 + setExercises(lesson.exercises); 89 + } 90 + }, [lesson, setExercises]); 91 + 92 + const currentExercise: Exercise | undefined = 93 + state.exercises[state.currentIndex]; 94 + 95 + const handleCheck = useCallback(() => { 96 + if (!currentExercise) return; 97 + const correct = isAnswerCorrect(currentExercise, state.selectedAnswer); 98 + checkAnswer(correct); 99 + }, [currentExercise, state.selectedAnswer, checkAnswer]); 100 + 101 + const handleContinue = useCallback(() => { 102 + nextExercise(); 103 + }, [nextExercise]); 104 + 105 + // Navigate to review when finished 106 + useEffect(() => { 107 + if (state.isFinished && lesson) { 108 + navigate('/review', { 109 + state: { 110 + correctCount: state.correctCount, 111 + total: state.exercises.length, 112 + xp: lesson.xpReward, 113 + topicId: lesson.topicId, 114 + lessonId: lesson.id, 115 + hearts: state.hearts, 116 + }, 117 + replace: true, 118 + }); 119 + } 120 + }, [state.isFinished, state.correctCount, state.exercises.length, state.hearts, lesson, navigate]); 121 + 122 + if (!lesson) { 123 + return ( 124 + <Box 125 + sx={{ 126 + display: 'flex', 127 + flexDirection: 'column', 128 + alignItems: 'center', 129 + justifyContent: 'center', 130 + flex: 1, 131 + py: 8, 132 + }} 133 + > 134 + <Typography color="text.secondary">Lesson not found</Typography> 135 + <Button 136 + variant="contained" 137 + onClick={() => navigate('/home')} 138 + sx={{ mt: 2 }} 139 + > 140 + Go Home 141 + </Button> 142 + </Box> 143 + ); 144 + } 145 + 146 + if (state.exercises.length === 0) { 147 + return ( 148 + <Box 149 + sx={{ 150 + display: 'flex', 151 + justifyContent: 'center', 152 + alignItems: 'center', 153 + flex: 1, 154 + }} 155 + > 156 + <CircularProgress color="primary" /> 157 + </Box> 158 + ); 159 + } 160 + 161 + if (!currentExercise) return null; 162 + 163 + const hasAnswer = state.selectedAnswer != null && state.selectedAnswer !== ''; 164 + 165 + return ( 166 + <Box 167 + sx={{ 168 + display: 'flex', 169 + flexDirection: 'column', 170 + flex: 1, 171 + maxWidth: 600, 172 + mx: 'auto', 173 + width: '100%', 174 + p: 2, 175 + }} 176 + > 177 + {/* Top bar: close, progress, hearts */} 178 + <Box 179 + sx={{ 180 + display: 'flex', 181 + alignItems: 'center', 182 + gap: 2, 183 + mb: 4, 184 + }} 185 + > 186 + <IconButton 187 + onClick={() => navigate('/home')} 188 + sx={{ color: 'text.secondary' }} 189 + > 190 + <CloseIcon /> 191 + </IconButton> 192 + <Box sx={{ flex: 1 }}> 193 + <ProgressBar 194 + current={state.currentIndex} 195 + total={state.exercises.length} 196 + /> 197 + </Box> 198 + <HeartsDisplay hearts={state.hearts} /> 199 + </Box> 200 + 201 + {/* Exercise area */} 202 + <Box 203 + sx={{ 204 + flex: 1, 205 + display: 'flex', 206 + flexDirection: 'column', 207 + justifyContent: 'center', 208 + pb: state.isChecked ? 12 : 0, 209 + }} 210 + > 211 + {currentExercise.type === 'multiple-choice' && ( 212 + <MultipleChoice 213 + exercise={currentExercise} 214 + selectedAnswer={ 215 + typeof state.selectedAnswer === 'number' 216 + ? state.selectedAnswer 217 + : null 218 + } 219 + isChecked={state.isChecked} 220 + isCorrect={state.isCorrect} 221 + onSelect={(index) => selectAnswer(index)} 222 + /> 223 + )} 224 + 225 + {currentExercise.type === 'translation' && ( 226 + <Translation 227 + exercise={currentExercise} 228 + selectedAnswer={ 229 + typeof state.selectedAnswer === 'string' 230 + ? state.selectedAnswer 231 + : null 232 + } 233 + isChecked={state.isChecked} 234 + onSelect={(text) => selectAnswer(text)} 235 + /> 236 + )} 237 + 238 + {currentExercise.type === 'matching-pairs' && ( 239 + <MatchingPairs 240 + exercise={currentExercise} 241 + selectedAnswer={ 242 + Array.isArray(state.selectedAnswer) 243 + ? (state.selectedAnswer as Array<[string, string]>) 244 + : null 245 + } 246 + isChecked={state.isChecked} 247 + onSelect={(pairs) => selectAnswer(pairs)} 248 + /> 249 + )} 250 + 251 + {currentExercise.type === 'fill-in-the-blank' && ( 252 + <FillInTheBlank 253 + exercise={currentExercise} 254 + selectedAnswer={ 255 + typeof state.selectedAnswer === 'string' 256 + ? state.selectedAnswer 257 + : null 258 + } 259 + isChecked={state.isChecked} 260 + onSelect={(text) => selectAnswer(text)} 261 + /> 262 + )} 263 + 264 + {currentExercise.type === 'speak' && ( 265 + <SpeakExercise 266 + exercise={currentExercise} 267 + selectedAnswer={ 268 + typeof state.selectedAnswer === 'string' 269 + ? state.selectedAnswer 270 + : null 271 + } 272 + isChecked={state.isChecked} 273 + onSelect={(text) => selectAnswer(text)} 274 + /> 275 + )} 276 + </Box> 277 + 278 + {/* Bottom: Check / Continue button */} 279 + <Box sx={{ py: 2 }}> 280 + {!state.isChecked ? ( 281 + <Button 282 + fullWidth 283 + variant="contained" 284 + color="primary" 285 + disabled={!hasAnswer} 286 + onClick={handleCheck} 287 + sx={{ py: 1.5 }} 288 + > 289 + Check 290 + </Button> 291 + ) : ( 292 + <Button 293 + fullWidth 294 + variant="contained" 295 + color={state.isCorrect ? 'primary' : 'error'} 296 + onClick={handleContinue} 297 + sx={{ py: 1.5 }} 298 + > 299 + Continue 300 + </Button> 301 + )} 302 + </Box> 303 + 304 + {/* Feedback banners */} 305 + {state.isChecked && state.isCorrect === true && <CorrectBanner />} 306 + {state.isChecked && state.isCorrect === false && ( 307 + <IncorrectBanner correctAnswer={getCorrectAnswer(currentExercise)} /> 308 + )} 309 + </Box> 310 + ); 311 + }
+138
web/src/pages/LoginPage.tsx
··· 1 + import { useState } from 'react'; 2 + import { Link as RouterLink, Navigate } from 'react-router-dom'; 3 + import Box from '@mui/material/Box'; 4 + import Card from '@mui/material/Card'; 5 + import CardContent from '@mui/material/CardContent'; 6 + import TextField from '@mui/material/TextField'; 7 + import Button from '@mui/material/Button'; 8 + import Typography from '@mui/material/Typography'; 9 + import Link from '@mui/material/Link'; 10 + import Alert from '@mui/material/Alert'; 11 + import CircularProgress from '@mui/material/CircularProgress'; 12 + import { useAuth } from '../hooks/useAuth'; 13 + import { Rambutan } from '../components/mascot/Rambutan'; 14 + 15 + export function LoginPage() { 16 + const { login, user, loading: authLoading } = useAuth(); 17 + const [email, setEmail] = useState(''); 18 + const [password, setPassword] = useState(''); 19 + const [error, setError] = useState(''); 20 + const [loading, setLoading] = useState(false); 21 + 22 + if (authLoading) { 23 + return ( 24 + <Box 25 + sx={{ 26 + display: 'flex', 27 + justifyContent: 'center', 28 + alignItems: 'center', 29 + minHeight: '100vh', 30 + }} 31 + > 32 + <CircularProgress color="primary" /> 33 + </Box> 34 + ); 35 + } 36 + 37 + if (user) { 38 + return <Navigate to="/home" replace />; 39 + } 40 + 41 + async function handleSubmit(e: React.FormEvent) { 42 + e.preventDefault(); 43 + setError(''); 44 + setLoading(true); 45 + 46 + try { 47 + await login(email, password); 48 + } catch (err) { 49 + setError(err instanceof Error ? err.message : 'Login failed'); 50 + } finally { 51 + setLoading(false); 52 + } 53 + } 54 + 55 + return ( 56 + <Box 57 + sx={{ 58 + display: 'flex', 59 + flexDirection: 'column', 60 + alignItems: 'center', 61 + justifyContent: 'center', 62 + minHeight: '100vh', 63 + px: 2, 64 + }} 65 + > 66 + {/* Mascot peeking over the card */} 67 + <Box sx={{ mb: -3, zIndex: 1 }}> 68 + <Rambutan mood="peek" size={100} /> 69 + </Box> 70 + 71 + <Card sx={{ width: '100%', maxWidth: 400, position: 'relative' }}> 72 + <CardContent sx={{ p: 4 }}> 73 + <Typography 74 + variant="h4" 75 + sx={{ textAlign: 'center', mb: 1, color: '#E8453C' }} 76 + > 77 + Ayos 78 + </Typography> 79 + <Typography 80 + variant="body2" 81 + color="text.secondary" 82 + sx={{ textAlign: 'center', mb: 3 }} 83 + > 84 + Learn Tagalog the fun way 85 + </Typography> 86 + 87 + {error && ( 88 + <Alert severity="error" sx={{ mb: 2 }}> 89 + {error} 90 + </Alert> 91 + )} 92 + 93 + <Box component="form" onSubmit={handleSubmit}> 94 + <TextField 95 + fullWidth 96 + label="Email" 97 + type="email" 98 + value={email} 99 + onChange={(e) => setEmail(e.target.value)} 100 + required 101 + sx={{ mb: 2 }} 102 + /> 103 + <TextField 104 + fullWidth 105 + label="Password" 106 + type="password" 107 + value={password} 108 + onChange={(e) => setPassword(e.target.value)} 109 + required 110 + sx={{ mb: 3 }} 111 + /> 112 + <Button 113 + fullWidth 114 + type="submit" 115 + variant="contained" 116 + color="primary" 117 + disabled={loading} 118 + sx={{ mb: 2 }} 119 + > 120 + {loading ? <CircularProgress size={24} color="inherit" /> : 'Log In'} 121 + </Button> 122 + </Box> 123 + 124 + <Typography 125 + variant="body2" 126 + color="text.secondary" 127 + sx={{ textAlign: 'center' }} 128 + > 129 + Don&apos;t have an account?{' '} 130 + <Link component={RouterLink} to="/register" color="secondary"> 131 + Sign up 132 + </Link> 133 + </Typography> 134 + </CardContent> 135 + </Card> 136 + </Box> 137 + ); 138 + }
+150
web/src/pages/RegisterPage.tsx
··· 1 + import { useState } from 'react'; 2 + import { Link as RouterLink, Navigate } from 'react-router-dom'; 3 + import Box from '@mui/material/Box'; 4 + import Card from '@mui/material/Card'; 5 + import CardContent from '@mui/material/CardContent'; 6 + import TextField from '@mui/material/TextField'; 7 + import Button from '@mui/material/Button'; 8 + import Typography from '@mui/material/Typography'; 9 + import Link from '@mui/material/Link'; 10 + import Alert from '@mui/material/Alert'; 11 + import CircularProgress from '@mui/material/CircularProgress'; 12 + import { useAuth } from '../hooks/useAuth'; 13 + import { Rambutan } from '../components/mascot/Rambutan'; 14 + 15 + export function RegisterPage() { 16 + const { register, user, loading: authLoading } = useAuth(); 17 + const [username, setUsername] = useState(''); 18 + const [email, setEmail] = useState(''); 19 + const [password, setPassword] = useState(''); 20 + const [error, setError] = useState(''); 21 + const [loading, setLoading] = useState(false); 22 + 23 + if (authLoading) { 24 + return ( 25 + <Box 26 + sx={{ 27 + display: 'flex', 28 + justifyContent: 'center', 29 + alignItems: 'center', 30 + minHeight: '100vh', 31 + }} 32 + > 33 + <CircularProgress color="primary" /> 34 + </Box> 35 + ); 36 + } 37 + 38 + if (user) { 39 + return <Navigate to="/home" replace />; 40 + } 41 + 42 + async function handleSubmit(e: React.FormEvent) { 43 + e.preventDefault(); 44 + setError(''); 45 + setLoading(true); 46 + 47 + try { 48 + await register(username, email, password); 49 + } catch (err) { 50 + setError(err instanceof Error ? err.message : 'Registration failed'); 51 + } finally { 52 + setLoading(false); 53 + } 54 + } 55 + 56 + return ( 57 + <Box 58 + sx={{ 59 + display: 'flex', 60 + flexDirection: 'column', 61 + alignItems: 'center', 62 + justifyContent: 'center', 63 + minHeight: '100vh', 64 + px: 2, 65 + }} 66 + > 67 + <Box sx={{ mb: -3, zIndex: 1 }}> 68 + <Rambutan mood="wave" size={100} /> 69 + </Box> 70 + 71 + <Card sx={{ width: '100%', maxWidth: 400, position: 'relative' }}> 72 + <CardContent sx={{ p: 4 }}> 73 + <Typography 74 + variant="h4" 75 + sx={{ textAlign: 'center', mb: 1, color: '#E8453C' }} 76 + > 77 + Join Ayos 78 + </Typography> 79 + <Typography 80 + variant="body2" 81 + color="text.secondary" 82 + sx={{ textAlign: 'center', mb: 3 }} 83 + > 84 + Start your Tagalog journey 85 + </Typography> 86 + 87 + {error && ( 88 + <Alert severity="error" sx={{ mb: 2 }}> 89 + {error} 90 + </Alert> 91 + )} 92 + 93 + <Box component="form" onSubmit={handleSubmit}> 94 + <TextField 95 + fullWidth 96 + label="Username" 97 + value={username} 98 + onChange={(e) => setUsername(e.target.value)} 99 + required 100 + sx={{ mb: 2 }} 101 + /> 102 + <TextField 103 + fullWidth 104 + label="Email" 105 + type="email" 106 + value={email} 107 + onChange={(e) => setEmail(e.target.value)} 108 + required 109 + sx={{ mb: 2 }} 110 + /> 111 + <TextField 112 + fullWidth 113 + label="Password" 114 + type="password" 115 + value={password} 116 + onChange={(e) => setPassword(e.target.value)} 117 + required 118 + sx={{ mb: 3 }} 119 + /> 120 + <Button 121 + fullWidth 122 + type="submit" 123 + variant="contained" 124 + color="primary" 125 + disabled={loading} 126 + sx={{ mb: 2 }} 127 + > 128 + {loading ? ( 129 + <CircularProgress size={24} color="inherit" /> 130 + ) : ( 131 + 'Create Account' 132 + )} 133 + </Button> 134 + </Box> 135 + 136 + <Typography 137 + variant="body2" 138 + color="text.secondary" 139 + sx={{ textAlign: 'center' }} 140 + > 141 + Already have an account?{' '} 142 + <Link component={RouterLink} to="/login" color="secondary"> 143 + Log in 144 + </Link> 145 + </Typography> 146 + </CardContent> 147 + </Card> 148 + </Box> 149 + ); 150 + }
+151
web/src/pages/ReviewPage.tsx
··· 1 + import { useEffect, useRef } from 'react'; 2 + import { useLocation, useNavigate } from 'react-router-dom'; 3 + import Box from '@mui/material/Box'; 4 + import Card from '@mui/material/Card'; 5 + import CardContent from '@mui/material/CardContent'; 6 + import Typography from '@mui/material/Typography'; 7 + import Button from '@mui/material/Button'; 8 + import Divider from '@mui/material/Divider'; 9 + import BoltIcon from '@mui/icons-material/Bolt'; 10 + import { Rambutan } from '../components/mascot/Rambutan'; 11 + import { apiClient } from '../api/client'; 12 + 13 + interface ReviewState { 14 + correctCount: number; 15 + total: number; 16 + xp: number; 17 + topicId: string; 18 + lessonId: string; 19 + hearts: number; 20 + } 21 + 22 + export function ReviewPage() { 23 + const location = useLocation(); 24 + const navigate = useNavigate(); 25 + const savedRef = useRef(false); 26 + 27 + const reviewState = location.state as ReviewState | null; 28 + 29 + useEffect(() => { 30 + if (!reviewState) { 31 + navigate('/home', { replace: true }); 32 + return; 33 + } 34 + 35 + if (savedRef.current) return; 36 + savedRef.current = true; 37 + 38 + const score = Math.round( 39 + (reviewState.correctCount / reviewState.total) * 100, 40 + ); 41 + 42 + apiClient 43 + .put('/api/progress', { 44 + topic_id: reviewState.topicId, 45 + lesson_id: reviewState.lessonId, 46 + score, 47 + xp_earned: reviewState.xp, 48 + }) 49 + .catch(() => { 50 + // Progress save failed silently - user can retry the lesson 51 + }); 52 + }, [reviewState, navigate]); 53 + 54 + if (!reviewState) return null; 55 + 56 + const { correctCount, total, xp, hearts } = reviewState; 57 + const percentage = Math.round((correctCount / total) * 100); 58 + const isGoodScore = percentage >= 70; 59 + const isPerfect = percentage === 100; 60 + 61 + function getMascotMood() { 62 + if (isPerfect) return 'celebrate' as const; 63 + if (isGoodScore) return 'happy' as const; 64 + if (hearts === 0) return 'sad' as const; 65 + return 'idle' as const; 66 + } 67 + 68 + function getMessage() { 69 + if (isPerfect) return 'Perfect score! Ang galing mo!'; 70 + if (isGoodScore) return 'Great job! Magaling!'; 71 + if (hearts === 0) return 'Out of hearts! Try again!'; 72 + return 'Keep practicing! Kaya mo yan!'; 73 + } 74 + 75 + return ( 76 + <Box 77 + sx={{ 78 + display: 'flex', 79 + flexDirection: 'column', 80 + alignItems: 'center', 81 + justifyContent: 'center', 82 + flex: 1, 83 + px: 2, 84 + py: 4, 85 + }} 86 + > 87 + <Rambutan mood={getMascotMood()} size={140} /> 88 + 89 + <Card sx={{ width: '100%', maxWidth: 400, mt: 3 }}> 90 + <CardContent sx={{ textAlign: 'center', p: 4 }}> 91 + <Typography variant="h4" sx={{ mb: 1 }}> 92 + {getMessage()} 93 + </Typography> 94 + 95 + <Divider sx={{ my: 3 }} /> 96 + 97 + {/* Score */} 98 + <Box sx={{ mb: 2 }}> 99 + <Typography 100 + variant="h2" 101 + sx={{ 102 + fontWeight: 700, 103 + color: isGoodScore ? '#4CAF50' : '#FF4B4B', 104 + }} 105 + > 106 + {correctCount}/{total} 107 + </Typography> 108 + <Typography variant="body2" color="text.secondary"> 109 + {percentage}% correct 110 + </Typography> 111 + </Box> 112 + 113 + {/* XP earned */} 114 + <Box 115 + sx={{ 116 + display: 'inline-flex', 117 + alignItems: 'center', 118 + justifyContent: 'center', 119 + gap: 1, 120 + mb: 3, 121 + py: 1.5, 122 + px: 3, 123 + bgcolor: 'rgba(255, 200, 0, 0.1)', 124 + borderRadius: 2, 125 + }} 126 + > 127 + <BoltIcon sx={{ color: '#FFC800', fontSize: 28 }} /> 128 + <Typography 129 + variant="h5" 130 + sx={{ color: '#FFC800', fontWeight: 700 }} 131 + > 132 + +{isGoodScore ? xp : Math.floor(xp * (percentage / 100))} XP 133 + </Typography> 134 + </Box> 135 + 136 + <Box sx={{ mt: 3 }}> 137 + <Button 138 + fullWidth 139 + variant="contained" 140 + color="primary" 141 + onClick={() => navigate('/home')} 142 + sx={{ py: 1.5 }} 143 + > 144 + Continue 145 + </Button> 146 + </Box> 147 + </CardContent> 148 + </Card> 149 + </Box> 150 + ); 151 + }
+130
web/src/theme.ts
··· 1 + import { createTheme } from '@mui/material/styles'; 2 + 3 + const theme = createTheme({ 4 + palette: { 5 + mode: 'dark', 6 + primary: { 7 + main: '#E8453C', 8 + light: '#EF6B63', 9 + dark: '#C02E26', 10 + contrastText: '#FFFFFF', 11 + }, 12 + success: { 13 + main: '#4CAF50', 14 + light: '#66BB6A', 15 + dark: '#388E3C', 16 + }, 17 + secondary: { 18 + main: '#CE82FF', 19 + light: '#DDA0FF', 20 + dark: '#A855E0', 21 + contrastText: '#FFFFFF', 22 + }, 23 + error: { 24 + main: '#FF4B4B', 25 + light: '#FF7070', 26 + dark: '#CC3C3C', 27 + }, 28 + warning: { 29 + main: '#FFC800', 30 + light: '#FFD633', 31 + dark: '#CCA000', 32 + }, 33 + background: { 34 + default: '#131F24', 35 + paper: '#1B2B32', 36 + }, 37 + text: { 38 + primary: '#FFFFFF', 39 + secondary: '#AFAFAF', 40 + }, 41 + }, 42 + typography: { 43 + fontFamily: '"Nunito", sans-serif', 44 + button: { 45 + fontWeight: 700, 46 + textTransform: 'uppercase', 47 + letterSpacing: '0.05em', 48 + }, 49 + h4: { 50 + fontWeight: 700, 51 + }, 52 + h5: { 53 + fontWeight: 700, 54 + }, 55 + h6: { 56 + fontWeight: 700, 57 + }, 58 + }, 59 + shape: { 60 + borderRadius: 16, 61 + }, 62 + components: { 63 + MuiButton: { 64 + styleOverrides: { 65 + root: { 66 + borderRadius: 16, 67 + fontWeight: 700, 68 + fontSize: '1rem', 69 + padding: '12px 24px', 70 + textTransform: 'uppercase', 71 + boxShadow: '0 4px 0 rgba(0,0,0,0.3)', 72 + transition: 'all 0.15s ease', 73 + '&:active': { 74 + boxShadow: '0 1px 0 rgba(0,0,0,0.3)', 75 + transform: 'translateY(3px)', 76 + }, 77 + }, 78 + containedPrimary: { 79 + boxShadow: '0 4px 0 #C02E26', 80 + '&:hover': { 81 + backgroundColor: '#EF6B63', 82 + boxShadow: '0 4px 0 #C02E26', 83 + }, 84 + '&:active': { 85 + boxShadow: '0 1px 0 #C02E26', 86 + }, 87 + }, 88 + containedSecondary: { 89 + boxShadow: '0 4px 0 #A855E0', 90 + '&:hover': { 91 + backgroundColor: '#DDA0FF', 92 + boxShadow: '0 4px 0 #A855E0', 93 + }, 94 + '&:active': { 95 + boxShadow: '0 1px 0 #A855E0', 96 + }, 97 + }, 98 + containedError: { 99 + boxShadow: '0 4px 0 #CC3C3C', 100 + '&:hover': { 101 + backgroundColor: '#FF7070', 102 + boxShadow: '0 4px 0 #CC3C3C', 103 + }, 104 + '&:active': { 105 + boxShadow: '0 1px 0 #CC3C3C', 106 + }, 107 + }, 108 + }, 109 + }, 110 + MuiCard: { 111 + styleOverrides: { 112 + root: { 113 + borderRadius: 16, 114 + backgroundImage: 'none', 115 + }, 116 + }, 117 + }, 118 + MuiTextField: { 119 + styleOverrides: { 120 + root: { 121 + '& .MuiOutlinedInput-root': { 122 + borderRadius: 12, 123 + }, 124 + }, 125 + }, 126 + }, 127 + }, 128 + }); 129 + 130 + export default theme;
+71
web/src/types/lesson.ts
··· 1 + export type ExerciseType = 2 + | 'multiple-choice' 3 + | 'translation' 4 + | 'matching-pairs' 5 + | 'fill-in-the-blank' 6 + | 'speak'; 7 + 8 + export interface BaseExercise { 9 + type: ExerciseType; 10 + prompt: string; 11 + promptAudio?: boolean; 12 + } 13 + 14 + export interface MultipleChoiceExercise extends BaseExercise { 15 + type: 'multiple-choice'; 16 + choices: string[]; 17 + correctIndex: number; 18 + } 19 + 20 + export interface TranslationExercise extends BaseExercise { 21 + type: 'translation'; 22 + acceptedAnswers: string[]; 23 + hint?: string; 24 + } 25 + 26 + export interface MatchingPairsExercise extends BaseExercise { 27 + type: 'matching-pairs'; 28 + pairs: { left: string; right: string }[]; 29 + } 30 + 31 + export interface FillInTheBlankExercise extends BaseExercise { 32 + type: 'fill-in-the-blank'; 33 + sentence: string; 34 + blank: string; 35 + hint?: string; 36 + wordBank?: string[]; 37 + } 38 + 39 + export interface SpeakExercise extends BaseExercise { 40 + type: 'speak'; 41 + phrase: string; 42 + acceptedAnswers: string[]; 43 + } 44 + 45 + export type Exercise = 46 + | MultipleChoiceExercise 47 + | TranslationExercise 48 + | MatchingPairsExercise 49 + | FillInTheBlankExercise 50 + | SpeakExercise; 51 + 52 + export interface Lesson { 53 + id: string; 54 + topicId: string; 55 + title: string; 56 + xpReward: number; 57 + exercises: Exercise[]; 58 + } 59 + 60 + export interface Topic { 61 + id: string; 62 + name: string; 63 + icon: string; 64 + description: string; 65 + prerequisites: string[]; 66 + lessons: string[]; 67 + } 68 + 69 + export interface TopicsConfig { 70 + topics: Topic[]; 71 + }
+18
web/src/types/progress.ts
··· 1 + export interface LessonProgress { 2 + topic_id: string; 3 + lesson_id: string; 4 + completed: boolean; 5 + best_score: number; 6 + completed_at: string | null; 7 + } 8 + 9 + export interface UserStats { 10 + xp: number; 11 + streak_days: number; 12 + hearts: number; 13 + } 14 + 15 + export interface ProgressData { 16 + lessons: LessonProgress[]; 17 + stats: UserStats; 18 + }
+28
web/tsconfig.app.json
··· 1 + { 2 + "compilerOptions": { 3 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 + "target": "ES2023", 5 + "useDefineForClassFields": true, 6 + "lib": ["ES2023", "DOM", "DOM.Iterable"], 7 + "module": "ESNext", 8 + "types": ["vite/client"], 9 + "skipLibCheck": true, 10 + 11 + /* Bundler mode */ 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "moduleDetection": "force", 16 + "noEmit": true, 17 + "jsx": "react-jsx", 18 + 19 + /* Linting */ 20 + "strict": true, 21 + "noUnusedLocals": true, 22 + "noUnusedParameters": true, 23 + "erasableSyntaxOnly": true, 24 + "noFallthroughCasesInSwitch": true, 25 + "noUncheckedSideEffectImports": true 26 + }, 27 + "include": ["src"] 28 + }
+7
web/tsconfig.json
··· 1 + { 2 + "files": [], 3 + "references": [ 4 + { "path": "./tsconfig.app.json" }, 5 + { "path": "./tsconfig.node.json" } 6 + ] 7 + }
+26
web/tsconfig.node.json
··· 1 + { 2 + "compilerOptions": { 3 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 + "target": "ES2023", 5 + "lib": ["ES2023"], 6 + "module": "ESNext", 7 + "types": ["node"], 8 + "skipLibCheck": true, 9 + 10 + /* Bundler mode */ 11 + "moduleResolution": "bundler", 12 + "allowImportingTsExtensions": true, 13 + "verbatimModuleSyntax": true, 14 + "moduleDetection": "force", 15 + "noEmit": true, 16 + 17 + /* Linting */ 18 + "strict": true, 19 + "noUnusedLocals": true, 20 + "noUnusedParameters": true, 21 + "erasableSyntaxOnly": true, 22 + "noFallthroughCasesInSwitch": true, 23 + "noUncheckedSideEffectImports": true 24 + }, 25 + "include": ["vite.config.ts"] 26 + }
+19
web/vite.config.ts
··· 1 + import { defineConfig } from 'vite' 2 + import react from '@vitejs/plugin-react' 3 + import path from 'path' 4 + 5 + // https://vite.dev/config/ 6 + export default defineConfig({ 7 + plugins: [react()], 8 + server: { 9 + proxy: { 10 + '/api': { 11 + target: 'http://localhost:3001', 12 + changeOrigin: true, 13 + }, 14 + }, 15 + fs: { 16 + allow: ['.', path.resolve(__dirname, '../content')], 17 + }, 18 + }, 19 + })