Fork of atp.tools as a universal profile for people on the ATmosphere

Font switcher

Natalie B. 9dbaef7d 8e651470

+2
package.json
··· 14 14 "@atcute/client": "^2.0.7", 15 15 "@atcute/oauth-browser-client": "^1.0.9", 16 16 "@radix-ui/react-dialog": "^1.1.4", 17 + "@radix-ui/react-popover": "^1.1.6", 17 18 "@radix-ui/react-progress": "^1.1.1", 18 19 "@radix-ui/react-separator": "^1.1.1", 19 20 "@radix-ui/react-slot": "^1.1.1", ··· 26 27 "lexicons": "link:@atcute/bluesky/lexicons", 27 28 "lucide-react": "^0.469.0", 28 29 "preact": "^10.25.4", 30 + "recharts": "^2.15.1", 29 31 "simple-icons": "^13.21.0", 30 32 "tailwind-merge": "^2.6.0", 31 33 "tailwindcss-animate": "^1.0.7"
+486
pnpm-lock.yaml
··· 23 23 '@radix-ui/react-dialog': 24 24 specifier: ^1.1.4 25 25 version: 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 26 + '@radix-ui/react-popover': 27 + specifier: ^1.1.6 28 + version: 1.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 26 29 '@radix-ui/react-progress': 27 30 specifier: ^1.1.1 28 31 version: 1.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 59 62 preact: 60 63 specifier: ^10.25.4 61 64 version: 10.25.4 65 + recharts: 66 + specifier: ^2.15.1 67 + version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 62 68 simple-icons: 63 69 specifier: ^13.21.0 64 70 version: 13.21.0 ··· 206 212 engines: {node: '>=6.9.0'} 207 213 peerDependencies: 208 214 '@babel/core': ^7.0.0-0 215 + 216 + '@babel/runtime@7.26.7': 217 + resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==} 218 + engines: {node: '>=6.9.0'} 209 219 210 220 '@babel/template@7.25.9': 211 221 resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} ··· 605 615 '@types/react-dom': 606 616 optional: true 607 617 618 + '@radix-ui/react-arrow@1.1.2': 619 + resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} 620 + peerDependencies: 621 + '@types/react': '*' 622 + '@types/react-dom': '*' 623 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 624 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 625 + peerDependenciesMeta: 626 + '@types/react': 627 + optional: true 628 + '@types/react-dom': 629 + optional: true 630 + 608 631 '@radix-ui/react-collection@1.1.1': 609 632 resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} 610 633 peerDependencies: ··· 660 683 661 684 '@radix-ui/react-dismissable-layer@1.1.3': 662 685 resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} 686 + peerDependencies: 687 + '@types/react': '*' 688 + '@types/react-dom': '*' 689 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 690 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 691 + peerDependenciesMeta: 692 + '@types/react': 693 + optional: true 694 + '@types/react-dom': 695 + optional: true 696 + 697 + '@radix-ui/react-dismissable-layer@1.1.5': 698 + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} 663 699 peerDependencies: 664 700 '@types/react': '*' 665 701 '@types/react-dom': '*' ··· 693 729 '@types/react-dom': 694 730 optional: true 695 731 732 + '@radix-ui/react-focus-scope@1.1.2': 733 + resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} 734 + peerDependencies: 735 + '@types/react': '*' 736 + '@types/react-dom': '*' 737 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 738 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 739 + peerDependenciesMeta: 740 + '@types/react': 741 + optional: true 742 + '@types/react-dom': 743 + optional: true 744 + 696 745 '@radix-ui/react-id@1.1.0': 697 746 resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} 698 747 peerDependencies: ··· 702 751 '@types/react': 703 752 optional: true 704 753 754 + '@radix-ui/react-popover@1.1.6': 755 + resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==} 756 + peerDependencies: 757 + '@types/react': '*' 758 + '@types/react-dom': '*' 759 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 760 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 761 + peerDependenciesMeta: 762 + '@types/react': 763 + optional: true 764 + '@types/react-dom': 765 + optional: true 766 + 705 767 '@radix-ui/react-popper@1.2.1': 706 768 resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} 707 769 peerDependencies: ··· 715 777 '@types/react-dom': 716 778 optional: true 717 779 780 + '@radix-ui/react-popper@1.2.2': 781 + resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} 782 + peerDependencies: 783 + '@types/react': '*' 784 + '@types/react-dom': '*' 785 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 786 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 787 + peerDependenciesMeta: 788 + '@types/react': 789 + optional: true 790 + '@types/react-dom': 791 + optional: true 792 + 718 793 '@radix-ui/react-portal@1.1.3': 719 794 resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} 720 795 peerDependencies: ··· 728 803 '@types/react-dom': 729 804 optional: true 730 805 806 + '@radix-ui/react-portal@1.1.4': 807 + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} 808 + peerDependencies: 809 + '@types/react': '*' 810 + '@types/react-dom': '*' 811 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 812 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 813 + peerDependenciesMeta: 814 + '@types/react': 815 + optional: true 816 + '@types/react-dom': 817 + optional: true 818 + 731 819 '@radix-ui/react-presence@1.1.2': 732 820 resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} 733 821 peerDependencies: ··· 743 831 744 832 '@radix-ui/react-primitive@2.0.1': 745 833 resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} 834 + peerDependencies: 835 + '@types/react': '*' 836 + '@types/react-dom': '*' 837 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 838 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 839 + peerDependenciesMeta: 840 + '@types/react': 841 + optional: true 842 + '@types/react-dom': 843 + optional: true 844 + 845 + '@radix-ui/react-primitive@2.0.2': 846 + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} 746 847 peerDependencies: 747 848 '@types/react': '*' 748 849 '@types/react-dom': '*' ··· 795 896 796 897 '@radix-ui/react-slot@1.1.1': 797 898 resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} 899 + peerDependencies: 900 + '@types/react': '*' 901 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 902 + peerDependenciesMeta: 903 + '@types/react': 904 + optional: true 905 + 906 + '@radix-ui/react-slot@1.1.2': 907 + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} 798 908 peerDependencies: 799 909 '@types/react': '*' 800 910 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc ··· 1143 1253 '@types/babel__traverse@7.20.6': 1144 1254 resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} 1145 1255 1256 + '@types/d3-array@3.2.1': 1257 + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} 1258 + 1259 + '@types/d3-color@3.1.3': 1260 + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} 1261 + 1262 + '@types/d3-ease@3.0.2': 1263 + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} 1264 + 1265 + '@types/d3-interpolate@3.0.4': 1266 + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} 1267 + 1268 + '@types/d3-path@3.1.1': 1269 + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} 1270 + 1271 + '@types/d3-scale@4.0.9': 1272 + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} 1273 + 1274 + '@types/d3-shape@3.1.7': 1275 + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} 1276 + 1277 + '@types/d3-time@3.0.4': 1278 + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} 1279 + 1280 + '@types/d3-timer@3.0.2': 1281 + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} 1282 + 1146 1283 '@types/diff@6.0.0': 1147 1284 resolution: {integrity: sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==} 1148 1285 ··· 1279 1416 csstype@3.1.3: 1280 1417 resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 1281 1418 1419 + d3-array@3.2.4: 1420 + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} 1421 + engines: {node: '>=12'} 1422 + 1423 + d3-color@3.1.0: 1424 + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} 1425 + engines: {node: '>=12'} 1426 + 1427 + d3-ease@3.0.1: 1428 + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} 1429 + engines: {node: '>=12'} 1430 + 1431 + d3-format@3.1.0: 1432 + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} 1433 + engines: {node: '>=12'} 1434 + 1435 + d3-interpolate@3.0.1: 1436 + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} 1437 + engines: {node: '>=12'} 1438 + 1439 + d3-path@3.1.0: 1440 + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} 1441 + engines: {node: '>=12'} 1442 + 1443 + d3-scale@4.0.2: 1444 + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} 1445 + engines: {node: '>=12'} 1446 + 1447 + d3-shape@3.2.0: 1448 + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} 1449 + engines: {node: '>=12'} 1450 + 1451 + d3-time-format@4.1.0: 1452 + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} 1453 + engines: {node: '>=12'} 1454 + 1455 + d3-time@3.1.0: 1456 + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} 1457 + engines: {node: '>=12'} 1458 + 1459 + d3-timer@3.0.1: 1460 + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} 1461 + engines: {node: '>=12'} 1462 + 1282 1463 debug@4.4.0: 1283 1464 resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 1284 1465 engines: {node: '>=6.0'} ··· 1288 1469 supports-color: 1289 1470 optional: true 1290 1471 1472 + decimal.js-light@2.5.1: 1473 + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} 1474 + 1291 1475 detect-node-es@1.1.0: 1292 1476 resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} 1293 1477 ··· 1300 1484 1301 1485 dlv@1.1.3: 1302 1486 resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 1487 + 1488 + dom-helpers@5.2.1: 1489 + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} 1303 1490 1304 1491 dom-serializer@2.0.0: 1305 1492 resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} ··· 1347 1534 estree-walker@2.0.2: 1348 1535 resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 1349 1536 1537 + eventemitter3@4.0.7: 1538 + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 1539 + 1540 + fast-equals@5.2.2: 1541 + resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} 1542 + engines: {node: '>=6.0.0'} 1543 + 1350 1544 fast-glob@3.3.3: 1351 1545 resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 1352 1546 engines: {node: '>=8.6.0'} ··· 1412 1606 he@1.2.0: 1413 1607 resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 1414 1608 hasBin: true 1609 + 1610 + internmap@2.0.3: 1611 + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} 1612 + engines: {node: '>=12'} 1415 1613 1416 1614 is-binary-path@2.1.0: 1417 1615 resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} ··· 1470 1668 lines-and-columns@1.2.4: 1471 1669 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 1472 1670 1671 + lodash@4.17.21: 1672 + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 1673 + 1674 + loose-envify@1.4.0: 1675 + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 1676 + hasBin: true 1677 + 1473 1678 lru-cache@10.4.3: 1474 1679 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 1475 1680 ··· 1615 1820 engines: {node: '>=14'} 1616 1821 hasBin: true 1617 1822 1823 + prop-types@15.8.1: 1824 + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 1825 + 1618 1826 queue-microtask@1.2.3: 1619 1827 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1620 1828 ··· 1623 1831 peerDependencies: 1624 1832 react: ^19.0.0 1625 1833 1834 + react-is@16.13.1: 1835 + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 1836 + 1837 + react-is@18.3.1: 1838 + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} 1839 + 1626 1840 react-remove-scroll-bar@2.3.8: 1627 1841 resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} 1628 1842 engines: {node: '>=10'} ··· 1643 1857 '@types/react': 1644 1858 optional: true 1645 1859 1860 + react-remove-scroll@2.6.3: 1861 + resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} 1862 + engines: {node: '>=10'} 1863 + peerDependencies: 1864 + '@types/react': '*' 1865 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc 1866 + peerDependenciesMeta: 1867 + '@types/react': 1868 + optional: true 1869 + 1870 + react-smooth@4.0.4: 1871 + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} 1872 + peerDependencies: 1873 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1874 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1875 + 1646 1876 react-style-singleton@2.2.3: 1647 1877 resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} 1648 1878 engines: {node: '>=10'} ··· 1652 1882 peerDependenciesMeta: 1653 1883 '@types/react': 1654 1884 optional: true 1885 + 1886 + react-transition-group@4.4.5: 1887 + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} 1888 + peerDependencies: 1889 + react: '>=16.6.0' 1890 + react-dom: '>=16.6.0' 1655 1891 1656 1892 react@19.0.0: 1657 1893 resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} ··· 1664 1900 resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1665 1901 engines: {node: '>=8.10.0'} 1666 1902 1903 + recharts-scale@0.4.5: 1904 + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} 1905 + 1906 + recharts@2.15.1: 1907 + resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==} 1908 + engines: {node: '>=14'} 1909 + peerDependencies: 1910 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1911 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1912 + 1913 + regenerator-runtime@0.14.1: 1914 + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} 1915 + 1667 1916 resolve-pkg-maps@1.0.0: 1668 1917 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1669 1918 ··· 1831 2080 util-deprecate@1.0.2: 1832 2081 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1833 2082 2083 + victory-vendor@36.9.2: 2084 + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} 2085 + 1834 2086 vite@6.0.7: 1835 2087 resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==} 1836 2088 engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} ··· 2030 2282 '@babel/types': 7.26.5 2031 2283 transitivePeerDependencies: 2032 2284 - supports-color 2285 + 2286 + '@babel/runtime@7.26.7': 2287 + dependencies: 2288 + regenerator-runtime: 0.14.1 2033 2289 2034 2290 '@babel/template@7.25.9': 2035 2291 dependencies: ··· 2307 2563 react: 19.0.0 2308 2564 react-dom: 19.0.0(react@19.0.0) 2309 2565 2566 + '@radix-ui/react-arrow@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2567 + dependencies: 2568 + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2569 + react: 19.0.0 2570 + react-dom: 19.0.0(react@19.0.0) 2571 + 2310 2572 '@radix-ui/react-collection@1.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2311 2573 dependencies: 2312 2574 '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) ··· 2357 2619 react: 19.0.0 2358 2620 react-dom: 19.0.0(react@19.0.0) 2359 2621 2622 + '@radix-ui/react-dismissable-layer@1.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2623 + dependencies: 2624 + '@radix-ui/primitive': 1.1.1 2625 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) 2626 + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2627 + '@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0) 2628 + '@radix-ui/react-use-escape-keydown': 1.1.0(react@19.0.0) 2629 + react: 19.0.0 2630 + react-dom: 19.0.0(react@19.0.0) 2631 + 2360 2632 '@radix-ui/react-focus-guards@1.1.1(react@19.0.0)': 2361 2633 dependencies: 2362 2634 react: 19.0.0 ··· 2369 2641 react: 19.0.0 2370 2642 react-dom: 19.0.0(react@19.0.0) 2371 2643 2644 + '@radix-ui/react-focus-scope@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2645 + dependencies: 2646 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) 2647 + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2648 + '@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0) 2649 + react: 19.0.0 2650 + react-dom: 19.0.0(react@19.0.0) 2651 + 2372 2652 '@radix-ui/react-id@1.1.0(react@19.0.0)': 2373 2653 dependencies: 2374 2654 '@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0) 2375 2655 react: 19.0.0 2376 2656 2657 + '@radix-ui/react-popover@1.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2658 + dependencies: 2659 + '@radix-ui/primitive': 1.1.1 2660 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) 2661 + '@radix-ui/react-context': 1.1.1(react@19.0.0) 2662 + '@radix-ui/react-dismissable-layer': 1.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2663 + '@radix-ui/react-focus-guards': 1.1.1(react@19.0.0) 2664 + '@radix-ui/react-focus-scope': 1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2665 + '@radix-ui/react-id': 1.1.0(react@19.0.0) 2666 + '@radix-ui/react-popper': 1.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2667 + '@radix-ui/react-portal': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2668 + '@radix-ui/react-presence': 1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2669 + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2670 + '@radix-ui/react-slot': 1.1.2(react@19.0.0) 2671 + '@radix-ui/react-use-controllable-state': 1.1.0(react@19.0.0) 2672 + aria-hidden: 1.2.4 2673 + react: 19.0.0 2674 + react-dom: 19.0.0(react@19.0.0) 2675 + react-remove-scroll: 2.6.3(react@19.0.0) 2676 + 2377 2677 '@radix-ui/react-popper@1.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2378 2678 dependencies: 2379 2679 '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 2389 2689 react: 19.0.0 2390 2690 react-dom: 19.0.0(react@19.0.0) 2391 2691 2692 + '@radix-ui/react-popper@1.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2693 + dependencies: 2694 + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2695 + '@radix-ui/react-arrow': 1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2696 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) 2697 + '@radix-ui/react-context': 1.1.1(react@19.0.0) 2698 + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2699 + '@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0) 2700 + '@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0) 2701 + '@radix-ui/react-use-rect': 1.1.0(react@19.0.0) 2702 + '@radix-ui/react-use-size': 1.1.0(react@19.0.0) 2703 + '@radix-ui/rect': 1.1.0 2704 + react: 19.0.0 2705 + react-dom: 19.0.0(react@19.0.0) 2706 + 2392 2707 '@radix-ui/react-portal@1.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2393 2708 dependencies: 2394 2709 '@radix-ui/react-primitive': 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 2396 2711 react: 19.0.0 2397 2712 react-dom: 19.0.0(react@19.0.0) 2398 2713 2714 + '@radix-ui/react-portal@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2715 + dependencies: 2716 + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2717 + '@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0) 2718 + react: 19.0.0 2719 + react-dom: 19.0.0(react@19.0.0) 2720 + 2399 2721 '@radix-ui/react-presence@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2400 2722 dependencies: 2401 2723 '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) ··· 2406 2728 '@radix-ui/react-primitive@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2407 2729 dependencies: 2408 2730 '@radix-ui/react-slot': 1.1.1(react@19.0.0) 2731 + react: 19.0.0 2732 + react-dom: 19.0.0(react@19.0.0) 2733 + 2734 + '@radix-ui/react-primitive@2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2735 + dependencies: 2736 + '@radix-ui/react-slot': 1.1.2(react@19.0.0) 2409 2737 react: 19.0.0 2410 2738 react-dom: 19.0.0(react@19.0.0) 2411 2739 ··· 2437 2765 react-dom: 19.0.0(react@19.0.0) 2438 2766 2439 2767 '@radix-ui/react-slot@1.1.1(react@19.0.0)': 2768 + dependencies: 2769 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) 2770 + react: 19.0.0 2771 + 2772 + '@radix-ui/react-slot@1.1.2(react@19.0.0)': 2440 2773 dependencies: 2441 2774 '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) 2442 2775 react: 19.0.0 ··· 2787 3120 dependencies: 2788 3121 '@babel/types': 7.26.5 2789 3122 3123 + '@types/d3-array@3.2.1': {} 3124 + 3125 + '@types/d3-color@3.1.3': {} 3126 + 3127 + '@types/d3-ease@3.0.2': {} 3128 + 3129 + '@types/d3-interpolate@3.0.4': 3130 + dependencies: 3131 + '@types/d3-color': 3.1.3 3132 + 3133 + '@types/d3-path@3.1.1': {} 3134 + 3135 + '@types/d3-scale@4.0.9': 3136 + dependencies: 3137 + '@types/d3-time': 3.0.4 3138 + 3139 + '@types/d3-shape@3.1.7': 3140 + dependencies: 3141 + '@types/d3-path': 3.1.1 3142 + 3143 + '@types/d3-time@3.0.4': {} 3144 + 3145 + '@types/d3-timer@3.0.2': {} 3146 + 2790 3147 '@types/diff@6.0.0': {} 2791 3148 2792 3149 '@types/estree@1.0.6': {} ··· 2918 3275 2919 3276 csstype@3.1.3: {} 2920 3277 3278 + d3-array@3.2.4: 3279 + dependencies: 3280 + internmap: 2.0.3 3281 + 3282 + d3-color@3.1.0: {} 3283 + 3284 + d3-ease@3.0.1: {} 3285 + 3286 + d3-format@3.1.0: {} 3287 + 3288 + d3-interpolate@3.0.1: 3289 + dependencies: 3290 + d3-color: 3.1.0 3291 + 3292 + d3-path@3.1.0: {} 3293 + 3294 + d3-scale@4.0.2: 3295 + dependencies: 3296 + d3-array: 3.2.4 3297 + d3-format: 3.1.0 3298 + d3-interpolate: 3.0.1 3299 + d3-time: 3.1.0 3300 + d3-time-format: 4.1.0 3301 + 3302 + d3-shape@3.2.0: 3303 + dependencies: 3304 + d3-path: 3.1.0 3305 + 3306 + d3-time-format@4.1.0: 3307 + dependencies: 3308 + d3-time: 3.1.0 3309 + 3310 + d3-time@3.1.0: 3311 + dependencies: 3312 + d3-array: 3.2.4 3313 + 3314 + d3-timer@3.0.1: {} 3315 + 2921 3316 debug@4.4.0: 2922 3317 dependencies: 2923 3318 ms: 2.1.3 2924 3319 3320 + decimal.js-light@2.5.1: {} 3321 + 2925 3322 detect-node-es@1.1.0: {} 2926 3323 2927 3324 didyoumean@1.2.2: {} ··· 2929 3326 diff@7.0.0: {} 2930 3327 2931 3328 dlv@1.1.3: {} 3329 + 3330 + dom-helpers@5.2.1: 3331 + dependencies: 3332 + '@babel/runtime': 7.26.7 3333 + csstype: 3.1.3 2932 3334 2933 3335 dom-serializer@2.0.0: 2934 3336 dependencies: ··· 3017 3419 3018 3420 estree-walker@2.0.2: {} 3019 3421 3422 + eventemitter3@4.0.7: {} 3423 + 3424 + fast-equals@5.2.2: {} 3425 + 3020 3426 fast-glob@3.3.3: 3021 3427 dependencies: 3022 3428 '@nodelib/fs.stat': 2.0.5 ··· 3082 3488 3083 3489 he@1.2.0: {} 3084 3490 3491 + internmap@2.0.3: {} 3492 + 3085 3493 is-binary-path@2.1.0: 3086 3494 dependencies: 3087 3495 binary-extensions: 2.3.0 ··· 3122 3530 3123 3531 lines-and-columns@1.2.4: {} 3124 3532 3533 + lodash@4.17.21: {} 3534 + 3535 + loose-envify@1.4.0: 3536 + dependencies: 3537 + js-tokens: 4.0.0 3538 + 3125 3539 lru-cache@10.4.3: {} 3126 3540 3127 3541 lru-cache@5.1.1: ··· 3238 3652 3239 3653 prettier@3.4.2: {} 3240 3654 3655 + prop-types@15.8.1: 3656 + dependencies: 3657 + loose-envify: 1.4.0 3658 + object-assign: 4.1.1 3659 + react-is: 16.13.1 3660 + 3241 3661 queue-microtask@1.2.3: {} 3242 3662 3243 3663 react-dom@19.0.0(react@19.0.0): ··· 3245 3665 react: 19.0.0 3246 3666 scheduler: 0.25.0 3247 3667 3668 + react-is@16.13.1: {} 3669 + 3670 + react-is@18.3.1: {} 3671 + 3248 3672 react-remove-scroll-bar@2.3.8(react@19.0.0): 3249 3673 dependencies: 3250 3674 react: 19.0.0 ··· 3260 3684 use-callback-ref: 1.3.3(react@19.0.0) 3261 3685 use-sidecar: 1.1.3(react@19.0.0) 3262 3686 3687 + react-remove-scroll@2.6.3(react@19.0.0): 3688 + dependencies: 3689 + react: 19.0.0 3690 + react-remove-scroll-bar: 2.3.8(react@19.0.0) 3691 + react-style-singleton: 2.2.3(react@19.0.0) 3692 + tslib: 2.8.1 3693 + use-callback-ref: 1.3.3(react@19.0.0) 3694 + use-sidecar: 1.1.3(react@19.0.0) 3695 + 3696 + react-smooth@4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 3697 + dependencies: 3698 + fast-equals: 5.2.2 3699 + prop-types: 15.8.1 3700 + react: 19.0.0 3701 + react-dom: 19.0.0(react@19.0.0) 3702 + react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 3703 + 3263 3704 react-style-singleton@2.2.3(react@19.0.0): 3264 3705 dependencies: 3265 3706 get-nonce: 1.0.1 3266 3707 react: 19.0.0 3267 3708 tslib: 2.8.1 3268 3709 3710 + react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 3711 + dependencies: 3712 + '@babel/runtime': 7.26.7 3713 + dom-helpers: 5.2.1 3714 + loose-envify: 1.4.0 3715 + prop-types: 15.8.1 3716 + react: 19.0.0 3717 + react-dom: 19.0.0(react@19.0.0) 3718 + 3269 3719 react@19.0.0: {} 3270 3720 3271 3721 read-cache@1.0.0: ··· 3275 3725 readdirp@3.6.0: 3276 3726 dependencies: 3277 3727 picomatch: 2.3.1 3728 + 3729 + recharts-scale@0.4.5: 3730 + dependencies: 3731 + decimal.js-light: 2.5.1 3732 + 3733 + recharts@2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 3734 + dependencies: 3735 + clsx: 2.1.1 3736 + eventemitter3: 4.0.7 3737 + lodash: 4.17.21 3738 + react: 19.0.0 3739 + react-dom: 19.0.0(react@19.0.0) 3740 + react-is: 18.3.1 3741 + react-smooth: 4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 3742 + recharts-scale: 0.4.5 3743 + tiny-invariant: 1.3.3 3744 + victory-vendor: 36.9.2 3745 + 3746 + regenerator-runtime@0.14.1: {} 3278 3747 3279 3748 resolve-pkg-maps@1.0.0: {} 3280 3749 ··· 3458 3927 react: 19.0.0 3459 3928 3460 3929 util-deprecate@1.0.2: {} 3930 + 3931 + victory-vendor@36.9.2: 3932 + dependencies: 3933 + '@types/d3-array': 3.2.1 3934 + '@types/d3-ease': 3.0.2 3935 + '@types/d3-interpolate': 3.0.4 3936 + '@types/d3-scale': 4.0.9 3937 + '@types/d3-shape': 3.1.7 3938 + '@types/d3-time': 3.0.4 3939 + '@types/d3-timer': 3.0.2 3940 + d3-array: 3.2.4 3941 + d3-ease: 3.0.1 3942 + d3-interpolate: 3.0.1 3943 + d3-scale: 4.0.2 3944 + d3-shape: 3.2.0 3945 + d3-time: 3.1.0 3946 + d3-timer: 3.0.1 3461 3947 3462 3948 vite@6.0.7(@types/node@22.10.7)(jiti@1.21.7)(tsx@4.19.2)(yaml@2.7.0): 3463 3949 dependencies:
+59
src/components/fontPicker.tsx
··· 1 + // 2. Create Font Picker Component 2 + import { useTheme, GOOGLE_FONTS } from "@/providers/themeProvider"; 3 + import { Button } from "./ui/button"; 4 + import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; 5 + import { Check, ChevronDown, Text } from "lucide-react"; 6 + import { useState } from "preact/hooks"; 7 + 8 + export function FontPicker() { 9 + const { font, setFont, loadFonts, loadedFonts } = useTheme(); 10 + const [isOpen, setIsOpen] = useState(false); 11 + 12 + const handleOpenChange = (open: boolean) => { 13 + setIsOpen(open); 14 + console.log("Setting open", open); 15 + if (open) { 16 + // Load all fonts when picker opens 17 + loadFonts(GOOGLE_FONTS); 18 + } 19 + }; 20 + 21 + return ( 22 + <Popover open={isOpen} onOpenChange={handleOpenChange}> 23 + <PopoverTrigger asChild> 24 + <Button variant="outline" className="gap-2"> 25 + <Text className="h-4 w-4" /> 26 + {font.name} 27 + <ChevronDown className="h-4 w-4 opacity-50" /> 28 + </Button> 29 + </PopoverTrigger> 30 + <PopoverContent className="w-[200px]"> 31 + <div className="grid gap-1 p-2"> 32 + {GOOGLE_FONTS.map((fontOption) => ( 33 + <button 34 + key={fontOption.name} 35 + onClick={() => { 36 + setFont(fontOption); 37 + setIsOpen(false); 38 + }} 39 + className="flex items-center justify-between rounded-sm p-2 text-sm hover:bg-accent" 40 + style={{ 41 + fontFamily: `'${fontOption.name}', ${fontOption.category}`, 42 + // Show loading state if font isn't loaded yet 43 + opacity: loadedFonts.has(fontOption.url) ? 1 : 0.6, 44 + }} 45 + > 46 + {fontOption.name} 47 + {font.name === fontOption.name && ( 48 + <Check className="h-4 w-4 text-primary" /> 49 + )} 50 + {!loadedFonts.has(fontOption.url) && ( 51 + <span className="h-4 w-4 animate-spin">⟳</span> 52 + )} 53 + </button> 54 + ))} 55 + </div> 56 + </PopoverContent> 57 + </Popover> 58 + ); 59 + }
+2
src/components/sidebar.tsx
··· 17 17 import { ColorToggle } from "./themeSwitcher"; 18 18 import { Link } from "@tanstack/react-router"; 19 19 import { ForwardRefExoticComponent, ReactNode } from "preact/compat"; 20 + import { FontPicker } from "./fontPicker"; 20 21 21 22 type BaseMenuItem = { 22 23 url: string; ··· 120 121 </SidebarGroup> 121 122 </SidebarContent> 122 123 <SidebarFooter> 124 + <FontPicker /> 123 125 <ColorToggle /> 124 126 </SidebarFooter> 125 127 <SidebarRail />
+2
src/components/smartSearchBar.tsx
··· 47 47 url: input.replace("https:/", "").replace("pds/", ""), 48 48 }, 49 49 }); 50 + } else if (input === "typing") { 51 + navigate({ to: "/rnfgrertt/typing" }); 50 52 } else { 51 53 navigate({ 52 54 to: `/at:/${input.replace("at:/", "")}`,
+2 -2
src/components/themeSwitcher.tsx
··· 5 5 import { Button } from "./ui/button"; 6 6 7 7 export const ColorToggle = () => { 8 - const { theme, toggleTheme } = useTheme(); 8 + const { theme, setTheme } = useTheme(); 9 9 const [mounted, setMounted] = useState(false); 10 10 useEffect(() => { 11 11 setMounted(true); ··· 20 20 className="flex items-center justify-center w-10 h-10 p-3 rounded-full bg-gray-200 dark:bg-gray-800 text-neutral-800 dark:text-neutral-300 cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-700" 21 21 aria-label="button" 22 22 onClick={() => { 23 - toggleTheme(); 23 + setTheme(theme === "dark" ? "light" : "dark"); 24 24 }} 25 25 > 26 26 <DarkLightIcon />
+1 -1
src/components/ui/kbdKey.tsx
··· 50 50 <div 51 51 ref={ref} 52 52 className={cn( 53 - "inline-flex items-center justify-center rounded-md border bg-muted px-2 py-1 text-xs font-mono shadow", 53 + "inline-flex items-center justify-center rounded-md border bg-muted px-2 py-1 text-xs font-mono shadow transition-colors duration-150", 54 54 keys ? "gap-x-[0.062rem]" : "gap-x-1", 55 55 allKeysPressed && "bg-green-300/50", 56 56 className,
+29
src/components/ui/popover.tsx
··· 1 + import * as React from "react" 2 + import * as PopoverPrimitive from "@radix-ui/react-popover" 3 + 4 + import { cn } from "@/lib/utils" 5 + 6 + const Popover = PopoverPrimitive.Root 7 + 8 + const PopoverTrigger = PopoverPrimitive.Trigger 9 + 10 + const PopoverContent = React.forwardRef< 11 + React.ElementRef<typeof PopoverPrimitive.Content>, 12 + React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 13 + >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 + <PopoverPrimitive.Portal> 15 + <PopoverPrimitive.Content 16 + ref={ref} 17 + align={align} 18 + sideOffset={sideOffset} 19 + className={cn( 20 + "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 21 + className 22 + )} 23 + {...props} 24 + /> 25 + </PopoverPrimitive.Portal> 26 + )) 27 + PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 + 29 + export { Popover, PopoverTrigger, PopoverContent }
+141 -119
src/index.css
··· 3 3 @tailwind utilities; 4 4 5 5 @layer base { 6 - :root { 7 - --background: 0 0% 100%; 8 - --foreground: 0 0% 3.9%; 9 - --card: 0 0% 100%; 10 - --card-foreground: 0 0% 3.9%; 11 - --popover: 0 0% 100%; 12 - --popover-foreground: 0 0% 3.9%; 13 - --primary: 0 0% 9%; 14 - --primary-foreground: 0 0% 98%; 15 - --secondary: 0 0% 96.1%; 16 - --secondary-foreground: 0 0% 9%; 17 - --muted: 0 0% 96.1%; 18 - --muted-foreground: 0 0% 45.1%; 19 - --accent: 0 0% 96.1%; 20 - --accent-foreground: 0 0% 9%; 21 - --destructive: 0 84.2% 60.2%; 22 - --destructive-foreground: 0 0% 98%; 23 - --border: 0 0% 89.8%; 24 - --input: 0 0% 89.8%; 25 - --ring: 0 0% 3.9%; 26 - --chart-1: 12 76% 61%; 27 - --chart-2: 173 58% 39%; 28 - --chart-3: 197 37% 24%; 29 - --chart-4: 43 74% 66%; 30 - --chart-5: 27 87% 67%; 31 - --radius: 0.5rem; 32 - --sidebar-background: 0 0% 98%; 33 - --sidebar-foreground: 240 5.3% 26.1%; 34 - --sidebar-primary: 240 5.9% 10%; 35 - --sidebar-primary-foreground: 0 0% 98%; 36 - --sidebar-accent: 240 4.8% 95.9%; 37 - --sidebar-accent-foreground: 240 5.9% 10%; 38 - --sidebar-border: 220 13% 91%; 39 - --sidebar-ring: 217.2 91.2% 59.8%; 40 - } 41 - .dark { 42 - --background: 0 0% 3.9%; 43 - --foreground: 0 0% 98%; 44 - --card: 0 0% 3.9%; 45 - --card-foreground: 0 0% 98%; 46 - --popover: 0 0% 3.9%; 47 - --popover-foreground: 0 0% 98%; 48 - --primary: 0 0% 98%; 49 - --primary-foreground: 0 0% 9%; 50 - --secondary: 0 0% 14.9%; 51 - --secondary-foreground: 0 0% 98%; 52 - --muted: 0 0% 14.9%; 53 - --muted-foreground: 0 0% 63.9%; 54 - --accent: 0 0% 14.9%; 55 - --accent-foreground: 0 0% 98%; 56 - --destructive: 0 62.8% 30.6%; 57 - --destructive-foreground: 0 0% 98%; 58 - --border: 0 0% 14.9%; 59 - --input: 0 0% 14.9%; 60 - --ring: 0 0% 83.1%; 61 - --chart-1: 220 70% 50%; 62 - --chart-2: 160 60% 45%; 63 - --chart-3: 30 80% 55%; 64 - --chart-4: 280 65% 60%; 65 - --chart-5: 340 75% 55%; 66 - --sidebar-background: 240 5.9% 10%; 67 - --sidebar-foreground: 240 4.8% 95.9%; 68 - --sidebar-primary: 224.3 76.3% 48%; 69 - --sidebar-primary-foreground: 0 0% 100%; 70 - --sidebar-accent: 240 3.7% 15.9%; 71 - --sidebar-accent-foreground: 240 4.8% 95.9%; 72 - --sidebar-border: 240 3.7% 15.9%; 73 - --sidebar-ring: 217.2 91.2% 59.8%; 74 - } 6 + :root { 7 + --background: 0 0% 100%; 8 + --foreground: 0 0% 3.9%; 9 + --card: 0 0% 100%; 10 + --card-foreground: 0 0% 3.9%; 11 + --popover: 0 0% 100%; 12 + --popover-foreground: 0 0% 3.9%; 13 + --primary: 0 0% 9%; 14 + --primary-foreground: 0 0% 98%; 15 + --secondary: 0 0% 96.1%; 16 + --secondary-foreground: 0 0% 9%; 17 + --muted: 0 0% 96.1%; 18 + --muted-foreground: 0 0% 45.1%; 19 + --accent: 0 0% 96.1%; 20 + --accent-foreground: 0 0% 9%; 21 + --destructive: 0 84.2% 60.2%; 22 + --destructive-foreground: 0 0% 98%; 23 + --border: 0 0% 89.8%; 24 + --input: 0 0% 89.8%; 25 + --ring: 0 0% 3.9%; 26 + --chart-1: 12 76% 61%; 27 + --chart-2: 173 58% 39%; 28 + --chart-3: 197 37% 24%; 29 + --chart-4: 43 74% 66%; 30 + --chart-5: 27 87% 67%; 31 + --radius: 0.5rem; 32 + --sidebar-background: 0 0% 98%; 33 + --sidebar-foreground: 240 5.3% 26.1%; 34 + --sidebar-primary: 240 5.9% 10%; 35 + --sidebar-primary-foreground: 0 0% 98%; 36 + --sidebar-accent: 240 4.8% 95.9%; 37 + --sidebar-accent-foreground: 240 5.9% 10%; 38 + --sidebar-border: 220 13% 91%; 39 + --sidebar-ring: 217.2 91.2% 59.8%; 40 + } 41 + .dark { 42 + --background: 0 0% 3.9%; 43 + --foreground: 0 0% 98%; 44 + --card: 0 0% 3.9%; 45 + --card-foreground: 0 0% 98%; 46 + --popover: 0 0% 3.9%; 47 + --popover-foreground: 0 0% 98%; 48 + --primary: 0 0% 98%; 49 + --primary-foreground: 0 0% 9%; 50 + --secondary: 0 0% 14.9%; 51 + --secondary-foreground: 0 0% 98%; 52 + --muted: 0 0% 14.9%; 53 + --muted-foreground: 0 0% 63.9%; 54 + --accent: 0 0% 14.9%; 55 + --accent-foreground: 0 0% 98%; 56 + --destructive: 0 62.8% 30.6%; 57 + --destructive-foreground: 0 0% 98%; 58 + --border: 0 0% 14.9%; 59 + --input: 0 0% 14.9%; 60 + --ring: 0 0% 83.1%; 61 + --chart-1: 220 70% 50%; 62 + --chart-2: 160 60% 45%; 63 + --chart-3: 30 80% 55%; 64 + --chart-4: 280 65% 60%; 65 + --chart-5: 340 75% 55%; 66 + --sidebar-background: 240 5.9% 10%; 67 + --sidebar-foreground: 240 4.8% 95.9%; 68 + --sidebar-primary: 224.3 76.3% 48%; 69 + --sidebar-primary-foreground: 0 0% 100%; 70 + --sidebar-accent: 240 3.7% 15.9%; 71 + --sidebar-accent-foreground: 240 4.8% 95.9%; 72 + --sidebar-border: 240 3.7% 15.9%; 73 + --sidebar-ring: 217.2 91.2% 59.8%; 74 + } 75 75 } 76 76 @layer base { 77 - * { 78 - @apply border-border; 79 - } 80 - body { 81 - @apply bg-background text-foreground; 82 - } 77 + * { 78 + @apply border-border; 79 + } 80 + body { 81 + @apply bg-background text-foreground; 82 + } 83 83 } 84 84 85 85 .loader { 86 - position: relative; 87 - width: 54px; 88 - height: 54px; 89 - border-radius: 10px; 86 + position: relative; 87 + width: 54px; 88 + height: 54px; 89 + border-radius: 10px; 90 90 } 91 91 92 92 .loader div { 93 - width: 8%; 94 - height: 24%; 95 - background: rgb(128, 128, 128); 96 - position: absolute; 97 - left: 50%; 98 - top: 30%; 99 - opacity: 0; 100 - border-radius: 50px; 101 - box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); 102 - animation: fade458 1s linear infinite; 93 + width: 8%; 94 + height: 24%; 95 + background: rgb(128, 128, 128); 96 + position: absolute; 97 + left: 50%; 98 + top: 30%; 99 + opacity: 0; 100 + border-radius: 50px; 101 + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); 102 + animation: fade458 1s linear infinite; 103 103 } 104 104 105 105 @keyframes fade458 { 106 - from { 107 - opacity: 1; 108 - } 106 + from { 107 + opacity: 1; 108 + } 109 109 110 - to { 111 - opacity: 0.25; 112 - } 110 + to { 111 + opacity: 0.25; 112 + } 113 113 } 114 114 115 115 .loader .bar1 { 116 - transform: rotate(0deg) translate(0, -130%); 117 - animation-delay: 0s; 116 + transform: rotate(0deg) translate(0, -130%); 117 + animation-delay: 0s; 118 118 } 119 119 120 120 .loader .bar2 { 121 - transform: rotate(30deg) translate(0, -130%); 122 - animation-delay: -1.1s; 121 + transform: rotate(30deg) translate(0, -130%); 122 + animation-delay: -1.1s; 123 123 } 124 124 125 125 .loader .bar3 { 126 - transform: rotate(60deg) translate(0, -130%); 127 - animation-delay: -1s; 126 + transform: rotate(60deg) translate(0, -130%); 127 + animation-delay: -1s; 128 128 } 129 129 130 130 .loader .bar4 { 131 - transform: rotate(90deg) translate(0, -130%); 132 - animation-delay: -0.9s; 131 + transform: rotate(90deg) translate(0, -130%); 132 + animation-delay: -0.9s; 133 133 } 134 134 135 135 .loader .bar5 { 136 - transform: rotate(120deg) translate(0, -130%); 137 - animation-delay: -0.8s; 136 + transform: rotate(120deg) translate(0, -130%); 137 + animation-delay: -0.8s; 138 138 } 139 139 140 140 .loader .bar6 { 141 - transform: rotate(150deg) translate(0, -130%); 142 - animation-delay: -0.7s; 141 + transform: rotate(150deg) translate(0, -130%); 142 + animation-delay: -0.7s; 143 143 } 144 144 145 145 .loader .bar7 { 146 - transform: rotate(180deg) translate(0, -130%); 147 - animation-delay: -0.6s; 146 + transform: rotate(180deg) translate(0, -130%); 147 + animation-delay: -0.6s; 148 148 } 149 149 150 150 .loader .bar8 { 151 - transform: rotate(210deg) translate(0, -130%); 152 - animation-delay: -0.5s; 151 + transform: rotate(210deg) translate(0, -130%); 152 + animation-delay: -0.5s; 153 153 } 154 154 155 155 .loader .bar9 { 156 - transform: rotate(240deg) translate(0, -130%); 157 - animation-delay: -0.4s; 156 + transform: rotate(240deg) translate(0, -130%); 157 + animation-delay: -0.4s; 158 158 } 159 159 160 160 .loader .bar10 { 161 - transform: rotate(270deg) translate(0, -130%); 162 - animation-delay: -0.3s; 161 + transform: rotate(270deg) translate(0, -130%); 162 + animation-delay: -0.3s; 163 163 } 164 164 165 165 .loader .bar11 { 166 - transform: rotate(300deg) translate(0, -130%); 167 - animation-delay: -0.2s; 166 + transform: rotate(300deg) translate(0, -130%); 167 + animation-delay: -0.2s; 168 168 } 169 169 170 170 .loader .bar12 { 171 - transform: rotate(330deg) translate(0, -130%); 172 - animation-delay: -0.1s; 171 + transform: rotate(330deg) translate(0, -130%); 172 + animation-delay: -0.1s; 173 + } 174 + 175 + @keyframes invertBlink { 176 + 0% { 177 + filter: invert(100%); 178 + } 179 + 30% { 180 + filter: invert(100%); 181 + } 182 + 50% { 183 + filter: invert(0%); 184 + } 185 + 80% { 186 + filter: invert(0%); 187 + } 188 + 100% { 189 + filter: invert(100%); 190 + } 191 + } 192 + 193 + .animate-blink { 194 + animation: invertBlink 1s infinite; 173 195 }
+137 -31
src/providers/themeProvider.tsx
··· 1 - import React, { createContext, useContext, useEffect, useState } from "react"; 1 + // 1. Create a Theme Context 2 + import { 3 + createContext, 4 + useCallback, 5 + useContext, 6 + useEffect, 7 + useState, 8 + } from "react"; 2 9 3 - type Theme = "dark" | "light"; 10 + export type FontConfig = { 11 + name: string; 12 + category: "sans" | "serif" | "mono"; 13 + url: string; 14 + }; 4 15 5 - type ThemeContextType = { 6 - theme: Theme; 7 - toggleTheme: () => void; 16 + export const GOOGLE_FONTS: FontConfig[] = [ 17 + { 18 + name: "Inter", 19 + category: "sans", 20 + url: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap", 21 + }, 22 + { 23 + name: "Roboto", 24 + category: "sans", 25 + url: "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap", 26 + }, 27 + { 28 + name: "IBM Plex Mono", 29 + category: "mono", 30 + url: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;700&display=swap", 31 + }, 32 + { 33 + name: "Space Mono", 34 + category: "mono", 35 + url: "https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap", 36 + }, 37 + { 38 + name: "DM Mono", 39 + category: "mono", 40 + url: "https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;700&display=swap", 41 + }, 42 + { 43 + name: "Fira Mono", 44 + category: "mono", 45 + url: "https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;700&display=swap", 46 + }, 47 + { 48 + name: "Overpass Mono", 49 + category: "mono", 50 + url: "https://fonts.googleapis.com/css2?family=Overpass+Mono:wght@400;700&display=swap", 51 + }, 52 + { 53 + name: "Atkinson Hyperlegible Mono", 54 + category: "mono", 55 + url: "https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@400;700&display=swap", 56 + }, 57 + { 58 + name: "Geist Mono", 59 + category: "mono", 60 + url: "https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;700&display=swap", 61 + }, 62 + ]; 63 + 64 + export type ThemeState = { 65 + theme: "light" | "dark"; 66 + font: FontConfig; 67 + loadedFonts: Set<string>; 68 + loadFonts: (fonts: FontConfig[]) => void; 69 + setTheme: (theme: "light" | "dark") => void; 70 + setFont: (font: FontConfig) => void; 8 71 }; 9 72 10 - const ThemeContext = createContext<ThemeContextType | undefined>(undefined); 73 + const ThemeContext = createContext<ThemeState>({} as ThemeState); 11 74 12 75 export function ThemeProvider({ children }: { children: React.ReactNode }) { 13 - // Initialize with undefined to prevent hydration mismatch 14 - const [theme, setTheme] = useState<Theme | undefined>(undefined); 76 + const [theme, setTheme] = useState<"light" | "dark">("light"); 77 + const [font, setFont] = useState<FontConfig>(GOOGLE_FONTS[0]); 78 + const [loadedFonts, setLoadedFonts] = useState<Set<string>>(new Set()); 15 79 80 + const loadFonts = useCallback( 81 + (fonts: FontConfig[]) => { 82 + console.log("Loading multiple fonts"); 83 + fonts.forEach((fontConfig) => { 84 + if (!loadedFonts.has(fontConfig.url)) { 85 + const link = document.createElement("link"); 86 + link.rel = "stylesheet"; 87 + link.href = fontConfig.url; 88 + link.crossOrigin = "anonymous"; 89 + link.dataset.font = fontConfig.name; 90 + document.head.appendChild(link); 91 + 92 + setLoadedFonts((prev) => new Set(prev).add(fontConfig.url)); 93 + } 94 + }); 95 + }, 96 + [loadedFonts], 97 + ); 98 + 99 + // Load saved theme/font from localStorage 16 100 useEffect(() => { 17 - // Get initial theme on mount 18 - const root = window.document.documentElement; 19 - const initialColorValue = root.className as Theme; 20 - setTheme(initialColorValue); 101 + const savedTheme = 102 + (localStorage.getItem("theme") as "light" | "dark") || "light"; 103 + const savedFont = 104 + JSON.parse(localStorage.getItem("font") || "null") || GOOGLE_FONTS[0]; 105 + setTheme(savedTheme); 106 + setFont(savedFont); 21 107 }, []); 22 108 109 + // Manage font loading 23 110 useEffect(() => { 24 - if (theme) { 25 - // Update class and localStorage when theme changes 26 - const root = window.document.documentElement; 27 - root.className = theme; 28 - localStorage.setItem("theme", theme); 111 + // Remove existing font links 112 + const existingLinks = document.querySelectorAll("link[data-font]"); 113 + existingLinks.forEach((link) => link.remove()); 114 + 115 + // Create new font link 116 + const link = document.createElement("link"); 117 + link.rel = "stylesheet"; 118 + link.href = font.url; 119 + link.dataset.font = font.name; 120 + document.head.appendChild(link); 121 + 122 + setLoadedFonts(new Set(font.url)); 123 + 124 + // Update CSS variables 125 + document.documentElement.style.setProperty( 126 + `--font-${font.category}`, 127 + `'${font.name}', ${font.category === "sans" ? "sans-serif" : font.category}`, 128 + ); 129 + // assume we want everything to be this font 130 + document.documentElement.style.setProperty( 131 + `--font-sans`, 132 + `'${font.name}', sans-serif`, 133 + ); 134 + 135 + if (font.category !== "mono") { 136 + document.documentElement.style.setProperty(`--font-mono`, `mono`); 29 137 } 30 - }, [theme]); 31 138 32 - const toggleTheme = () => { 33 - setTheme((prev) => (prev === "light" ? "dark" : "light")); 34 - }; 139 + // Save to localStorage 140 + localStorage.setItem("font", JSON.stringify(font)); 141 + localStorage.setItem("theme", theme); 142 + }, [font, theme]); 35 143 36 - // Only render children once theme is set 37 - if (theme === undefined) return null; 144 + // Update theme class 145 + useEffect(() => { 146 + document.documentElement.setAttribute("data-theme", theme); 147 + }, [theme]); 38 148 39 149 return ( 40 - <ThemeContext.Provider value={{ theme, toggleTheme }}> 150 + <ThemeContext.Provider 151 + value={{ theme, font, setTheme, setFont, loadFonts, loadedFonts }} 152 + > 41 153 {children} 42 154 </ThemeContext.Provider> 43 155 ); 44 156 } 45 157 46 - export function useTheme() { 47 - const context = useContext(ThemeContext); 48 - if (context === undefined) { 49 - throw new Error("useTheme must be used within a ThemeProvider"); 50 - } 51 - return context; 52 - } 158 + export const useTheme = () => useContext(ThemeContext);
+68 -10
src/routeTree.gen.ts
··· 14 14 15 15 import { Route as rootRoute } from './routes/__root' 16 16 import { Route as FirehoseIndexImport } from './routes/firehose/index' 17 - import { Route as PdsUrlIndexImport } from './routes/pds/$url.index' 18 17 import { Route as AtHandleIndexImport } from './routes/at:/$handle.index' 19 18 20 19 // Create Virtual Routes ··· 22 21 const CounterLazyImport = createFileRoute('/counter')() 23 22 const AboutLazyImport = createFileRoute('/about')() 24 23 const IndexLazyImport = createFileRoute('/')() 24 + const RnfgrerttIndexLazyImport = createFileRoute('/rnfgrertt/')() 25 + const RnfgrerttTypingLazyImport = createFileRoute('/rnfgrertt/typing')() 26 + const PdsUrlIndexLazyImport = createFileRoute('/pds/$url/')() 25 27 const AtHandleCollectionIndexLazyImport = createFileRoute( 26 28 '/at:/$handle/$collection/', 27 29 )() ··· 49 51 getParentRoute: () => rootRoute, 50 52 } as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) 51 53 54 + const RnfgrerttIndexLazyRoute = RnfgrerttIndexLazyImport.update({ 55 + id: '/rnfgrertt/', 56 + path: '/rnfgrertt/', 57 + getParentRoute: () => rootRoute, 58 + } as any).lazy(() => 59 + import('./routes/rnfgrertt/index.lazy').then((d) => d.Route), 60 + ) 61 + 52 62 const FirehoseIndexRoute = FirehoseIndexImport.update({ 53 63 id: '/firehose/', 54 64 path: '/firehose/', 55 65 getParentRoute: () => rootRoute, 56 66 } as any) 57 67 58 - const PdsUrlIndexRoute = PdsUrlIndexImport.update({ 68 + const RnfgrerttTypingLazyRoute = RnfgrerttTypingLazyImport.update({ 69 + id: '/rnfgrertt/typing', 70 + path: '/rnfgrertt/typing', 71 + getParentRoute: () => rootRoute, 72 + } as any).lazy(() => 73 + import('./routes/rnfgrertt/typing.lazy').then((d) => d.Route), 74 + ) 75 + 76 + const PdsUrlIndexLazyRoute = PdsUrlIndexLazyImport.update({ 59 77 id: '/pds/$url/', 60 78 path: '/pds/$url/', 61 79 getParentRoute: () => rootRoute, 62 - } as any) 80 + } as any).lazy(() => 81 + import('./routes/pds/$url.index.lazy').then((d) => d.Route), 82 + ) 63 83 64 84 const AtHandleIndexRoute = AtHandleIndexImport.update({ 65 85 id: '/at:/$handle/', ··· 111 131 preLoaderRoute: typeof CounterLazyImport 112 132 parentRoute: typeof rootRoute 113 133 } 134 + '/rnfgrertt/typing': { 135 + id: '/rnfgrertt/typing' 136 + path: '/rnfgrertt/typing' 137 + fullPath: '/rnfgrertt/typing' 138 + preLoaderRoute: typeof RnfgrerttTypingLazyImport 139 + parentRoute: typeof rootRoute 140 + } 114 141 '/firehose/': { 115 142 id: '/firehose/' 116 143 path: '/firehose' ··· 118 145 preLoaderRoute: typeof FirehoseIndexImport 119 146 parentRoute: typeof rootRoute 120 147 } 148 + '/rnfgrertt/': { 149 + id: '/rnfgrertt/' 150 + path: '/rnfgrertt' 151 + fullPath: '/rnfgrertt' 152 + preLoaderRoute: typeof RnfgrerttIndexLazyImport 153 + parentRoute: typeof rootRoute 154 + } 121 155 '/at:/$handle/': { 122 156 id: '/at:/$handle/' 123 157 path: '/at:/$handle' ··· 129 163 id: '/pds/$url/' 130 164 path: '/pds/$url' 131 165 fullPath: '/pds/$url' 132 - preLoaderRoute: typeof PdsUrlIndexImport 166 + preLoaderRoute: typeof PdsUrlIndexLazyImport 133 167 parentRoute: typeof rootRoute 134 168 } 135 169 '/at:/$handle/$collection/$rkey': { ··· 155 189 '/': typeof IndexLazyRoute 156 190 '/about': typeof AboutLazyRoute 157 191 '/counter': typeof CounterLazyRoute 192 + '/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute 158 193 '/firehose': typeof FirehoseIndexRoute 194 + '/rnfgrertt': typeof RnfgrerttIndexLazyRoute 159 195 '/at:/$handle': typeof AtHandleIndexRoute 160 - '/pds/$url': typeof PdsUrlIndexRoute 196 + '/pds/$url': typeof PdsUrlIndexLazyRoute 161 197 '/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute 162 198 '/at:/$handle/$collection': typeof AtHandleCollectionIndexLazyRoute 163 199 } ··· 166 202 '/': typeof IndexLazyRoute 167 203 '/about': typeof AboutLazyRoute 168 204 '/counter': typeof CounterLazyRoute 205 + '/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute 169 206 '/firehose': typeof FirehoseIndexRoute 207 + '/rnfgrertt': typeof RnfgrerttIndexLazyRoute 170 208 '/at:/$handle': typeof AtHandleIndexRoute 171 - '/pds/$url': typeof PdsUrlIndexRoute 209 + '/pds/$url': typeof PdsUrlIndexLazyRoute 172 210 '/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute 173 211 '/at:/$handle/$collection': typeof AtHandleCollectionIndexLazyRoute 174 212 } ··· 178 216 '/': typeof IndexLazyRoute 179 217 '/about': typeof AboutLazyRoute 180 218 '/counter': typeof CounterLazyRoute 219 + '/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute 181 220 '/firehose/': typeof FirehoseIndexRoute 221 + '/rnfgrertt/': typeof RnfgrerttIndexLazyRoute 182 222 '/at:/$handle/': typeof AtHandleIndexRoute 183 - '/pds/$url/': typeof PdsUrlIndexRoute 223 + '/pds/$url/': typeof PdsUrlIndexLazyRoute 184 224 '/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute 185 225 '/at:/$handle/$collection/': typeof AtHandleCollectionIndexLazyRoute 186 226 } ··· 191 231 | '/' 192 232 | '/about' 193 233 | '/counter' 234 + | '/rnfgrertt/typing' 194 235 | '/firehose' 236 + | '/rnfgrertt' 195 237 | '/at:/$handle' 196 238 | '/pds/$url' 197 239 | '/at:/$handle/$collection/$rkey' ··· 201 243 | '/' 202 244 | '/about' 203 245 | '/counter' 246 + | '/rnfgrertt/typing' 204 247 | '/firehose' 248 + | '/rnfgrertt' 205 249 | '/at:/$handle' 206 250 | '/pds/$url' 207 251 | '/at:/$handle/$collection/$rkey' ··· 211 255 | '/' 212 256 | '/about' 213 257 | '/counter' 258 + | '/rnfgrertt/typing' 214 259 | '/firehose/' 260 + | '/rnfgrertt/' 215 261 | '/at:/$handle/' 216 262 | '/pds/$url/' 217 263 | '/at:/$handle/$collection/$rkey' ··· 223 269 IndexLazyRoute: typeof IndexLazyRoute 224 270 AboutLazyRoute: typeof AboutLazyRoute 225 271 CounterLazyRoute: typeof CounterLazyRoute 272 + RnfgrerttTypingLazyRoute: typeof RnfgrerttTypingLazyRoute 226 273 FirehoseIndexRoute: typeof FirehoseIndexRoute 274 + RnfgrerttIndexLazyRoute: typeof RnfgrerttIndexLazyRoute 227 275 AtHandleIndexRoute: typeof AtHandleIndexRoute 228 - PdsUrlIndexRoute: typeof PdsUrlIndexRoute 276 + PdsUrlIndexLazyRoute: typeof PdsUrlIndexLazyRoute 229 277 AtHandleCollectionRkeyLazyRoute: typeof AtHandleCollectionRkeyLazyRoute 230 278 AtHandleCollectionIndexLazyRoute: typeof AtHandleCollectionIndexLazyRoute 231 279 } ··· 234 282 IndexLazyRoute: IndexLazyRoute, 235 283 AboutLazyRoute: AboutLazyRoute, 236 284 CounterLazyRoute: CounterLazyRoute, 285 + RnfgrerttTypingLazyRoute: RnfgrerttTypingLazyRoute, 237 286 FirehoseIndexRoute: FirehoseIndexRoute, 287 + RnfgrerttIndexLazyRoute: RnfgrerttIndexLazyRoute, 238 288 AtHandleIndexRoute: AtHandleIndexRoute, 239 - PdsUrlIndexRoute: PdsUrlIndexRoute, 289 + PdsUrlIndexLazyRoute: PdsUrlIndexLazyRoute, 240 290 AtHandleCollectionRkeyLazyRoute: AtHandleCollectionRkeyLazyRoute, 241 291 AtHandleCollectionIndexLazyRoute: AtHandleCollectionIndexLazyRoute, 242 292 } ··· 254 304 "/", 255 305 "/about", 256 306 "/counter", 307 + "/rnfgrertt/typing", 257 308 "/firehose/", 309 + "/rnfgrertt/", 258 310 "/at:/$handle/", 259 311 "/pds/$url/", 260 312 "/at:/$handle/$collection/$rkey", ··· 270 322 "/counter": { 271 323 "filePath": "counter.lazy.tsx" 272 324 }, 325 + "/rnfgrertt/typing": { 326 + "filePath": "rnfgrertt/typing.lazy.tsx" 327 + }, 273 328 "/firehose/": { 274 329 "filePath": "firehose/index.tsx" 275 330 }, 331 + "/rnfgrertt/": { 332 + "filePath": "rnfgrertt/index.lazy.tsx" 333 + }, 276 334 "/at:/$handle/": { 277 335 "filePath": "at:/$handle.index.tsx" 278 336 }, 279 337 "/pds/$url/": { 280 - "filePath": "pds/$url.index.tsx" 338 + "filePath": "pds/$url.index.lazy.tsx" 281 339 }, 282 340 "/at:/$handle/$collection/$rkey": { 283 341 "filePath": "at:/$handle/$collection.$rkey.lazy.tsx"
+167
src/routes/pds/$url.index.lazy.tsx
··· 1 + import ShowError from '@/components/error' 2 + import { Loader } from '@/components/ui/loader' 3 + import { useDocumentTitle } from '@/hooks/useDocumentTitle' 4 + import { QtClient, useXrpc } from '@/providers/qtprovider' 5 + import { ComAtprotoSyncListRepos } from '@atcute/client/lexicons' 6 + import { createLazyFileRoute, Link } from '@tanstack/react-router' 7 + import { ShieldX } from 'lucide-react' 8 + import { useState, useEffect, useRef } from 'preact/compat' 9 + 10 + interface PdsData { 11 + records?: ComAtprotoSyncListRepos.Output['repos'] 12 + cursor?: string 13 + health?: PdsHealth 14 + isLoading: boolean 15 + error: Error | null 16 + fetchMore: (cursor: string) => Promise<void> 17 + } 18 + 19 + interface PdsHealth { 20 + version: string 21 + } 22 + 23 + function useRepoData(baseUrl: string): PdsData { 24 + const xrpc = useXrpc() 25 + const [state, setState] = useState<PdsData>({ 26 + isLoading: true, 27 + error: null, 28 + fetchMore: async () => {}, 29 + }) 30 + 31 + useDocumentTitle(baseUrl ? `${baseUrl} | atp.tools` : 'atp.tools') 32 + const abortController = new AbortController() 33 + 34 + async function fetchRepoData(cursor?: string) { 35 + try { 36 + setState((prev) => ({ ...prev, isLoading: true })) 37 + 38 + // we dont use the main authenticated client here 39 + const rpc = new QtClient(new URL('https://' + baseUrl)) 40 + // get the PDS 41 + const response = await rpc 42 + .getXrpcClient() 43 + .get('com.atproto.sync.listRepos', { 44 + params: { limit: 1000, cursor }, 45 + signal: abortController.signal, 46 + }) 47 + 48 + const health = await rpc 49 + .getXrpcClient() 50 + .request({ nsid: '_health', type: 'get' }) 51 + 52 + setState((prev) => ({ 53 + ...prev, 54 + records: cursor 55 + ? [...(prev.records || []), ...response.data.repos] 56 + : response.data.repos, 57 + cursor: response.data.cursor, 58 + health: health.data, 59 + isLoading: false, 60 + error: null, 61 + })) 62 + 63 + // todo: actual errors 64 + } catch (err: any) { 65 + if (err.name === 'AbortError') return 66 + 67 + setState((prev) => ({ 68 + ...prev, 69 + isLoading: false, 70 + error: err instanceof Error ? err : new Error('An error occurred'), 71 + })) 72 + } 73 + } 74 + 75 + useEffect(() => { 76 + fetchRepoData() 77 + 78 + return () => { 79 + abortController.abort() 80 + } 81 + }, [baseUrl, xrpc]) 82 + 83 + const fetchMore = async (cursor: string) => { 84 + if (cursor && !state.isLoading) { 85 + await fetchRepoData(cursor) 86 + } 87 + } 88 + 89 + return { ...state, fetchMore } 90 + } 91 + 92 + export const Route = createLazyFileRoute('/pds/$url/')({ 93 + component: RouteComponent, 94 + }) 95 + 96 + function RouteComponent() { 97 + const { url } = Route.useParams() 98 + const { cursor, records, fetchMore, isLoading, error } = useRepoData(url) 99 + 100 + useDocumentTitle(records ? `${url} (PDS) | atp.tools` : 'atp.tools') 101 + 102 + const loaderRef = useRef<HTMLDivElement>(null) 103 + useEffect(() => { 104 + if (!loaderRef.current) return 105 + 106 + const observer = new IntersectionObserver( 107 + (entries) => { 108 + const target = entries[0] 109 + if (target.isIntersecting && !isLoading && cursor) { 110 + fetchMore(cursor) 111 + } 112 + }, 113 + { threshold: 0.1, rootMargin: '50px' }, 114 + ) 115 + 116 + observer.observe(loaderRef.current) 117 + return () => observer.disconnect() 118 + }, [cursor, isLoading, fetchMore]) 119 + 120 + if (error) { 121 + return <ShowError error={error} /> 122 + } 123 + 124 + if ((isLoading && !cursor) || !records) { 125 + return <Loader className="max-h-[calc(100vh-5rem)] h-screen" /> 126 + } 127 + 128 + return ( 129 + <div className="flex flex-row justify-center w-full"> 130 + <div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2"> 131 + <div> 132 + PDS: {url.includes('bsky.network') && '🍄'} {url} 133 + </div> 134 + 135 + <div> 136 + <h2 className="text-xl font-bold">Repositories (accounts)</h2> 137 + <ul> 138 + {records?.map((c) => ( 139 + <li key={c.did} className="text-blue-500"> 140 + <Link 141 + to="/at:/$handle" 142 + params={{ 143 + handle: c.did, 144 + }} 145 + > 146 + {c.did}{' '} 147 + {!c.active && <ShieldX className="inline text-red-500" />} 148 + </Link> 149 + </li> 150 + ))} 151 + </ul> 152 + <div 153 + ref={loaderRef} 154 + className="flex flex-row justify-center h-10 -pt-16" 155 + > 156 + {isLoading && <Loader className="max-h-16 h-screen" />} 157 + {!isLoading && !cursor && ( 158 + <div className="text-center text-sm text-muted-foreground mx-10 mt-2"> 159 + that's all, folks! 160 + </div> 161 + )} 162 + </div> 163 + </div> 164 + </div> 165 + </div> 166 + ) 167 + }
-167
src/routes/pds/$url.index.tsx
··· 1 - import ShowError from "@/components/error"; 2 - import { Loader } from "@/components/ui/loader"; 3 - import { useDocumentTitle } from "@/hooks/useDocumentTitle"; 4 - import { QtClient, useXrpc } from "@/providers/qtprovider"; 5 - import { ComAtprotoSyncListRepos } from "@atcute/client/lexicons"; 6 - import { createFileRoute, Link } from "@tanstack/react-router"; 7 - import { ShieldX } from "lucide-react"; 8 - import { useState, useEffect, useRef } from "preact/compat"; 9 - 10 - interface PdsData { 11 - records?: ComAtprotoSyncListRepos.Output["repos"]; 12 - cursor?: string; 13 - health?: PdsHealth; 14 - isLoading: boolean; 15 - error: Error | null; 16 - fetchMore: (cursor: string) => Promise<void>; 17 - } 18 - 19 - interface PdsHealth { 20 - version: string; 21 - } 22 - 23 - function useRepoData(baseUrl: string): PdsData { 24 - const xrpc = useXrpc(); 25 - const [state, setState] = useState<PdsData>({ 26 - isLoading: true, 27 - error: null, 28 - fetchMore: async () => {}, 29 - }); 30 - 31 - useDocumentTitle(baseUrl ? `${baseUrl} | atp.tools` : "atp.tools"); 32 - const abortController = new AbortController(); 33 - 34 - async function fetchRepoData(cursor?: string) { 35 - try { 36 - setState((prev) => ({ ...prev, isLoading: true })); 37 - 38 - // we dont use the main authenticated client here 39 - const rpc = new QtClient(new URL("https://" + baseUrl)); 40 - // get the PDS 41 - const response = await rpc 42 - .getXrpcClient() 43 - .get("com.atproto.sync.listRepos", { 44 - params: { limit: 1000, cursor }, 45 - signal: abortController.signal, 46 - }); 47 - 48 - const health = await rpc 49 - .getXrpcClient() 50 - .request({ nsid: "_health", type: "get" }); 51 - 52 - setState((prev) => ({ 53 - ...prev, 54 - records: cursor 55 - ? [...(prev.records || []), ...response.data.repos] 56 - : response.data.repos, 57 - cursor: response.data.cursor, 58 - health: health.data, 59 - isLoading: false, 60 - error: null, 61 - })); 62 - 63 - // todo: actual errors 64 - } catch (err: any) { 65 - if (err.name === "AbortError") return; 66 - 67 - setState((prev) => ({ 68 - ...prev, 69 - isLoading: false, 70 - error: err instanceof Error ? err : new Error("An error occurred"), 71 - })); 72 - } 73 - } 74 - 75 - useEffect(() => { 76 - fetchRepoData(); 77 - 78 - return () => { 79 - abortController.abort(); 80 - }; 81 - }, [baseUrl, xrpc]); 82 - 83 - const fetchMore = async (cursor: string) => { 84 - if (cursor && !state.isLoading) { 85 - await fetchRepoData(cursor); 86 - } 87 - }; 88 - 89 - return { ...state, fetchMore }; 90 - } 91 - 92 - export const Route = createFileRoute("/pds/$url/")({ 93 - component: RouteComponent, 94 - }); 95 - 96 - function RouteComponent() { 97 - const { url } = Route.useParams(); 98 - const { cursor, records, fetchMore, isLoading, error } = useRepoData(url); 99 - 100 - useDocumentTitle(records ? `${url} (PDS) | atp.tools` : "atp.tools"); 101 - 102 - const loaderRef = useRef<HTMLDivElement>(null); 103 - useEffect(() => { 104 - if (!loaderRef.current) return; 105 - 106 - const observer = new IntersectionObserver( 107 - (entries) => { 108 - const target = entries[0]; 109 - if (target.isIntersecting && !isLoading && cursor) { 110 - fetchMore(cursor); 111 - } 112 - }, 113 - { threshold: 0.1, rootMargin: "50px" }, 114 - ); 115 - 116 - observer.observe(loaderRef.current); 117 - return () => observer.disconnect(); 118 - }, [cursor, isLoading, fetchMore]); 119 - 120 - if (error) { 121 - return <ShowError error={error} />; 122 - } 123 - 124 - if ((isLoading && !cursor) || !records) { 125 - return <Loader className="max-h-[calc(100vh-5rem)] h-screen" />; 126 - } 127 - 128 - return ( 129 - <div className="flex flex-row justify-center w-full"> 130 - <div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2"> 131 - <div> 132 - PDS: {url.includes("bsky.network") && "🍄"} {url} 133 - </div> 134 - 135 - <div> 136 - <h2 className="text-xl font-bold">Repositories (accounts)</h2> 137 - <ul> 138 - {records?.map((c) => ( 139 - <li key={c.did} className="text-blue-500"> 140 - <Link 141 - to="/at:/$handle" 142 - params={{ 143 - handle: c.did, 144 - }} 145 - > 146 - {c.did}{" "} 147 - {!c.active && <ShieldX className="inline text-red-500" />} 148 - </Link> 149 - </li> 150 - ))} 151 - </ul> 152 - <div 153 - ref={loaderRef} 154 - className="flex flex-row justify-center h-10 -pt-16" 155 - > 156 - {isLoading && <Loader className="max-h-16 h-screen" />} 157 - {!isLoading && !cursor && ( 158 - <div className="text-center text-sm text-muted-foreground mx-10 mt-2"> 159 - that's all, folks! 160 - </div> 161 - )} 162 - </div> 163 - </div> 164 - </div> 165 - </div> 166 - ); 167 - }
+9
src/routes/rnfgrertt/index.lazy.tsx
··· 1 + import { createLazyFileRoute } from '@tanstack/react-router' 2 + 3 + export const Route = createLazyFileRoute('/rnfgrertt/')({ 4 + component: RouteComponent, 5 + }) 6 + 7 + function RouteComponent() { 8 + return <div>Hello "/rnfrgrertt/"!</div> 9 + }
+418
src/routes/rnfgrertt/typing.lazy.tsx
··· 1 + import { createLazyFileRoute } from "@tanstack/react-router"; 2 + import { useState, useEffect, useRef } from "preact/hooks"; 3 + import { 4 + LineChart, 5 + Line, 6 + XAxis, 7 + YAxis, 8 + CartesianGrid, 9 + Tooltip, 10 + Legend, 11 + } from "recharts"; 12 + 13 + // Type definitions 14 + type TypingError = { 15 + position: number; 16 + timestamp: number; 17 + expected: string; 18 + actual: string; 19 + }; 20 + 21 + type WPMDataPoint = { 22 + time: number; 23 + wpm: number; 24 + errorsPerSecond: number; 25 + }; 26 + 27 + type TypingStats = { 28 + accuracy: string; 29 + wpm: string; 30 + time: string; 31 + errorCount: number; 32 + }; 33 + 34 + // Constants 35 + const SAMPLE_TEXT = "The quick brown fox jumps over the lazy dog"; 36 + const UPDATE_INTERVAL = 500; 37 + const CHART_MARGIN = { top: 5, right: 30, left: 20, bottom: 5 }; 38 + 39 + // Custom hooks 40 + const useWpmTracker = ( 41 + startTime: number | null, 42 + isFinished: boolean, 43 + userInput: string, 44 + ) => { 45 + const [wpmData, setWpmData] = useState<WPMDataPoint[]>([]); 46 + const userInputRef = useRef(userInput); 47 + const lastUpdateRef = useRef<number>(0); 48 + const prevErrorsRef = useRef<number>(0); 49 + 50 + useEffect(() => { 51 + userInputRef.current = userInput; 52 + if (userInputRef.current.length >= 5 && wpmData.length < 1) { 53 + updateWPMData(); 54 + } 55 + }, [userInput]); 56 + 57 + const calculateMetrics = (currentTime: number) => { 58 + if (startTime === null) 59 + return { timeElapsed: 0, correctChars: 0, currentErrors: 0 }; 60 + const timeElapsed = (currentTime - startTime) / 1000; 61 + const currentInput = userInputRef.current; 62 + 63 + // Calculate correct characters and current errors 64 + let correctChars = 0; 65 + let currentErrors = 0; 66 + 67 + for (let i = 0; i < currentInput.length; i++) { 68 + if (currentInput[i] === SAMPLE_TEXT[i]) { 69 + correctChars++; 70 + } else { 71 + currentErrors++; 72 + } 73 + } 74 + 75 + return { timeElapsed, correctChars, currentErrors }; 76 + }; 77 + 78 + const updateWPMData = () => { 79 + const now = Date.now(); 80 + const { timeElapsed, correctChars, currentErrors } = calculateMetrics(now); 81 + 82 + // Calculate errors per second 83 + const timeDiff = (now - (lastUpdateRef.current || now)) / 1000; 84 + const errorDelta = currentErrors - prevErrorsRef.current; 85 + const errorsPerSecond = timeDiff > 0 ? errorDelta / timeDiff : 0; 86 + 87 + setWpmData((prev) => [ 88 + ...prev, 89 + { 90 + time: Number(timeElapsed.toFixed(1)), 91 + wpm: Math.round(correctChars / 5 / (timeElapsed / 60)), 92 + errorsPerSecond: Number(errorsPerSecond.toFixed(1)), 93 + }, 94 + ]); 95 + 96 + lastUpdateRef.current = now; 97 + prevErrorsRef.current = currentErrors; 98 + }; 99 + 100 + useEffect(() => { 101 + if (!startTime || isFinished) return; 102 + 103 + // Subsequent updates at interval 104 + const intervalId = setInterval(updateWPMData, UPDATE_INTERVAL); 105 + 106 + return () => clearInterval(intervalId); 107 + }, [startTime, isFinished]); 108 + 109 + const resetWpmData = () => setWpmData([]); 110 + return { wpmData, resetWpmData }; 111 + }; 112 + 113 + const useErrorTracker = (errors: TypingError[]) => { 114 + const [errorDistribution, setErrorDistribution] = useState< 115 + { position: number; count: number }[] 116 + >([]); 117 + 118 + useEffect(() => { 119 + const distribution = new Array(SAMPLE_TEXT.length).fill(0); 120 + errors.forEach(({ position }) => distribution[position]++); 121 + setErrorDistribution( 122 + distribution 123 + .map((count, position) => ({ position, count })) 124 + .filter((d) => d.count > 0), 125 + ); 126 + }, [errors]); 127 + 128 + return errorDistribution; 129 + }; 130 + 131 + const useTypingTest = () => { 132 + const [state, setState] = useState({ 133 + userInput: "", 134 + isFinished: false, 135 + startTime: null as number | null, 136 + endTime: null as number | null, 137 + errors: [] as TypingError[], 138 + }); 139 + 140 + const handleInput = (input: string) => { 141 + if (state.isFinished) return; 142 + 143 + const newInput = input.slice(0, SAMPLE_TEXT.length); 144 + const newErrors = [...state.errors]; 145 + 146 + // Track new errors 147 + if (newInput.length > state.userInput.length) { 148 + const newCharIndex = newInput.length - 1; 149 + if (newInput[newCharIndex] !== SAMPLE_TEXT[newCharIndex]) { 150 + newErrors.push({ 151 + position: newCharIndex, 152 + timestamp: Date.now(), 153 + expected: SAMPLE_TEXT[newCharIndex], 154 + actual: newInput[newCharIndex], 155 + }); 156 + } 157 + } 158 + 159 + setState((prev) => ({ 160 + ...prev, 161 + userInput: newInput, 162 + errors: newErrors, 163 + startTime: prev.startTime || (newInput.length > 0 ? Date.now() : null), 164 + isFinished: newInput.length === SAMPLE_TEXT.length, 165 + endTime: 166 + newInput.length === SAMPLE_TEXT.length ? Date.now() : prev.endTime, 167 + })); 168 + }; 169 + 170 + const resetTest = () => { 171 + setState({ 172 + userInput: "", 173 + isFinished: false, 174 + startTime: null, 175 + endTime: null, 176 + errors: [], 177 + }); 178 + }; 179 + 180 + return { ...state, handleInput, resetTest }; 181 + }; 182 + 183 + // UI Components 184 + const ResultsView = ({ 185 + stats, 186 + wpmData, 187 + resetTest, 188 + }: { 189 + stats: TypingStats; 190 + wpmData: WPMDataPoint[]; 191 + resetTest: () => void; 192 + }) => ( 193 + <div className="m-auto px-4 py-16 flex-1 max-w-screen-sm text-center bg-card space-y-4 p-6 rounded-lg"> 194 + <h2 className="text-2xl font-bold mb-4">Test Results</h2> 195 + <StatsGrid stats={stats} /> 196 + <PerformanceChart wpmData={wpmData} /> 197 + <ResetButton onClick={resetTest} /> 198 + </div> 199 + ); 200 + 201 + const StatsGrid = ({ stats }: { stats: TypingStats }) => ( 202 + <div className="grid grid-cols-4 gap-4"> 203 + {Object.entries(stats).map(([key, value]) => ( 204 + <StatBox key={key} label={key} value={value} /> 205 + ))} 206 + </div> 207 + ); 208 + 209 + const StatBox = ({ 210 + label, 211 + value, 212 + }: { 213 + label: string; 214 + value: string | number; 215 + }) => ( 216 + <div className="bg-muted p-4 rounded"> 217 + <div className="text-gray-500 text-sm">{label}</div> 218 + <div className="text-3xl font-bold">{value}</div> 219 + <div className="text-gray-500 text-sm">{getUnit(label)}</div> 220 + </div> 221 + ); 222 + 223 + const PerformanceChart = ({ wpmData }: { wpmData: WPMDataPoint[] }) => ( 224 + <div className="mt-8 flex flex-col justify-center items-center"> 225 + <h3 className="text-xl font-bold mb-4">Performance Over Time</h3> 226 + <LineChart width={650} height={300} data={wpmData} margin={CHART_MARGIN}> 227 + <CartesianGrid strokeDasharray="3 3" /> 228 + <XAxis 229 + dataKey="time" 230 + type="number" 231 + label={{ value: "Time (seconds)", position: "bottom" }} 232 + domain={[0, "auto"]} 233 + /> 234 + <YAxis 235 + yAxisId="left" 236 + label={{ value: "WPM", angle: -90, position: "insideLeft" }} 237 + /> 238 + <YAxis 239 + yAxisId="right" 240 + orientation="right" 241 + label={{ value: "Errors/sec", angle: 90, position: "insideRight" }} 242 + domain={[0, Math.max(...wpmData.map((d) => d.errorsPerSecond), 1)]} 243 + /> 244 + <Tooltip content={<CustomTooltip />} /> 245 + <Legend /> 246 + <Line 247 + yAxisId="left" 248 + type="monotone" 249 + dataKey="wpm" 250 + stroke="#8884d8" 251 + name="WPM" 252 + strokeWidth={2} 253 + dot={false} 254 + /> 255 + <Line 256 + yAxisId="right" 257 + type="monotone" 258 + dataKey="errorsPerSecond" 259 + stroke="#ff4757" 260 + name="Errors/sec" 261 + strokeWidth={2} 262 + dot={false} 263 + /> 264 + </LineChart> 265 + </div> 266 + ); 267 + 268 + const CustomTooltip = ({ active, payload }: any) => { 269 + if (active && payload?.length) { 270 + const data = payload[0].payload; 271 + return ( 272 + <div className="bg-white p-2 border rounded shadow"> 273 + <p className="text-sm">Time: {data.time}s</p> 274 + <p className="text-sm text-blue-500">WPM: {data.wpm}</p> 275 + <p className="text-sm text-red-500"> 276 + Errors/sec: {data.errorsPerSecond} 277 + </p> 278 + </div> 279 + ); 280 + } 281 + return null; 282 + }; 283 + 284 + const TypingArea = ({ 285 + userInput, 286 + handleInput, 287 + }: { 288 + userInput: string; 289 + handleInput: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; 290 + }) => ( 291 + <div className="m-auto flex-1 relative p-4 bg-muted rounded-lg h-min max-w-xl"> 292 + <TextDisplay userInput={userInput} /> 293 + <textarea 294 + className="absolute top-0 left-0 w-full h-full p-4 resize-none bg-transparent text-transparent caret-transparent outline-none" 295 + value={userInput} 296 + onChange={handleInput} 297 + autoFocus 298 + /> 299 + </div> 300 + ); 301 + 302 + const TextDisplay = ({ userInput }: { userInput: string }) => ( 303 + <div className="whitespace-pre-wrap break-words"> 304 + {SAMPLE_TEXT.split("").map((char, i) => { 305 + const status = 306 + i < userInput.length 307 + ? userInput[i] === char 308 + ? "text-green-500" 309 + : "text-red-500 underline" 310 + : ""; 311 + return ( 312 + <span 313 + key={i} 314 + className={`${status} ${i === userInput.length ? "bg-muted animate-blink" : ""}`} 315 + > 316 + {char} 317 + </span> 318 + ); 319 + })} 320 + </div> 321 + ); 322 + 323 + // Helper components 324 + 325 + const CustomErrorDot = (props: any) => 326 + props.payload.errors >= 1 ? ( 327 + <circle cx={props.cx} cy={props.cy} r={4} fill="#ff4757" stroke="none" /> 328 + ) : null; 329 + 330 + const ResetButton = ({ onClick }: { onClick: () => void }) => ( 331 + <button 332 + onClick={onClick} 333 + className="mt-6 px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors" 334 + > 335 + Try Again 336 + </button> 337 + ); 338 + 339 + // Helper functions 340 + const getUnit = (label: string) => { 341 + switch (label) { 342 + case "accuracy": 343 + return "%"; 344 + case "wpm": 345 + return "WPM"; 346 + case "time": 347 + return "seconds"; 348 + case "errorCount": 349 + return "total"; 350 + default: 351 + return ""; 352 + } 353 + }; 354 + 355 + const calculateStats = ( 356 + userInput: string, 357 + errors: TypingError[], 358 + startTime: number, 359 + endTime: number, 360 + ): TypingStats => { 361 + const timeElapsed = (endTime - startTime) / 1000; 362 + const correctChars = userInput 363 + .split("") 364 + .filter((char, i) => char === SAMPLE_TEXT[i]).length; 365 + 366 + return { 367 + accuracy: ((correctChars / SAMPLE_TEXT.length) * 100).toFixed(1), 368 + wpm: (correctChars / 5 / (timeElapsed / 60)).toFixed(1), 369 + time: timeElapsed.toFixed(1), 370 + errorCount: errors.length, 371 + }; 372 + }; 373 + 374 + // Main component 375 + export const Route = createLazyFileRoute("/rnfgrertt/typing")({ 376 + component: () => { 377 + const { 378 + userInput, 379 + isFinished, 380 + startTime, 381 + endTime, 382 + errors, 383 + handleInput, 384 + resetTest, 385 + } = useTypingTest(); 386 + const { wpmData, resetWpmData } = useWpmTracker( 387 + startTime, 388 + isFinished, 389 + userInput, 390 + ); 391 + //const errorDistribution = useErrorTracker(errors); 392 + 393 + const resetAll = () => { 394 + resetTest(); 395 + resetWpmData(); 396 + }; 397 + 398 + const stats = 399 + startTime && endTime 400 + ? calculateStats(userInput, errors, startTime, endTime) 401 + : ({} as TypingStats); 402 + 403 + return ( 404 + <main className="h-screen relative max-h-[calc(100vh-5rem)] flex"> 405 + {isFinished ? ( 406 + <ResultsView stats={stats} wpmData={wpmData} resetTest={resetAll} /> 407 + ) : ( 408 + <TypingArea 409 + userInput={userInput} 410 + handleInput={(e) => 411 + handleInput((e.target as HTMLTextAreaElement).value) 412 + } 413 + /> 414 + )} 415 + </main> 416 + ); 417 + }, 418 + });
+69 -64
tailwind.config.js
··· 1 + import { fontFamily } from "tailwindcss/defaultTheme"; 2 + 1 3 /** @type {import('tailwindcss').Config} */ 2 - module.exports = { 3 - darkMode: ["class"], 4 - content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 5 - theme: { 6 - extend: { 7 - borderRadius: { 8 - lg: 'var(--radius)', 9 - md: 'calc(var(--radius) - 2px)', 10 - sm: 'calc(var(--radius) - 4px)' 11 - }, 12 - colors: { 13 - background: 'hsl(var(--background))', 14 - foreground: 'hsl(var(--foreground))', 15 - card: { 16 - DEFAULT: 'hsl(var(--card))', 17 - foreground: 'hsl(var(--card-foreground))' 18 - }, 19 - popover: { 20 - DEFAULT: 'hsl(var(--popover))', 21 - foreground: 'hsl(var(--popover-foreground))' 22 - }, 23 - primary: { 24 - DEFAULT: 'hsl(var(--primary))', 25 - foreground: 'hsl(var(--primary-foreground))' 26 - }, 27 - secondary: { 28 - DEFAULT: 'hsl(var(--secondary))', 29 - foreground: 'hsl(var(--secondary-foreground))' 30 - }, 31 - muted: { 32 - DEFAULT: 'hsl(var(--muted))', 33 - foreground: 'hsl(var(--muted-foreground))' 34 - }, 35 - accent: { 36 - DEFAULT: 'hsl(var(--accent))', 37 - foreground: 'hsl(var(--accent-foreground))' 38 - }, 39 - destructive: { 40 - DEFAULT: 'hsl(var(--destructive))', 41 - foreground: 'hsl(var(--destructive-foreground))' 42 - }, 43 - border: 'hsl(var(--border))', 44 - input: 'hsl(var(--input))', 45 - ring: 'hsl(var(--ring))', 46 - chart: { 47 - '1': 'hsl(var(--chart-1))', 48 - '2': 'hsl(var(--chart-2))', 49 - '3': 'hsl(var(--chart-3))', 50 - '4': 'hsl(var(--chart-4))', 51 - '5': 'hsl(var(--chart-5))' 52 - }, 53 - sidebar: { 54 - DEFAULT: 'hsl(var(--sidebar-background))', 55 - foreground: 'hsl(var(--sidebar-foreground))', 56 - primary: 'hsl(var(--sidebar-primary))', 57 - 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', 58 - accent: 'hsl(var(--sidebar-accent))', 59 - 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', 60 - border: 'hsl(var(--sidebar-border))', 61 - ring: 'hsl(var(--sidebar-ring))' 62 - } 63 - } 64 - } 4 + export const darkMode = ["class"]; 5 + export const content = ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"]; 6 + export const theme = { 7 + extend: { 8 + fontFamily: { 9 + sans: ["var(--font-sans)", ...fontFamily.sans], 10 + serif: ["var(--font-serif)", ...fontFamily.serif], 11 + mono: ["var(--font-mono)", ...fontFamily.mono], 12 + }, 13 + borderRadius: { 14 + lg: "var(--radius)", 15 + md: "calc(var(--radius) - 2px)", 16 + sm: "calc(var(--radius) - 4px)", 17 + }, 18 + colors: { 19 + background: "hsl(var(--background))", 20 + foreground: "hsl(var(--foreground))", 21 + card: { 22 + DEFAULT: "hsl(var(--card))", 23 + foreground: "hsl(var(--card-foreground))", 24 + }, 25 + popover: { 26 + DEFAULT: "hsl(var(--popover))", 27 + foreground: "hsl(var(--popover-foreground))", 28 + }, 29 + primary: { 30 + DEFAULT: "hsl(var(--primary))", 31 + foreground: "hsl(var(--primary-foreground))", 32 + }, 33 + secondary: { 34 + DEFAULT: "hsl(var(--secondary))", 35 + foreground: "hsl(var(--secondary-foreground))", 36 + }, 37 + muted: { 38 + DEFAULT: "hsl(var(--muted))", 39 + foreground: "hsl(var(--muted-foreground))", 40 + }, 41 + accent: { 42 + DEFAULT: "hsl(var(--accent))", 43 + foreground: "hsl(var(--accent-foreground))", 44 + }, 45 + destructive: { 46 + DEFAULT: "hsl(var(--destructive))", 47 + foreground: "hsl(var(--destructive-foreground))", 48 + }, 49 + border: "hsl(var(--border))", 50 + input: "hsl(var(--input))", 51 + ring: "hsl(var(--ring))", 52 + chart: { 53 + 1: "hsl(var(--chart-1))", 54 + 2: "hsl(var(--chart-2))", 55 + 3: "hsl(var(--chart-3))", 56 + 4: "hsl(var(--chart-4))", 57 + 5: "hsl(var(--chart-5))", 58 + }, 59 + sidebar: { 60 + DEFAULT: "hsl(var(--sidebar-background))", 61 + foreground: "hsl(var(--sidebar-foreground))", 62 + primary: "hsl(var(--sidebar-primary))", 63 + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", 64 + accent: "hsl(var(--sidebar-accent))", 65 + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", 66 + border: "hsl(var(--sidebar-border))", 67 + ring: "hsl(var(--sidebar-ring))", 68 + }, 69 + }, 65 70 }, 66 - plugins: [require("tailwindcss-animate")], 67 71 }; 72 + export const plugins = [require("tailwindcss-animate")];