this repo has no description
0
fork

Configure Feed

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

feat: money

+1747 -74
+1
package.json
··· 18 18 "next-themes": "^0.4.6", 19 19 "react": "19.2.5", 20 20 "react-dom": "19.2.5", 21 + "recharts": "3.8.0", 21 22 "shadcn": "^4.2.0", 22 23 "tailwind-merge": "^3.5.0", 23 24 "tw-animate-css": "^1.4.0"
+293
pnpm-lock.yaml
··· 32 32 react-dom: 33 33 specifier: 19.2.5 34 34 version: 19.2.5(react@19.2.5) 35 + recharts: 36 + specifier: 3.8.0 37 + version: 3.8.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.5)(react@19.2.5)(redux@5.0.1) 35 38 shadcn: 36 39 specifier: ^4.2.0 37 40 version: 4.2.0(@types/node@25.6.0)(typescript@6.0.2) ··· 629 632 '@open-draft/until@2.1.0': 630 633 resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} 631 634 635 + '@reduxjs/toolkit@2.11.2': 636 + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} 637 + peerDependencies: 638 + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 639 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 640 + peerDependenciesMeta: 641 + react: 642 + optional: true 643 + react-redux: 644 + optional: true 645 + 632 646 '@sec-ant/readable-stream@0.4.1': 633 647 resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} 634 648 635 649 '@sindresorhus/merge-streams@4.0.0': 636 650 resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} 637 651 engines: {node: '>=18'} 652 + 653 + '@standard-schema/spec@1.1.0': 654 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 655 + 656 + '@standard-schema/utils@0.3.0': 657 + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} 638 658 639 659 '@swc/helpers@0.5.15': 640 660 resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} ··· 734 754 '@ts-morph/common@0.27.0': 735 755 resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} 736 756 757 + '@types/d3-array@3.2.2': 758 + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} 759 + 760 + '@types/d3-color@3.1.3': 761 + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} 762 + 763 + '@types/d3-ease@3.0.2': 764 + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} 765 + 766 + '@types/d3-interpolate@3.0.4': 767 + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} 768 + 769 + '@types/d3-path@3.1.1': 770 + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} 771 + 772 + '@types/d3-scale@4.0.9': 773 + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} 774 + 775 + '@types/d3-shape@3.1.8': 776 + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} 777 + 778 + '@types/d3-time@3.0.4': 779 + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} 780 + 781 + '@types/d3-timer@3.0.2': 782 + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} 783 + 737 784 '@types/node@25.6.0': 738 785 resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} 739 786 ··· 747 794 748 795 '@types/statuses@2.0.6': 749 796 resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} 797 + 798 + '@types/use-sync-external-store@0.0.6': 799 + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} 750 800 751 801 '@types/validate-npm-package-name@4.0.2': 752 802 resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} ··· 937 987 csstype@3.2.3: 938 988 resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 939 989 990 + d3-array@3.2.4: 991 + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} 992 + engines: {node: '>=12'} 993 + 994 + d3-color@3.1.0: 995 + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} 996 + engines: {node: '>=12'} 997 + 998 + d3-ease@3.0.1: 999 + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} 1000 + engines: {node: '>=12'} 1001 + 1002 + d3-format@3.1.2: 1003 + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} 1004 + engines: {node: '>=12'} 1005 + 1006 + d3-interpolate@3.0.1: 1007 + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} 1008 + engines: {node: '>=12'} 1009 + 1010 + d3-path@3.1.0: 1011 + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} 1012 + engines: {node: '>=12'} 1013 + 1014 + d3-scale@4.0.2: 1015 + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} 1016 + engines: {node: '>=12'} 1017 + 1018 + d3-shape@3.2.0: 1019 + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} 1020 + engines: {node: '>=12'} 1021 + 1022 + d3-time-format@4.1.0: 1023 + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} 1024 + engines: {node: '>=12'} 1025 + 1026 + d3-time@3.1.0: 1027 + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} 1028 + engines: {node: '>=12'} 1029 + 1030 + d3-timer@3.0.1: 1031 + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} 1032 + engines: {node: '>=12'} 1033 + 940 1034 data-uri-to-buffer@4.0.1: 941 1035 resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 942 1036 engines: {node: '>= 12'} ··· 952 1046 peerDependenciesMeta: 953 1047 supports-color: 954 1048 optional: true 1049 + 1050 + decimal.js-light@2.5.1: 1051 + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} 955 1052 956 1053 dedent@1.7.2: 957 1054 resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} ··· 1040 1137 resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 1041 1138 engines: {node: '>= 0.4'} 1042 1139 1140 + es-toolkit@1.45.1: 1141 + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} 1142 + 1043 1143 escalade@3.2.0: 1044 1144 resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 1045 1145 engines: {node: '>=6'} ··· 1055 1155 etag@1.8.1: 1056 1156 resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 1057 1157 engines: {node: '>= 0.6'} 1158 + 1159 + eventemitter3@5.0.4: 1160 + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} 1058 1161 1059 1162 eventsource-parser@3.0.6: 1060 1163 resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} ··· 1228 1331 resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 1229 1332 engines: {node: '>= 4'} 1230 1333 1334 + immer@10.2.0: 1335 + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} 1336 + 1337 + immer@11.1.4: 1338 + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} 1339 + 1231 1340 import-fresh@3.3.1: 1232 1341 resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 1233 1342 engines: {node: '>=6'} ··· 1235 1344 inherits@2.0.4: 1236 1345 resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 1237 1346 1347 + internmap@2.0.3: 1348 + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} 1349 + engines: {node: '>=12'} 1350 + 1238 1351 ip-address@10.1.0: 1239 1352 resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} 1240 1353 engines: {node: '>= 12'} ··· 1714 1827 peerDependencies: 1715 1828 react: ^19.2.5 1716 1829 1830 + react-is@19.2.5: 1831 + resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==} 1832 + 1833 + react-redux@9.2.0: 1834 + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} 1835 + peerDependencies: 1836 + '@types/react': ^18.2.25 || ^19 1837 + react: ^18.0 || ^19 1838 + redux: ^5.0.0 1839 + peerDependenciesMeta: 1840 + '@types/react': 1841 + optional: true 1842 + redux: 1843 + optional: true 1844 + 1717 1845 react@19.2.5: 1718 1846 resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} 1719 1847 engines: {node: '>=0.10.0'} ··· 1721 1849 recast@0.23.11: 1722 1850 resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} 1723 1851 engines: {node: '>= 4'} 1852 + 1853 + recharts@3.8.0: 1854 + resolution: {integrity: sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==} 1855 + engines: {node: '>=18'} 1856 + peerDependencies: 1857 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1858 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1859 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1860 + 1861 + redux-thunk@3.1.0: 1862 + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} 1863 + peerDependencies: 1864 + redux: ^5.0.0 1865 + 1866 + redux@5.0.1: 1867 + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} 1724 1868 1725 1869 require-directory@2.1.1: 1726 1870 resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} ··· 1992 2136 vary@1.1.2: 1993 2137 resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 1994 2138 engines: {node: '>= 0.8'} 2139 + 2140 + victory-vendor@37.3.6: 2141 + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} 1995 2142 1996 2143 web-streams-polyfill@3.3.3: 1997 2144 resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} ··· 2584 2731 2585 2732 '@open-draft/until@2.1.0': {} 2586 2733 2734 + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)': 2735 + dependencies: 2736 + '@standard-schema/spec': 1.1.0 2737 + '@standard-schema/utils': 0.3.0 2738 + immer: 11.1.4 2739 + redux: 5.0.1 2740 + redux-thunk: 3.1.0(redux@5.0.1) 2741 + reselect: 5.1.1 2742 + optionalDependencies: 2743 + react: 19.2.5 2744 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1) 2745 + 2587 2746 '@sec-ant/readable-stream@0.4.1': {} 2588 2747 2589 2748 '@sindresorhus/merge-streams@4.0.0': {} 2749 + 2750 + '@standard-schema/spec@1.1.0': {} 2751 + 2752 + '@standard-schema/utils@0.3.0': {} 2590 2753 2591 2754 '@swc/helpers@0.5.15': 2592 2755 dependencies: ··· 2667 2830 minimatch: 10.2.5 2668 2831 path-browserify: 1.0.1 2669 2832 2833 + '@types/d3-array@3.2.2': {} 2834 + 2835 + '@types/d3-color@3.1.3': {} 2836 + 2837 + '@types/d3-ease@3.0.2': {} 2838 + 2839 + '@types/d3-interpolate@3.0.4': 2840 + dependencies: 2841 + '@types/d3-color': 3.1.3 2842 + 2843 + '@types/d3-path@3.1.1': {} 2844 + 2845 + '@types/d3-scale@4.0.9': 2846 + dependencies: 2847 + '@types/d3-time': 3.0.4 2848 + 2849 + '@types/d3-shape@3.1.8': 2850 + dependencies: 2851 + '@types/d3-path': 3.1.1 2852 + 2853 + '@types/d3-time@3.0.4': {} 2854 + 2855 + '@types/d3-timer@3.0.2': {} 2856 + 2670 2857 '@types/node@25.6.0': 2671 2858 dependencies: 2672 2859 undici-types: 7.19.2 ··· 2680 2867 csstype: 3.2.3 2681 2868 2682 2869 '@types/statuses@2.0.6': {} 2870 + 2871 + '@types/use-sync-external-store@0.0.6': {} 2683 2872 2684 2873 '@types/validate-npm-package-name@4.0.2': {} 2685 2874 ··· 2845 3034 2846 3035 csstype@3.2.3: {} 2847 3036 3037 + d3-array@3.2.4: 3038 + dependencies: 3039 + internmap: 2.0.3 3040 + 3041 + d3-color@3.1.0: {} 3042 + 3043 + d3-ease@3.0.1: {} 3044 + 3045 + d3-format@3.1.2: {} 3046 + 3047 + d3-interpolate@3.0.1: 3048 + dependencies: 3049 + d3-color: 3.1.0 3050 + 3051 + d3-path@3.1.0: {} 3052 + 3053 + d3-scale@4.0.2: 3054 + dependencies: 3055 + d3-array: 3.2.4 3056 + d3-format: 3.1.2 3057 + d3-interpolate: 3.0.1 3058 + d3-time: 3.1.0 3059 + d3-time-format: 4.1.0 3060 + 3061 + d3-shape@3.2.0: 3062 + dependencies: 3063 + d3-path: 3.1.0 3064 + 3065 + d3-time-format@4.1.0: 3066 + dependencies: 3067 + d3-time: 3.1.0 3068 + 3069 + d3-time@3.1.0: 3070 + dependencies: 3071 + d3-array: 3.2.4 3072 + 3073 + d3-timer@3.0.1: {} 3074 + 2848 3075 data-uri-to-buffer@4.0.1: {} 2849 3076 2850 3077 date-fns@4.1.0: {} ··· 2852 3079 debug@4.4.3: 2853 3080 dependencies: 2854 3081 ms: 2.1.3 3082 + 3083 + decimal.js-light@2.5.1: {} 2855 3084 2856 3085 dedent@1.7.2: {} 2857 3086 ··· 2916 3145 dependencies: 2917 3146 es-errors: 1.3.0 2918 3147 3148 + es-toolkit@1.45.1: {} 3149 + 2919 3150 escalade@3.2.0: {} 2920 3151 2921 3152 escape-html@1.0.3: {} ··· 2923 3154 esprima@4.0.1: {} 2924 3155 2925 3156 etag@1.8.1: {} 3157 + 3158 + eventemitter3@5.0.4: {} 2926 3159 2927 3160 eventsource-parser@3.0.6: {} 2928 3161 ··· 3135 3368 3136 3369 ignore@5.3.2: {} 3137 3370 3371 + immer@10.2.0: {} 3372 + 3373 + immer@11.1.4: {} 3374 + 3138 3375 import-fresh@3.3.1: 3139 3376 dependencies: 3140 3377 parent-module: 1.0.1 3141 3378 resolve-from: 4.0.0 3142 3379 3143 3380 inherits@2.0.4: {} 3381 + 3382 + internmap@2.0.3: {} 3144 3383 3145 3384 ip-address@10.1.0: {} 3146 3385 ··· 3536 3775 react: 19.2.5 3537 3776 scheduler: 0.27.0 3538 3777 3778 + react-is@19.2.5: {} 3779 + 3780 + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1): 3781 + dependencies: 3782 + '@types/use-sync-external-store': 0.0.6 3783 + react: 19.2.5 3784 + use-sync-external-store: 1.6.0(react@19.2.5) 3785 + optionalDependencies: 3786 + '@types/react': 19.2.14 3787 + redux: 5.0.1 3788 + 3539 3789 react@19.2.5: {} 3540 3790 3541 3791 recast@0.23.11: ··· 3546 3796 tiny-invariant: 1.3.3 3547 3797 tslib: 2.8.1 3548 3798 3799 + recharts@3.8.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.5)(react@19.2.5)(redux@5.0.1): 3800 + dependencies: 3801 + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5) 3802 + clsx: 2.1.1 3803 + decimal.js-light: 2.5.1 3804 + es-toolkit: 1.45.1 3805 + eventemitter3: 5.0.4 3806 + immer: 10.2.0 3807 + react: 19.2.5 3808 + react-dom: 19.2.5(react@19.2.5) 3809 + react-is: 19.2.5 3810 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1) 3811 + reselect: 5.1.1 3812 + tiny-invariant: 1.3.3 3813 + use-sync-external-store: 1.6.0(react@19.2.5) 3814 + victory-vendor: 37.3.6 3815 + transitivePeerDependencies: 3816 + - '@types/react' 3817 + - redux 3818 + 3819 + redux-thunk@3.1.0(redux@5.0.1): 3820 + dependencies: 3821 + redux: 5.0.1 3822 + 3823 + redux@5.0.1: {} 3824 + 3549 3825 require-directory@2.1.1: {} 3550 3826 3551 3827 require-from-string@2.0.2: {} ··· 3857 4133 validate-npm-package-name@7.0.2: {} 3858 4134 3859 4135 vary@1.1.2: {} 4136 + 4137 + victory-vendor@37.3.6: 4138 + dependencies: 4139 + '@types/d3-array': 3.2.2 4140 + '@types/d3-ease': 3.0.2 4141 + '@types/d3-interpolate': 3.0.4 4142 + '@types/d3-scale': 4.0.9 4143 + '@types/d3-shape': 3.1.8 4144 + '@types/d3-time': 3.0.4 4145 + '@types/d3-timer': 3.0.2 4146 + d3-array: 3.2.4 4147 + d3-ease: 3.0.1 4148 + d3-interpolate: 3.0.1 4149 + d3-scale: 4.0.2 4150 + d3-shape: 3.2.0 4151 + d3-time: 3.1.0 4152 + d3-timer: 3.0.1 3860 4153 3861 4154 web-streams-polyfill@3.3.3: {} 3862 4155
+186
src/app/calculator/page.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useState } from "react"; 4 + import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; 5 + import { 6 + Card, 7 + CardContent, 8 + CardDescription, 9 + CardHeader, 10 + CardTitle, 11 + } from "@/components/ui/card"; 12 + import type { ChartConfig } from "@/components/ui/chart"; 13 + import { 14 + ChartContainer, 15 + ChartTooltip, 16 + ChartTooltipContent, 17 + } from "@/components/ui/chart"; 18 + import { 19 + InputGroup, 20 + InputGroupAddon, 21 + InputGroupInput, 22 + } from "@/components/ui/input-group"; 23 + import { Label } from "@/components/ui/label"; 24 + 25 + const chartConfig = { 26 + openingBalance: { label: "Principal + Contribution", color: "#3b82f6" }, 27 + interest: { label: "Interest", color: "#14b8a6" }, 28 + } satisfies ChartConfig; 29 + 30 + interface GrowthHistory { 31 + year: number; 32 + interest: number; 33 + openingBalance: number; 34 + balance: number; 35 + } 36 + 37 + const calculateYearlyGrowth = ( 38 + principal: number, 39 + pmt: number, 40 + rate: number, 41 + years: number, 42 + ): GrowthHistory[] => { 43 + const r = rate / 100; 44 + let balance = principal; 45 + const history: GrowthHistory[] = []; 46 + 47 + for (let year = 1; year <= years; year++) { 48 + const openingBalance = balance + pmt; 49 + const interest = openingBalance * r; 50 + balance = openingBalance + interest; 51 + history.push({ 52 + year, 53 + interest: parseFloat(interest.toFixed(2)), 54 + openingBalance: parseFloat(openingBalance.toFixed(2)), 55 + balance: parseFloat(balance.toFixed(2)), 56 + }); 57 + } 58 + return history; 59 + }; 60 + 61 + export default function CalculatorPage() { 62 + const [principal, setPrincipal] = useState("1000"); 63 + const [pmt, setPmt] = useState("500"); 64 + const [rate, setRate] = useState("7"); 65 + const [years, setYears] = useState("5"); 66 + const [history, setHistory] = useState<GrowthHistory[]>([]); 67 + 68 + useEffect(() => { 69 + const p = parseFloat(principal); 70 + const m = parseFloat(pmt); 71 + const r = parseFloat(rate); 72 + const y = parseInt(years, 10); 73 + 74 + if ( 75 + !Number.isNaN(p) && 76 + !Number.isNaN(m) && 77 + !Number.isNaN(r) && 78 + !Number.isNaN(y) && 79 + y > 0 80 + ) { 81 + setHistory(calculateYearlyGrowth(p, m, r, y)); 82 + } else { 83 + setHistory([]); 84 + } 85 + }, [principal, pmt, rate, years]); 86 + 87 + return ( 88 + <main className="flex flex-1 items-center justify-center p-4"> 89 + <Card className="w-full max-w-4xl"> 90 + <CardHeader> 91 + <CardTitle>Growth Calculator</CardTitle> 92 + <CardDescription> 93 + Project the growth of your investment over time, year by year. 94 + </CardDescription> 95 + </CardHeader> 96 + <CardContent> 97 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> 98 + <div className="space-y-2"> 99 + <Label htmlFor="principal">Principal</Label> 100 + <InputGroup> 101 + <InputGroupAddon>$</InputGroupAddon> 102 + <InputGroupInput 103 + id="principal" 104 + type="number" 105 + value={principal} 106 + onChange={(e) => setPrincipal(e.target.value)} 107 + /> 108 + </InputGroup> 109 + </div> 110 + <div className="space-y-2"> 111 + <Label htmlFor="pmt">Annual Contribution</Label> 112 + <InputGroup> 113 + <InputGroupAddon>$</InputGroupAddon> 114 + <InputGroupInput 115 + id="pmt" 116 + type="number" 117 + value={pmt} 118 + onChange={(e) => setPmt(e.target.value)} 119 + /> 120 + </InputGroup> 121 + </div> 122 + <div className="space-y-2"> 123 + <Label htmlFor="rate">Interest Rate</Label> 124 + <InputGroup> 125 + <InputGroupInput 126 + id="rate" 127 + type="number" 128 + value={rate} 129 + onChange={(e) => setRate(e.target.value)} 130 + /> 131 + <InputGroupAddon>%</InputGroupAddon> 132 + </InputGroup> 133 + </div> 134 + <div className="space-y-2"> 135 + <Label htmlFor="years">Years</Label> 136 + <InputGroup> 137 + <InputGroupInput 138 + id="years" 139 + type="number" 140 + value={years} 141 + onChange={(e) => setYears(e.target.value)} 142 + /> 143 + </InputGroup> 144 + </div> 145 + </div> 146 + 147 + {history.length > 0 && ( 148 + <div className="mt-8"> 149 + <ChartContainer 150 + config={chartConfig} 151 + className="min-h-[200px] w-full" 152 + > 153 + <BarChart accessibilityLayer data={history}> 154 + <CartesianGrid vertical={false} /> 155 + <XAxis 156 + dataKey="year" 157 + tickLine={false} 158 + tickMargin={10} 159 + axisLine={false} 160 + tickFormatter={(value) => `Year ${value}`} 161 + /> 162 + <YAxis 163 + tickFormatter={(value) => `$${value.toLocaleString()}`} 164 + /> 165 + <ChartTooltip content={<ChartTooltipContent />} /> 166 + <Bar 167 + dataKey="openingBalance" 168 + stackId="a" 169 + fill="var(--color-openingBalance)" 170 + radius={[0, 0, 4, 4]} 171 + /> 172 + <Bar 173 + dataKey="interest" 174 + stackId="a" 175 + fill="var(--color-interest)" 176 + radius={[4, 4, 0, 0]} 177 + /> 178 + </BarChart> 179 + </ChartContainer> 180 + </div> 181 + )} 182 + </CardContent> 183 + </Card> 184 + </main> 185 + ); 186 + }
+17 -2
src/app/clock/page.tsx
··· 1 - import ClockView from "@/components/clock-view"; 1 + "use client"; 2 + 3 + import { useEffect, useState } from "react"; 2 4 3 5 export default function ClockPage() { 6 + const [time, setTime] = useState(""); 7 + 8 + useEffect(() => { 9 + const timerId = setInterval(() => { 10 + setTime(new Date().toLocaleTimeString("en-US")); 11 + }, 1000); 12 + return () => clearInterval(timerId); 13 + }, []); 14 + 4 15 return ( 5 16 <main className="flex flex-1 items-center justify-center"> 6 - <ClockView /> 17 + {time && ( 18 + <h1 className="text-7xl font-black tracking-tighter text-primary"> 19 + {time} 20 + </h1> 21 + )} 7 22 </main> 8 23 ); 9 24 }
+24 -2
src/app/layout.tsx
··· 1 1 import type { Metadata } from "next"; 2 + import Link from "next/link"; 2 3 import "./globals.css"; 3 4 import { Inter } from "next/font/google"; 4 - import { MainNav } from "@/components/main-nav"; 5 5 import { ThemeProvider } from "@/components/theme-provider"; 6 + import { 7 + NavigationMenu, 8 + NavigationMenuItem, 9 + NavigationMenuLink, 10 + NavigationMenuList, 11 + } from "@/components/ui/navigation-menu"; 6 12 import { cn } from "@/lib/utils"; 7 13 8 14 const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); ··· 12 18 description: "Don't use it", 13 19 }; 14 20 21 + const navItems = [ 22 + { href: "/", label: "Home" }, 23 + { href: "/clock", label: "Clock" }, 24 + { href: "/calculator", label: "Calculator" }, 25 + ]; 26 + 15 27 export default function RootLayout({ 16 28 children, 17 29 }: Readonly<{ ··· 31 43 disableTransitionOnChange 32 44 > 33 45 <header className="p-4 flex justify-center"> 34 - <MainNav /> 46 + <NavigationMenu> 47 + <NavigationMenuList> 48 + {navItems.map((item) => ( 49 + <NavigationMenuItem key={item.href}> 50 + <Link href={item.href} legacyBehavior passHref> 51 + <NavigationMenuLink>{item.label}</NavigationMenuLink> 52 + </Link> 53 + </NavigationMenuItem> 54 + ))} 55 + </NavigationMenuList> 56 + </NavigationMenu> 35 57 </header> 36 58 {children} 37 59 </ThemeProvider>
+14 -16
src/app/page.tsx
··· 1 - export default function Home() { 2 - return ( 3 - <main className="flex flex-1 flex-col items-center justify-center p-24"> 4 - <div className="text-center"> 5 - <h1 className="text-6xl font-extrabold tracking-tighter"> 6 - <span className="bg-gradient-to-r from-blue-500 to-teal-400 bg-clip-text text-transparent"> 7 - Very Good Tools 8 - </span> 9 - </h1> 10 - <p className="mt-4 text-lg text-muted-foreground"> 11 - A collection of simple, yet powerful tools. 12 - </p> 13 - </div> 14 - </main> 15 - ); 16 - } 1 + const HomePage = () => ( 2 + <main className="flex flex-1 flex-col items-center justify-center p-24 text-center"> 3 + <h1 className="text-6xl font-extrabold tracking-tighter"> 4 + <span className="bg-gradient-to-r from-blue-500 to-teal-400 bg-clip-text text-transparent"> 5 + Very Good Tools 6 + </span> 7 + </h1> 8 + <p className="mt-4 text-lg text-muted-foreground"> 9 + A collection of simple, yet powerful tools. 10 + </p> 11 + </main> 12 + ); 13 + 14 + export default HomePage;
-26
src/components/clock-view.tsx
··· 1 - "use client"; 2 - 3 - import { useEffect, useState } from "react"; 4 - 5 - export default function ClockView() { 6 - const [time, setTime] = useState(""); 7 - 8 - useEffect(() => { 9 - setTime(new Date().toLocaleTimeString("en-US")); 10 - const id = setInterval( 11 - () => setTime(new Date().toLocaleTimeString("en-US")), 12 - 1000, 13 - ); 14 - return () => clearInterval(id); 15 - }, []); 16 - 17 - if (!time) return null; 18 - 19 - return ( 20 - <div className="flex h-full items-center justify-center"> 21 - <h1 className="text-7xl font-black tracking-tighter text-primary"> 22 - {time} 23 - </h1> 24 - </div> 25 - ); 26 - }
-28
src/components/main-nav.tsx
··· 1 - "use client"; 2 - 3 - import Link from "next/link"; 4 - import { 5 - NavigationMenu, 6 - NavigationMenuItem, 7 - NavigationMenuLink, 8 - NavigationMenuList, 9 - } from "@/components/ui/navigation-menu"; 10 - 11 - export function MainNav() { 12 - return ( 13 - <NavigationMenu> 14 - <NavigationMenuList> 15 - <NavigationMenuItem> 16 - <Link href="/" legacyBehavior passHref> 17 - <NavigationMenuLink>Home</NavigationMenuLink> 18 - </Link> 19 - </NavigationMenuItem> 20 - <NavigationMenuItem> 21 - <Link href="/clock" legacyBehavior passHref> 22 - <NavigationMenuLink>Clock</NavigationMenuLink> 23 - </Link> 24 - </NavigationMenuItem> 25 - </NavigationMenuList> 26 - </NavigationMenu> 27 - ); 28 - }
+58
src/components/ui/button.tsx
··· 1 + import { Button as ButtonPrimitive } from "@base-ui/react/button"; 2 + import { cva, type VariantProps } from "class-variance-authority"; 3 + 4 + import { cn } from "@/lib/utils"; 5 + 6 + const buttonVariants = cva( 7 + "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 8 + { 9 + variants: { 10 + variant: { 11 + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", 12 + outline: 13 + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", 14 + secondary: 15 + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", 16 + ghost: 17 + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", 18 + destructive: 19 + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", 20 + link: "text-primary underline-offset-4 hover:underline", 21 + }, 22 + size: { 23 + default: 24 + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", 25 + xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", 26 + sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", 27 + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", 28 + icon: "size-8", 29 + "icon-xs": 30 + "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", 31 + "icon-sm": 32 + "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", 33 + "icon-lg": "size-9", 34 + }, 35 + }, 36 + defaultVariants: { 37 + variant: "default", 38 + size: "default", 39 + }, 40 + }, 41 + ); 42 + 43 + function Button({ 44 + className, 45 + variant = "default", 46 + size = "default", 47 + ...props 48 + }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) { 49 + return ( 50 + <ButtonPrimitive 51 + data-slot="button" 52 + className={cn(buttonVariants({ variant, size, className }))} 53 + {...props} 54 + /> 55 + ); 56 + } 57 + 58 + export { Button, buttonVariants };
+103
src/components/ui/card.tsx
··· 1 + import type * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Card({ 6 + className, 7 + size = "default", 8 + ...props 9 + }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { 10 + return ( 11 + <div 12 + data-slot="card" 13 + data-size={size} 14 + className={cn( 15 + "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", 16 + className, 17 + )} 18 + {...props} 19 + /> 20 + ); 21 + } 22 + 23 + function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 24 + return ( 25 + <div 26 + data-slot="card-header" 27 + className={cn( 28 + "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3", 29 + className, 30 + )} 31 + {...props} 32 + /> 33 + ); 34 + } 35 + 36 + function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 37 + return ( 38 + <div 39 + data-slot="card-title" 40 + className={cn( 41 + "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", 42 + className, 43 + )} 44 + {...props} 45 + /> 46 + ); 47 + } 48 + 49 + function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 50 + return ( 51 + <div 52 + data-slot="card-description" 53 + className={cn("text-sm text-muted-foreground", className)} 54 + {...props} 55 + /> 56 + ); 57 + } 58 + 59 + function CardAction({ className, ...props }: React.ComponentProps<"div">) { 60 + return ( 61 + <div 62 + data-slot="card-action" 63 + className={cn( 64 + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", 65 + className, 66 + )} 67 + {...props} 68 + /> 69 + ); 70 + } 71 + 72 + function CardContent({ className, ...props }: React.ComponentProps<"div">) { 73 + return ( 74 + <div 75 + data-slot="card-content" 76 + className={cn("px-4 group-data-[size=sm]/card:px-3", className)} 77 + {...props} 78 + /> 79 + ); 80 + } 81 + 82 + function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 83 + return ( 84 + <div 85 + data-slot="card-footer" 86 + className={cn( 87 + "flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3", 88 + className, 89 + )} 90 + {...props} 91 + /> 92 + ); 93 + } 94 + 95 + export { 96 + Card, 97 + CardAction, 98 + CardContent, 99 + CardDescription, 100 + CardFooter, 101 + CardHeader, 102 + CardTitle, 103 + };
+373
src/components/ui/chart.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import type { TooltipValueType } from "recharts"; 5 + import * as RechartsPrimitive from "recharts"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + // Format: { THEME_NAME: CSS_SELECTOR } 10 + const THEMES = { light: "", dark: ".dark" } as const; 11 + 12 + const INITIAL_DIMENSION = { width: 320, height: 200 } as const; 13 + type TooltipNameType = number | string; 14 + 15 + export type ChartConfig = Record< 16 + string, 17 + { 18 + label?: React.ReactNode; 19 + icon?: React.ComponentType; 20 + } & ( 21 + | { color?: string; theme?: never } 22 + | { color?: never; theme: Record<keyof typeof THEMES, string> } 23 + ) 24 + >; 25 + 26 + type ChartContextProps = { 27 + config: ChartConfig; 28 + }; 29 + 30 + const ChartContext = React.createContext<ChartContextProps | null>(null); 31 + 32 + function useChart() { 33 + const context = React.useContext(ChartContext); 34 + 35 + if (!context) { 36 + throw new Error("useChart must be used within a <ChartContainer />"); 37 + } 38 + 39 + return context; 40 + } 41 + 42 + function ChartContainer({ 43 + id, 44 + className, 45 + children, 46 + config, 47 + initialDimension = INITIAL_DIMENSION, 48 + ...props 49 + }: React.ComponentProps<"div"> & { 50 + config: ChartConfig; 51 + children: React.ComponentProps< 52 + typeof RechartsPrimitive.ResponsiveContainer 53 + >["children"]; 54 + initialDimension?: { 55 + width: number; 56 + height: number; 57 + }; 58 + }) { 59 + const uniqueId = React.useId(); 60 + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`; 61 + 62 + return ( 63 + <ChartContext.Provider value={{ config }}> 64 + <div 65 + data-slot="chart" 66 + data-chart={chartId} 67 + className={cn( 68 + "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", 69 + className, 70 + )} 71 + {...props} 72 + > 73 + <ChartStyle id={chartId} config={config} /> 74 + <RechartsPrimitive.ResponsiveContainer 75 + initialDimension={initialDimension} 76 + > 77 + {children} 78 + </RechartsPrimitive.ResponsiveContainer> 79 + </div> 80 + </ChartContext.Provider> 81 + ); 82 + } 83 + 84 + const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 85 + const colorConfig = Object.entries(config).filter( 86 + ([, config]) => config.theme ?? config.color, 87 + ); 88 + 89 + if (!colorConfig.length) { 90 + return null; 91 + } 92 + 93 + return ( 94 + <style 95 + dangerouslySetInnerHTML={{ 96 + __html: Object.entries(THEMES) 97 + .map( 98 + ([theme, prefix]) => ` 99 + ${prefix} [data-chart=${id}] { 100 + ${colorConfig 101 + .map(([key, itemConfig]) => { 102 + const color = 103 + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ?? 104 + itemConfig.color; 105 + return color ? ` --color-${key}: ${color};` : null; 106 + }) 107 + .join("\n")} 108 + } 109 + `, 110 + ) 111 + .join("\n"), 112 + }} 113 + /> 114 + ); 115 + }; 116 + 117 + const ChartTooltip = RechartsPrimitive.Tooltip; 118 + 119 + function ChartTooltipContent({ 120 + active, 121 + payload, 122 + className, 123 + indicator = "dot", 124 + hideLabel = false, 125 + hideIndicator = false, 126 + label, 127 + labelFormatter, 128 + labelClassName, 129 + formatter, 130 + color, 131 + nameKey, 132 + labelKey, 133 + }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & 134 + React.ComponentProps<"div"> & { 135 + hideLabel?: boolean; 136 + hideIndicator?: boolean; 137 + indicator?: "line" | "dot" | "dashed"; 138 + nameKey?: string; 139 + labelKey?: string; 140 + } & Omit< 141 + RechartsPrimitive.DefaultTooltipContentProps< 142 + TooltipValueType, 143 + TooltipNameType 144 + >, 145 + "accessibilityLayer" 146 + >) { 147 + const { config } = useChart(); 148 + 149 + const tooltipLabel = React.useMemo(() => { 150 + if (hideLabel || !payload?.length) { 151 + return null; 152 + } 153 + 154 + const [item] = payload; 155 + const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`; 156 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 157 + const value = 158 + !labelKey && typeof label === "string" 159 + ? (config[label]?.label ?? label) 160 + : itemConfig?.label; 161 + 162 + if (labelFormatter) { 163 + return ( 164 + <div className={cn("font-medium", labelClassName)}> 165 + {labelFormatter(value, payload)} 166 + </div> 167 + ); 168 + } 169 + 170 + if (!value) { 171 + return null; 172 + } 173 + 174 + return <div className={cn("font-medium", labelClassName)}>{value}</div>; 175 + }, [ 176 + label, 177 + labelFormatter, 178 + payload, 179 + hideLabel, 180 + labelClassName, 181 + config, 182 + labelKey, 183 + ]); 184 + 185 + if (!active || !payload?.length) { 186 + return null; 187 + } 188 + 189 + const nestLabel = payload.length === 1 && indicator !== "dot"; 190 + 191 + return ( 192 + <div 193 + className={cn( 194 + "grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", 195 + className, 196 + )} 197 + > 198 + {!nestLabel ? tooltipLabel : null} 199 + <div className="grid gap-1.5"> 200 + {payload 201 + .filter((item) => item.type !== "none") 202 + .map((item, index) => { 203 + const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; 204 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 205 + const indicatorColor = color ?? item.payload?.fill ?? item.color; 206 + 207 + return ( 208 + <div 209 + key={index} 210 + className={cn( 211 + "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", 212 + indicator === "dot" && "items-center", 213 + )} 214 + > 215 + {formatter && item?.value !== undefined && item.name ? ( 216 + formatter(item.value, item.name, item, index, item.payload) 217 + ) : ( 218 + <> 219 + {itemConfig?.icon ? ( 220 + <itemConfig.icon /> 221 + ) : ( 222 + !hideIndicator && ( 223 + <div 224 + className={cn( 225 + "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", 226 + { 227 + "h-2.5 w-2.5": indicator === "dot", 228 + "w-1": indicator === "line", 229 + "w-0 border-[1.5px] border-dashed bg-transparent": 230 + indicator === "dashed", 231 + "my-0.5": nestLabel && indicator === "dashed", 232 + }, 233 + )} 234 + style={ 235 + { 236 + "--color-bg": indicatorColor, 237 + "--color-border": indicatorColor, 238 + } as React.CSSProperties 239 + } 240 + /> 241 + ) 242 + )} 243 + <div 244 + className={cn( 245 + "flex flex-1 justify-between leading-none", 246 + nestLabel ? "items-end" : "items-center", 247 + )} 248 + > 249 + <div className="grid gap-1.5"> 250 + {nestLabel ? tooltipLabel : null} 251 + <span className="text-muted-foreground"> 252 + {itemConfig?.label ?? item.name} 253 + </span> 254 + </div> 255 + {item.value != null && ( 256 + <span className="font-mono font-medium text-foreground tabular-nums"> 257 + {typeof item.value === "number" 258 + ? item.value.toLocaleString() 259 + : String(item.value)} 260 + </span> 261 + )} 262 + </div> 263 + </> 264 + )} 265 + </div> 266 + ); 267 + })} 268 + </div> 269 + </div> 270 + ); 271 + } 272 + 273 + const ChartLegend = RechartsPrimitive.Legend; 274 + 275 + function ChartLegendContent({ 276 + className, 277 + hideIcon = false, 278 + payload, 279 + verticalAlign = "bottom", 280 + nameKey, 281 + }: React.ComponentProps<"div"> & { 282 + hideIcon?: boolean; 283 + nameKey?: string; 284 + } & RechartsPrimitive.DefaultLegendContentProps) { 285 + const { config } = useChart(); 286 + 287 + if (!payload?.length) { 288 + return null; 289 + } 290 + 291 + return ( 292 + <div 293 + className={cn( 294 + "flex items-center justify-center gap-4", 295 + verticalAlign === "top" ? "pb-3" : "pt-3", 296 + className, 297 + )} 298 + > 299 + {payload 300 + .filter((item) => item.type !== "none") 301 + .map((item, index) => { 302 + const key = `${nameKey ?? item.dataKey ?? "value"}`; 303 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 304 + 305 + return ( 306 + <div 307 + key={index} 308 + className={cn( 309 + "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", 310 + )} 311 + > 312 + {itemConfig?.icon && !hideIcon ? ( 313 + <itemConfig.icon /> 314 + ) : ( 315 + <div 316 + className="h-2 w-2 shrink-0 rounded-[2px]" 317 + style={{ 318 + backgroundColor: item.color, 319 + }} 320 + /> 321 + )} 322 + {itemConfig?.label} 323 + </div> 324 + ); 325 + })} 326 + </div> 327 + ); 328 + } 329 + 330 + function getPayloadConfigFromPayload( 331 + config: ChartConfig, 332 + payload: unknown, 333 + key: string, 334 + ) { 335 + if (typeof payload !== "object" || payload === null) { 336 + return undefined; 337 + } 338 + 339 + const payloadPayload = 340 + "payload" in payload && 341 + typeof payload.payload === "object" && 342 + payload.payload !== null 343 + ? payload.payload 344 + : undefined; 345 + 346 + let configLabelKey: string = key; 347 + 348 + if ( 349 + key in payload && 350 + typeof payload[key as keyof typeof payload] === "string" 351 + ) { 352 + configLabelKey = payload[key as keyof typeof payload] as string; 353 + } else if ( 354 + payloadPayload && 355 + key in payloadPayload && 356 + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" 357 + ) { 358 + configLabelKey = payloadPayload[ 359 + key as keyof typeof payloadPayload 360 + ] as string; 361 + } 362 + 363 + return configLabelKey in config ? config[configLabelKey] : config[key]; 364 + } 365 + 366 + export { 367 + ChartContainer, 368 + ChartLegend, 369 + ChartLegendContent, 370 + ChartStyle, 371 + ChartTooltip, 372 + ChartTooltipContent, 373 + };
+237
src/components/ui/field.tsx
··· 1 + "use client"; 2 + 3 + import { cva, type VariantProps } from "class-variance-authority"; 4 + import { useMemo } from "react"; 5 + import { Label } from "@/components/ui/label"; 6 + import { Separator } from "@/components/ui/separator"; 7 + import { cn } from "@/lib/utils"; 8 + 9 + function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { 10 + return ( 11 + <fieldset 12 + data-slot="field-set" 13 + className={cn( 14 + "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", 15 + className, 16 + )} 17 + {...props} 18 + /> 19 + ); 20 + } 21 + 22 + function FieldLegend({ 23 + className, 24 + variant = "legend", 25 + ...props 26 + }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { 27 + return ( 28 + <legend 29 + data-slot="field-legend" 30 + data-variant={variant} 31 + className={cn( 32 + "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base", 33 + className, 34 + )} 35 + {...props} 36 + /> 37 + ); 38 + } 39 + 40 + function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { 41 + return ( 42 + <div 43 + data-slot="field-group" 44 + className={cn( 45 + "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4", 46 + className, 47 + )} 48 + {...props} 49 + /> 50 + ); 51 + } 52 + 53 + const fieldVariants = cva( 54 + "group/field flex w-full gap-2 data-[invalid=true]:text-destructive", 55 + { 56 + variants: { 57 + orientation: { 58 + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", 59 + horizontal: 60 + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", 61 + responsive: 62 + "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", 63 + }, 64 + }, 65 + defaultVariants: { 66 + orientation: "vertical", 67 + }, 68 + }, 69 + ); 70 + 71 + function Field({ 72 + className, 73 + orientation = "vertical", 74 + ...props 75 + }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { 76 + return ( 77 + <div 78 + role="group" 79 + data-slot="field" 80 + data-orientation={orientation} 81 + className={cn(fieldVariants({ orientation }), className)} 82 + {...props} 83 + /> 84 + ); 85 + } 86 + 87 + function FieldContent({ className, ...props }: React.ComponentProps<"div">) { 88 + return ( 89 + <div 90 + data-slot="field-content" 91 + className={cn( 92 + "group/field-content flex flex-1 flex-col gap-0.5 leading-snug", 93 + className, 94 + )} 95 + {...props} 96 + /> 97 + ); 98 + } 99 + 100 + function FieldLabel({ 101 + className, 102 + ...props 103 + }: React.ComponentProps<typeof Label>) { 104 + return ( 105 + <Label 106 + data-slot="field-label" 107 + className={cn( 108 + "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10", 109 + "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", 110 + className, 111 + )} 112 + {...props} 113 + /> 114 + ); 115 + } 116 + 117 + function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { 118 + return ( 119 + <div 120 + data-slot="field-label" 121 + className={cn( 122 + "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50", 123 + className, 124 + )} 125 + {...props} 126 + /> 127 + ); 128 + } 129 + 130 + function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { 131 + return ( 132 + <p 133 + data-slot="field-description" 134 + className={cn( 135 + "text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5", 136 + "last:mt-0 nth-last-2:-mt-1", 137 + "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", 138 + className, 139 + )} 140 + {...props} 141 + /> 142 + ); 143 + } 144 + 145 + function FieldSeparator({ 146 + children, 147 + className, 148 + ...props 149 + }: React.ComponentProps<"div"> & { 150 + children?: React.ReactNode; 151 + }) { 152 + return ( 153 + <div 154 + data-slot="field-separator" 155 + data-content={!!children} 156 + className={cn( 157 + "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", 158 + className, 159 + )} 160 + {...props} 161 + > 162 + <Separator className="absolute inset-0 top-1/2" /> 163 + {children && ( 164 + <span 165 + className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground" 166 + data-slot="field-separator-content" 167 + > 168 + {children} 169 + </span> 170 + )} 171 + </div> 172 + ); 173 + } 174 + 175 + function FieldError({ 176 + className, 177 + children, 178 + errors, 179 + ...props 180 + }: React.ComponentProps<"div"> & { 181 + errors?: Array<{ message?: string } | undefined>; 182 + }) { 183 + const content = useMemo(() => { 184 + if (children) { 185 + return children; 186 + } 187 + 188 + if (!errors?.length) { 189 + return null; 190 + } 191 + 192 + const uniqueErrors = [ 193 + ...new Map(errors.map((error) => [error?.message, error])).values(), 194 + ]; 195 + 196 + if (uniqueErrors?.length === 1) { 197 + return uniqueErrors[0]?.message; 198 + } 199 + 200 + return ( 201 + <ul className="ml-4 flex list-disc flex-col gap-1"> 202 + {uniqueErrors.map( 203 + (error, index) => 204 + error?.message && <li key={index}>{error.message}</li>, 205 + )} 206 + </ul> 207 + ); 208 + }, [children, errors]); 209 + 210 + if (!content) { 211 + return null; 212 + } 213 + 214 + return ( 215 + <div 216 + role="alert" 217 + data-slot="field-error" 218 + className={cn("text-sm font-normal text-destructive", className)} 219 + {...props} 220 + > 221 + {content} 222 + </div> 223 + ); 224 + } 225 + 226 + export { 227 + Field, 228 + FieldContent, 229 + FieldDescription, 230 + FieldError, 231 + FieldGroup, 232 + FieldLabel, 233 + FieldLegend, 234 + FieldSeparator, 235 + FieldSet, 236 + FieldTitle, 237 + };
+157
src/components/ui/input-group.tsx
··· 1 + "use client"; 2 + 3 + import { cva, type VariantProps } from "class-variance-authority"; 4 + import type * as React from "react"; 5 + import { Button } from "@/components/ui/button"; 6 + import { Input } from "@/components/ui/input"; 7 + import { Textarea } from "@/components/ui/textarea"; 8 + import { cn } from "@/lib/utils"; 9 + 10 + function InputGroup({ className, ...props }: React.ComponentProps<"div">) { 11 + return ( 12 + <div 13 + data-slot="input-group" 14 + role="group" 15 + className={cn( 16 + "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", 17 + className, 18 + )} 19 + {...props} 20 + /> 21 + ); 22 + } 23 + 24 + const inputGroupAddonVariants = cva( 25 + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", 26 + { 27 + variants: { 28 + align: { 29 + "inline-start": 30 + "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", 31 + "inline-end": 32 + "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", 33 + "block-start": 34 + "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", 35 + "block-end": 36 + "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", 37 + }, 38 + }, 39 + defaultVariants: { 40 + align: "inline-start", 41 + }, 42 + }, 43 + ); 44 + 45 + function InputGroupAddon({ 46 + className, 47 + align = "inline-start", 48 + ...props 49 + }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { 50 + return ( 51 + <div 52 + role="group" 53 + data-slot="input-group-addon" 54 + data-align={align} 55 + className={cn(inputGroupAddonVariants({ align }), className)} 56 + onClick={(e) => { 57 + if ((e.target as HTMLElement).closest("button")) { 58 + return; 59 + } 60 + e.currentTarget.parentElement?.querySelector("input")?.focus(); 61 + }} 62 + {...props} 63 + /> 64 + ); 65 + } 66 + 67 + const inputGroupButtonVariants = cva( 68 + "flex items-center gap-2 text-sm shadow-none", 69 + { 70 + variants: { 71 + size: { 72 + xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", 73 + sm: "", 74 + "icon-xs": 75 + "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", 76 + "icon-sm": "size-8 p-0 has-[>svg]:p-0", 77 + }, 78 + }, 79 + defaultVariants: { 80 + size: "xs", 81 + }, 82 + }, 83 + ); 84 + 85 + function InputGroupButton({ 86 + className, 87 + type = "button", 88 + variant = "ghost", 89 + size = "xs", 90 + ...props 91 + }: Omit<React.ComponentProps<typeof Button>, "size" | "type"> & 92 + VariantProps<typeof inputGroupButtonVariants> & { 93 + type?: "button" | "submit" | "reset"; 94 + }) { 95 + return ( 96 + <Button 97 + type={type} 98 + data-size={size} 99 + variant={variant} 100 + className={cn(inputGroupButtonVariants({ size }), className)} 101 + {...props} 102 + /> 103 + ); 104 + } 105 + 106 + function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { 107 + return ( 108 + <span 109 + className={cn( 110 + "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", 111 + className, 112 + )} 113 + {...props} 114 + /> 115 + ); 116 + } 117 + 118 + function InputGroupInput({ 119 + className, 120 + ...props 121 + }: React.ComponentProps<"input">) { 122 + return ( 123 + <Input 124 + data-slot="input-group-control" 125 + className={cn( 126 + "flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent", 127 + className, 128 + )} 129 + {...props} 130 + /> 131 + ); 132 + } 133 + 134 + function InputGroupTextarea({ 135 + className, 136 + ...props 137 + }: React.ComponentProps<"textarea">) { 138 + return ( 139 + <Textarea 140 + data-slot="input-group-control" 141 + className={cn( 142 + "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent", 143 + className, 144 + )} 145 + {...props} 146 + /> 147 + ); 148 + } 149 + 150 + export { 151 + InputGroup, 152 + InputGroupAddon, 153 + InputGroupButton, 154 + InputGroupInput, 155 + InputGroupText, 156 + InputGroupTextarea, 157 + };
+20
src/components/ui/input.tsx
··· 1 + import { Input as InputPrimitive } from "@base-ui/react/input"; 2 + import type * as React from "react"; 3 + 4 + import { cn } from "@/lib/utils"; 5 + 6 + function Input({ className, type, ...props }: React.ComponentProps<"input">) { 7 + return ( 8 + <InputPrimitive 9 + type={type} 10 + data-slot="input" 11 + className={cn( 12 + "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", 13 + className, 14 + )} 15 + {...props} 16 + /> 17 + ); 18 + } 19 + 20 + export { Input };
+20
src/components/ui/label.tsx
··· 1 + "use client"; 2 + 3 + import type * as React from "react"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + function Label({ className, ...props }: React.ComponentProps<"label">) { 8 + return ( 9 + <label 10 + data-slot="label" 11 + className={cn( 12 + "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", 13 + className, 14 + )} 15 + {...props} 16 + /> 17 + ); 18 + } 19 + 20 + export { Label };
+201
src/components/ui/select.tsx
··· 1 + "use client"; 2 + 3 + import { Select as SelectPrimitive } from "@base-ui/react/select"; 4 + import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + import { cn } from "@/lib/utils"; 7 + 8 + const Select = SelectPrimitive.Root; 9 + 10 + function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { 11 + return ( 12 + <SelectPrimitive.Group 13 + data-slot="select-group" 14 + className={cn("scroll-my-1 p-1", className)} 15 + {...props} 16 + /> 17 + ); 18 + } 19 + 20 + function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { 21 + return ( 22 + <SelectPrimitive.Value 23 + data-slot="select-value" 24 + className={cn("flex flex-1 text-left", className)} 25 + {...props} 26 + /> 27 + ); 28 + } 29 + 30 + function SelectTrigger({ 31 + className, 32 + size = "default", 33 + children, 34 + ...props 35 + }: SelectPrimitive.Trigger.Props & { 36 + size?: "sm" | "default"; 37 + }) { 38 + return ( 39 + <SelectPrimitive.Trigger 40 + data-slot="select-trigger" 41 + data-size={size} 42 + className={cn( 43 + "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 44 + className, 45 + )} 46 + {...props} 47 + > 48 + {children} 49 + <SelectPrimitive.Icon 50 + render={ 51 + <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" /> 52 + } 53 + /> 54 + </SelectPrimitive.Trigger> 55 + ); 56 + } 57 + 58 + function SelectContent({ 59 + className, 60 + children, 61 + side = "bottom", 62 + sideOffset = 4, 63 + align = "center", 64 + alignOffset = 0, 65 + alignItemWithTrigger = true, 66 + ...props 67 + }: SelectPrimitive.Popup.Props & 68 + Pick< 69 + SelectPrimitive.Positioner.Props, 70 + "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" 71 + >) { 72 + return ( 73 + <SelectPrimitive.Portal> 74 + <SelectPrimitive.Positioner 75 + side={side} 76 + sideOffset={sideOffset} 77 + align={align} 78 + alignOffset={alignOffset} 79 + alignItemWithTrigger={alignItemWithTrigger} 80 + className="isolate z-50" 81 + > 82 + <SelectPrimitive.Popup 83 + data-slot="select-content" 84 + data-align-trigger={alignItemWithTrigger} 85 + className={cn( 86 + "relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", 87 + className, 88 + )} 89 + {...props} 90 + > 91 + <SelectScrollUpButton /> 92 + <SelectPrimitive.List>{children}</SelectPrimitive.List> 93 + <SelectScrollDownButton /> 94 + </SelectPrimitive.Popup> 95 + </SelectPrimitive.Positioner> 96 + </SelectPrimitive.Portal> 97 + ); 98 + } 99 + 100 + function SelectLabel({ 101 + className, 102 + ...props 103 + }: SelectPrimitive.GroupLabel.Props) { 104 + return ( 105 + <SelectPrimitive.GroupLabel 106 + data-slot="select-label" 107 + className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)} 108 + {...props} 109 + /> 110 + ); 111 + } 112 + 113 + function SelectItem({ 114 + className, 115 + children, 116 + ...props 117 + }: SelectPrimitive.Item.Props) { 118 + return ( 119 + <SelectPrimitive.Item 120 + data-slot="select-item" 121 + className={cn( 122 + "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", 123 + className, 124 + )} 125 + {...props} 126 + > 127 + <SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap"> 128 + {children} 129 + </SelectPrimitive.ItemText> 130 + <SelectPrimitive.ItemIndicator 131 + render={ 132 + <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> 133 + } 134 + > 135 + <CheckIcon className="pointer-events-none" /> 136 + </SelectPrimitive.ItemIndicator> 137 + </SelectPrimitive.Item> 138 + ); 139 + } 140 + 141 + function SelectSeparator({ 142 + className, 143 + ...props 144 + }: SelectPrimitive.Separator.Props) { 145 + return ( 146 + <SelectPrimitive.Separator 147 + data-slot="select-separator" 148 + className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)} 149 + {...props} 150 + /> 151 + ); 152 + } 153 + 154 + function SelectScrollUpButton({ 155 + className, 156 + ...props 157 + }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { 158 + return ( 159 + <SelectPrimitive.ScrollUpArrow 160 + data-slot="select-scroll-up-button" 161 + className={cn( 162 + "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", 163 + className, 164 + )} 165 + {...props} 166 + > 167 + <ChevronUpIcon /> 168 + </SelectPrimitive.ScrollUpArrow> 169 + ); 170 + } 171 + 172 + function SelectScrollDownButton({ 173 + className, 174 + ...props 175 + }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { 176 + return ( 177 + <SelectPrimitive.ScrollDownArrow 178 + data-slot="select-scroll-down-button" 179 + className={cn( 180 + "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", 181 + className, 182 + )} 183 + {...props} 184 + > 185 + <ChevronDownIcon /> 186 + </SelectPrimitive.ScrollDownArrow> 187 + ); 188 + } 189 + 190 + export { 191 + Select, 192 + SelectContent, 193 + SelectGroup, 194 + SelectItem, 195 + SelectLabel, 196 + SelectScrollDownButton, 197 + SelectScrollUpButton, 198 + SelectSeparator, 199 + SelectTrigger, 200 + SelectValue, 201 + };
+25
src/components/ui/separator.tsx
··· 1 + "use client"; 2 + 3 + import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + function Separator({ 8 + className, 9 + orientation = "horizontal", 10 + ...props 11 + }: SeparatorPrimitive.Props) { 12 + return ( 13 + <SeparatorPrimitive 14 + data-slot="separator" 15 + orientation={orientation} 16 + className={cn( 17 + "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", 18 + className, 19 + )} 20 + {...props} 21 + /> 22 + ); 23 + } 24 + 25 + export { Separator };
+18
src/components/ui/textarea.tsx
··· 1 + import type * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 + return ( 7 + <textarea 8 + data-slot="textarea" 9 + className={cn( 10 + "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", 11 + className, 12 + )} 13 + {...props} 14 + /> 15 + ); 16 + } 17 + 18 + export { Textarea };