learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs
5
fork

Configure Feed

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

feat(wip): add note and card creation with article import functionality

+1892 -174
+582 -63
Cargo.lock
··· 92 92 "bytes", 93 93 "form_urlencoded", 94 94 "futures-util", 95 - "http", 96 - "http-body", 95 + "http 1.4.0", 96 + "http-body 1.0.1", 97 97 "http-body-util", 98 - "hyper", 98 + "hyper 1.8.1", 99 99 "hyper-util", 100 100 "itoa", 101 101 "matchit", ··· 107 107 "serde_json", 108 108 "serde_path_to_error", 109 109 "serde_urlencoded", 110 - "sync_wrapper", 110 + "sync_wrapper 1.0.2", 111 111 "tokio", 112 112 "tower", 113 113 "tower-layer", ··· 123 123 dependencies = [ 124 124 "bytes", 125 125 "futures-core", 126 - "http", 127 - "http-body", 126 + "http 1.4.0", 127 + "http-body 1.0.1", 128 128 "http-body-util", 129 129 "mime", 130 130 "pin-project-lite", 131 - "sync_wrapper", 131 + "sync_wrapper 1.0.2", 132 132 "tower-layer", 133 133 "tower-service", 134 134 "tracing", ··· 136 136 137 137 [[package]] 138 138 name = "base64" 139 + version = "0.21.7" 140 + source = "registry+https://github.com/rust-lang/crates.io-index" 141 + checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 142 + 143 + [[package]] 144 + name = "base64" 139 145 version = "0.22.1" 140 146 source = "registry+https://github.com/rust-lang/crates.io-index" 141 147 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 142 148 143 149 [[package]] 144 150 name = "bitflags" 151 + version = "1.3.2" 152 + source = "registry+https://github.com/rust-lang/crates.io-index" 153 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 154 + 155 + [[package]] 156 + name = "bitflags" 145 157 version = "2.10.0" 146 158 source = "registry+https://github.com/rust-lang/crates.io-index" 147 159 checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" ··· 219 231 "heck", 220 232 "proc-macro2", 221 233 "quote", 222 - "syn", 234 + "syn 2.0.111", 223 235 ] 224 236 225 237 [[package]] ··· 278 290 dependencies = [ 279 291 "proc-macro2", 280 292 "quote", 281 - "syn", 293 + "syn 2.0.111", 282 294 ] 283 295 284 296 [[package]] ··· 349 361 ] 350 362 351 363 [[package]] 364 + name = "futf" 365 + version = "0.1.5" 366 + source = "registry+https://github.com/rust-lang/crates.io-index" 367 + checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 368 + dependencies = [ 369 + "mac", 370 + "new_debug_unreachable", 371 + ] 372 + 373 + [[package]] 352 374 name = "futures-channel" 353 375 version = "0.3.31" 354 376 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 364 386 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 365 387 366 388 [[package]] 389 + name = "futures-io" 390 + version = "0.3.31" 391 + source = "registry+https://github.com/rust-lang/crates.io-index" 392 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 393 + 394 + [[package]] 367 395 name = "futures-macro" 368 396 version = "0.3.31" 369 397 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 371 399 dependencies = [ 372 400 "proc-macro2", 373 401 "quote", 374 - "syn", 402 + "syn 2.0.111", 375 403 ] 376 404 377 405 [[package]] ··· 393 421 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 394 422 dependencies = [ 395 423 "futures-core", 424 + "futures-io", 396 425 "futures-macro", 397 426 "futures-task", 427 + "memchr", 398 428 "pin-project-lite", 399 429 "pin-utils", 400 430 "slab", ··· 425 455 426 456 [[package]] 427 457 name = "h2" 458 + version = "0.3.27" 459 + source = "registry+https://github.com/rust-lang/crates.io-index" 460 + checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" 461 + dependencies = [ 462 + "bytes", 463 + "fnv", 464 + "futures-core", 465 + "futures-sink", 466 + "futures-util", 467 + "http 0.2.12", 468 + "indexmap", 469 + "slab", 470 + "tokio", 471 + "tokio-util", 472 + "tracing", 473 + ] 474 + 475 + [[package]] 476 + name = "h2" 428 477 version = "0.4.12" 429 478 source = "registry+https://github.com/rust-lang/crates.io-index" 430 479 checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" ··· 434 483 "fnv", 435 484 "futures-core", 436 485 "futures-sink", 437 - "http", 486 + "http 1.4.0", 438 487 "indexmap", 439 488 "slab", 440 489 "tokio", ··· 455 504 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 456 505 457 506 [[package]] 507 + name = "html5ever" 508 + version = "0.26.0" 509 + source = "registry+https://github.com/rust-lang/crates.io-index" 510 + checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" 511 + dependencies = [ 512 + "log", 513 + "mac", 514 + "markup5ever", 515 + "proc-macro2", 516 + "quote", 517 + "syn 1.0.109", 518 + ] 519 + 520 + [[package]] 521 + name = "http" 522 + version = "0.2.12" 523 + source = "registry+https://github.com/rust-lang/crates.io-index" 524 + checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 525 + dependencies = [ 526 + "bytes", 527 + "fnv", 528 + "itoa", 529 + ] 530 + 531 + [[package]] 458 532 name = "http" 459 533 version = "1.4.0" 460 534 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 466 540 467 541 [[package]] 468 542 name = "http-body" 543 + version = "0.4.6" 544 + source = "registry+https://github.com/rust-lang/crates.io-index" 545 + checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 546 + dependencies = [ 547 + "bytes", 548 + "http 0.2.12", 549 + "pin-project-lite", 550 + ] 551 + 552 + [[package]] 553 + name = "http-body" 469 554 version = "1.0.1" 470 555 source = "registry+https://github.com/rust-lang/crates.io-index" 471 556 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 472 557 dependencies = [ 473 558 "bytes", 474 - "http", 559 + "http 1.4.0", 475 560 ] 476 561 477 562 [[package]] ··· 482 567 dependencies = [ 483 568 "bytes", 484 569 "futures-core", 485 - "http", 486 - "http-body", 570 + "http 1.4.0", 571 + "http-body 1.0.1", 487 572 "pin-project-lite", 488 573 ] 489 574 ··· 501 586 502 587 [[package]] 503 588 name = "hyper" 589 + version = "0.14.32" 590 + source = "registry+https://github.com/rust-lang/crates.io-index" 591 + checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" 592 + dependencies = [ 593 + "bytes", 594 + "futures-channel", 595 + "futures-core", 596 + "futures-util", 597 + "h2 0.3.27", 598 + "http 0.2.12", 599 + "http-body 0.4.6", 600 + "httparse", 601 + "httpdate", 602 + "itoa", 603 + "pin-project-lite", 604 + "socket2 0.5.10", 605 + "tokio", 606 + "tower-service", 607 + "tracing", 608 + "want", 609 + ] 610 + 611 + [[package]] 612 + name = "hyper" 504 613 version = "1.8.1" 505 614 source = "registry+https://github.com/rust-lang/crates.io-index" 506 615 checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" ··· 509 618 "bytes", 510 619 "futures-channel", 511 620 "futures-core", 512 - "h2", 513 - "http", 514 - "http-body", 621 + "h2 0.4.12", 622 + "http 1.4.0", 623 + "http-body 1.0.1", 515 624 "httparse", 516 625 "httpdate", 517 626 "itoa", ··· 528 637 source = "registry+https://github.com/rust-lang/crates.io-index" 529 638 checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 530 639 dependencies = [ 531 - "http", 532 - "hyper", 640 + "http 1.4.0", 641 + "hyper 1.8.1", 533 642 "hyper-util", 534 643 "rustls", 535 644 "rustls-pki-types", ··· 540 649 541 650 [[package]] 542 651 name = "hyper-tls" 652 + version = "0.5.0" 653 + source = "registry+https://github.com/rust-lang/crates.io-index" 654 + checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 655 + dependencies = [ 656 + "bytes", 657 + "hyper 0.14.32", 658 + "native-tls", 659 + "tokio", 660 + "tokio-native-tls", 661 + ] 662 + 663 + [[package]] 664 + name = "hyper-tls" 543 665 version = "0.6.0" 544 666 source = "registry+https://github.com/rust-lang/crates.io-index" 545 667 checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 546 668 dependencies = [ 547 669 "bytes", 548 670 "http-body-util", 549 - "hyper", 671 + "hyper 1.8.1", 550 672 "hyper-util", 551 673 "native-tls", 552 674 "tokio", ··· 560 682 source = "registry+https://github.com/rust-lang/crates.io-index" 561 683 checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 562 684 dependencies = [ 563 - "base64", 685 + "base64 0.22.1", 564 686 "bytes", 565 687 "futures-channel", 566 688 "futures-core", 567 689 "futures-util", 568 - "http", 569 - "http-body", 570 - "hyper", 690 + "http 1.4.0", 691 + "http-body 1.0.1", 692 + "hyper 1.8.1", 571 693 "ipnet", 572 694 "libc", 573 695 "percent-encoding", 574 696 "pin-project-lite", 575 - "socket2", 576 - "system-configuration", 697 + "socket2 0.6.1", 698 + "system-configuration 0.6.1", 577 699 "tokio", 578 700 "tower-service", 579 701 "tracing", ··· 794 916 checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 795 917 796 918 [[package]] 919 + name = "mac" 920 + version = "0.1.1" 921 + source = "registry+https://github.com/rust-lang/crates.io-index" 922 + checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 923 + 924 + [[package]] 797 925 name = "malfestio-cli" 798 926 version = "0.1.0" 799 927 dependencies = [ ··· 819 947 "axum", 820 948 "chrono", 821 949 "malfestio-core", 822 - "reqwest", 950 + "readability", 951 + "regex", 952 + "reqwest 0.12.28", 823 953 "serde", 824 954 "serde_json", 825 955 "tokio", ··· 832 962 ] 833 963 834 964 [[package]] 965 + name = "markup5ever" 966 + version = "0.11.0" 967 + source = "registry+https://github.com/rust-lang/crates.io-index" 968 + checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" 969 + dependencies = [ 970 + "log", 971 + "phf", 972 + "phf_codegen", 973 + "string_cache", 974 + "string_cache_codegen", 975 + "tendril", 976 + ] 977 + 978 + [[package]] 979 + name = "markup5ever_rcdom" 980 + version = "0.2.0" 981 + source = "registry+https://github.com/rust-lang/crates.io-index" 982 + checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" 983 + dependencies = [ 984 + "html5ever", 985 + "markup5ever", 986 + "tendril", 987 + "xml5ever", 988 + ] 989 + 990 + [[package]] 835 991 name = "matchers" 836 992 version = "0.2.0" 837 993 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 887 1043 ] 888 1044 889 1045 [[package]] 1046 + name = "new_debug_unreachable" 1047 + version = "1.0.6" 1048 + source = "registry+https://github.com/rust-lang/crates.io-index" 1049 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 1050 + 1051 + [[package]] 890 1052 name = "nu-ansi-term" 891 1053 version = "0.50.3" 892 1054 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 928 1090 source = "registry+https://github.com/rust-lang/crates.io-index" 929 1091 checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 930 1092 dependencies = [ 931 - "bitflags", 1093 + "bitflags 2.10.0", 932 1094 "cfg-if", 933 1095 "foreign-types", 934 1096 "libc", ··· 945 1107 dependencies = [ 946 1108 "proc-macro2", 947 1109 "quote", 948 - "syn", 1110 + "syn 2.0.111", 949 1111 ] 950 1112 951 1113 [[package]] ··· 996 1158 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 997 1159 998 1160 [[package]] 1161 + name = "phf" 1162 + version = "0.10.1" 1163 + source = "registry+https://github.com/rust-lang/crates.io-index" 1164 + checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" 1165 + dependencies = [ 1166 + "phf_shared 0.10.0", 1167 + ] 1168 + 1169 + [[package]] 1170 + name = "phf_codegen" 1171 + version = "0.10.0" 1172 + source = "registry+https://github.com/rust-lang/crates.io-index" 1173 + checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" 1174 + dependencies = [ 1175 + "phf_generator 0.10.0", 1176 + "phf_shared 0.10.0", 1177 + ] 1178 + 1179 + [[package]] 1180 + name = "phf_generator" 1181 + version = "0.10.0" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" 1184 + dependencies = [ 1185 + "phf_shared 0.10.0", 1186 + "rand 0.8.5", 1187 + ] 1188 + 1189 + [[package]] 1190 + name = "phf_generator" 1191 + version = "0.11.3" 1192 + source = "registry+https://github.com/rust-lang/crates.io-index" 1193 + checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 1194 + dependencies = [ 1195 + "phf_shared 0.11.3", 1196 + "rand 0.8.5", 1197 + ] 1198 + 1199 + [[package]] 1200 + name = "phf_shared" 1201 + version = "0.10.0" 1202 + source = "registry+https://github.com/rust-lang/crates.io-index" 1203 + checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 1204 + dependencies = [ 1205 + "siphasher 0.3.11", 1206 + ] 1207 + 1208 + [[package]] 1209 + name = "phf_shared" 1210 + version = "0.11.3" 1211 + source = "registry+https://github.com/rust-lang/crates.io-index" 1212 + checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 1213 + dependencies = [ 1214 + "siphasher 1.0.1", 1215 + ] 1216 + 1217 + [[package]] 999 1218 name = "pin-project-lite" 1000 1219 version = "0.2.16" 1001 1220 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1036 1255 dependencies = [ 1037 1256 "zerocopy", 1038 1257 ] 1258 + 1259 + [[package]] 1260 + name = "precomputed-hash" 1261 + version = "0.1.1" 1262 + source = "registry+https://github.com/rust-lang/crates.io-index" 1263 + checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1039 1264 1040 1265 [[package]] 1041 1266 name = "proc-macro2" ··· 1063 1288 1064 1289 [[package]] 1065 1290 name = "rand" 1291 + version = "0.8.5" 1292 + source = "registry+https://github.com/rust-lang/crates.io-index" 1293 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1294 + dependencies = [ 1295 + "libc", 1296 + "rand_chacha 0.3.1", 1297 + "rand_core 0.6.4", 1298 + ] 1299 + 1300 + [[package]] 1301 + name = "rand" 1066 1302 version = "0.9.2" 1067 1303 source = "registry+https://github.com/rust-lang/crates.io-index" 1068 1304 checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 1069 1305 dependencies = [ 1070 - "rand_chacha", 1071 - "rand_core", 1306 + "rand_chacha 0.9.0", 1307 + "rand_core 0.9.3", 1308 + ] 1309 + 1310 + [[package]] 1311 + name = "rand_chacha" 1312 + version = "0.3.1" 1313 + source = "registry+https://github.com/rust-lang/crates.io-index" 1314 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1315 + dependencies = [ 1316 + "ppv-lite86", 1317 + "rand_core 0.6.4", 1072 1318 ] 1073 1319 1074 1320 [[package]] ··· 1078 1324 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1079 1325 dependencies = [ 1080 1326 "ppv-lite86", 1081 - "rand_core", 1327 + "rand_core 0.9.3", 1328 + ] 1329 + 1330 + [[package]] 1331 + name = "rand_core" 1332 + version = "0.6.4" 1333 + source = "registry+https://github.com/rust-lang/crates.io-index" 1334 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1335 + dependencies = [ 1336 + "getrandom 0.2.16", 1082 1337 ] 1083 1338 1084 1339 [[package]] ··· 1091 1346 ] 1092 1347 1093 1348 [[package]] 1349 + name = "readability" 1350 + version = "0.3.0" 1351 + source = "registry+https://github.com/rust-lang/crates.io-index" 1352 + checksum = "e56596e20a6d3cf715182d9b6829220621e6e985cec04d00410cee29821b4220" 1353 + dependencies = [ 1354 + "html5ever", 1355 + "lazy_static", 1356 + "markup5ever_rcdom", 1357 + "regex", 1358 + "reqwest 0.11.27", 1359 + "url", 1360 + ] 1361 + 1362 + [[package]] 1094 1363 name = "redox_syscall" 1095 1364 version = "0.5.18" 1096 1365 source = "registry+https://github.com/rust-lang/crates.io-index" 1097 1366 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1098 1367 dependencies = [ 1099 - "bitflags", 1368 + "bitflags 2.10.0", 1369 + ] 1370 + 1371 + [[package]] 1372 + name = "regex" 1373 + version = "1.12.2" 1374 + source = "registry+https://github.com/rust-lang/crates.io-index" 1375 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 1376 + dependencies = [ 1377 + "aho-corasick", 1378 + "memchr", 1379 + "regex-automata", 1380 + "regex-syntax", 1100 1381 ] 1101 1382 1102 1383 [[package]] ··· 1118 1399 1119 1400 [[package]] 1120 1401 name = "reqwest" 1402 + version = "0.11.27" 1403 + source = "registry+https://github.com/rust-lang/crates.io-index" 1404 + checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 1405 + dependencies = [ 1406 + "base64 0.21.7", 1407 + "bytes", 1408 + "encoding_rs", 1409 + "futures-core", 1410 + "futures-util", 1411 + "h2 0.3.27", 1412 + "http 0.2.12", 1413 + "http-body 0.4.6", 1414 + "hyper 0.14.32", 1415 + "hyper-tls 0.5.0", 1416 + "ipnet", 1417 + "js-sys", 1418 + "log", 1419 + "mime", 1420 + "native-tls", 1421 + "once_cell", 1422 + "percent-encoding", 1423 + "pin-project-lite", 1424 + "rustls-pemfile", 1425 + "serde", 1426 + "serde_json", 1427 + "serde_urlencoded", 1428 + "sync_wrapper 0.1.2", 1429 + "system-configuration 0.5.1", 1430 + "tokio", 1431 + "tokio-native-tls", 1432 + "tower-service", 1433 + "url", 1434 + "wasm-bindgen", 1435 + "wasm-bindgen-futures", 1436 + "web-sys", 1437 + "winreg", 1438 + ] 1439 + 1440 + [[package]] 1441 + name = "reqwest" 1121 1442 version = "0.12.28" 1122 1443 source = "registry+https://github.com/rust-lang/crates.io-index" 1123 1444 checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 1124 1445 dependencies = [ 1125 - "base64", 1446 + "base64 0.22.1", 1126 1447 "bytes", 1127 1448 "encoding_rs", 1128 1449 "futures-core", 1129 - "h2", 1130 - "http", 1131 - "http-body", 1450 + "h2 0.4.12", 1451 + "http 1.4.0", 1452 + "http-body 1.0.1", 1132 1453 "http-body-util", 1133 - "hyper", 1454 + "hyper 1.8.1", 1134 1455 "hyper-rustls", 1135 - "hyper-tls", 1456 + "hyper-tls 0.6.0", 1136 1457 "hyper-util", 1137 1458 "js-sys", 1138 1459 "log", ··· 1144 1465 "serde", 1145 1466 "serde_json", 1146 1467 "serde_urlencoded", 1147 - "sync_wrapper", 1468 + "sync_wrapper 1.0.2", 1148 1469 "tokio", 1149 1470 "tokio-native-tls", 1150 1471 "tower", ··· 1176 1497 source = "registry+https://github.com/rust-lang/crates.io-index" 1177 1498 checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 1178 1499 dependencies = [ 1179 - "bitflags", 1500 + "bitflags 2.10.0", 1180 1501 "errno", 1181 1502 "libc", 1182 1503 "linux-raw-sys", ··· 1197 1518 ] 1198 1519 1199 1520 [[package]] 1521 + name = "rustls-pemfile" 1522 + version = "1.0.4" 1523 + source = "registry+https://github.com/rust-lang/crates.io-index" 1524 + checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1525 + dependencies = [ 1526 + "base64 0.21.7", 1527 + ] 1528 + 1529 + [[package]] 1200 1530 name = "rustls-pki-types" 1201 1531 version = "1.13.2" 1202 1532 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1249 1579 source = "registry+https://github.com/rust-lang/crates.io-index" 1250 1580 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1251 1581 dependencies = [ 1252 - "bitflags", 1582 + "bitflags 2.10.0", 1253 1583 "core-foundation", 1254 1584 "core-foundation-sys", 1255 1585 "libc", ··· 1293 1623 dependencies = [ 1294 1624 "proc-macro2", 1295 1625 "quote", 1296 - "syn", 1626 + "syn 2.0.111", 1297 1627 ] 1298 1628 1299 1629 [[package]] ··· 1358 1688 ] 1359 1689 1360 1690 [[package]] 1691 + name = "siphasher" 1692 + version = "0.3.11" 1693 + source = "registry+https://github.com/rust-lang/crates.io-index" 1694 + checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1695 + 1696 + [[package]] 1697 + name = "siphasher" 1698 + version = "1.0.1" 1699 + source = "registry+https://github.com/rust-lang/crates.io-index" 1700 + checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1701 + 1702 + [[package]] 1361 1703 name = "slab" 1362 1704 version = "0.4.11" 1363 1705 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1371 1713 1372 1714 [[package]] 1373 1715 name = "socket2" 1716 + version = "0.5.10" 1717 + source = "registry+https://github.com/rust-lang/crates.io-index" 1718 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 1719 + dependencies = [ 1720 + "libc", 1721 + "windows-sys 0.52.0", 1722 + ] 1723 + 1724 + [[package]] 1725 + name = "socket2" 1374 1726 version = "0.6.1" 1375 1727 source = "registry+https://github.com/rust-lang/crates.io-index" 1376 1728 checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" ··· 1386 1738 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1387 1739 1388 1740 [[package]] 1741 + name = "string_cache" 1742 + version = "0.8.9" 1743 + source = "registry+https://github.com/rust-lang/crates.io-index" 1744 + checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" 1745 + dependencies = [ 1746 + "new_debug_unreachable", 1747 + "parking_lot", 1748 + "phf_shared 0.11.3", 1749 + "precomputed-hash", 1750 + "serde", 1751 + ] 1752 + 1753 + [[package]] 1754 + name = "string_cache_codegen" 1755 + version = "0.5.4" 1756 + source = "registry+https://github.com/rust-lang/crates.io-index" 1757 + checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 1758 + dependencies = [ 1759 + "phf_generator 0.11.3", 1760 + "phf_shared 0.11.3", 1761 + "proc-macro2", 1762 + "quote", 1763 + ] 1764 + 1765 + [[package]] 1389 1766 name = "strsim" 1390 1767 version = "0.11.1" 1391 1768 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1399 1776 1400 1777 [[package]] 1401 1778 name = "syn" 1779 + version = "1.0.109" 1780 + source = "registry+https://github.com/rust-lang/crates.io-index" 1781 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1782 + dependencies = [ 1783 + "proc-macro2", 1784 + "quote", 1785 + "unicode-ident", 1786 + ] 1787 + 1788 + [[package]] 1789 + name = "syn" 1402 1790 version = "2.0.111" 1403 1791 source = "registry+https://github.com/rust-lang/crates.io-index" 1404 1792 checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" ··· 1410 1798 1411 1799 [[package]] 1412 1800 name = "sync_wrapper" 1801 + version = "0.1.2" 1802 + source = "registry+https://github.com/rust-lang/crates.io-index" 1803 + checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1804 + 1805 + [[package]] 1806 + name = "sync_wrapper" 1413 1807 version = "1.0.2" 1414 1808 source = "registry+https://github.com/rust-lang/crates.io-index" 1415 1809 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" ··· 1425 1819 dependencies = [ 1426 1820 "proc-macro2", 1427 1821 "quote", 1428 - "syn", 1822 + "syn 2.0.111", 1823 + ] 1824 + 1825 + [[package]] 1826 + name = "system-configuration" 1827 + version = "0.5.1" 1828 + source = "registry+https://github.com/rust-lang/crates.io-index" 1829 + checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1830 + dependencies = [ 1831 + "bitflags 1.3.2", 1832 + "core-foundation", 1833 + "system-configuration-sys 0.5.0", 1429 1834 ] 1430 1835 1431 1836 [[package]] ··· 1434 1839 source = "registry+https://github.com/rust-lang/crates.io-index" 1435 1840 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1436 1841 dependencies = [ 1437 - "bitflags", 1842 + "bitflags 2.10.0", 1438 1843 "core-foundation", 1439 - "system-configuration-sys", 1844 + "system-configuration-sys 0.6.0", 1845 + ] 1846 + 1847 + [[package]] 1848 + name = "system-configuration-sys" 1849 + version = "0.5.0" 1850 + source = "registry+https://github.com/rust-lang/crates.io-index" 1851 + checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1852 + dependencies = [ 1853 + "core-foundation-sys", 1854 + "libc", 1440 1855 ] 1441 1856 1442 1857 [[package]] ··· 1463 1878 ] 1464 1879 1465 1880 [[package]] 1881 + name = "tendril" 1882 + version = "0.4.3" 1883 + source = "registry+https://github.com/rust-lang/crates.io-index" 1884 + checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 1885 + dependencies = [ 1886 + "futf", 1887 + "mac", 1888 + "utf-8", 1889 + ] 1890 + 1891 + [[package]] 1466 1892 name = "thiserror" 1467 1893 version = "2.0.17" 1468 1894 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1479 1905 dependencies = [ 1480 1906 "proc-macro2", 1481 1907 "quote", 1482 - "syn", 1908 + "syn 2.0.111", 1483 1909 ] 1484 1910 1485 1911 [[package]] ··· 1544 1970 "parking_lot", 1545 1971 "pin-project-lite", 1546 1972 "signal-hook-registry", 1547 - "socket2", 1973 + "socket2 0.6.1", 1548 1974 "tokio-macros", 1549 1975 "windows-sys 0.61.2", 1550 1976 ] ··· 1557 1983 dependencies = [ 1558 1984 "proc-macro2", 1559 1985 "quote", 1560 - "syn", 1986 + "syn 2.0.111", 1561 1987 ] 1562 1988 1563 1989 [[package]] ··· 1602 2028 "futures-core", 1603 2029 "futures-util", 1604 2030 "pin-project-lite", 1605 - "sync_wrapper", 2031 + "sync_wrapper 1.0.2", 1606 2032 "tokio", 1607 2033 "tower-layer", 1608 2034 "tower-service", ··· 1618 2044 "axum-core", 1619 2045 "cookie", 1620 2046 "futures-util", 1621 - "http", 2047 + "http 1.4.0", 1622 2048 "parking_lot", 1623 2049 "pin-project-lite", 1624 2050 "tower-layer", ··· 1631 2057 source = "registry+https://github.com/rust-lang/crates.io-index" 1632 2058 checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 1633 2059 dependencies = [ 1634 - "bitflags", 2060 + "bitflags 2.10.0", 1635 2061 "bytes", 1636 2062 "futures-util", 1637 - "http", 1638 - "http-body", 2063 + "http 1.4.0", 2064 + "http-body 1.0.1", 1639 2065 "iri-string", 1640 2066 "pin-project-lite", 1641 2067 "tower", ··· 1676 2102 dependencies = [ 1677 2103 "proc-macro2", 1678 2104 "quote", 1679 - "syn", 2105 + "syn 2.0.111", 1680 2106 ] 1681 2107 1682 2108 [[package]] ··· 1749 2175 ] 1750 2176 1751 2177 [[package]] 2178 + name = "utf-8" 2179 + version = "0.7.6" 2180 + source = "registry+https://github.com/rust-lang/crates.io-index" 2181 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2182 + 2183 + [[package]] 1752 2184 name = "utf8_iter" 1753 2185 version = "1.0.4" 1754 2186 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1768 2200 dependencies = [ 1769 2201 "getrandom 0.3.4", 1770 2202 "js-sys", 1771 - "rand", 2203 + "rand 0.9.2", 1772 2204 "wasm-bindgen", 1773 2205 ] 1774 2206 ··· 1859 2291 "bumpalo", 1860 2292 "proc-macro2", 1861 2293 "quote", 1862 - "syn", 2294 + "syn 2.0.111", 1863 2295 "wasm-bindgen-shared", 1864 2296 ] 1865 2297 ··· 1903 2335 dependencies = [ 1904 2336 "proc-macro2", 1905 2337 "quote", 1906 - "syn", 2338 + "syn 2.0.111", 1907 2339 ] 1908 2340 1909 2341 [[package]] ··· 1914 2346 dependencies = [ 1915 2347 "proc-macro2", 1916 2348 "quote", 1917 - "syn", 2349 + "syn 2.0.111", 1918 2350 ] 1919 2351 1920 2352 [[package]] ··· 1954 2386 1955 2387 [[package]] 1956 2388 name = "windows-sys" 2389 + version = "0.48.0" 2390 + source = "registry+https://github.com/rust-lang/crates.io-index" 2391 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2392 + dependencies = [ 2393 + "windows-targets 0.48.5", 2394 + ] 2395 + 2396 + [[package]] 2397 + name = "windows-sys" 1957 2398 version = "0.52.0" 1958 2399 source = "registry+https://github.com/rust-lang/crates.io-index" 1959 2400 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" ··· 1981 2422 1982 2423 [[package]] 1983 2424 name = "windows-targets" 2425 + version = "0.48.5" 2426 + source = "registry+https://github.com/rust-lang/crates.io-index" 2427 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2428 + dependencies = [ 2429 + "windows_aarch64_gnullvm 0.48.5", 2430 + "windows_aarch64_msvc 0.48.5", 2431 + "windows_i686_gnu 0.48.5", 2432 + "windows_i686_msvc 0.48.5", 2433 + "windows_x86_64_gnu 0.48.5", 2434 + "windows_x86_64_gnullvm 0.48.5", 2435 + "windows_x86_64_msvc 0.48.5", 2436 + ] 2437 + 2438 + [[package]] 2439 + name = "windows-targets" 1984 2440 version = "0.52.6" 1985 2441 source = "registry+https://github.com/rust-lang/crates.io-index" 1986 2442 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" ··· 2014 2470 2015 2471 [[package]] 2016 2472 name = "windows_aarch64_gnullvm" 2473 + version = "0.48.5" 2474 + source = "registry+https://github.com/rust-lang/crates.io-index" 2475 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2476 + 2477 + [[package]] 2478 + name = "windows_aarch64_gnullvm" 2017 2479 version = "0.52.6" 2018 2480 source = "registry+https://github.com/rust-lang/crates.io-index" 2019 2481 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" ··· 2026 2488 2027 2489 [[package]] 2028 2490 name = "windows_aarch64_msvc" 2491 + version = "0.48.5" 2492 + source = "registry+https://github.com/rust-lang/crates.io-index" 2493 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2494 + 2495 + [[package]] 2496 + name = "windows_aarch64_msvc" 2029 2497 version = "0.52.6" 2030 2498 source = "registry+https://github.com/rust-lang/crates.io-index" 2031 2499 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" ··· 2035 2503 version = "0.53.1" 2036 2504 source = "registry+https://github.com/rust-lang/crates.io-index" 2037 2505 checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 2506 + 2507 + [[package]] 2508 + name = "windows_i686_gnu" 2509 + version = "0.48.5" 2510 + source = "registry+https://github.com/rust-lang/crates.io-index" 2511 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2038 2512 2039 2513 [[package]] 2040 2514 name = "windows_i686_gnu" ··· 2062 2536 2063 2537 [[package]] 2064 2538 name = "windows_i686_msvc" 2539 + version = "0.48.5" 2540 + source = "registry+https://github.com/rust-lang/crates.io-index" 2541 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2542 + 2543 + [[package]] 2544 + name = "windows_i686_msvc" 2065 2545 version = "0.52.6" 2066 2546 source = "registry+https://github.com/rust-lang/crates.io-index" 2067 2547 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" ··· 2074 2554 2075 2555 [[package]] 2076 2556 name = "windows_x86_64_gnu" 2557 + version = "0.48.5" 2558 + source = "registry+https://github.com/rust-lang/crates.io-index" 2559 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2560 + 2561 + [[package]] 2562 + name = "windows_x86_64_gnu" 2077 2563 version = "0.52.6" 2078 2564 source = "registry+https://github.com/rust-lang/crates.io-index" 2079 2565 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" ··· 2086 2572 2087 2573 [[package]] 2088 2574 name = "windows_x86_64_gnullvm" 2575 + version = "0.48.5" 2576 + source = "registry+https://github.com/rust-lang/crates.io-index" 2577 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2578 + 2579 + [[package]] 2580 + name = "windows_x86_64_gnullvm" 2089 2581 version = "0.52.6" 2090 2582 source = "registry+https://github.com/rust-lang/crates.io-index" 2091 2583 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" ··· 2098 2590 2099 2591 [[package]] 2100 2592 name = "windows_x86_64_msvc" 2593 + version = "0.48.5" 2594 + source = "registry+https://github.com/rust-lang/crates.io-index" 2595 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2596 + 2597 + [[package]] 2598 + name = "windows_x86_64_msvc" 2101 2599 version = "0.52.6" 2102 2600 source = "registry+https://github.com/rust-lang/crates.io-index" 2103 2601 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" ··· 2109 2607 checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 2110 2608 2111 2609 [[package]] 2610 + name = "winreg" 2611 + version = "0.50.0" 2612 + source = "registry+https://github.com/rust-lang/crates.io-index" 2613 + checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 2614 + dependencies = [ 2615 + "cfg-if", 2616 + "windows-sys 0.48.0", 2617 + ] 2618 + 2619 + [[package]] 2112 2620 name = "wit-bindgen" 2113 2621 version = "0.46.0" 2114 2622 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2121 2629 checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 2122 2630 2123 2631 [[package]] 2632 + name = "xml5ever" 2633 + version = "0.17.0" 2634 + source = "registry+https://github.com/rust-lang/crates.io-index" 2635 + checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" 2636 + dependencies = [ 2637 + "log", 2638 + "mac", 2639 + "markup5ever", 2640 + ] 2641 + 2642 + [[package]] 2124 2643 name = "yoke" 2125 2644 version = "0.8.1" 2126 2645 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2139 2658 dependencies = [ 2140 2659 "proc-macro2", 2141 2660 "quote", 2142 - "syn", 2661 + "syn 2.0.111", 2143 2662 "synstructure", 2144 2663 ] 2145 2664 ··· 2160 2679 dependencies = [ 2161 2680 "proc-macro2", 2162 2681 "quote", 2163 - "syn", 2682 + "syn 2.0.111", 2164 2683 ] 2165 2684 2166 2685 [[package]] ··· 2180 2699 dependencies = [ 2181 2700 "proc-macro2", 2182 2701 "quote", 2183 - "syn", 2702 + "syn 2.0.111", 2184 2703 "synstructure", 2185 2704 ] 2186 2705 ··· 2220 2739 dependencies = [ 2221 2740 "proc-macro2", 2222 2741 "quote", 2223 - "syn", 2742 + "syn 2.0.111", 2224 2743 ] 2225 2744 2226 2745 [[package]]
+10 -1
crates/core/src/model.rs
··· 2 2 3 3 #[derive(Debug, Clone, Serialize, Deserialize)] 4 4 pub struct Note { 5 + pub id: String, 6 + pub owner_did: String, 5 7 pub title: String, 6 8 pub body: String, 7 9 pub tags: Vec<String>, 10 + pub visibility: Visibility, 11 + pub published_at: Option<String>, 12 + #[serde(default)] 13 + pub links: Vec<String>, 8 14 } 9 15 10 16 #[derive(Debug, Clone, Serialize, Deserialize)] 11 17 pub struct Card { 18 + pub id: String, 19 + pub owner_did: String, 20 + pub deck_id: String, 12 21 pub front: String, 13 22 pub back: String, 14 - pub deck_ref: Option<String>, 23 + pub media_url: Option<String>, 15 24 } 16 25 17 26 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+2
crates/server/Cargo.toml
··· 7 7 axum = "0.8.8" 8 8 chrono = { version = "0.4.42", features = ["serde"] } 9 9 malfestio-core = { version = "0.1.0", path = "../core" } 10 + readability = "0.3.0" 11 + regex = "1.12.2" 10 12 reqwest = { version = "0.12.28", features = ["json"] } 11 13 serde = "1.0.228" 12 14 serde_json = "1.0.148"
+84
crates/server/src/api/card.rs
··· 1 + use crate::middleware::auth::UserContext; 2 + use crate::state::SharedState; 3 + use axum::{ 4 + Json, 5 + extract::{Extension, Path, State}, 6 + http::StatusCode, 7 + response::IntoResponse, 8 + }; 9 + use malfestio_core::model::{Card, Visibility}; 10 + use serde::Deserialize; 11 + use serde_json::json; 12 + 13 + #[derive(Deserialize)] 14 + pub struct CreateCardRequest { 15 + deck_id: String, 16 + front: String, 17 + back: String, 18 + media_url: Option<String>, 19 + } 20 + 21 + pub async fn create_card( 22 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, Json(payload): Json<CreateCardRequest>, 23 + ) -> impl IntoResponse { 24 + let user = match ctx { 25 + Some(axum::Extension(user)) => user, 26 + None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "Unauthorized"}))).into_response(), 27 + }; 28 + 29 + { 30 + let decks = state.decks.read().unwrap(); 31 + let deck = decks.iter().find(|d| d.id == payload.deck_id); 32 + match deck { 33 + Some(d) => { 34 + if d.owner_did != user.did { 35 + return (StatusCode::FORBIDDEN, Json(json!({"error": "Not deck owner"}))).into_response(); 36 + } 37 + } 38 + None => return (StatusCode::NOT_FOUND, Json(json!({"error": "Deck not found"}))).into_response(), 39 + } 40 + } 41 + 42 + let new_card = Card { 43 + id: uuid::Uuid::new_v4().to_string(), 44 + owner_did: user.did, 45 + deck_id: payload.deck_id, 46 + front: payload.front, 47 + back: payload.back, 48 + media_url: payload.media_url, 49 + }; 50 + 51 + state.cards.write().unwrap().push(new_card.clone()); 52 + 53 + (StatusCode::CREATED, Json(new_card)).into_response() 54 + } 55 + 56 + pub async fn list_cards( 57 + State(state): State<SharedState>, Path(deck_id): Path<String>, ctx: Option<axum::Extension<UserContext>>, 58 + ) -> impl IntoResponse { 59 + let user_did = ctx.map(|Extension(u)| u.did); 60 + 61 + { 62 + let decks = state.decks.read().unwrap(); 63 + if let Some(deck) = decks.iter().find(|d| d.id == deck_id) { 64 + let is_owner = user_did.as_ref() == Some(&deck.owner_did); 65 + if deck.visibility == Visibility::Private && !is_owner { 66 + return (StatusCode::FORBIDDEN, Json(json!({"error": "Private deck"}))).into_response(); 67 + } 68 + 69 + if let Visibility::SharedWith(dids) = &deck.visibility 70 + && !is_owner 71 + && (user_did.is_none() || !dids.contains(user_did.as_ref().unwrap())) 72 + { 73 + return (StatusCode::FORBIDDEN, Json(json!({"error": "Access denied"}))).into_response(); 74 + } 75 + } else { 76 + return (StatusCode::NOT_FOUND, Json(json!({"error": "Deck not found"}))).into_response(); 77 + } 78 + } 79 + 80 + let cards = state.cards.read().unwrap(); 81 + let deck_cards: Vec<Card> = cards.iter().filter(|c| c.deck_id == deck_id).cloned().collect(); 82 + 83 + Json(deck_cards).into_response() 84 + }
+31 -31
crates/server/src/api/deck.rs
··· 1 1 use crate::middleware::auth::UserContext; 2 + use crate::state::SharedState; 2 3 3 4 use axum::{ 4 5 Json, ··· 9 10 use malfestio_core::model::{Deck, Visibility}; 10 11 use serde::Deserialize; 11 12 use serde_json::json; 12 - use std::sync::{Arc, RwLock}; 13 - 14 - type Db = Arc<RwLock<Vec<Deck>>>; 15 13 16 14 #[derive(Deserialize)] 17 15 pub struct CreateDeckRequest { ··· 24 22 #[derive(Deserialize)] 25 23 pub struct PublishDeckRequest { 26 24 pub published: bool, 27 - } 28 - 29 - pub fn init_db() -> Db { 30 - Arc::new(RwLock::new(Vec::new())) 31 25 } 32 26 33 27 pub async fn create_deck( 34 - State(db): State<Db>, ctx: Option<axum::Extension<UserContext>>, Json(payload): Json<CreateDeckRequest>, 28 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, Json(payload): Json<CreateDeckRequest>, 35 29 ) -> impl IntoResponse { 36 30 let user = match ctx { 37 31 Some(axum::Extension(user)) => user, ··· 49 43 fork_of: None, 50 44 }; 51 45 52 - db.write().unwrap().push(new_deck.clone()); 46 + state.decks.write().unwrap().push(new_deck.clone()); 53 47 54 48 (StatusCode::CREATED, Json(new_deck)).into_response() 55 49 } 56 50 57 - pub async fn list_decks(State(db): State<Db>, ctx: Option<axum::Extension<UserContext>>) -> impl IntoResponse { 51 + pub async fn list_decks( 52 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, 53 + ) -> impl IntoResponse { 58 54 let user_did = ctx.map(|Extension(u)| u.did); 59 55 60 - let decks = db.read().unwrap(); 56 + let decks = state.decks.read().unwrap(); 61 57 62 58 let visible_decks: Vec<Deck> = decks 63 59 .iter() ··· 85 81 } 86 82 87 83 pub async fn get_deck( 88 - State(db): State<Db>, ctx: Option<axum::Extension<UserContext>>, Path(id): Path<String>, 84 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, Path(id): Path<String>, 89 85 ) -> impl IntoResponse { 90 86 let user_did = ctx.map(|Extension(u)| u.did); 91 - let decks = db.read().unwrap(); 87 + let decks = state.decks.read().unwrap(); 92 88 93 89 if let Some(deck) = decks.iter().find(|d| d.id == id) { 94 90 let is_owner = user_did.as_ref() == Some(&deck.owner_did); ··· 115 111 116 112 /// NOTE: Unpublishing sets visibility to Private and clears published_at 117 113 pub async fn publish_deck( 118 - State(db): State<Db>, ctx: Option<axum::Extension<UserContext>>, Path(id): Path<String>, 114 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, Path(id): Path<String>, 119 115 Json(payload): Json<PublishDeckRequest>, 120 116 ) -> impl IntoResponse { 121 117 let user = match ctx { ··· 123 119 None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "Unauthorized"}))).into_response(), 124 120 }; 125 121 126 - let mut decks = db.write().unwrap(); 122 + let mut decks = state.decks.write().unwrap(); 127 123 if let Some(deck) = decks.iter_mut().find(|d| d.id == id) { 128 124 if deck.owner_did != user.did { 129 125 return (StatusCode::FORBIDDEN, Json(json!({"error": "Only owner can publish"}))).into_response(); ··· 145 141 #[cfg(test)] 146 142 mod tests { 147 143 use super::*; 144 + use crate::state::AppState; 148 145 use axum::extract::State; 149 146 150 - fn mock_db() -> Db { 151 - Arc::new(RwLock::new(vec![ 147 + fn mock_state() -> SharedState { 148 + let state = AppState::new(); 149 + let mut decks = state.decks.write().unwrap(); 150 + *decks = vec![ 152 151 Deck { 153 152 id: "deck-public".to_string(), 154 153 owner_did: "did:plc:owner".to_string(), ··· 179 178 published_at: None, 180 179 fork_of: None, 181 180 }, 182 - ])) 181 + ]; 182 + state.clone() 183 183 } 184 184 185 185 #[tokio::test] 186 186 async fn test_get_public_deck() { 187 - let db = mock_db(); 188 - let response = get_deck(State(db), None, Path("deck-public".to_string())) 187 + let state = mock_state(); 188 + let response = get_deck(State(state), None, Path("deck-public".to_string())) 189 189 .await 190 190 .into_response(); 191 191 ··· 194 194 195 195 #[tokio::test] 196 196 async fn test_get_private_deck_owner() { 197 - let db = mock_db(); 197 + let state = mock_state(); 198 198 let ctx = Some(Extension(UserContext { 199 199 did: "did:plc:owner".to_string(), 200 200 handle: "owner.bsky.social".to_string(), 201 201 })); 202 202 203 - let response = get_deck(State(db), ctx, Path("deck-private".to_string())) 203 + let response = get_deck(State(state), ctx, Path("deck-private".to_string())) 204 204 .await 205 205 .into_response(); 206 206 ··· 209 209 210 210 #[tokio::test] 211 211 async fn test_get_private_deck_stranger() { 212 - let db = mock_db(); 212 + let state = mock_state(); 213 213 let ctx = Some(Extension(UserContext { 214 214 did: "did:plc:stranger".to_string(), 215 215 handle: "stranger.bsky.social".to_string(), 216 216 })); 217 217 218 - let response = get_deck(State(db), ctx, Path("deck-private".to_string())) 218 + let response = get_deck(State(state), ctx, Path("deck-private".to_string())) 219 219 .await 220 220 .into_response(); 221 221 ··· 224 224 225 225 #[tokio::test] 226 226 async fn test_get_shared_deck_permitted() { 227 - let db = mock_db(); 227 + let state = mock_state(); 228 228 let ctx = Some(Extension(UserContext { 229 229 did: "did:plc:friend".to_string(), 230 230 handle: "friend.bsky.social".to_string(), 231 231 })); 232 232 233 - let response = get_deck(State(db), ctx, Path("deck-shared".to_string())) 233 + let response = get_deck(State(state), ctx, Path("deck-shared".to_string())) 234 234 .await 235 235 .into_response(); 236 236 ··· 239 239 240 240 #[tokio::test] 241 241 async fn test_get_shared_deck_unpermitted() { 242 - let db = mock_db(); 242 + let state = mock_state(); 243 243 let ctx = Some(Extension(UserContext { 244 244 did: "did:plc:stranger".to_string(), 245 245 handle: "stranger.bsky.social".to_string(), 246 246 })); 247 247 248 - let response = get_deck(State(db), ctx, Path("deck-shared".to_string())) 248 + let response = get_deck(State(state), ctx, Path("deck-shared".to_string())) 249 249 .await 250 250 .into_response(); 251 251 ··· 254 254 255 255 #[tokio::test] 256 256 async fn test_publish_deck() { 257 - let db = mock_db(); 257 + let state = mock_state(); 258 258 let ctx = Some(Extension(UserContext { 259 259 did: "did:plc:owner".to_string(), 260 260 handle: "owner.bsky.social".to_string(), 261 261 })); 262 262 263 263 let response = publish_deck( 264 - State(db.clone()), 264 + State(state.clone()), 265 265 ctx, 266 266 Path("deck-private".to_string()), 267 267 Json(PublishDeckRequest { published: true }), ··· 271 271 272 272 assert_eq!(response.status(), StatusCode::OK); 273 273 274 - let decks = db.read().unwrap(); 274 + let decks = state.decks.read().unwrap(); 275 275 let deck = decks.iter().find(|d| d.id == "deck-private").unwrap(); 276 276 assert_eq!(deck.visibility, Visibility::Public); 277 277 assert!(deck.published_at.is_some());
+40
crates/server/src/api/importer.rs
··· 1 + use axum::{Json, http::StatusCode, response::IntoResponse}; 2 + use readability::extractor; 3 + use serde::Deserialize; 4 + use serde_json::json; 5 + 6 + #[derive(Deserialize)] 7 + pub struct ImportRequest { 8 + url: String, 9 + } 10 + 11 + pub async fn import_article(Json(payload): Json<ImportRequest>) -> impl IntoResponse { 12 + if payload.url.trim().is_empty() { 13 + return (StatusCode::BAD_REQUEST, Json(json!({"error": "URL is required"}))).into_response(); 14 + } 15 + 16 + let url = payload.url.clone(); 17 + let url_for_task = url.clone(); 18 + 19 + let result = tokio::task::spawn_blocking(move || extractor::scrape(&url_for_task)).await; 20 + 21 + match result { 22 + Ok(Ok(product)) => Json(json!({ 23 + "title": product.title, 24 + "content": product.content, 25 + "text": product.text, 26 + "url": url 27 + })) 28 + .into_response(), 29 + Ok(Err(e)) => ( 30 + StatusCode::INTERNAL_SERVER_ERROR, 31 + Json(json!({"error": format!("Readability error: {}", e)})), 32 + ) 33 + .into_response(), 34 + Err(e) => ( 35 + StatusCode::INTERNAL_SERVER_ERROR, 36 + Json(json!({"error": format!("Task join error: {}", e)})), 37 + ) 38 + .into_response(), 39 + } 40 + }
+3
crates/server/src/api/mod.rs
··· 1 1 pub mod auth; 2 + pub mod card; 2 3 pub mod deck; 4 + pub mod importer; 5 + pub mod note;
+117
crates/server/src/api/note.rs
··· 1 + use crate::middleware::auth::UserContext; 2 + use crate::state::SharedState; 3 + use axum::{ 4 + Json, 5 + extract::{Extension, Path, State}, 6 + http::StatusCode, 7 + response::IntoResponse, 8 + }; 9 + use malfestio_core::model::{Note, Visibility}; 10 + use regex::Regex; 11 + use serde::Deserialize; 12 + use serde_json::json; 13 + 14 + #[derive(Deserialize)] 15 + pub struct CreateNoteRequest { 16 + title: String, 17 + body: String, 18 + tags: Vec<String>, 19 + visibility: Visibility, 20 + } 21 + 22 + fn extract_links(body: &str) -> Vec<String> { 23 + let re = Regex::new(r"\[\[(.*?)\]\]").unwrap(); 24 + re.captures_iter(body).map(|cap| cap[1].to_string()).collect() 25 + } 26 + 27 + pub async fn create_note( 28 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, Json(payload): Json<CreateNoteRequest>, 29 + ) -> impl IntoResponse { 30 + let user = match ctx { 31 + Some(axum::Extension(user)) => user, 32 + None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "Unauthorized"}))).into_response(), 33 + }; 34 + 35 + let links = extract_links(&payload.body); 36 + 37 + let new_note = Note { 38 + id: uuid::Uuid::new_v4().to_string(), 39 + owner_did: user.did, 40 + title: payload.title, 41 + body: payload.body, 42 + tags: payload.tags, 43 + visibility: payload.visibility, 44 + published_at: None, 45 + links, 46 + }; 47 + 48 + state.notes.write().unwrap().push(new_note.clone()); 49 + 50 + (StatusCode::CREATED, Json(new_note)).into_response() 51 + } 52 + 53 + pub async fn list_notes( 54 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, 55 + ) -> impl IntoResponse { 56 + let user_did = ctx.map(|Extension(u)| u.did); 57 + let notes = state.notes.read().unwrap(); 58 + 59 + let visible_notes: Vec<Note> = notes 60 + .iter() 61 + .filter(|n| { 62 + if let Some(did) = &user_did 63 + && &n.owner_did == did 64 + { 65 + return true; 66 + } 67 + if n.visibility == Visibility::Public { 68 + return true; 69 + } 70 + if let Visibility::SharedWith(dids) = &n.visibility 71 + && let Some(did) = &user_did 72 + && dids.contains(did) 73 + { 74 + return true; 75 + } 76 + false 77 + }) 78 + .cloned() 79 + .collect(); 80 + 81 + Json(visible_notes).into_response() 82 + } 83 + 84 + pub async fn get_note( 85 + State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, Path(id): Path<String>, 86 + ) -> impl IntoResponse { 87 + let user_did = ctx.map(|Extension(u)| u.did); 88 + let notes = state.notes.read().unwrap(); 89 + 90 + if let Some(note) = notes.iter().find(|n| n.id == id) { 91 + let is_owner = user_did.as_ref() == Some(&note.owner_did); 92 + 93 + if note.visibility == Visibility::Public || is_owner { 94 + let backlinks: Vec<String> = notes 95 + .iter() 96 + .filter(|n| n.links.contains(&note.title) && n.id != note.id) // Naive matching by title 97 + .map(|n| n.id.clone()) 98 + .collect(); 99 + 100 + let mut response = serde_json::to_value(note).unwrap(); 101 + response["backlinks"] = json!(backlinks); 102 + 103 + return Json(response).into_response(); 104 + } 105 + 106 + if let Visibility::SharedWith(dids) = &note.visibility 107 + && let Some(did) = &user_did 108 + && dids.contains(did) 109 + { 110 + return Json(note).into_response(); 111 + } 112 + 113 + return (StatusCode::FORBIDDEN, Json(json!({"error": "Access denied"}))).into_response(); 114 + } 115 + 116 + (StatusCode::NOT_FOUND, Json(json!({"error": "Note not found"}))).into_response() 117 + }
+9 -2
crates/server/src/lib.rs
··· 1 1 pub mod api; 2 2 pub mod middleware; 3 + pub mod state; 3 4 4 5 use axum::http::Method; 5 6 use axum::{ ··· 27 28 28 29 tracing::info!("Starting Malfestio Server..."); 29 30 30 - let db = api::deck::init_db(); 31 + let state = state::AppState::new(); 31 32 32 33 let auth_routes = Router::new() 33 34 .route("/me", get(api::auth::me)) 34 35 .route("/decks", post(api::deck::create_deck)) 36 + .route("/notes", post(api::note::create_note)) 37 + .route("/cards", post(api::card::create_card)) 35 38 .layer(axum_middleware::from_fn(middleware::auth::auth_middleware)); 36 39 37 40 let app = Router::new() ··· 39 42 .route("/api/auth/login", post(api::auth::login)) 40 43 .route("/api/decks", get(api::deck::list_decks)) 41 44 .route("/api/decks/{id}", get(api::deck::get_deck)) 45 + .route("/api/decks/{id}/cards", get(api::card::list_cards)) 46 + .route("/api/notes", get(api::note::list_notes)) 47 + .route("/api/notes/{id}", get(api::note::get_note)) 48 + .route("/api/import/article", post(api::importer::import_article)) 42 49 .nest("/api", auth_routes) 43 50 .layer(TraceLayer::new_for_http()) 44 51 .layer( ··· 47 54 .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) 48 55 .allow_headers(Any), 49 56 ) 50 - .with_state(db); 57 + .with_state(state); 51 58 52 59 let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); 53 60
+20
crates/server/src/state.rs
··· 1 + use malfestio_core::model::{Card, Deck, Note}; 2 + use std::sync::{Arc, RwLock}; 3 + 4 + pub type SharedState = Arc<AppState>; 5 + 6 + pub struct AppState { 7 + pub decks: RwLock<Vec<Deck>>, 8 + pub notes: RwLock<Vec<Note>>, 9 + pub cards: RwLock<Vec<Card>>, 10 + } 11 + 12 + impl AppState { 13 + pub fn new() -> SharedState { 14 + Arc::new(Self { 15 + decks: RwLock::new(Vec::new()), 16 + notes: RwLock::new(Vec::new()), 17 + cards: RwLock::new(Vec::new()), 18 + }) 19 + } 20 + }
+4 -4
docs/todo.md
··· 55 55 56 56 - Note editor (markdown + attachments + backlinks) 57 57 - Card editor: 58 - - basic front/back + cloze v1 59 - - images/audio attachments (optional) 58 + - basic front/back + cloze 59 + - images/audio attachments 60 60 - Deck builder: 61 61 - tags, ordering, sections 62 - - Importers v1: 62 + - Importers: 63 63 - article URL -> extracted snapshot + highlights 64 64 - lecture URL -> outline + timestamps (manual entry initially) 65 65 ··· 71 71 72 72 #### Deliverables 73 73 74 - - SRS scheduler v1 (SM-2 baseline) 74 + - SRS scheduler (SM-2 baseline) 75 75 - grade 0–5, EF, interval, repetition count 76 76 - Review queue generation rules 77 77 - Study session UI:
+2 -1
web/eslint.config.js
··· 1 1 import js from "@eslint/js"; 2 2 import * as tsParser from "@typescript-eslint/parser"; 3 3 import solid from "eslint-plugin-solid/configs/typescript"; 4 + import globals from "globals"; 4 5 5 6 export default [js.configs.recommended, { 6 7 files: ["**/*.{ts,tsx}"], 7 8 ...solid, 8 - languageOptions: { parser: tsParser, parserOptions: { project: "./tsconfig.app.json" }, globals: globals.browser }, 9 + languageOptions: { parser: tsParser, globals: globals.browser }, 9 10 }];
+10 -1
web/package.json
··· 11 11 "check": "tsc --noEmit" 12 12 }, 13 13 "dependencies": { 14 + "@solidjs/meta": "^0.29.4", 14 15 "@solidjs/router": "^0.15.4", 15 16 "@tailwindcss/vite": "^4.1.18", 17 + "clsx": "^2.1.1", 18 + "rehype-external-links": "^3.0.0", 19 + "rehype-sanitize": "^6.0.0", 20 + "rehype-stringify": "^10.0.1", 21 + "remark-parse": "^11.0.0", 22 + "remark-rehype": "^11.1.2", 16 23 "solid-js": "^1.9.10", 17 - "tailwindcss": "^4.1.18" 24 + "tailwind-merge": "^3.4.0", 25 + "tailwindcss": "^4.1.18", 26 + "unified": "^11.0.5" 18 27 }, 19 28 "devDependencies": { 20 29 "@eslint/js": "^9.39.2",
+579
web/pnpm-lock.yaml
··· 11 11 12 12 .: 13 13 dependencies: 14 + '@solidjs/meta': 15 + specifier: ^0.29.4 16 + version: 0.29.4(solid-js@1.9.10) 14 17 '@solidjs/router': 15 18 specifier: ^0.15.4 16 19 version: 0.15.4(solid-js@1.9.10) 17 20 '@tailwindcss/vite': 18 21 specifier: ^4.1.18 19 22 version: 4.1.18(rolldown-vite@7.2.5(@types/node@24.10.4)(jiti@2.6.1)) 23 + clsx: 24 + specifier: ^2.1.1 25 + version: 2.1.1 26 + rehype-external-links: 27 + specifier: ^3.0.0 28 + version: 3.0.0 29 + rehype-sanitize: 30 + specifier: ^6.0.0 31 + version: 6.0.0 32 + rehype-stringify: 33 + specifier: ^10.0.1 34 + version: 10.0.1 35 + remark-parse: 36 + specifier: ^11.0.0 37 + version: 11.0.0 38 + remark-rehype: 39 + specifier: ^11.1.2 40 + version: 11.1.2 20 41 solid-js: 21 42 specifier: ^1.9.10 22 43 version: 1.9.10 44 + tailwind-merge: 45 + specifier: ^3.4.0 46 + version: 3.4.0 23 47 tailwindcss: 24 48 specifier: ^4.1.18 25 49 version: 4.1.18 50 + unified: 51 + specifier: ^11.0.5 52 + version: 11.0.5 26 53 devDependencies: 27 54 '@eslint/js': 28 55 specifier: ^9.39.2 ··· 385 412 '@rolldown/pluginutils@1.0.0-beta.50': 386 413 resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} 387 414 415 + '@solidjs/meta@0.29.4': 416 + resolution: {integrity: sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g==} 417 + peerDependencies: 418 + solid-js: '>=1.8.4' 419 + 388 420 '@solidjs/router@0.15.4': 389 421 resolution: {integrity: sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==} 390 422 peerDependencies: ··· 528 560 '@types/chai@5.2.3': 529 561 resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 530 562 563 + '@types/debug@4.1.12': 564 + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} 565 + 531 566 '@types/deep-eql@4.0.2': 532 567 resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 533 568 534 569 '@types/estree@1.0.8': 535 570 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 536 571 572 + '@types/hast@3.0.4': 573 + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} 574 + 537 575 '@types/json-schema@7.0.15': 538 576 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 539 577 578 + '@types/mdast@4.0.4': 579 + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} 580 + 581 + '@types/ms@2.1.0': 582 + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} 583 + 540 584 '@types/node@24.10.4': 541 585 resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} 542 586 587 + '@types/unist@3.0.3': 588 + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 589 + 543 590 '@typescript-eslint/parser@8.50.1': 544 591 resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} 545 592 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} ··· 583 630 '@typescript-eslint/visitor-keys@8.50.1': 584 631 resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} 585 632 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 633 + 634 + '@ungap/structured-clone@1.3.0': 635 + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} 586 636 587 637 '@vitest/expect@4.0.16': 588 638 resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} ··· 670 720 solid-js: 671 721 optional: true 672 722 723 + bail@2.0.2: 724 + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} 725 + 673 726 balanced-match@1.0.2: 674 727 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 675 728 ··· 698 751 caniuse-lite@1.0.30001761: 699 752 resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} 700 753 754 + ccount@2.0.1: 755 + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} 756 + 701 757 chai@6.2.2: 702 758 resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} 703 759 engines: {node: '>=18'} ··· 706 762 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 707 763 engines: {node: '>=10'} 708 764 765 + character-entities-html4@2.1.0: 766 + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} 767 + 768 + character-entities-legacy@3.0.0: 769 + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} 770 + 771 + character-entities@2.0.2: 772 + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} 773 + 774 + clsx@2.1.1: 775 + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} 776 + engines: {node: '>=6'} 777 + 709 778 color-convert@2.0.1: 710 779 resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 711 780 engines: {node: '>=7.0.0'} 712 781 713 782 color-name@1.1.4: 714 783 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 784 + 785 + comma-separated-tokens@2.0.3: 786 + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 715 787 716 788 concat-map@0.0.1: 717 789 resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} ··· 753 825 decimal.js@10.6.0: 754 826 resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} 755 827 828 + decode-named-character-reference@1.2.0: 829 + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} 830 + 756 831 deep-is@0.1.4: 757 832 resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 758 833 ··· 763 838 detect-libc@2.1.2: 764 839 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 765 840 engines: {node: '>=8'} 841 + 842 + devlop@1.1.0: 843 + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 766 844 767 845 dom-accessibility-api@0.5.16: 768 846 resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} ··· 848 926 resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 849 927 engines: {node: '>=12.0.0'} 850 928 929 + extend@3.0.2: 930 + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} 931 + 851 932 fast-deep-equal@3.1.3: 852 933 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 853 934 ··· 909 990 resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 910 991 engines: {node: '>=8'} 911 992 993 + hast-util-is-element@3.0.0: 994 + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} 995 + 996 + hast-util-sanitize@5.0.2: 997 + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} 998 + 999 + hast-util-to-html@9.0.5: 1000 + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} 1001 + 1002 + hast-util-whitespace@3.0.0: 1003 + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} 1004 + 912 1005 html-encoding-sniffer@6.0.0: 913 1006 resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} 914 1007 engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} ··· 919 1012 html-tags@3.3.1: 920 1013 resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} 921 1014 engines: {node: '>=8'} 1015 + 1016 + html-void-elements@3.0.0: 1017 + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} 922 1018 923 1019 http-proxy-agent@7.0.2: 924 1020 resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} ··· 947 1043 inline-style-parser@0.2.7: 948 1044 resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} 949 1045 1046 + is-absolute-url@4.0.1: 1047 + resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} 1048 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1049 + 950 1050 is-extglob@2.1.1: 951 1051 resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 952 1052 engines: {node: '>=0.10.0'} ··· 958 1058 is-html@2.0.0: 959 1059 resolution: {integrity: sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==} 960 1060 engines: {node: '>=8'} 1061 + 1062 + is-plain-obj@4.1.0: 1063 + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} 1064 + engines: {node: '>=12'} 961 1065 962 1066 is-potential-custom-element-name@1.0.1: 963 1067 resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} ··· 1112 1216 magic-string@0.30.21: 1113 1217 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1114 1218 1219 + mdast-util-from-markdown@2.0.2: 1220 + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} 1221 + 1222 + mdast-util-to-hast@13.2.1: 1223 + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} 1224 + 1225 + mdast-util-to-string@4.0.0: 1226 + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} 1227 + 1115 1228 mdn-data@2.12.2: 1116 1229 resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} 1117 1230 ··· 1119 1232 resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 1120 1233 engines: {node: '>=12.13'} 1121 1234 1235 + micromark-core-commonmark@2.0.3: 1236 + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} 1237 + 1238 + micromark-factory-destination@2.0.1: 1239 + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} 1240 + 1241 + micromark-factory-label@2.0.1: 1242 + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} 1243 + 1244 + micromark-factory-space@2.0.1: 1245 + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} 1246 + 1247 + micromark-factory-title@2.0.1: 1248 + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} 1249 + 1250 + micromark-factory-whitespace@2.0.1: 1251 + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} 1252 + 1253 + micromark-util-character@2.1.1: 1254 + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} 1255 + 1256 + micromark-util-chunked@2.0.1: 1257 + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} 1258 + 1259 + micromark-util-classify-character@2.0.1: 1260 + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} 1261 + 1262 + micromark-util-combine-extensions@2.0.1: 1263 + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} 1264 + 1265 + micromark-util-decode-numeric-character-reference@2.0.2: 1266 + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} 1267 + 1268 + micromark-util-decode-string@2.0.1: 1269 + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} 1270 + 1271 + micromark-util-encode@2.0.1: 1272 + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} 1273 + 1274 + micromark-util-html-tag-name@2.0.1: 1275 + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} 1276 + 1277 + micromark-util-normalize-identifier@2.0.1: 1278 + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} 1279 + 1280 + micromark-util-resolve-all@2.0.1: 1281 + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} 1282 + 1283 + micromark-util-sanitize-uri@2.0.1: 1284 + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} 1285 + 1286 + micromark-util-subtokenize@2.1.0: 1287 + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} 1288 + 1289 + micromark-util-symbol@2.0.1: 1290 + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} 1291 + 1292 + micromark-util-types@2.0.2: 1293 + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} 1294 + 1295 + micromark@4.0.2: 1296 + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} 1297 + 1122 1298 min-indent@1.0.1: 1123 1299 resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} 1124 1300 engines: {node: '>=4'} ··· 1199 1375 resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} 1200 1376 engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} 1201 1377 1378 + property-information@7.1.0: 1379 + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} 1380 + 1202 1381 punycode@2.3.1: 1203 1382 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1204 1383 engines: {node: '>=6'} ··· 1210 1389 resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} 1211 1390 engines: {node: '>=8'} 1212 1391 1392 + rehype-external-links@3.0.0: 1393 + resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} 1394 + 1395 + rehype-sanitize@6.0.0: 1396 + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} 1397 + 1398 + rehype-stringify@10.0.1: 1399 + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} 1400 + 1401 + remark-parse@11.0.0: 1402 + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} 1403 + 1404 + remark-rehype@11.1.2: 1405 + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} 1406 + 1213 1407 require-from-string@2.0.2: 1214 1408 resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 1215 1409 engines: {node: '>=0.10.0'} ··· 1309 1503 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1310 1504 engines: {node: '>=0.10.0'} 1311 1505 1506 + space-separated-tokens@2.0.2: 1507 + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} 1508 + 1312 1509 stackback@0.0.2: 1313 1510 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1314 1511 1315 1512 std-env@3.10.0: 1316 1513 resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 1514 + 1515 + stringify-entities@4.0.4: 1516 + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} 1317 1517 1318 1518 strip-indent@3.0.0: 1319 1519 resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} ··· 1333 1533 symbol-tree@3.2.4: 1334 1534 resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} 1335 1535 1536 + tailwind-merge@3.4.0: 1537 + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} 1538 + 1336 1539 tailwindcss@4.1.18: 1337 1540 resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} 1338 1541 ··· 1369 1572 tr46@6.0.0: 1370 1573 resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} 1371 1574 engines: {node: '>=20'} 1575 + 1576 + trim-lines@3.0.1: 1577 + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} 1578 + 1579 + trough@2.2.0: 1580 + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} 1372 1581 1373 1582 ts-api-utils@2.2.0: 1374 1583 resolution: {integrity: sha512-L6f5oQRAoLU1RwXz0Ab9mxsE7LtxeVB6AIR1lpkZMsOyg/JXeaxBaXa/FVCBZyNr9S9I4wkHrlZTklX+im+WMw==} ··· 1391 1600 undici-types@7.16.0: 1392 1601 resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 1393 1602 1603 + unified@11.0.5: 1604 + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} 1605 + 1606 + unist-util-is@6.0.1: 1607 + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} 1608 + 1609 + unist-util-position@5.0.0: 1610 + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} 1611 + 1612 + unist-util-stringify-position@4.0.0: 1613 + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} 1614 + 1615 + unist-util-visit-parents@6.0.2: 1616 + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} 1617 + 1618 + unist-util-visit@5.0.0: 1619 + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} 1620 + 1394 1621 update-browserslist-db@1.2.3: 1395 1622 resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} 1396 1623 hasBin: true ··· 1399 1626 1400 1627 uri-js@4.4.1: 1401 1628 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1629 + 1630 + vfile-message@4.0.3: 1631 + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} 1632 + 1633 + vfile@6.0.3: 1634 + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} 1402 1635 1403 1636 vite-plugin-solid@2.11.10: 1404 1637 resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} ··· 1507 1740 yocto-queue@0.1.0: 1508 1741 resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1509 1742 engines: {node: '>=10'} 1743 + 1744 + zwitch@2.0.4: 1745 + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 1510 1746 1511 1747 snapshots: 1512 1748 ··· 1818 2054 1819 2055 '@rolldown/pluginutils@1.0.0-beta.50': {} 1820 2056 2057 + '@solidjs/meta@0.29.4(solid-js@1.9.10)': 2058 + dependencies: 2059 + solid-js: 1.9.10 2060 + 1821 2061 '@solidjs/router@0.15.4(solid-js@1.9.10)': 1822 2062 dependencies: 1823 2063 solid-js: 1.9.10 ··· 1956 2196 '@types/deep-eql': 4.0.2 1957 2197 assertion-error: 2.0.1 1958 2198 2199 + '@types/debug@4.1.12': 2200 + dependencies: 2201 + '@types/ms': 2.1.0 2202 + 1959 2203 '@types/deep-eql@4.0.2': {} 1960 2204 1961 2205 '@types/estree@1.0.8': {} 1962 2206 2207 + '@types/hast@3.0.4': 2208 + dependencies: 2209 + '@types/unist': 3.0.3 2210 + 1963 2211 '@types/json-schema@7.0.15': {} 2212 + 2213 + '@types/mdast@4.0.4': 2214 + dependencies: 2215 + '@types/unist': 3.0.3 2216 + 2217 + '@types/ms@2.1.0': {} 1964 2218 1965 2219 '@types/node@24.10.4': 1966 2220 dependencies: 1967 2221 undici-types: 7.16.0 1968 2222 2223 + '@types/unist@3.0.3': {} 2224 + 1969 2225 '@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': 1970 2226 dependencies: 1971 2227 '@typescript-eslint/scope-manager': 8.50.1 ··· 2028 2284 dependencies: 2029 2285 '@typescript-eslint/types': 8.50.1 2030 2286 eslint-visitor-keys: 4.2.1 2287 + 2288 + '@ungap/structured-clone@1.3.0': {} 2031 2289 2032 2290 '@vitest/expect@4.0.16': 2033 2291 dependencies: ··· 2117 2375 optionalDependencies: 2118 2376 solid-js: 1.9.10 2119 2377 2378 + bail@2.0.2: {} 2379 + 2120 2380 balanced-match@1.0.2: {} 2121 2381 2122 2382 baseline-browser-mapping@2.9.11: {} ··· 2146 2406 2147 2407 caniuse-lite@1.0.30001761: {} 2148 2408 2409 + ccount@2.0.1: {} 2410 + 2149 2411 chai@6.2.2: {} 2150 2412 2151 2413 chalk@4.1.2: ··· 2153 2415 ansi-styles: 4.3.0 2154 2416 supports-color: 7.2.0 2155 2417 2418 + character-entities-html4@2.1.0: {} 2419 + 2420 + character-entities-legacy@3.0.0: {} 2421 + 2422 + character-entities@2.0.2: {} 2423 + 2424 + clsx@2.1.1: {} 2425 + 2156 2426 color-convert@2.0.1: 2157 2427 dependencies: 2158 2428 color-name: 1.1.4 2159 2429 2160 2430 color-name@1.1.4: {} 2431 + 2432 + comma-separated-tokens@2.0.3: {} 2161 2433 2162 2434 concat-map@0.0.1: {} 2163 2435 ··· 2195 2467 2196 2468 decimal.js@10.6.0: {} 2197 2469 2470 + decode-named-character-reference@1.2.0: 2471 + dependencies: 2472 + character-entities: 2.0.2 2473 + 2198 2474 deep-is@0.1.4: {} 2199 2475 2200 2476 dequal@2.0.3: {} 2201 2477 2202 2478 detect-libc@2.1.2: {} 2479 + 2480 + devlop@1.1.0: 2481 + dependencies: 2482 + dequal: 2.0.3 2203 2483 2204 2484 dom-accessibility-api@0.5.16: {} 2205 2485 ··· 2307 2587 2308 2588 expect-type@1.3.0: {} 2309 2589 2590 + extend@3.0.2: {} 2591 + 2310 2592 fast-deep-equal@3.1.3: {} 2311 2593 2312 2594 fast-json-stable-stringify@2.1.0: {} ··· 2350 2632 2351 2633 has-flag@4.0.0: {} 2352 2634 2635 + hast-util-is-element@3.0.0: 2636 + dependencies: 2637 + '@types/hast': 3.0.4 2638 + 2639 + hast-util-sanitize@5.0.2: 2640 + dependencies: 2641 + '@types/hast': 3.0.4 2642 + '@ungap/structured-clone': 1.3.0 2643 + unist-util-position: 5.0.0 2644 + 2645 + hast-util-to-html@9.0.5: 2646 + dependencies: 2647 + '@types/hast': 3.0.4 2648 + '@types/unist': 3.0.3 2649 + ccount: 2.0.1 2650 + comma-separated-tokens: 2.0.3 2651 + hast-util-whitespace: 3.0.0 2652 + html-void-elements: 3.0.0 2653 + mdast-util-to-hast: 13.2.1 2654 + property-information: 7.1.0 2655 + space-separated-tokens: 2.0.2 2656 + stringify-entities: 4.0.4 2657 + zwitch: 2.0.4 2658 + 2659 + hast-util-whitespace@3.0.0: 2660 + dependencies: 2661 + '@types/hast': 3.0.4 2662 + 2353 2663 html-encoding-sniffer@6.0.0: 2354 2664 dependencies: 2355 2665 '@exodus/bytes': 1.6.0 ··· 2360 2670 2361 2671 html-tags@3.3.1: {} 2362 2672 2673 + html-void-elements@3.0.0: {} 2674 + 2363 2675 http-proxy-agent@7.0.2: 2364 2676 dependencies: 2365 2677 agent-base: 7.1.4 ··· 2387 2699 2388 2700 inline-style-parser@0.2.7: {} 2389 2701 2702 + is-absolute-url@4.0.1: {} 2703 + 2390 2704 is-extglob@2.1.1: {} 2391 2705 2392 2706 is-glob@4.0.3: ··· 2396 2710 is-html@2.0.0: 2397 2711 dependencies: 2398 2712 html-tags: 3.3.1 2713 + 2714 + is-plain-obj@4.1.0: {} 2399 2715 2400 2716 is-potential-custom-element-name@1.0.1: {} 2401 2717 ··· 2529 2845 dependencies: 2530 2846 '@jridgewell/sourcemap-codec': 1.5.5 2531 2847 2848 + mdast-util-from-markdown@2.0.2: 2849 + dependencies: 2850 + '@types/mdast': 4.0.4 2851 + '@types/unist': 3.0.3 2852 + decode-named-character-reference: 1.2.0 2853 + devlop: 1.1.0 2854 + mdast-util-to-string: 4.0.0 2855 + micromark: 4.0.2 2856 + micromark-util-decode-numeric-character-reference: 2.0.2 2857 + micromark-util-decode-string: 2.0.1 2858 + micromark-util-normalize-identifier: 2.0.1 2859 + micromark-util-symbol: 2.0.1 2860 + micromark-util-types: 2.0.2 2861 + unist-util-stringify-position: 4.0.0 2862 + transitivePeerDependencies: 2863 + - supports-color 2864 + 2865 + mdast-util-to-hast@13.2.1: 2866 + dependencies: 2867 + '@types/hast': 3.0.4 2868 + '@types/mdast': 4.0.4 2869 + '@ungap/structured-clone': 1.3.0 2870 + devlop: 1.1.0 2871 + micromark-util-sanitize-uri: 2.0.1 2872 + trim-lines: 3.0.1 2873 + unist-util-position: 5.0.0 2874 + unist-util-visit: 5.0.0 2875 + vfile: 6.0.3 2876 + 2877 + mdast-util-to-string@4.0.0: 2878 + dependencies: 2879 + '@types/mdast': 4.0.4 2880 + 2532 2881 mdn-data@2.12.2: {} 2533 2882 2534 2883 merge-anything@5.1.7: 2535 2884 dependencies: 2536 2885 is-what: 4.1.16 2537 2886 2887 + micromark-core-commonmark@2.0.3: 2888 + dependencies: 2889 + decode-named-character-reference: 1.2.0 2890 + devlop: 1.1.0 2891 + micromark-factory-destination: 2.0.1 2892 + micromark-factory-label: 2.0.1 2893 + micromark-factory-space: 2.0.1 2894 + micromark-factory-title: 2.0.1 2895 + micromark-factory-whitespace: 2.0.1 2896 + micromark-util-character: 2.1.1 2897 + micromark-util-chunked: 2.0.1 2898 + micromark-util-classify-character: 2.0.1 2899 + micromark-util-html-tag-name: 2.0.1 2900 + micromark-util-normalize-identifier: 2.0.1 2901 + micromark-util-resolve-all: 2.0.1 2902 + micromark-util-subtokenize: 2.1.0 2903 + micromark-util-symbol: 2.0.1 2904 + micromark-util-types: 2.0.2 2905 + 2906 + micromark-factory-destination@2.0.1: 2907 + dependencies: 2908 + micromark-util-character: 2.1.1 2909 + micromark-util-symbol: 2.0.1 2910 + micromark-util-types: 2.0.2 2911 + 2912 + micromark-factory-label@2.0.1: 2913 + dependencies: 2914 + devlop: 1.1.0 2915 + micromark-util-character: 2.1.1 2916 + micromark-util-symbol: 2.0.1 2917 + micromark-util-types: 2.0.2 2918 + 2919 + micromark-factory-space@2.0.1: 2920 + dependencies: 2921 + micromark-util-character: 2.1.1 2922 + micromark-util-types: 2.0.2 2923 + 2924 + micromark-factory-title@2.0.1: 2925 + dependencies: 2926 + micromark-factory-space: 2.0.1 2927 + micromark-util-character: 2.1.1 2928 + micromark-util-symbol: 2.0.1 2929 + micromark-util-types: 2.0.2 2930 + 2931 + micromark-factory-whitespace@2.0.1: 2932 + dependencies: 2933 + micromark-factory-space: 2.0.1 2934 + micromark-util-character: 2.1.1 2935 + micromark-util-symbol: 2.0.1 2936 + micromark-util-types: 2.0.2 2937 + 2938 + micromark-util-character@2.1.1: 2939 + dependencies: 2940 + micromark-util-symbol: 2.0.1 2941 + micromark-util-types: 2.0.2 2942 + 2943 + micromark-util-chunked@2.0.1: 2944 + dependencies: 2945 + micromark-util-symbol: 2.0.1 2946 + 2947 + micromark-util-classify-character@2.0.1: 2948 + dependencies: 2949 + micromark-util-character: 2.1.1 2950 + micromark-util-symbol: 2.0.1 2951 + micromark-util-types: 2.0.2 2952 + 2953 + micromark-util-combine-extensions@2.0.1: 2954 + dependencies: 2955 + micromark-util-chunked: 2.0.1 2956 + micromark-util-types: 2.0.2 2957 + 2958 + micromark-util-decode-numeric-character-reference@2.0.2: 2959 + dependencies: 2960 + micromark-util-symbol: 2.0.1 2961 + 2962 + micromark-util-decode-string@2.0.1: 2963 + dependencies: 2964 + decode-named-character-reference: 1.2.0 2965 + micromark-util-character: 2.1.1 2966 + micromark-util-decode-numeric-character-reference: 2.0.2 2967 + micromark-util-symbol: 2.0.1 2968 + 2969 + micromark-util-encode@2.0.1: {} 2970 + 2971 + micromark-util-html-tag-name@2.0.1: {} 2972 + 2973 + micromark-util-normalize-identifier@2.0.1: 2974 + dependencies: 2975 + micromark-util-symbol: 2.0.1 2976 + 2977 + micromark-util-resolve-all@2.0.1: 2978 + dependencies: 2979 + micromark-util-types: 2.0.2 2980 + 2981 + micromark-util-sanitize-uri@2.0.1: 2982 + dependencies: 2983 + micromark-util-character: 2.1.1 2984 + micromark-util-encode: 2.0.1 2985 + micromark-util-symbol: 2.0.1 2986 + 2987 + micromark-util-subtokenize@2.1.0: 2988 + dependencies: 2989 + devlop: 1.1.0 2990 + micromark-util-chunked: 2.0.1 2991 + micromark-util-symbol: 2.0.1 2992 + micromark-util-types: 2.0.2 2993 + 2994 + micromark-util-symbol@2.0.1: {} 2995 + 2996 + micromark-util-types@2.0.2: {} 2997 + 2998 + micromark@4.0.2: 2999 + dependencies: 3000 + '@types/debug': 4.1.12 3001 + debug: 4.4.3 3002 + decode-named-character-reference: 1.2.0 3003 + devlop: 1.1.0 3004 + micromark-core-commonmark: 2.0.3 3005 + micromark-factory-space: 2.0.1 3006 + micromark-util-character: 2.1.1 3007 + micromark-util-chunked: 2.0.1 3008 + micromark-util-combine-extensions: 2.0.1 3009 + micromark-util-decode-numeric-character-reference: 2.0.2 3010 + micromark-util-encode: 2.0.1 3011 + micromark-util-normalize-identifier: 2.0.1 3012 + micromark-util-resolve-all: 2.0.1 3013 + micromark-util-sanitize-uri: 2.0.1 3014 + micromark-util-subtokenize: 2.1.0 3015 + micromark-util-symbol: 2.0.1 3016 + micromark-util-types: 2.0.2 3017 + transitivePeerDependencies: 3018 + - supports-color 3019 + 2538 3020 min-indent@1.0.1: {} 2539 3021 2540 3022 minimatch@3.1.2: ··· 2608 3090 ansi-styles: 5.2.0 2609 3091 react-is: 17.0.2 2610 3092 3093 + property-information@7.1.0: {} 3094 + 2611 3095 punycode@2.3.1: {} 2612 3096 2613 3097 react-is@17.0.2: {} ··· 2617 3101 indent-string: 4.0.0 2618 3102 strip-indent: 3.0.0 2619 3103 3104 + rehype-external-links@3.0.0: 3105 + dependencies: 3106 + '@types/hast': 3.0.4 3107 + '@ungap/structured-clone': 1.3.0 3108 + hast-util-is-element: 3.0.0 3109 + is-absolute-url: 4.0.1 3110 + space-separated-tokens: 2.0.2 3111 + unist-util-visit: 5.0.0 3112 + 3113 + rehype-sanitize@6.0.0: 3114 + dependencies: 3115 + '@types/hast': 3.0.4 3116 + hast-util-sanitize: 5.0.2 3117 + 3118 + rehype-stringify@10.0.1: 3119 + dependencies: 3120 + '@types/hast': 3.0.4 3121 + hast-util-to-html: 9.0.5 3122 + unified: 11.0.5 3123 + 3124 + remark-parse@11.0.0: 3125 + dependencies: 3126 + '@types/mdast': 4.0.4 3127 + mdast-util-from-markdown: 2.0.2 3128 + micromark-util-types: 2.0.2 3129 + unified: 11.0.5 3130 + transitivePeerDependencies: 3131 + - supports-color 3132 + 3133 + remark-rehype@11.1.2: 3134 + dependencies: 3135 + '@types/hast': 3.0.4 3136 + '@types/mdast': 4.0.4 3137 + mdast-util-to-hast: 13.2.1 3138 + unified: 11.0.5 3139 + vfile: 6.0.3 3140 + 2620 3141 require-from-string@2.0.2: {} 2621 3142 2622 3143 resolve-from@4.0.0: {} ··· 2694 3215 2695 3216 source-map-js@1.2.1: {} 2696 3217 3218 + space-separated-tokens@2.0.2: {} 3219 + 2697 3220 stackback@0.0.2: {} 2698 3221 2699 3222 std-env@3.10.0: {} 2700 3223 3224 + stringify-entities@4.0.4: 3225 + dependencies: 3226 + character-entities-html4: 2.1.0 3227 + character-entities-legacy: 3.0.0 3228 + 2701 3229 strip-indent@3.0.0: 2702 3230 dependencies: 2703 3231 min-indent: 1.0.1 ··· 2713 3241 has-flag: 4.0.0 2714 3242 2715 3243 symbol-tree@3.2.4: {} 3244 + 3245 + tailwind-merge@3.4.0: {} 2716 3246 2717 3247 tailwindcss@4.1.18: {} 2718 3248 ··· 2742 3272 tr46@6.0.0: 2743 3273 dependencies: 2744 3274 punycode: 2.3.1 3275 + 3276 + trim-lines@3.0.1: {} 3277 + 3278 + trough@2.2.0: {} 2745 3279 2746 3280 ts-api-utils@2.2.0(typescript@5.9.3): 2747 3281 dependencies: ··· 2758 3292 2759 3293 undici-types@7.16.0: {} 2760 3294 3295 + unified@11.0.5: 3296 + dependencies: 3297 + '@types/unist': 3.0.3 3298 + bail: 2.0.2 3299 + devlop: 1.1.0 3300 + extend: 3.0.2 3301 + is-plain-obj: 4.1.0 3302 + trough: 2.2.0 3303 + vfile: 6.0.3 3304 + 3305 + unist-util-is@6.0.1: 3306 + dependencies: 3307 + '@types/unist': 3.0.3 3308 + 3309 + unist-util-position@5.0.0: 3310 + dependencies: 3311 + '@types/unist': 3.0.3 3312 + 3313 + unist-util-stringify-position@4.0.0: 3314 + dependencies: 3315 + '@types/unist': 3.0.3 3316 + 3317 + unist-util-visit-parents@6.0.2: 3318 + dependencies: 3319 + '@types/unist': 3.0.3 3320 + unist-util-is: 6.0.1 3321 + 3322 + unist-util-visit@5.0.0: 3323 + dependencies: 3324 + '@types/unist': 3.0.3 3325 + unist-util-is: 6.0.1 3326 + unist-util-visit-parents: 6.0.2 3327 + 2761 3328 update-browserslist-db@1.2.3(browserslist@4.28.1): 2762 3329 dependencies: 2763 3330 browserslist: 4.28.1 ··· 2767 3334 uri-js@4.4.1: 2768 3335 dependencies: 2769 3336 punycode: 2.3.1 3337 + 3338 + vfile-message@4.0.3: 3339 + dependencies: 3340 + '@types/unist': 3.0.3 3341 + unist-util-stringify-position: 4.0.0 3342 + 3343 + vfile@6.0.3: 3344 + dependencies: 3345 + '@types/unist': 3.0.3 3346 + vfile-message: 4.0.3 2770 3347 2771 3348 vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(rolldown-vite@7.2.5(@types/node@24.10.4)(jiti@2.6.1))(solid-js@1.9.10): 2772 3349 dependencies: ··· 2858 3435 yallist@3.1.1: {} 2859 3436 2860 3437 yocto-queue@0.1.0: {} 3438 + 3439 + zwitch@2.0.4: {}
+14 -6
web/src/App.tsx
··· 5 5 import { authStore } from "./lib/store"; 6 6 import DeckNew from "./pages/DeckNew"; 7 7 import Home from "./pages/Home"; 8 + import Import from "./pages/Import"; 8 9 import Landing from "./pages/Landing"; 9 10 import Login from "./pages/Login"; 11 + import NoteNew from "./pages/NoteNew"; 10 12 11 - const Root: Component = () => { 13 + const ProtectedRoute: Component<{ component: Component }> = (props) => { 12 14 return ( 13 15 <Show when={authStore.isAuthenticated()} fallback={<Landing />}> 14 16 <AppLayout> 15 - <Router> 16 - <Route path="/" component={Home} /> 17 - <Route path="/decks/new" component={DeckNew} /> 18 - </Router> 17 + <props.component /> 19 18 </AppLayout> 20 19 </Show> 21 20 ); ··· 24 23 const App: Component = () => { 25 24 return ( 26 25 <Router> 27 - <Route path="/" component={Root} /> 28 26 <Route path="/login" component={Login} /> 27 + 28 + {/* Protected Routes */} 29 + <Route path="/" component={() => <ProtectedRoute component={Home} />} /> 30 + <Route path="/decks/new" component={() => <ProtectedRoute component={DeckNew} />} /> 31 + <Route path="/notes/new" component={() => <ProtectedRoute component={NoteNew} />} /> 32 + <Route path="/import" component={() => <ProtectedRoute component={Import} />} /> 33 + 34 + { 35 + /* TODO: Catch-all or 404 */ 36 + } 29 37 </Router> 30 38 ); 31 39 };
+69
web/src/components/CardEditor.tsx
··· 1 + import { createSignal, Show } from "solid-js"; 2 + import { Button } from "./ui/Button"; 3 + 4 + type CardEditorProps = { 5 + front?: string; 6 + back?: string; 7 + mediaUrl?: string; 8 + onSave: (data: { front: string; back: string; mediaUrl?: string }) => void; 9 + onCancel?: () => void; 10 + }; 11 + 12 + export function CardEditor(props: CardEditorProps) { 13 + const [front, setFront] = createSignal(props.front || ""); 14 + const [back, setBack] = createSignal(props.back || ""); 15 + const [mediaUrl, setMediaUrl] = createSignal(props.mediaUrl || ""); 16 + 17 + const handleSubmit = (e: Event) => { 18 + e.preventDefault(); 19 + props.onSave({ front: front(), back: back(), mediaUrl: mediaUrl() || undefined }); 20 + if (!props.front) { 21 + setFront(""); 22 + setBack(""); 23 + setMediaUrl(""); 24 + } 25 + }; 26 + 27 + return ( 28 + <form onSubmit={handleSubmit} class="space-y-4 p-4 border border-gray-800 rounded bg-gray-900/50"> 29 + <div> 30 + <label class="block text-sm font-medium text-gray-400 mb-1">Front</label> 31 + <textarea 32 + value={front()} 33 + onInput={(e) => setFront(e.target.value)} 34 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" 35 + placeholder="Front of card..." 36 + rows={2} 37 + required /> 38 + </div> 39 + 40 + <div> 41 + <label class="block text-sm font-medium text-gray-400 mb-1">Back</label> 42 + <textarea 43 + value={back()} 44 + onInput={(e) => setBack(e.target.value)} 45 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" 46 + placeholder="Back of card..." 47 + rows={3} 48 + required /> 49 + </div> 50 + 51 + <div> 52 + <label class="block text-sm font-medium text-gray-400 mb-1">Media URL (Optional)</label> 53 + <input 54 + type="url" 55 + value={mediaUrl()} 56 + onInput={(e) => setMediaUrl(e.target.value)} 57 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" 58 + placeholder="https://..." /> 59 + </div> 60 + 61 + <div class="flex justify-end gap-2"> 62 + <Show when={props.onCancel}> 63 + <Button type="button" variant="ghost" onClick={props.onCancel}>Cancel</Button> 64 + </Show> 65 + <Button type="submit">{props.front ? "Update Card" : "Add Card"}</Button> 66 + </div> 67 + </form> 68 + ); 69 + }
+5 -2
web/src/components/DeckEditor.test.tsx
··· 35 35 36 36 expect(api.post).toHaveBeenCalledWith( 37 37 "/decks", 38 - expect.objectContaining({ title: "My Deck", visibility: "Private" }), 38 + expect.objectContaining({ title: "My Deck", visibility: { type: "Private" } }), 39 39 ); 40 40 }); 41 41 ··· 55 55 56 56 expect(api.post).toHaveBeenCalledWith( 57 57 "/decks", 58 - expect.objectContaining({ title: "Shared Deck", visibility: { SharedWith: ["did:plc:123", "did:plc:456"] } }), 58 + expect.objectContaining({ 59 + title: "Shared Deck", 60 + visibility: { type: "SharedWith", content: ["did:plc:123", "did:plc:456"] }, 61 + }), 59 62 ); 60 63 }); 61 64 });
+93 -54
web/src/components/DeckEditor.tsx
··· 1 - import { createSignal, Show } from "solid-js"; 1 + import { createSignal, For, Show } from "solid-js"; 2 2 import { api } from "../lib/api"; 3 3 import type { Visibility } from "../lib/store"; 4 4 import { toast } from "../lib/toast"; 5 + import { CardEditor } from "./CardEditor"; 6 + import { Button } from "./ui/Button"; 5 7 6 8 export function DeckEditor(props: { onSave?: (data: any) => void }) { 7 9 const [title, setTitle] = createSignal(""); ··· 9 11 const [visibilityType, setVisibilityType] = createSignal<string>("Private"); 10 12 const [sharedWith, setSharedWith] = createSignal(""); 11 13 14 + const [cards, setCards] = createSignal<any[]>([]); 15 + const [showCardEditor, setShowCardEditor] = createSignal(false); 16 + 12 17 const handleSubmit = async (e: Event) => { 13 18 e.preventDefault(); 14 19 15 20 let visibility: Visibility; 16 21 if (visibilityType() === "SharedWith") { 17 - visibility = { SharedWith: sharedWith().split(",").map(s => s.trim()).filter(s => s) }; 22 + visibility = { type: "SharedWith", content: sharedWith().split(",").map(s => s.trim()).filter(s => s) }; 18 23 } else { 19 - visibility = visibilityType() as Visibility; 24 + visibility = { type: visibilityType() as "Private" | "Unlisted" | "Public" }; 20 25 } 21 26 22 - const payload = { title: title(), description: description(), tags: [], visibility }; 27 + const payload = { title: title(), description: description(), tags: [], visibility, cards: cards() }; 23 28 24 29 if (props.onSave) { 25 30 props.onSave(payload); ··· 27 32 } 28 33 29 34 try { 30 - await api.post("/decks", payload); 35 + const _res = await api.post("/decks", payload); 31 36 toast.success("Deck created!"); 32 37 } catch { 33 38 toast.error("Failed to create deck"); 34 39 } 35 40 }; 36 41 42 + const addCard = (cardData: any) => { 43 + setCards([...cards(), cardData]); 44 + setShowCardEditor(false); 45 + }; 46 + 37 47 return ( 38 - <form onSubmit={handleSubmit} class="space-y-4 max-w-md mx-auto p-4 border rounded"> 39 - <div> 40 - <label for="title" class="block text-sm font-medium">Title</label> 41 - <input 42 - id="title" 43 - type="text" 44 - value={title()} 45 - onInput={(e) => setTitle(e.target.value)} 46 - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2" 47 - required /> 48 - </div> 48 + <div class="space-y-8"> 49 + <form 50 + onSubmit={handleSubmit} 51 + class="space-y-4 max-w-3xl mx-auto p-6 border border-gray-800 rounded bg-gray-900/40"> 52 + <div class="grid grid-cols-1 gap-6"> 53 + <div> 54 + <label for="title" class="block text-sm font-medium text-gray-400 mb-1">Title</label> 55 + <input 56 + id="title" 57 + type="text" 58 + value={title()} 59 + onInput={(e) => setTitle(e.target.value)} 60 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" 61 + required /> 62 + </div> 63 + 64 + <div> 65 + <label for="description" class="block text-sm font-medium text-gray-400 mb-1">Description</label> 66 + <textarea 67 + id="description" 68 + value={description()} 69 + onInput={(e) => setDescription(e.target.value)} 70 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" /> 71 + </div> 72 + 73 + <div> 74 + <label for="visibility" class="block text-sm font-medium text-gray-400 mb-1">Visibility</label> 75 + <select 76 + id="visibility" 77 + value={visibilityType()} 78 + onChange={(e) => setVisibilityType(e.target.value)} 79 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" 80 + aria-label="Visibility"> 81 + <option value="Private">Private</option> 82 + <option value="Unlisted">Unlisted</option> 83 + <option value="Public">Public</option> 84 + <option value="SharedWith">Shared With...</option> 85 + </select> 86 + </div> 87 + 88 + <Show when={visibilityType() === "SharedWith"}> 89 + <div> 90 + <label class="block text-sm font-medium text-gray-400 mb-1">Share with DIDs (comma separated)</label> 91 + <input 92 + type="text" 93 + value={sharedWith()} 94 + onInput={(e) => setSharedWith(e.target.value)} 95 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" 96 + placeholder="did:plc:..., did:plc:..." /> 97 + </div> 98 + </Show> 99 + </div> 49 100 50 - <div> 51 - <label for="description" class="block text-sm font-medium">Description</label> 52 - <textarea 53 - id="description" 54 - value={description()} 55 - onInput={(e) => setDescription(e.target.value)} 56 - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2" /> 57 - </div> 101 + <div class="pt-4 border-t border-gray-800"> 102 + <h3 class="text-lg font-medium text-white mb-4">Cards ({cards().length})</h3> 58 103 59 - <div> 60 - <label for="visibility" class="block text-sm font-medium">Visibility</label> 61 - <select 62 - id="visibility" 63 - value={visibilityType()} 64 - onChange={(e) => setVisibilityType(e.target.value)} 65 - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2" 66 - aria-label="Visibility"> 67 - <option value="Private">Private</option> 68 - <option value="Unlisted">Unlisted</option> 69 - <option value="Public">Public</option> 70 - <option value="SharedWith">Shared With...</option> 71 - </select> 72 - </div> 104 + <div class="space-y-4 mb-4"> 105 + <For each={cards()}> 106 + {(card, i) => ( 107 + <div class="p-4 border border-gray-800 rounded bg-gray-900 flex justify-between items-center"> 108 + <div class="truncate pr-4 font-mono text-sm text-gray-300">{card.front}</div> 109 + <div class="text-gray-500 text-xs">Card {i() + 1}</div> 110 + </div> 111 + )} 112 + </For> 113 + </div> 73 114 74 - <Show when={visibilityType() === "SharedWith"}> 75 - <div> 76 - <label class="block text-sm font-medium">Share with DIDs (comma separated)</label> 77 - <input 78 - type="text" 79 - value={sharedWith()} 80 - onInput={(e) => setSharedWith(e.target.value)} 81 - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2" 82 - placeholder="did:plc:..., did:plc:..." /> 115 + <Show 116 + when={showCardEditor()} 117 + fallback={ 118 + <Button type="button" variant="secondary" onClick={() => setShowCardEditor(true)} class="w-full"> 119 + Add Card 120 + </Button> 121 + }> 122 + <CardEditor onSave={addCard} onCancel={() => setShowCardEditor(false)} /> 123 + </Show> 83 124 </div> 84 - </Show> 85 125 86 - <button 87 - type="submit" 88 - class="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none"> 89 - Create Deck 90 - </button> 91 - </form> 126 + <div class="pt-6 flex justify-end"> 127 + <Button type="submit" size="lg">Create Deck</Button> 128 + </div> 129 + </form> 130 + </div> 92 131 ); 93 132 }
+121
web/src/components/NoteEditor.tsx
··· 1 + import rehypeExternalLinks from "rehype-external-links"; 2 + import rehypeSanitize from "rehype-sanitize"; 3 + import rehypeStringify from "rehype-stringify"; 4 + import remarkParse from "remark-parse"; 5 + import remarkRehype from "remark-rehype"; 6 + import { createEffect, createSignal } from "solid-js"; 7 + import { unified } from "unified"; 8 + import { api } from "../lib/api"; 9 + import { toast } from "../lib/toast"; 10 + import { Button } from "./ui/Button"; 11 + 12 + interface NoteEditorProps { 13 + noteId?: string; // If editing existing 14 + initialTitle?: string; 15 + initialContent?: string; 16 + } 17 + 18 + export function NoteEditor(props: NoteEditorProps) { 19 + const [title, setTitle] = createSignal(props.initialTitle || ""); 20 + const [content, setContent] = createSignal(props.initialContent || ""); 21 + const [preview, setPreview] = createSignal(""); 22 + const [tags, setTags] = createSignal(""); // Comma sep 23 + const [visibility, setVisibility] = createSignal("Private"); 24 + 25 + const processor = unified().use(remarkParse) // .use(remarkWikiLink) // Would need a plugin for wikilinks -> links 26 + .use(remarkRehype).use(rehypeSanitize) // Safety first 27 + .use(rehypeExternalLinks, { target: "_blank", rel: ["nofollow"] }).use(rehypeStringify); 28 + 29 + createEffect(async () => { 30 + try { 31 + const file = await processor.process(content()); 32 + setPreview(String(file)); 33 + } catch (e) { 34 + console.error(e); 35 + } 36 + }); 37 + 38 + const handleSubmit = async (e: Event) => { 39 + e.preventDefault(); 40 + try { 41 + const payload = { 42 + title: title(), 43 + body: content(), 44 + tags: tags().split(",").map(t => t.trim()).filter(t => t), 45 + visibility: { type: visibility() as "Private" | "Public" }, 46 + }; 47 + 48 + await api.post("/notes", payload); 49 + toast.success("Note saved!"); 50 + if (!props.noteId) { 51 + setTitle(""); 52 + setContent(""); 53 + setTags(""); 54 + } 55 + } catch (e) { 56 + console.error(e); 57 + toast.error("Failed to save note"); 58 + } 59 + }; 60 + 61 + return ( 62 + <div class="max-w-4xl mx-auto p-6 grid grid-cols-1 md:grid-cols-2 gap-6"> 63 + <div class="space-y-4"> 64 + <h1 class="text-2xl font-bold text-white">Note Editor</h1> 65 + 66 + <form onSubmit={handleSubmit} class="space-y-4"> 67 + <div> 68 + <label class="block text-sm font-medium text-gray-400 mb-1">Title</label> 69 + <input 70 + type="text" 71 + value={title()} 72 + onInput={e => setTitle(e.target.value)} 73 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2" 74 + placeholder="Note Title" 75 + required /> 76 + </div> 77 + 78 + <div> 79 + <label class="block text-sm font-medium text-gray-400 mb-1">Content (Markdown + [[WikiLinks]])</label> 80 + <textarea 81 + value={content()} 82 + onInput={e => setContent(e.target.value)} 83 + class="w-full h-96 bg-gray-800 border-gray-700 text-white rounded p-2 font-mono text-sm leading-relaxed" 84 + placeholder="# Heading&#10;&#10;Write your thoughts... Link to other notes with [[Title]]" /> 85 + </div> 86 + 87 + <div class="grid grid-cols-2 gap-4"> 88 + <div> 89 + <label class="block text-sm font-medium text-gray-400 mb-1">Tags</label> 90 + <input 91 + type="text" 92 + value={tags()} 93 + onInput={e => setTags(e.target.value)} 94 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2" 95 + placeholder="rust, learning, ..." /> 96 + </div> 97 + <div> 98 + <label class="block text-sm font-medium text-gray-400 mb-1">Visibility</label> 99 + <select 100 + value={visibility()} 101 + onChange={e => setVisibility(e.target.value)} 102 + class="w-full bg-gray-800 border-gray-700 text-white rounded p-2"> 103 + <option value="Private">Private</option> 104 + <option value="Public">Public</option> 105 + </select> 106 + </div> 107 + </div> 108 + 109 + <Button type="submit">Save Note</Button> 110 + </form> 111 + </div> 112 + 113 + <div class="space-y-4"> 114 + <h2 class="text-xl font-semibold text-gray-300">Preview</h2> 115 + <div 116 + class="prose prose-invert max-w-none bg-gray-900/50 p-6 rounded border border-gray-800 min-h-[500px]" 117 + innerHTML={preview()} /> 118 + </div> 119 + </div> 120 + ); 121 + }
+1 -1
web/src/components/layout/AppLayout.tsx
··· 7 7 export const AppLayout: Component<AppLayoutProps> = (props) => { 8 8 return ( 9 9 <div class="min-h-screen bg-[#161616] text-[#F4F4F4] font-sans selection:bg-[#0F62FE]/30"> 10 - <Toaster /> 11 10 <Header /> 12 11 <main class="container mx-auto px-4 py-8 md:px-6 lg:px-8 max-w-7xl">{props.children}</main> 12 + <Toaster /> 13 13 </div> 14 14 ); 15 15 };
+1 -1
web/src/lib/api.ts
··· 1 1 import { authStore } from "./store"; 2 2 3 - const API_BASE = "http://localhost:8080/api"; 3 + const API_BASE = "/api"; 4 4 5 5 export async function apiFetch(path: string, options: RequestInit = {}) { 6 6 const token = authStore.accessJwt();
+4 -1
web/src/lib/store.ts
··· 41 41 42 42 export const authStore = createRoot(createAuthStore); 43 43 44 - export type Visibility = "Private" | "Unlisted" | "Public" | { SharedWith: string[] }; 44 + export type Visibility = { type: "Private" } | { type: "Unlisted" } | { type: "Public" } | { 45 + type: "SharedWith"; 46 + content: string[]; 47 + }; 45 48 46 49 export type Deck = { 47 50 id: string;
+13 -1
web/src/pages/DeckNew.tsx
··· 9 9 10 10 const handleSave = async (data: any) => { 11 11 try { 12 - const res = await api.post("/decks", data); 12 + const { cards, ...deckPayload } = data; 13 + const res = await api.post("/decks", deckPayload); 14 + 13 15 if (res.ok) { 14 16 const deck = await res.json(); 17 + 18 + if (cards && cards.length > 0) { 19 + await Promise.all( 20 + cards.map((c: any) => 21 + api.post("/cards", { deck_id: deck.id, front: c.front, back: c.back, media_url: c.mediaUrl }) 22 + ), 23 + ); 24 + } 25 + 15 26 toast.success("Deck created successfully"); 16 27 navigate(`/decks/${deck.id}`); 17 28 } else { ··· 19 30 toast.error(err.error || "Failed to create deck"); 20 31 } 21 32 } catch (e) { 33 + console.error(e); 22 34 toast.error("Network error"); 23 35 } 24 36 };
+4 -5
web/src/pages/Home.tsx
··· 2 2 import type { Component } from "solid-js"; 3 3 import { createResource, For, Show } from "solid-js"; 4 4 import { api } from "../lib/api"; 5 - 6 - type DeckVisibility = "Private" | "Unlisted" | "Public"; 5 + import type { Visibility } from "../lib/store"; 7 6 8 7 type Deck = { 9 8 id: string; 10 9 title: string; 11 10 description: string; 12 11 tags: string[]; 13 - visibility: DeckVisibility; 12 + visibility: Visibility; 14 13 owner_did: string; 15 14 }; 16 15 ··· 27 26 <h3 class="text-lg font-normal text-[#F4F4F4] group-hover:text-[#0F62FE] transition-colors line-clamp-1"> 28 27 {props.deck.title} 29 28 </h3> 30 - <Show when={props.deck.visibility !== "Public"}> 29 + <Show when={props.deck.visibility.type !== "Public"}> 31 30 <span class="text-[10px] uppercase font-bold tracking-widest px-2 py-0.5 bg-[#393939] text-[#C6C6C6]"> 32 - {props.deck.visibility} 31 + {props.deck.visibility.type} 33 32 </span> 34 33 </Show> 35 34 </div>
+61
web/src/pages/Import.tsx
··· 1 + import { createSignal, Show } from "solid-js"; 2 + import { NoteEditor } from "../components/NoteEditor"; 3 + import { Button } from "../components/ui/Button"; 4 + import { api } from "../lib/api"; 5 + import { toast } from "../lib/toast"; 6 + 7 + export default function Import() { 8 + const [url, setUrl] = createSignal(""); 9 + const [loading, setLoading] = createSignal(false); 10 + const [importedData, setImportedData] = createSignal<{ title: string; content: string } | null>(null); 11 + 12 + const handleImport = async (e: Event) => { 13 + e.preventDefault(); 14 + setLoading(true); 15 + setImportedData(null); 16 + try { 17 + const res = await api.post("/import/article", { url: url() }); 18 + if (res.ok) { 19 + const data = await res.json(); 20 + const content = `Source: [${data.title}](${data.url})\n\n${data.text}`; 21 + setImportedData({ title: data.title, content }); 22 + toast.success("Article imported!"); 23 + } else { 24 + const err = await res.json(); 25 + toast.error(err.error || "Failed to import"); 26 + } 27 + } catch (e) { 28 + console.error(e); 29 + toast.error("Network error"); 30 + } finally { 31 + setLoading(false); 32 + } 33 + }; 34 + 35 + return ( 36 + <div class="max-w-4xl mx-auto space-y-8"> 37 + <div class="space-y-2"> 38 + <h1 class="text-3xl font-light text-[#F4F4F4]">Import Article</h1> 39 + <p class="text-[#C6C6C6]">Extract content from web pages to create notes.</p> 40 + </div> 41 + 42 + <form onSubmit={handleImport} class="flex gap-4"> 43 + <input 44 + type="url" 45 + value={url()} 46 + onInput={(e) => setUrl(e.target.value)} 47 + class="flex-1 bg-gray-800 border-gray-700 text-white rounded p-2 focus:ring-blue-500 focus:border-blue-500" 48 + placeholder="https://example.com/article" 49 + required /> 50 + <Button type="submit" disabled={loading()}>{loading() ? "Importing..." : "Import"}</Button> 51 + </form> 52 + 53 + <Show when={importedData()}> 54 + <div class="pt-8 border-t border-gray-800"> 55 + <h2 class="text-xl font-semibold text-white mb-4">Create Note from Import</h2> 56 + <NoteEditor initialTitle={importedData()?.title} initialContent={importedData()?.content} /> 57 + </div> 58 + </Show> 59 + </div> 60 + ); 61 + }
+12
web/src/pages/NoteNew.tsx
··· 1 + import { useNavigate } from "@solidjs/router"; 2 + import { NoteEditor } from "../components/NoteEditor"; 3 + 4 + const NoteNew = () => { 5 + return ( 6 + <div class="max-w-4xl mx-auto"> 7 + <NoteEditor /> 8 + </div> 9 + ); 10 + }; 11 + 12 + export default NoteNew;
+1
web/vite.config.ts
··· 4 4 5 5 export default defineConfig({ 6 6 plugins: [solid(), tailwindcss()], 7 + server: { proxy: { "/api": { target: "http://localhost:8080", changeOrigin: true } } }, 7 8 test: { 8 9 environment: "jsdom", 9 10 ui: false,