From 153dfcf8737920dc5723c83a961ffadcecdd3db8 Mon Sep 17 00:00:00 2001 From: shramko Date: Mon, 8 Jun 2026 14:48:31 -0400 Subject: [PATCH 01/22] chore: update deps --- package.json | 6 +- pnpm-lock.yaml | 338 ++++++++++++++++++++++++------------------------- 2 files changed, 172 insertions(+), 172 deletions(-) diff --git a/package.json b/package.json index f40ee44c..15660db9 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@shikijs/rehype": "4.2.0", "@shikijs/transformers": "4.2.0", "@tailwindcss/postcss": "4.3.0", - "@tailwindcss/typography": "0.5.19", + "@tailwindcss/typography": "0.5.20", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", @@ -62,8 +62,8 @@ "jest": "30.4.2", "jest-environment-jsdom": "30.4.1", "next-sitemap": "4.2.3", - "oxfmt": "0.53.0", - "oxlint": "1.68.0", + "oxfmt": "0.54.0", + "oxlint": "1.69.0", "postcss": "8.5.15", "prisma": "7.8.0", "rehype-autolink-headings": "7.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4687d281..0d34fd7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,8 +83,8 @@ importers: specifier: 4.3.0 version: 4.3.0 '@tailwindcss/typography': - specifier: 0.5.19 - version: 0.5.19(tailwindcss@4.3.0) + specifier: 0.5.20 + version: 0.5.20(tailwindcss@4.3.0) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -119,11 +119,11 @@ importers: specifier: 4.2.3 version: 4.2.3(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) oxfmt: - specifier: 0.53.0 - version: 0.53.0 + specifier: 0.54.0 + version: 0.54.0 oxlint: - specifier: 1.68.0 - version: 1.68.0 + specifier: 1.69.0 + version: 1.69.0 postcss: specifier: ^8.5.10 version: 8.5.15 @@ -895,246 +895,246 @@ packages: resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} engines: {node: '>=14'} - '@oxfmt/binding-android-arm-eabi@0.53.0': - resolution: {integrity: sha512-XfVM8AmIovBTKXCt14Op5wbfcoM8418nttd+nhMgM3RAVaJg1MtJc73FyWfUt0oxLyBGVwfniNVUsbV/b3VmPg==} + '@oxfmt/binding-android-arm-eabi@0.54.0': + resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.53.0': - resolution: {integrity: sha512-btHDfXckwdf9zgyAVznfZkf+GVyB0I1m1hlvaOMRx2xoyz3hphfPX97s89J3wfCN8QBETLtk4lQUaeOkrMuQOg==} + '@oxfmt/binding-android-arm64@0.54.0': + resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.53.0': - resolution: {integrity: sha512-k2RjMcSTkHjoOlsVGbL35JVzXL+oQco3GHPl/5kjebVF4oHNfE24In8F5isqBh9LBJucycWHKDXdGrCchdWcHQ==} + '@oxfmt/binding-darwin-arm64@0.54.0': + resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.53.0': - resolution: {integrity: sha512-65jIBE2H1l5SSs16fmv6/7b6sAx/WpvnsgDhVWK9qSjNFDUro7MPQ6q5UhpY7kl46yltfR046iAnxy/Bzqbiew==} + '@oxfmt/binding-darwin-x64@0.54.0': + resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.53.0': - resolution: {integrity: sha512-oYe1gkz7U49PCYrS9147d2fJZj8mDI4Di6AvlsU5fu9p+Tq8S7qqOMSZjUiVTLX8bXuSA9Lk/tIxuegVjkNYRA==} + '@oxfmt/binding-freebsd-x64@0.54.0': + resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.53.0': - resolution: {integrity: sha512-ailB2vLzGi629tymdAb2VYJyEHref7oqGxP+tRBrtRBxQrb6NV55JMT7xtGZ8uTeG2+Y9zojqW4LhJYxQnz9Pg==} + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.53.0': - resolution: {integrity: sha512-abh4mWBvOvD966sobqF7r103y2yYx7Rb4WGHLOS4+5igGqLbbPxS9aK5+45D6iUY7dWMsk3Muz9a8gUtufvqJA==} + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.53.0': - resolution: {integrity: sha512-z73PvuhJ8qA+cDbaiqbtopHglA91U4+y5wn2sTJJrnpB957d5P33FEuyP3DQIFd7ofljmDmfVT4G0CVGHZaJWg==} + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.53.0': - resolution: {integrity: sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==} + '@oxfmt/binding-linux-arm64-musl@0.54.0': + resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.53.0': - resolution: {integrity: sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==} + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.53.0': - resolution: {integrity: sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==} + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.53.0': - resolution: {integrity: sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==} + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.53.0': - resolution: {integrity: sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==} + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.53.0': - resolution: {integrity: sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==} + '@oxfmt/binding-linux-x64-gnu@0.54.0': + resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.53.0': - resolution: {integrity: sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==} + '@oxfmt/binding-linux-x64-musl@0.54.0': + resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.53.0': - resolution: {integrity: sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==} + '@oxfmt/binding-openharmony-arm64@0.54.0': + resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.53.0': - resolution: {integrity: sha512-Yq9sOZoIOJ5xPjO0qOyHJS4CiPuTkB2en9auxZz7Ar2p5RaC7BzLyVVmAA7zz9/L9YnjjY1DwNxN+ivKXimN/A==} + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.53.0': - resolution: {integrity: sha512-es1fVNZEkBqEcQtBpn19SYFgZF7FawlkCjkT/iImfEAus4gun8fBwB1E9hpV5LcR9B0DBNvRIXhW8BQk3JaE+Q==} + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.53.0': - resolution: {integrity: sha512-QFmJs2bEu9AO4O6qsmEaZNGi6dFq8N+rT8EHAAnZIq/B9SeJDUbc4DzVxQ48MfDsL7D3sCZzo37zuTuspcURgg==} + '@oxfmt/binding-win32-x64-msvc@0.54.0': + resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.68.0': - resolution: {integrity: sha512-wEdsIspexXLLMCPAEOcCuFLMt6aE3AzTuA/nQKLPRnoJ+EQTturmGheDkhHuuVHx0GbutjQ3JKmEn+Gz6Ag28Q==} + '@oxlint/binding-android-arm-eabi@1.69.0': + resolution: {integrity: sha512-DKQQbD5cZ/MYfDgDI7YGyGD9FSxABlsBsYFo5p26lloob543tP9+4N3guwdXIYJN+7HSZxLe8YJuwcOWw5qnHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.68.0': - resolution: {integrity: sha512-6aZRNNXQTsYtgaus8HTb9nuCcsrQTlKXGnktwvwW0n/SooRWNxNb3925grDkC63aEYZuCIyOVLV16IdYIoC2aQ==} + '@oxlint/binding-android-arm64@1.69.0': + resolution: {integrity: sha512-lEhb+I5pr4inux+JFwfCa1HRq3Os7NirEFQ0H1I35SVEHPm6byX0Ah47xmRha3qi6LAkxUcxViL8o/9PivjzBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.68.0': - resolution: {integrity: sha512-lVTbsE3kO4bLpZELgjRZuAJc8kP98wb83yMXWH8gaPaFZ+cM2IDeZto4ByoUAYj0Mxv2rvw+A1ssZequSepVSg==} + '@oxlint/binding-darwin-arm64@1.69.0': + resolution: {integrity: sha512-GY2YE8lOZW59BW1Ia1y+1gR0XyjrZRvVWHAr8LGeGhYHE0OQJ/7cRKXTkx1P+E9/6awEc3SX8a68SFTjh/E//A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.68.0': - resolution: {integrity: sha512-nCmw2XrmQskjBUh/sfP5yKs93V68LijQgjd1cuuZ/q4SCARngLYs60/qqyzuMsg8QQ9KArDI98hxs/RDGE4KRQ==} + '@oxlint/binding-darwin-x64@1.69.0': + resolution: {integrity: sha512-ax1oZnOjHX3LB7myQyHEaQkDwfLb6str3/nSP6O7EVUviQGNkEGzGV0EqcBJWK+Ufwx0l4xPgyYayurvhAdl2Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.68.0': - resolution: {integrity: sha512-TI4ovQJliYE9V6e06cEv+qEI9uj7Ao65fmif4er4HD+aouyYyh0P31q2jh3KtqsOHHcQqv2PZ61TjJFLpBDGWQ==} + '@oxlint/binding-freebsd-x64@1.69.0': + resolution: {integrity: sha512-kHWeHv4g2h8NY+mpCxzCtY4uerMJWTN/TSnNj1CPbakFpHEJ6cTya2wWV0pDSYWOJ2+0UiEbhn3AtXxHtsnKjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.68.0': - resolution: {integrity: sha512-LcNnEi9g71Cmry5ZpLbKT+oVv+/zYG3hYVAbBBB5X85nOQZSk8l92CnDkxJMcxUg0NCnMCOFZuaVDlMyv4tYJw==} + '@oxlint/binding-linux-arm-gnueabihf@1.69.0': + resolution: {integrity: sha512-gq84vM1a1oEehXo27YCDzGVcxPsZDI1yswZwz2Da1/cbnWtrL16XZZnz0G/+gIU8edtHpfjxq5c+vWEHqJfWoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.68.0': - resolution: {integrity: sha512-OovHahL3FX4UaK+hgSf11llUx2vszqjSdQQ61Ck9InOEI/ptZoC4XSQJurITqItVvd53JSlmkLMeaNjM1PoQew==} + '@oxlint/binding-linux-arm-musleabihf@1.69.0': + resolution: {integrity: sha512-kIqEa98JQ0VRyrcncxA417m2AzasqTlD+FyVT1AksjvjkqQcvm7pBWYvoW3/mpyOP2XYvi5nSCCTIe6De1yu5g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.68.0': - resolution: {integrity: sha512-YbzTglnHLzzi9zv5or8Ztz5fykAoZE8W9iM42/bOrF4HBSB6rJTqdLQWuoP76EHQw9DuKl76K1QmFlG29sPJXQ==} + '@oxlint/binding-linux-arm64-gnu@1.69.0': + resolution: {integrity: sha512-j+xYiXozxGWx2cpjCrwwGR4awTxPFsRv3JZrv23RCogEPMc4R7UqjHW47p/RG0aRlbWiROCJ8coUfCwy0dvzHA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.68.0': - resolution: {integrity: sha512-qVKtCZNic+OoNnOr/hCQAu22HSQzflI7Fsq/Blzkw02SnLuv163k3kfmrVpZjSBlUHgsRKj6WgQiw30d3SX02Q==} + '@oxlint/binding-linux-arm64-musl@1.69.0': + resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.68.0': - resolution: {integrity: sha512-zExyZ8ZOUuAyQ0y9jpTcyjKUz62YY9JhKPyVxzvjTpXzZ3ujdqiVwfPWDdnA1SsIOrxdtxHn7KErDHLWskFjXg==} + '@oxlint/binding-linux-ppc64-gnu@1.69.0': + resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.68.0': - resolution: {integrity: sha512-6C4MPuwewyDavA7sxM14wzgRi5GGL68HPIxRCdVyS75U4MDbpFVYzKO9WNR6KLKTMPq2pcz3THwo1sK2uiqngw==} + '@oxlint/binding-linux-riscv64-gnu@1.69.0': + resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.68.0': - resolution: {integrity: sha512-bnZooVeHAcvA+dH0EDLgx+7HY/DRi6e0hFszg3P+OBatuUjV6EvfIyNIzWOusmqAVh4L6r21GGTZtiKE4iqM4Q==} + '@oxlint/binding-linux-riscv64-musl@1.69.0': + resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.68.0': - resolution: {integrity: sha512-dIqnZnJSmHCMOUpUcWQOiV14o3DDPVx1DSsMaSzvdhNjC1tB1iEPZbdiMSCIEYbkgbsYznHXWqFdKL8WUB3F8g==} + '@oxlint/binding-linux-s390x-gnu@1.69.0': + resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.68.0': - resolution: {integrity: sha512-zc9lEnfV/HreDTY6gdMlZe+irkwHSxQ4/B1pS9GyK7RVaA5LxhoZY/w6/o2vIwLLEYiXQ5ujGxOM1ZazeFAAIA==} + '@oxlint/binding-linux-x64-gnu@1.69.0': + resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.68.0': - resolution: {integrity: sha512-Dl5QEX0TCo/40Cdh1o1JdPS//+YiWqjC+Hrrya5OQmStZZr4svAFtdlqcpCrU9yq2Mo3vRVyO9B3h0dzD8s36Q==} + '@oxlint/binding-linux-x64-musl@1.69.0': + resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.68.0': - resolution: {integrity: sha512-/qy6dOvi4S3/LeXq0l5BT5pRKPYA7oj3uKwJOAZOr5HRLL+HK6jdBynvWuXIA2wwfE01RzNYmbBdM7vwYx00sA==} + '@oxlint/binding-openharmony-arm64@1.69.0': + resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.68.0': - resolution: {integrity: sha512-fHNtVqPHSYE7UFDSLVFUjxQjnSVXxseNJmRW+XuP4pXXDwePdPda43NL7/BBCFTxHjycOc44JNDaOPtFDNui9A==} + '@oxlint/binding-win32-arm64-msvc@1.69.0': + resolution: {integrity: sha512-9RExaLgmaw6IoIkU9cTpT71mLfI0xZ86iZH8x518LVsOkjquJMYqb9P7KpC8lgd1t0Dxs41p2pxynq4XR3Ttzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.68.0': - resolution: {integrity: sha512-NnKXr4Wgo4nps3erhrE0f8shBvBPZMHg72nDsvX0JyrRvsNiP3f1JNvbCKh+A6VFvpF7ZoJxu904P3cKMhvZnA==} + '@oxlint/binding-win32-ia32-msvc@1.69.0': + resolution: {integrity: sha512-1907kRPF8/PrcIw1E7LMs9JbVrpgnt/MvFdss3an8oDkYNAACXzTntV3t3869ZZhMZxb2AzRGbz1pA/jdFatXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.68.0': - resolution: {integrity: sha512-zg5pA+84AlU6XHJ3ruiRxziO71QTrz8nLsk6u01JGS5+tL9/bnlakFiklFrcy4R1/V7ktWtaNitN3JZWmKnf6g==} + '@oxlint/binding-win32-x64-msvc@1.69.0': + resolution: {integrity: sha512-w8SOXv3mT9Fi6jY8OXdXCfnvX/3KNLXGNr4HEz2TA7S4Mv/PYAOmpB8y/ge40mxvBMgGNaSaaDwZpAsQn7HtWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1750,10 +1750,10 @@ packages: '@tailwindcss/postcss@4.3.0': resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==} - '@tailwindcss/typography@0.5.19': - resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + '@tailwindcss/typography@0.5.20': + resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} peerDependencies: - tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} @@ -3699,8 +3699,8 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - oxfmt@0.53.0: - resolution: {integrity: sha512-9cB5glS3Ip6NMuZ+6NYTao9FCWkDhRtPYCtR3QBu/NxHoFbgzzTvi41N4jxz/GqGfuLKspui1qb/LlSu2IbMcw==} + oxfmt@0.54.0: + resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3712,8 +3712,8 @@ packages: vite-plus: optional: true - oxlint@1.68.0: - resolution: {integrity: sha512-dXcbq+xsmLrMy6T8d0euf3IYUfLmjHIE11pOxiUSi5LHkFZaYPv568R6sEjcavVpUxoaQe66UBuK4HEi74NxpA==} + oxlint@1.69.0: + resolution: {integrity: sha512-ypZkK/aDc5NQV8zIR6s2H2Tl3aNW8FmJ1m9+2qsaYuRenl8vgnHNCGwTHviWJdUQzglOlHFchgopdtGhSy17Rw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5438,118 +5438,118 @@ snapshots: '@opentelemetry/semantic-conventions@1.40.0': {} - '@oxfmt/binding-android-arm-eabi@0.53.0': + '@oxfmt/binding-android-arm-eabi@0.54.0': optional: true - '@oxfmt/binding-android-arm64@0.53.0': + '@oxfmt/binding-android-arm64@0.54.0': optional: true - '@oxfmt/binding-darwin-arm64@0.53.0': + '@oxfmt/binding-darwin-arm64@0.54.0': optional: true - '@oxfmt/binding-darwin-x64@0.53.0': + '@oxfmt/binding-darwin-x64@0.54.0': optional: true - '@oxfmt/binding-freebsd-x64@0.53.0': + '@oxfmt/binding-freebsd-x64@0.54.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.53.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.53.0': + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.53.0': + '@oxfmt/binding-linux-arm64-gnu@0.54.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.53.0': + '@oxfmt/binding-linux-arm64-musl@0.54.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.53.0': + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.53.0': + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.53.0': + '@oxfmt/binding-linux-riscv64-musl@0.54.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.53.0': + '@oxfmt/binding-linux-s390x-gnu@0.54.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.53.0': + '@oxfmt/binding-linux-x64-gnu@0.54.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.53.0': + '@oxfmt/binding-linux-x64-musl@0.54.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.53.0': + '@oxfmt/binding-openharmony-arm64@0.54.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.53.0': + '@oxfmt/binding-win32-arm64-msvc@0.54.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.53.0': + '@oxfmt/binding-win32-ia32-msvc@0.54.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.53.0': + '@oxfmt/binding-win32-x64-msvc@0.54.0': optional: true - '@oxlint/binding-android-arm-eabi@1.68.0': + '@oxlint/binding-android-arm-eabi@1.69.0': optional: true - '@oxlint/binding-android-arm64@1.68.0': + '@oxlint/binding-android-arm64@1.69.0': optional: true - '@oxlint/binding-darwin-arm64@1.68.0': + '@oxlint/binding-darwin-arm64@1.69.0': optional: true - '@oxlint/binding-darwin-x64@1.68.0': + '@oxlint/binding-darwin-x64@1.69.0': optional: true - '@oxlint/binding-freebsd-x64@1.68.0': + '@oxlint/binding-freebsd-x64@1.69.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.68.0': + '@oxlint/binding-linux-arm-gnueabihf@1.69.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.68.0': + '@oxlint/binding-linux-arm-musleabihf@1.69.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.68.0': + '@oxlint/binding-linux-arm64-gnu@1.69.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.68.0': + '@oxlint/binding-linux-arm64-musl@1.69.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.68.0': + '@oxlint/binding-linux-ppc64-gnu@1.69.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.68.0': + '@oxlint/binding-linux-riscv64-gnu@1.69.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.68.0': + '@oxlint/binding-linux-riscv64-musl@1.69.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.68.0': + '@oxlint/binding-linux-s390x-gnu@1.69.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.68.0': + '@oxlint/binding-linux-x64-gnu@1.69.0': optional: true - '@oxlint/binding-linux-x64-musl@1.68.0': + '@oxlint/binding-linux-x64-musl@1.69.0': optional: true - '@oxlint/binding-openharmony-arm64@1.68.0': + '@oxlint/binding-openharmony-arm64@1.69.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.68.0': + '@oxlint/binding-win32-arm64-msvc@1.69.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.68.0': + '@oxlint/binding-win32-ia32-msvc@1.69.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.68.0': + '@oxlint/binding-win32-x64-msvc@1.69.0': optional: true '@pkgjs/parseargs@0.11.0': @@ -6131,7 +6131,7 @@ snapshots: postcss: 8.5.15 tailwindcss: 4.3.0 - '@tailwindcss/typography@0.5.19(tailwindcss@4.3.0)': + '@tailwindcss/typography@0.5.20(tailwindcss@4.3.0)': dependencies: postcss-selector-parser: 6.0.10 tailwindcss: 4.3.0 @@ -8472,51 +8472,51 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - oxfmt@0.53.0: + oxfmt@0.54.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.53.0 - '@oxfmt/binding-android-arm64': 0.53.0 - '@oxfmt/binding-darwin-arm64': 0.53.0 - '@oxfmt/binding-darwin-x64': 0.53.0 - '@oxfmt/binding-freebsd-x64': 0.53.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.53.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.53.0 - '@oxfmt/binding-linux-arm64-gnu': 0.53.0 - '@oxfmt/binding-linux-arm64-musl': 0.53.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.53.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.53.0 - '@oxfmt/binding-linux-riscv64-musl': 0.53.0 - '@oxfmt/binding-linux-s390x-gnu': 0.53.0 - '@oxfmt/binding-linux-x64-gnu': 0.53.0 - '@oxfmt/binding-linux-x64-musl': 0.53.0 - '@oxfmt/binding-openharmony-arm64': 0.53.0 - '@oxfmt/binding-win32-arm64-msvc': 0.53.0 - '@oxfmt/binding-win32-ia32-msvc': 0.53.0 - '@oxfmt/binding-win32-x64-msvc': 0.53.0 - - oxlint@1.68.0: + '@oxfmt/binding-android-arm-eabi': 0.54.0 + '@oxfmt/binding-android-arm64': 0.54.0 + '@oxfmt/binding-darwin-arm64': 0.54.0 + '@oxfmt/binding-darwin-x64': 0.54.0 + '@oxfmt/binding-freebsd-x64': 0.54.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.54.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.54.0 + '@oxfmt/binding-linux-arm64-gnu': 0.54.0 + '@oxfmt/binding-linux-arm64-musl': 0.54.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-musl': 0.54.0 + '@oxfmt/binding-linux-s390x-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-musl': 0.54.0 + '@oxfmt/binding-openharmony-arm64': 0.54.0 + '@oxfmt/binding-win32-arm64-msvc': 0.54.0 + '@oxfmt/binding-win32-ia32-msvc': 0.54.0 + '@oxfmt/binding-win32-x64-msvc': 0.54.0 + + oxlint@1.69.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.68.0 - '@oxlint/binding-android-arm64': 1.68.0 - '@oxlint/binding-darwin-arm64': 1.68.0 - '@oxlint/binding-darwin-x64': 1.68.0 - '@oxlint/binding-freebsd-x64': 1.68.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.68.0 - '@oxlint/binding-linux-arm-musleabihf': 1.68.0 - '@oxlint/binding-linux-arm64-gnu': 1.68.0 - '@oxlint/binding-linux-arm64-musl': 1.68.0 - '@oxlint/binding-linux-ppc64-gnu': 1.68.0 - '@oxlint/binding-linux-riscv64-gnu': 1.68.0 - '@oxlint/binding-linux-riscv64-musl': 1.68.0 - '@oxlint/binding-linux-s390x-gnu': 1.68.0 - '@oxlint/binding-linux-x64-gnu': 1.68.0 - '@oxlint/binding-linux-x64-musl': 1.68.0 - '@oxlint/binding-openharmony-arm64': 1.68.0 - '@oxlint/binding-win32-arm64-msvc': 1.68.0 - '@oxlint/binding-win32-ia32-msvc': 1.68.0 - '@oxlint/binding-win32-x64-msvc': 1.68.0 + '@oxlint/binding-android-arm-eabi': 1.69.0 + '@oxlint/binding-android-arm64': 1.69.0 + '@oxlint/binding-darwin-arm64': 1.69.0 + '@oxlint/binding-darwin-x64': 1.69.0 + '@oxlint/binding-freebsd-x64': 1.69.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.69.0 + '@oxlint/binding-linux-arm-musleabihf': 1.69.0 + '@oxlint/binding-linux-arm64-gnu': 1.69.0 + '@oxlint/binding-linux-arm64-musl': 1.69.0 + '@oxlint/binding-linux-ppc64-gnu': 1.69.0 + '@oxlint/binding-linux-riscv64-gnu': 1.69.0 + '@oxlint/binding-linux-riscv64-musl': 1.69.0 + '@oxlint/binding-linux-s390x-gnu': 1.69.0 + '@oxlint/binding-linux-x64-gnu': 1.69.0 + '@oxlint/binding-linux-x64-musl': 1.69.0 + '@oxlint/binding-openharmony-arm64': 1.69.0 + '@oxlint/binding-win32-arm64-msvc': 1.69.0 + '@oxlint/binding-win32-ia32-msvc': 1.69.0 + '@oxlint/binding-win32-x64-msvc': 1.69.0 p-limit@2.3.0: dependencies: From cdf449bc4f040b36279f6de0b0af6844acacd407 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:18:12 -0400 Subject: [PATCH 02/22] build: add recharts dependency --- package.json | 1 + pnpm-lock.yaml | 288 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) diff --git a/package.json b/package.json index 15660db9..a6fb79da 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react": "19.2.7", "react-dom": "19.2.7", "reading-time": "1.5.0", + "recharts": "^3.8.1", "shiki": "4.2.0", "swr": "2.4.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d34fd7e..7d135b8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: reading-time: specifier: 1.5.0 version: 1.5.0 + recharts: + specifier: ^3.8.1 + version: 3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.6)(react@19.2.7)(redux@5.0.1) shiki: specifier: 4.2.0 version: 4.2.0 @@ -1284,6 +1287,17 @@ packages: '@types/react': optional: true + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@resvg/resvg-wasm@2.4.0': resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==} engines: {node: '>= 10'} @@ -1655,6 +1669,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1814,6 +1831,33 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -1885,6 +1929,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -2473,6 +2520,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -2486,6 +2577,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -2676,6 +2770,9 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2910,6 +3007,12 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.8: + resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2945,6 +3048,10 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -3953,6 +4060,18 @@ packages: react-is@19.2.6: resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==} + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -3964,6 +4083,14 @@ packages: reading-time@1.5.0: resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + recma-build-jsx@1.0.0: resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} @@ -3982,6 +4109,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -4034,6 +4169,9 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -4293,6 +4431,9 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyexec@1.1.1: resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} @@ -4440,6 +4581,9 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -5712,6 +5856,18 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.8 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.7 + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) + '@resvg/resvg-wasm@2.4.0': {} '@rollup/plugin-commonjs@28.0.1(rollup@4.60.3)': @@ -6058,6 +6214,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -6210,6 +6368,30 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -6291,6 +6473,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': @@ -6799,6 +6983,44 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -6808,6 +7030,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} decode-named-character-reference@1.3.0: @@ -6963,6 +7187,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + eventemitter3@5.0.4: {} + events@3.3.0: {} execa@5.1.1: @@ -7254,6 +7480,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + immer@10.2.0: {} + + immer@11.1.8: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7286,6 +7516,8 @@ snapshots: inline-style-parser@0.2.7: {} + internmap@2.0.3: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -8742,12 +8974,41 @@ snapshots: react-is@19.2.6: {} + react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + redux: 5.0.1 + react@19.2.7: {} readdirp@5.0.0: {} reading-time@1.5.0: {} + recharts@3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.6)(react@19.2.7)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.46.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-is: 19.2.6 + react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.7) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + recma-build-jsx@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -8782,6 +9043,12 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -8875,6 +9142,8 @@ snapshots: transitivePeerDependencies: - supports-color + reselect@5.1.1: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -9165,6 +9434,8 @@ snapshots: tiny-inflate@1.0.3: {} + tiny-invariant@1.3.3: {} + tinyexec@1.1.1: {} tinypool@2.1.0: {} @@ -9344,6 +9615,23 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 From 3c4df9c666205fa87aed00d88747858aa38153c3 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:18:39 -0400 Subject: [PATCH 03/22] test: ignore recharts-poc from coverage --- jest.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jest.config.js b/jest.config.js index 7f032ca9..7d5ce937 100644 --- a/jest.config.js +++ b/jest.config.js @@ -48,6 +48,10 @@ const config = { '/node_modules/', '^.+\\.module\\.(css|sass|scss)$', ], + coveragePathIgnorePatterns: [ + '/node_modules/', + '/components/recharts-poc/(?!primitives/(periodOverPeriod|colors)\\.ts$)', + ], }; export default config; From 1d19570ccb4abb1a21812f56c11b607dc5b0ef35 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:23:31 -0400 Subject: [PATCH 04/22] feat: add periodOverPeriod helper --- .../primitives/periodOverPeriod.test.ts | 51 +++++++++++++++++++ .../primitives/periodOverPeriod.ts | 23 +++++++++ 2 files changed, 74 insertions(+) create mode 100644 components/recharts-poc/primitives/periodOverPeriod.test.ts create mode 100644 components/recharts-poc/primitives/periodOverPeriod.ts diff --git a/components/recharts-poc/primitives/periodOverPeriod.test.ts b/components/recharts-poc/primitives/periodOverPeriod.test.ts new file mode 100644 index 00000000..b1913d04 --- /dev/null +++ b/components/recharts-poc/primitives/periodOverPeriod.test.ts @@ -0,0 +1,51 @@ +import { periodOverPeriod } from './periodOverPeriod'; + +describe('periodOverPeriod', () => { + it('returns up direction with positive abs and pct deltas when current > previous', () => { + expect(periodOverPeriod(110, 100)).toEqual({ + absDelta: 10, + pctDelta: 10, + direction: 'up', + }); + }); + + it('returns down direction with negative abs and pct deltas when current < previous', () => { + expect(periodOverPeriod(80, 100)).toEqual({ + absDelta: -20, + pctDelta: -20, + direction: 'down', + }); + }); + + it('returns flat direction with zero deltas when current === previous', () => { + expect(periodOverPeriod(100, 100)).toEqual({ + absDelta: 0, + pctDelta: 0, + direction: 'flat', + }); + }); + + it('handles previous === 0 by returning null pctDelta and up direction when current > 0', () => { + expect(periodOverPeriod(50, 0)).toEqual({ + absDelta: 50, + pctDelta: null, + direction: 'up', + }); + }); + + it('handles current === 0 and previous > 0 with -100% delta and down direction', () => { + expect(periodOverPeriod(0, 100)).toEqual({ + absDelta: -100, + pctDelta: -100, + direction: 'down', + }); + }); + + it('handles both zero by returning flat with null pct', () => { + expect(periodOverPeriod(0, 0)).toEqual({ + absDelta: 0, + pctDelta: null, + direction: 'flat', + }); + }); +}); diff --git a/components/recharts-poc/primitives/periodOverPeriod.ts b/components/recharts-poc/primitives/periodOverPeriod.ts new file mode 100644 index 00000000..4d87d503 --- /dev/null +++ b/components/recharts-poc/primitives/periodOverPeriod.ts @@ -0,0 +1,23 @@ +export type Direction = 'up' | 'down' | 'flat'; + +export interface PoPResult { + absDelta: number; + pctDelta: number | null; + direction: Direction; +} + +export function periodOverPeriod(current: number, previous: number): PoPResult { + const absDelta = current - previous; + if (previous === 0) { + return { + absDelta, + pctDelta: null, + direction: current > 0 ? 'up' : 'flat', + }; + } + const pctDelta = (absDelta / previous) * 100; + let direction: Direction = 'flat'; + if (absDelta > 0) direction = 'up'; + else if (absDelta < 0) direction = 'down'; + return { absDelta, pctDelta, direction }; +} From fad01d0a9b6aa1daaffdb520c4f78855ba3e3b46 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:27:26 -0400 Subject: [PATCH 05/22] feat: add palette and useIsMounted hook --- .../recharts-poc/primitives/colors.test.ts | 40 +++++++++++++ components/recharts-poc/primitives/colors.ts | 56 +++++++++++++++++++ .../recharts-poc/primitives/useIsMounted.ts | 7 +++ 3 files changed, 103 insertions(+) create mode 100644 components/recharts-poc/primitives/colors.test.ts create mode 100644 components/recharts-poc/primitives/colors.ts create mode 100644 components/recharts-poc/primitives/useIsMounted.ts diff --git a/components/recharts-poc/primitives/colors.test.ts b/components/recharts-poc/primitives/colors.test.ts new file mode 100644 index 00000000..a7d5b25d --- /dev/null +++ b/components/recharts-poc/primitives/colors.test.ts @@ -0,0 +1,40 @@ +import { LIGHT_PALETTE, DARK_PALETTE, resolvePalette } from './colors'; + +describe('palettes', () => { + it('light and dark palettes contain the same keys', () => { + expect(Object.keys(LIGHT_PALETTE).sort()).toEqual( + Object.keys(DARK_PALETTE).sort(), + ); + }); + + it('light and dark palettes are not identical', () => { + expect(LIGHT_PALETTE).not.toEqual(DARK_PALETTE); + }); + + it('every palette value is a hex color string', () => { + for (const value of Object.values(LIGHT_PALETTE)) { + expect(value).toMatch(/^#[0-9a-fA-F]{6}$/); + } + for (const value of Object.values(DARK_PALETTE)) { + expect(value).toMatch(/^#[0-9a-fA-F]{6}$/); + } + }); +}); + +describe('resolvePalette', () => { + it('returns the light palette for "light"', () => { + expect(resolvePalette('light')).toBe(LIGHT_PALETTE); + }); + + it('returns the dark palette for "dark"', () => { + expect(resolvePalette('dark')).toBe(DARK_PALETTE); + }); + + it('returns the light palette for undefined (pre-mount)', () => { + expect(resolvePalette(undefined)).toBe(LIGHT_PALETTE); + }); + + it('returns the light palette for unknown values', () => { + expect(resolvePalette('system')).toBe(LIGHT_PALETTE); + }); +}); diff --git a/components/recharts-poc/primitives/colors.ts b/components/recharts-poc/primitives/colors.ts new file mode 100644 index 00000000..ee973544 --- /dev/null +++ b/components/recharts-poc/primitives/colors.ts @@ -0,0 +1,56 @@ +import { useTheme } from 'next-themes'; + +import { useIsMounted } from './useIsMounted'; + +export interface Palette { + primary: string; + primaryStrong: string; + secondary: string; + secondaryStrong: string; + neutral: string; + axis: string; + grid: string; + up: string; + down: string; + background: string; + textLabel: string; +} + +export const LIGHT_PALETTE: Palette = { + primary: '#b6dcfe', + primaryStrong: '#4a90e2', + secondary: '#cfe6ff', + secondaryStrong: '#3b78c4', + neutral: '#9ca3af', + axis: '#6b7280', + grid: '#e5e7eb', + up: '#16a34a', + down: '#dc2626', + background: '#ffffff', + textLabel: '#374151', +}; + +export const DARK_PALETTE: Palette = { + primary: '#1e3a5f', + primaryStrong: '#60a5fa', + secondary: '#1f2937', + secondaryStrong: '#93c5fd', + neutral: '#6b7280', + axis: '#9ca3af', + grid: '#374151', + up: '#22c55e', + down: '#ef4444', + background: '#111827', + textLabel: '#d1d5db', +}; + +export function resolvePalette(theme: string | undefined): Palette { + return theme === 'dark' ? DARK_PALETTE : LIGHT_PALETTE; +} + +export function usePalette(): Palette { + const mounted = useIsMounted(); + const { resolvedTheme } = useTheme(); + if (!mounted) return LIGHT_PALETTE; + return resolvePalette(resolvedTheme); +} diff --git a/components/recharts-poc/primitives/useIsMounted.ts b/components/recharts-poc/primitives/useIsMounted.ts new file mode 100644 index 00000000..977c1d71 --- /dev/null +++ b/components/recharts-poc/primitives/useIsMounted.ts @@ -0,0 +1,7 @@ +import { useEffect, useState } from 'react'; + +export function useIsMounted(): boolean { + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + return mounted; +} From a04b27fb0f9f4f2d8326750a9152df07018c82e3 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:30:36 -0400 Subject: [PATCH 06/22] feat: add ChartCard frame with ssr guard --- .../recharts-poc/primitives/ChartCard.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 components/recharts-poc/primitives/ChartCard.tsx diff --git a/components/recharts-poc/primitives/ChartCard.tsx b/components/recharts-poc/primitives/ChartCard.tsx new file mode 100644 index 00000000..b209de2c --- /dev/null +++ b/components/recharts-poc/primitives/ChartCard.tsx @@ -0,0 +1,40 @@ +import { type ReactElement, type ReactNode } from 'react'; +import { ResponsiveContainer } from 'recharts'; + +import { useIsMounted } from './useIsMounted'; + +interface ChartCardProps { + title: string; + actions?: ReactNode; + height: number; + children: ReactNode; +} + +export function ChartCard(props: ChartCardProps) { + const { title, actions, height, children } = props; + const mounted = useIsMounted(); + + return ( +
+
+

+ {title} +

+ {actions ? ( +
{actions}
+ ) : null} +
+ {mounted ? ( + + {children as ReactElement} + + ) : ( +
+ ); +} From 6dfc2304fe6b10641fdcc946d9c9efd1e7f26a37 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:30:51 -0400 Subject: [PATCH 07/22] feat: add LabelsToggle control --- .../recharts-poc/primitives/LabelsToggle.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 components/recharts-poc/primitives/LabelsToggle.tsx diff --git a/components/recharts-poc/primitives/LabelsToggle.tsx b/components/recharts-poc/primitives/LabelsToggle.tsx new file mode 100644 index 00000000..4d2b2ebf --- /dev/null +++ b/components/recharts-poc/primitives/LabelsToggle.tsx @@ -0,0 +1,18 @@ +interface LabelsToggleProps { + value: boolean; + onChange: (next: boolean) => void; +} + +export function LabelsToggle(props: LabelsToggleProps) { + const { value, onChange } = props; + return ( + + ); +} From 5b57f5d4c6fa11b00b685dd0907234a5df42bfbd Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:34:10 -0400 Subject: [PATCH 08/22] feat: add recharts-poc mock data --- .../recharts-poc/mocks/devicePerformance.ts | 9 ++++ .../recharts-poc/mocks/improvedKeywords.ts | 26 ++++++++++++ .../recharts-poc/mocks/keywordLength.ts | 18 ++++++++ components/recharts-poc/mocks/keywords.ts | 22 ++++++++++ components/recharts-poc/mocks/kpis.ts | 41 +++++++++++++++++++ components/recharts-poc/mocks/salesAsp.ts | 14 +++++++ 6 files changed, 130 insertions(+) create mode 100644 components/recharts-poc/mocks/devicePerformance.ts create mode 100644 components/recharts-poc/mocks/improvedKeywords.ts create mode 100644 components/recharts-poc/mocks/keywordLength.ts create mode 100644 components/recharts-poc/mocks/keywords.ts create mode 100644 components/recharts-poc/mocks/kpis.ts create mode 100644 components/recharts-poc/mocks/salesAsp.ts diff --git a/components/recharts-poc/mocks/devicePerformance.ts b/components/recharts-poc/mocks/devicePerformance.ts new file mode 100644 index 00000000..035e8240 --- /dev/null +++ b/components/recharts-poc/mocks/devicePerformance.ts @@ -0,0 +1,9 @@ +export interface DeviceSlice { + name: string; + value: number; +} + +export const DEVICE_PERFORMANCE: DeviceSlice[] = [ + { name: 'Desktop', value: 89.7 }, + { name: 'Mobile', value: 10.3 }, +]; diff --git a/components/recharts-poc/mocks/improvedKeywords.ts b/components/recharts-poc/mocks/improvedKeywords.ts new file mode 100644 index 00000000..ef52881e --- /dev/null +++ b/components/recharts-poc/mocks/improvedKeywords.ts @@ -0,0 +1,26 @@ +export interface ImprovedKeywordRow { + keyword: string; + positionNow: number; + positionBefore: number; +} + +export const IMPROVED_KEYWORDS: ImprovedKeywordRow[] = [ + { keyword: 'technical seo examples', positionNow: 1, positionBefore: 57 }, + { keyword: 'items ids', positionNow: 15, positionBefore: 39 }, + { keyword: 'seo report how to', positionNow: 45, positionBefore: 69 }, + { keyword: 'agencydashboard', positionNow: 9, positionBefore: 30 }, + { keyword: 'marketing report writing', positionNow: 23, positionBefore: 42 }, + { + keyword: 'how to access google analytics', + positionNow: 2, + positionBefore: 19, + }, + { + keyword: 'seo analysis report example', + positionNow: 6, + positionBefore: 19, + }, + { keyword: 'best google ads software', positionNow: 10, positionBefore: 22 }, + { keyword: 'supermetrics alternative', positionNow: 13, positionBefore: 25 }, + { keyword: 'example of audience name', positionNow: 23, positionBefore: 35 }, +]; diff --git a/components/recharts-poc/mocks/keywordLength.ts b/components/recharts-poc/mocks/keywordLength.ts new file mode 100644 index 00000000..a98c6be8 --- /dev/null +++ b/components/recharts-poc/mocks/keywordLength.ts @@ -0,0 +1,18 @@ +export interface KeywordLengthBucket { + length: number; + currentPeriod: number; + previousPeriod: number; +} + +export const KEYWORD_LENGTH: KeywordLengthBucket[] = [ + { length: 1, currentPeriod: 300, previousPeriod: 280 }, + { length: 2, currentPeriod: 1559, previousPeriod: 1400 }, + { length: 3, currentPeriod: 4183, previousPeriod: 3900 }, + { length: 4, currentPeriod: 4478, previousPeriod: 4100 }, + { length: 5, currentPeriod: 3112, previousPeriod: 3200 }, + { length: 6, currentPeriod: 1466, previousPeriod: 1500 }, + { length: 7, currentPeriod: 732, previousPeriod: 690 }, + { length: 8, currentPeriod: 414, previousPeriod: 450 }, + { length: 9, currentPeriod: 261, previousPeriod: 240 }, + { length: 10, currentPeriod: 179, previousPeriod: 200 }, +]; diff --git a/components/recharts-poc/mocks/keywords.ts b/components/recharts-poc/mocks/keywords.ts new file mode 100644 index 00000000..95e3f986 --- /dev/null +++ b/components/recharts-poc/mocks/keywords.ts @@ -0,0 +1,22 @@ +export interface KeywordPoint { + date: string; + shortTail: number; + longTail: number; +} + +export const KEYWORDS: KeywordPoint[] = [ + { date: 'Aug 25', shortTail: 35, longTail: 62 }, + { date: 'Aug 27', shortTail: 50, longTail: 68 }, + { date: 'Aug 29', shortTail: 42, longTail: 55 }, + { date: 'Aug 31', shortTail: 22, longTail: 35 }, + { date: 'Sep 2', shortTail: 48, longTail: 60 }, + { date: 'Sep 4', shortTail: 38, longTail: 50 }, + { date: 'Sep 6', shortTail: 20, longTail: 30 }, + { date: 'Sep 8', shortTail: 70, longTail: 92 }, + { date: 'Sep 10', shortTail: 55, longTail: 70 }, + { date: 'Sep 12', shortTail: 38, longTail: 48 }, + { date: 'Sep 14', shortTail: 18, longTail: 28 }, + { date: 'Sep 16', shortTail: 45, longTail: 62 }, + { date: 'Sep 18', shortTail: 42, longTail: 55 }, + { date: 'Sep 20', shortTail: 30, longTail: 38 }, +]; diff --git a/components/recharts-poc/mocks/kpis.ts b/components/recharts-poc/mocks/kpis.ts new file mode 100644 index 00000000..953b98cc --- /dev/null +++ b/components/recharts-poc/mocks/kpis.ts @@ -0,0 +1,41 @@ +import type { LucideIcon } from 'lucide-react'; +import { Eye, FileText, MousePointerClick, Users } from 'lucide-react'; + +export interface KpiTile { + label: string; + current: number; + previous: number; + format: 'count' | 'percent'; + icon: LucideIcon; +} + +export const KPIS: KpiTile[] = [ + { + label: 'Unique Pages', + current: 570, + previous: 555, + format: 'count', + icon: FileText, + }, + { + label: 'Page Views', + current: 12480, + previous: 11200, + format: 'count', + icon: Eye, + }, + { + label: 'CTR', + current: 4.3, + previous: 4.6, + format: 'percent', + icon: MousePointerClick, + }, + { + label: 'Active Users', + current: 1820, + previous: 1900, + format: 'count', + icon: Users, + }, +]; diff --git a/components/recharts-poc/mocks/salesAsp.ts b/components/recharts-poc/mocks/salesAsp.ts new file mode 100644 index 00000000..aa687385 --- /dev/null +++ b/components/recharts-poc/mocks/salesAsp.ts @@ -0,0 +1,14 @@ +export interface SalesAspPoint { + month: string; + asp: number; + sales: number; +} + +export const SALES_ASP: SalesAspPoint[] = [ + { month: 'May', asp: 24, sales: 55 }, + { month: 'Jun', asp: 27, sales: 60 }, + { month: 'Jul', asp: 25, sales: 70 }, + { month: 'Aug', asp: 20, sales: 130 }, + { month: 'Sep', asp: 17, sales: 200 }, + { month: 'Oct', asp: 15, sales: 245 }, +]; From dc369d2047e6b3189691b12d38cc7aaa3968246e Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:36:39 -0400 Subject: [PATCH 09/22] feat: add DeltaBadge for delta indicators --- components/recharts-poc/kpi/DeltaBadge.tsx | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 components/recharts-poc/kpi/DeltaBadge.tsx diff --git a/components/recharts-poc/kpi/DeltaBadge.tsx b/components/recharts-poc/kpi/DeltaBadge.tsx new file mode 100644 index 00000000..49d3f455 --- /dev/null +++ b/components/recharts-poc/kpi/DeltaBadge.tsx @@ -0,0 +1,49 @@ +import { ArrowDown, ArrowUp, Minus } from 'lucide-react'; + +import type { PoPResult } from '@/components/recharts-poc/primitives/periodOverPeriod'; + +interface DeltaBadgeProps { + result: PoPResult; + format?: 'count' | 'percent'; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function DeltaBadge(props: DeltaBadgeProps) { + const { result, format = 'count' } = props; + const { absDelta, pctDelta, direction } = result; + + if (direction === 'flat') { + return ( + + + ); + } + + const isUp = direction === 'up'; + const Icon = isUp ? ArrowUp : ArrowDown; + const color = isUp + ? 'text-green-600 dark:text-green-400' + : 'text-red-600 dark:text-red-400'; + const sign = isUp ? '+' : ''; + const baseline = pctDelta === null ? ' (no baseline)' : ''; + const numeric = + pctDelta === null + ? `${sign}${NUMBER_FMT.format(absDelta)}${format === 'percent' ? '%' : ''}` + : `${sign}${pctDelta.toFixed(1)}%`; + + return ( + + + ); +} From cbcb190fe54345808589ab9aaa250d016b62806b Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:37:01 -0400 Subject: [PATCH 10/22] feat: add KpiCard component --- components/recharts-poc/kpi/KpiCard.tsx | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 components/recharts-poc/kpi/KpiCard.tsx diff --git a/components/recharts-poc/kpi/KpiCard.tsx b/components/recharts-poc/kpi/KpiCard.tsx new file mode 100644 index 00000000..50197692 --- /dev/null +++ b/components/recharts-poc/kpi/KpiCard.tsx @@ -0,0 +1,41 @@ +import type { LucideIcon } from 'lucide-react'; + +import { periodOverPeriod } from '@/components/recharts-poc/primitives/periodOverPeriod'; + +import { DeltaBadge } from './DeltaBadge'; + +interface KpiCardProps { + label: string; + current: number; + previous: number; + format: 'count' | 'percent'; + icon: LucideIcon; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function KpiCard(props: KpiCardProps) { + const { label, current, previous, format, icon: Icon } = props; + const result = periodOverPeriod(current, previous); + const formatted = + format === 'percent' + ? `${current.toFixed(1)}%` + : NUMBER_FMT.format(current); + + return ( +
+
+
+
+ + {label} + + + {formatted} + + +
+
+ ); +} From ccb35a3cfdb1494cba787ec78562702446168d33 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:41:41 -0400 Subject: [PATCH 11/22] feat: add BarChartCard with position picker --- .../recharts-poc/charts/BarChartCard.tsx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 components/recharts-poc/charts/BarChartCard.tsx diff --git a/components/recharts-poc/charts/BarChartCard.tsx b/components/recharts-poc/charts/BarChartCard.tsx new file mode 100644 index 00000000..248affa3 --- /dev/null +++ b/components/recharts-poc/charts/BarChartCard.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + Cell, + LabelList, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { KEYWORD_LENGTH } from '@/components/recharts-poc/mocks/keywordLength'; +import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; +import { usePalette } from '@/components/recharts-poc/primitives/colors'; +import { LabelsToggle } from '@/components/recharts-poc/primitives/LabelsToggle'; + +const POSITIONS = ['top', 'inside', 'center', 'insideTop'] as const; +type Position = (typeof POSITIONS)[number]; + +interface BarChartCardProps { + perfMode: boolean; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function BarChartCard(props: BarChartCardProps) { + const { perfMode } = props; + const [labelsOn, setLabelsOn] = useState(true); + const [position, setPosition] = useState('top'); + const palette = usePalette(); + + const colors = [ + palette.primary, + palette.primary, + palette.primaryStrong, + palette.primaryStrong, + palette.primaryStrong, + palette.primary, + palette.primary, + palette.primary, + palette.primary, + palette.primary, + ]; + + return ( + + + + + } + > + + + + + NUMBER_FMT.format(Number(v))} + /> + + {KEYWORD_LENGTH.map((_, idx) => ( + + ))} + {labelsOn ? ( + NUMBER_FMT.format(Number(v))} + fill={palette.textLabel} + fontSize={12} + /> + ) : null} + + + + ); +} + +function LabelPositionPicker(props: { + value: Position; + onChange: (next: Position) => void; +}) { + const { value, onChange } = props; + return ( +
+ {POSITIONS.map((p) => ( + + ))} +
+ ); +} From da00114bfe27e36e884799e45e29c53af5b8ee26 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:46:11 -0400 Subject: [PATCH 12/22] feat: add LineChartCard with legend toggle --- .../recharts-poc/charts/LineChartCard.tsx | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 components/recharts-poc/charts/LineChartCard.tsx diff --git a/components/recharts-poc/charts/LineChartCard.tsx b/components/recharts-poc/charts/LineChartCard.tsx new file mode 100644 index 00000000..cf0ff32e --- /dev/null +++ b/components/recharts-poc/charts/LineChartCard.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { + Area, + CartesianGrid, + ComposedChart, + Legend, + Line, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { KEYWORDS } from '@/components/recharts-poc/mocks/keywords'; +import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; +import { usePalette } from '@/components/recharts-poc/primitives/colors'; +import { LabelsToggle } from '@/components/recharts-poc/primitives/LabelsToggle'; + +interface LineChartCardProps { + perfMode: boolean; +} + +export function LineChartCard(props: LineChartCardProps) { + const { perfMode } = props; + const [labelsOn, setLabelsOn] = useState(false); + const [hidden, setHidden] = useState>(new Set()); + const palette = usePalette(); + + const toggleSeries = (key: string) => { + setHidden((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + return ( + } + > + + + + + + { + if (entry && typeof entry.dataKey === 'string') + toggleSeries(entry.dataKey); + }} + /> + + + + + ); +} From 9cc37b25f937afe189e89261decaff40fd158eed Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:46:35 -0400 Subject: [PATCH 13/22] feat: add dual axis chart card --- .../recharts-poc/charts/DualAxisChartCard.tsx | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 components/recharts-poc/charts/DualAxisChartCard.tsx diff --git a/components/recharts-poc/charts/DualAxisChartCard.tsx b/components/recharts-poc/charts/DualAxisChartCard.tsx new file mode 100644 index 00000000..7f107cb6 --- /dev/null +++ b/components/recharts-poc/charts/DualAxisChartCard.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { + CartesianGrid, + ComposedChart, + Legend, + Line, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { SALES_ASP } from '@/components/recharts-poc/mocks/salesAsp'; +import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; +import { usePalette } from '@/components/recharts-poc/primitives/colors'; + +interface DualAxisChartCardProps { + perfMode: boolean; +} + +export function DualAxisChartCard(props: DualAxisChartCardProps) { + const { perfMode } = props; + const [hidden, setHidden] = useState>(new Set()); + const palette = usePalette(); + + const toggleSeries = (key: string) => { + setHidden((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + return ( + + + + + `$${v}`} + /> + + + { + if (entry && typeof entry.dataKey === 'string') + toggleSeries(entry.dataKey); + }} + /> + + + + + ); +} From 9e9e81285b84850a2bf7731489e9105307dd8e35 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:47:00 -0400 Subject: [PATCH 14/22] feat: add histogram card with two metrics --- .../recharts-poc/charts/HistogramCard.tsx | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 components/recharts-poc/charts/HistogramCard.tsx diff --git a/components/recharts-poc/charts/HistogramCard.tsx b/components/recharts-poc/charts/HistogramCard.tsx new file mode 100644 index 00000000..9a860a1c --- /dev/null +++ b/components/recharts-poc/charts/HistogramCard.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + LabelList, + Legend, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { KEYWORD_LENGTH } from '@/components/recharts-poc/mocks/keywordLength'; +import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; +import { usePalette } from '@/components/recharts-poc/primitives/colors'; +import { LabelsToggle } from '@/components/recharts-poc/primitives/LabelsToggle'; + +interface HistogramCardProps { + perfMode: boolean; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function HistogramCard(props: HistogramCardProps) { + const { perfMode } = props; + const [labelsOn, setLabelsOn] = useState(false); + const [hidden, setHidden] = useState>(new Set()); + const palette = usePalette(); + + const toggle = (key: string) => { + setHidden((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + return ( + } + > + + + + + NUMBER_FMT.format(Number(v))} + /> + { + if (entry && typeof entry.dataKey === 'string') + toggle(entry.dataKey); + }} + /> + + {labelsOn ? ( + NUMBER_FMT.format(Number(v))} + fill={palette.textLabel} + fontSize={10} + /> + ) : null} + + + {labelsOn ? ( + NUMBER_FMT.format(Number(v))} + fill={palette.textLabel} + fontSize={10} + /> + ) : null} + + + + ); +} From cc870bee6f5372745b98074a2e723c33ea123a13 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:48:41 -0400 Subject: [PATCH 15/22] feat: add donut chart card --- components/recharts-poc/charts/DonutCard.tsx | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 components/recharts-poc/charts/DonutCard.tsx diff --git a/components/recharts-poc/charts/DonutCard.tsx b/components/recharts-poc/charts/DonutCard.tsx new file mode 100644 index 00000000..5ebb5a60 --- /dev/null +++ b/components/recharts-poc/charts/DonutCard.tsx @@ -0,0 +1,79 @@ +import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'; + +import { DEVICE_PERFORMANCE } from '@/components/recharts-poc/mocks/devicePerformance'; +import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; +import { usePalette } from '@/components/recharts-poc/primitives/colors'; + +interface DonutCardProps { + perfMode: boolean; +} + +interface PercentLabelProps { + cx: number; + cy: number; + midAngle: number; + innerRadius: number; + outerRadius: number; + percent: number; +} + +const renderPercentLabel = (props: PercentLabelProps) => { + const { cx, cy, midAngle, innerRadius, outerRadius, percent } = props; + const RADIAN = Math.PI / 180; + const r = innerRadius + (outerRadius - innerRadius) * 1.25; + const x = cx + r * Math.cos(-midAngle * RADIAN); + const y = cy + r * Math.sin(-midAngle * RADIAN); + return ( + cx ? 'start' : 'end'} + dominantBaseline="central" + fontSize={14} + fill="currentColor" + > + {`${(percent * 100).toFixed(1)}%`} + + ); +}; + +export function DonutCard(props: DonutCardProps) { + const { perfMode } = props; + const palette = usePalette(); + + return ( + + + + + renderPercentLabel(p as PercentLabelProps)} + labelLine={false} + isAnimationActive={!perfMode} + activeShape={ + { outerRadius: 120 } as unknown as React.SVGProps + } + > + {DEVICE_PERFORMANCE.map((entry, idx) => ( + + ))} + + + + ); +} From 54db7272bc05c12806db04d8a3a81a0666befe5e Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:53:42 -0400 Subject: [PATCH 16/22] feat: add improved keywords breakdown table --- .../recharts-poc/table/BreakdownTable.tsx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 components/recharts-poc/table/BreakdownTable.tsx diff --git a/components/recharts-poc/table/BreakdownTable.tsx b/components/recharts-poc/table/BreakdownTable.tsx new file mode 100644 index 00000000..ce34cab3 --- /dev/null +++ b/components/recharts-poc/table/BreakdownTable.tsx @@ -0,0 +1,146 @@ +import { useMemo, useState } from 'react'; + +import { DeltaBadge } from '@/components/recharts-poc/kpi/DeltaBadge'; +import { IMPROVED_KEYWORDS } from '@/components/recharts-poc/mocks/improvedKeywords'; +import { periodOverPeriod } from '@/components/recharts-poc/primitives/periodOverPeriod'; + +type SortKey = 'keyword' | 'positionNow' | 'positionBefore' | 'delta'; +type SortDir = 'asc' | 'desc'; + +export function BreakdownTable() { + const [sortKey, setSortKey] = useState('delta'); + const [sortDir, setSortDir] = useState('desc'); + + const rows = useMemo(() => { + const enriched = IMPROVED_KEYWORDS.map((row) => { + const result = periodOverPeriod(row.positionBefore, row.positionNow); + return { ...row, delta: result.absDelta, result }; + }); + enriched.sort((a, b) => { + const dir = sortDir === 'asc' ? 1 : -1; + if (sortKey === 'keyword') + return a.keyword.localeCompare(b.keyword) * dir; + const av = a[sortKey]; + const bv = b[sortKey]; + return (Number(av) - Number(bv)) * dir; + }); + return enriched; + }, [sortKey, sortDir]); + + const maxDelta = useMemo( + () => Math.max(...rows.map((r) => Math.abs(r.delta))), + [rows], + ); + + const onHeaderClick = (key: SortKey) => { + if (sortKey === key) { + setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); + } else { + setSortKey(key); + setSortDir('desc'); + } + }; + + return ( +
+

+ Improved Keywords +

+
+ + + + + + + + + + + {rows.map((row) => { + const intensity = + maxDelta === 0 ? 0 : Math.abs(row.delta) / maxDelta; + const tint = `rgba(74, 144, 226, ${(intensity * 0.25).toFixed(3)})`; + return ( + + + + + + + ); + })} + +
onHeaderClick('keyword')} + active={sortKey === 'keyword'} + dir={sortDir} + > + Keyword + onHeaderClick('positionNow')} + active={sortKey === 'positionNow'} + dir={sortDir} + align="right" + > + Position now + onHeaderClick('positionBefore')} + active={sortKey === 'positionBefore'} + dir={sortDir} + align="right" + > + Position before + onHeaderClick('delta')} + active={sortKey === 'delta'} + dir={sortDir} + align="right" + > + Δ +
+ {row.keyword} + + {row.positionNow} + + {row.positionBefore} + + +
+
+
+ ); +} + +interface ThProps { + onClick: () => void; + active: boolean; + dir: SortDir; + align?: 'left' | 'right'; + children: React.ReactNode; +} + +function Th(props: ThProps) { + const { onClick, active, dir, align = 'left', children } = props; + const arrow = active ? (dir === 'asc' ? ' ▲' : ' ▼') : ''; + return ( + + + + ); +} From 49eff534dabc37d9fa6962fbaa76b92a0b865a43 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:56:59 -0400 Subject: [PATCH 17/22] feat: compose recharts poc dashboard --- components/recharts-poc/RechartsPocPage.tsx | 54 +++++++++++++++++++++ components/recharts-poc/index.ts | 1 + 2 files changed, 55 insertions(+) create mode 100644 components/recharts-poc/RechartsPocPage.tsx create mode 100644 components/recharts-poc/index.ts diff --git a/components/recharts-poc/RechartsPocPage.tsx b/components/recharts-poc/RechartsPocPage.tsx new file mode 100644 index 00000000..37a4e879 --- /dev/null +++ b/components/recharts-poc/RechartsPocPage.tsx @@ -0,0 +1,54 @@ +import { useRouter } from 'next/router'; + +import { BarChartCard } from './charts/BarChartCard'; +import { DonutCard } from './charts/DonutCard'; +import { DualAxisChartCard } from './charts/DualAxisChartCard'; +import { HistogramCard } from './charts/HistogramCard'; +import { LineChartCard } from './charts/LineChartCard'; +import { KpiCard } from './kpi/KpiCard'; +import { KPIS } from './mocks/kpis'; +import { BreakdownTable } from './table/BreakdownTable'; + +export function RechartsPocPage() { + const router = useRouter(); + const perfMode = router.query.perf === '1'; + + return ( +
+
+

+ Recharts POC +

+

+ Single-page validation of Recharts against our acceptance criteria. + Add{' '} + + ?perf=1 + {' '} + to disable animations. + {perfMode ? ( + + perf mode + + ) : null} +

+
+ +
+ {KPIS.map((kpi) => ( + + ))} +
+ +
+ + + + + +
+ + +
+ ); +} diff --git a/components/recharts-poc/index.ts b/components/recharts-poc/index.ts new file mode 100644 index 00000000..0d73aded --- /dev/null +++ b/components/recharts-poc/index.ts @@ -0,0 +1 @@ +export { RechartsPocPage } from './RechartsPocPage'; From 83f1fb947638f79ad18937b98787803de4900a39 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 07:58:15 -0400 Subject: [PATCH 18/22] feat: add labs recharts route --- pages/labs/recharts.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 pages/labs/recharts.tsx diff --git a/pages/labs/recharts.tsx b/pages/labs/recharts.tsx new file mode 100644 index 00000000..102d55a0 --- /dev/null +++ b/pages/labs/recharts.tsx @@ -0,0 +1,19 @@ +import Head from 'next/head'; + +import { RechartsPocPage } from '@/components/recharts-poc'; + +export default function RechartsLabPage() { + return ( + <> + + Recharts POC | Serhii Shramko + + + + + + ); +} From 3b8edeebc6e1a6526db1be5b1b0b8e3309c709af Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 08:03:25 -0400 Subject: [PATCH 19/22] docs: add recharts poc report --- docs/recharts-poc-report.md | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/recharts-poc-report.md diff --git a/docs/recharts-poc-report.md b/docs/recharts-poc-report.md new file mode 100644 index 00000000..fd34bc39 --- /dev/null +++ b/docs/recharts-poc-report.md @@ -0,0 +1,70 @@ +# Recharts — POC Report + +**Date:** 2026-06-09 +**Author:** Serhii Shramko +**Demo route:** `/labs/recharts` (animations on) / `/labs/recharts?perf=1` (animations off) +**Source:** `components/recharts-poc/` +**Plan:** `docs/superpowers/plans/2026-06-09-recharts-poc-plan.md` (local-only) +**Design spec:** `docs/superpowers/specs/2026-06-09-recharts-poc-design.md` (local-only) + +## Verdict + +**Ship — conditional on perf finding on Marta's Lenovo.** Recharts cleanly satisfies every functional acceptance criterion attempted in this POC: bar/line/dual-axis/histogram/donut charts, a sortable breakdown table, period-over-period deltas, label position control, label toggling, legend-driven series visibility, and tooltips. The library is actively maintained, fully typed, well-documented, and the API surface for our use cases is small. The only open question is rendering performance on low-spec hardware, captured in the "Weak-laptop test" section below; the `?perf=1` opt-out gives Marta a clean A/B test on her Lenovo. + +## Functional acceptance criteria + +| # | Criterion | Status | Evidence | Notes | +|---|-----------|:------:|----------|-------| +| 1 | Bar chart w/ rounded corners + custom colors | ✅ | `components/recharts-poc/charts/BarChartCard.tsx` | `radius={[8,8,0,0]}` on ``; per-bar ``; colors driven by `usePalette()` so the palette adapts to theme. | +| 2 | Line chart w/ splines | ✅ | `components/recharts-poc/charts/LineChartCard.tsx` | `` + `` fill overlay for the long-tail series. | +| 3 | Dual-axis different scales | ✅ | `components/recharts-poc/charts/DualAxisChartCard.tsx` | `` + two `` with independent orientations, tick formatters, and colours. | +| 4 | Histogram comparing 2 metrics | ⚠️ | `components/recharts-poc/charts/HistogramCard.tsx` | Recharts has no native histogram primitive — we pre-bucket data in `mocks/keywordLength.ts` and render two `` series side-by-side. Standard, idiomatic Recharts approach. | +| 5 | Pie/donut chart with %-labels | ✅ | `components/recharts-poc/charts/DonutCard.tsx` | `` + custom percent-label renderer positioned outside the slice. Hover-grow via `activeShape={{ outerRadius: 120 }}` (note: Recharts 3.x drives this through internal Redux tooltip state, not the deprecated `activeIndex` prop). | +| 6 | Table with metric breakdown | ✅ | `components/recharts-poc/table/BreakdownTable.tsx` | Plain HTML table; sortable column headers; heat-tinted "position now" cell scaled by `\|delta\|/maxDelta`; Δ column rendered via shared `DeltaBadge`. | +| 7 | Data labels with position control + format hooks | ✅ | `` children across charts; position picker on `BarChartCard` | The bar chart exposes a `top / inside / center / insideTop` picker so a reviewer can see position control without reading source. Formatter hook used on bar + histogram (`Intl.NumberFormat('en-US')`). | +| 8 | Toggle data labels on/off | ✅ | `primitives/LabelsToggle.tsx` per chart | Bar, line, histogram each have an independent toggle. Donut renders labels always (it's unreadable without them). | +| 9 | Period-over-period delta | ✅ | `primitives/periodOverPeriod.ts` + `kpi/DeltaBadge.tsx` | Pure helper, fully tested (positive, negative, flat, zero-prev, zero-curr, both-zero). Renders `▲ +N%` / `▼ -N%` / `— 0%` and gracefully degrades to absolute delta when there's no prior-period baseline. | +| 10 | Tooltips, legend visible, series toggle, responsive layout | ✅ | `` + `` + `hide` prop | Legend click toggles a local `Set` of hidden series; the corresponding `` / `` reads from it. Applied to line, histogram, dual-axis (bar and donut don't need it — single effective series). | + +## Non-functional + +| Criterion | Status | Evidence | +|-----------|:------:|----------| +| Last release ≤ 6 months | ✅ | `recharts@3.8.1` released **2026-03-25** (npm registry). Today is 2026-06-09 — about 2.5 months ago. Well within the window. | +| TypeScript types | ✅ | Recharts ships its own types — no `@types/recharts` needed. The POC's `pnpm typecheck` is clean across 21 new files. Two type widenings were needed in v3 strict types: `Tooltip.formatter` / `LabelList.formatter` accept `(v: unknown) => ...`, and `Pie.activeShape` needs a cast to attach a plain object — both documented inline in the chart files. | +| Documentation quality | ✅ | recharts.org + Storybook stories were sufficient for every chart attempted. The only "discoverability" pain point is the v3 API rewrite around `Pie` hover behaviour, which has migrated from props to internal state — but the new behaviour is correct and the types make the right shape obvious. | +| Bundle impact | ⚠️ | Measured from `next build` output via `.next/build-manifest.json`. **First Load JS (gzipped) — `/labs/recharts`: 315.0 kB vs homepage `/`: 206.0 kB → Δ ≈ 109 kB.** Raw uncompressed delta: ~393 kB (Recharts + d3-* family + Redux Toolkit + react-redux + immer, all from Recharts' transitive graph). Pages Router code-splits per route, so this is **route-local — the homepage and other pages are unaffected**. Reasonable for an analytics page; heavy for a marketing page. | + +## Weak-laptop test (Marta's Lenovo) + +To be filled in after running on the target hardware. Test instructions: + +1. Open `http://shramko.dev/labs/recharts` (or local dev URL). +2. Note cold-load time, scroll smoothness, tooltip lag, series-toggle responsiveness. +3. Re-open `http://shramko.dev/labs/recharts?perf=1` (animations off) and repeat. +4. Fill in the scores below (1 = unusable, 5 = smooth). + +| Scenario | Animations on | Animations off | +|----------|---------------|----------------| +| Cold load to interactive | _ s | _ s | +| Scroll smoothness (1–5) | _ | _ | +| Tooltip lag (1–5) | _ | _ | +| Series toggle response (1–5) | _ | _ | +| Subjective verdict | _ | _ | + +## Known limitations / workarounds + +- **No native histogram primitive.** Recharts has no `HistogramChart` — we pre-bucket data and render with `` and two `` series. This is the library-idiomatic approach. +- **Legend `onClick` not wired by default.** We attach a small handler that toggles a local `Set` of hidden series; each series reads `hide={hiddenSet.has(key)}`. ~6 lines per chart. +- **`ResponsiveContainer` + SSR.** Under Pages Router SSR, `ResponsiveContainer` measures DOM dimensions before they exist and would render at 0×0 on first paint. Mitigated by a `useIsMounted()` guard in `ChartCard` that renders a same-height skeleton until the component mounts. No hydration warnings observed. +- **`next-themes` flash on first render.** `resolvedTheme` is `undefined` on first render. `usePalette()` defaults to the light palette until mounted, then switches once `resolvedTheme` is available. Combined with the `ChartCard` mount guard, charts only paint once the theme is resolved. +- **`Pie.activeIndex` removed in v3.** Hover-grow now flows through the chart's internal Redux tooltip state; the consumer just declares `activeShape={...}` and the active sector renders with those props automatically. Documented inline in `DonutCard.tsx`. +- **Bundle size.** ~109 kB gzipped per route is real. Pages Router code-splits per route, so this is contained to `/labs/recharts` and any future chart pages; the rest of the site is unaffected. + +## Extraction notes + +To lift this POC into a separate app: + +1. Copy `pages/labs/recharts.tsx` and the entire `components/recharts-poc/` folder. +2. Install: `recharts`, `next-themes`, `clsx`, `lucide-react`, plus the new app's normal Next.js + Tailwind setup. +3. The POC has zero imports reaching outside its own folder. Only the page file imports from the barrel `@/components/recharts-poc`. Verified by `grep -rn "recharts-poc" pages/ components/`. From 2880d77b6273a2283d2f098c8c24f8318274cea9 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 08:04:33 -0400 Subject: [PATCH 20/22] fix: exclude labs paths from sitemap --- next-sitemap.config.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next-sitemap.config.cjs b/next-sitemap.config.cjs index 652b1a2a..02fe23b2 100644 --- a/next-sitemap.config.cjs +++ b/next-sitemap.config.cjs @@ -10,7 +10,7 @@ const pathLevelToPriority = { 3: 0.7, }; -const EXCLUDED_PATHS = ['/feed.xml']; +const EXCLUDED_PATHS = ['/feed.xml', '/labs/*']; const getPathDepthLevel = (urlPath) => { if (urlPath === '/') { From f0f5837dd5f0b958373747b9c0fb1d0e5f5b51b1 Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 08:04:59 -0400 Subject: [PATCH 21/22] style: format poc report tables --- docs/recharts-poc-report.md | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/recharts-poc-report.md b/docs/recharts-poc-report.md index fd34bc39..44efad10 100644 --- a/docs/recharts-poc-report.md +++ b/docs/recharts-poc-report.md @@ -13,27 +13,27 @@ ## Functional acceptance criteria -| # | Criterion | Status | Evidence | Notes | -|---|-----------|:------:|----------|-------| -| 1 | Bar chart w/ rounded corners + custom colors | ✅ | `components/recharts-poc/charts/BarChartCard.tsx` | `radius={[8,8,0,0]}` on ``; per-bar ``; colors driven by `usePalette()` so the palette adapts to theme. | -| 2 | Line chart w/ splines | ✅ | `components/recharts-poc/charts/LineChartCard.tsx` | `` + `` fill overlay for the long-tail series. | -| 3 | Dual-axis different scales | ✅ | `components/recharts-poc/charts/DualAxisChartCard.tsx` | `` + two `` with independent orientations, tick formatters, and colours. | -| 4 | Histogram comparing 2 metrics | ⚠️ | `components/recharts-poc/charts/HistogramCard.tsx` | Recharts has no native histogram primitive — we pre-bucket data in `mocks/keywordLength.ts` and render two `` series side-by-side. Standard, idiomatic Recharts approach. | -| 5 | Pie/donut chart with %-labels | ✅ | `components/recharts-poc/charts/DonutCard.tsx` | `` + custom percent-label renderer positioned outside the slice. Hover-grow via `activeShape={{ outerRadius: 120 }}` (note: Recharts 3.x drives this through internal Redux tooltip state, not the deprecated `activeIndex` prop). | -| 6 | Table with metric breakdown | ✅ | `components/recharts-poc/table/BreakdownTable.tsx` | Plain HTML table; sortable column headers; heat-tinted "position now" cell scaled by `\|delta\|/maxDelta`; Δ column rendered via shared `DeltaBadge`. | -| 7 | Data labels with position control + format hooks | ✅ | `` children across charts; position picker on `BarChartCard` | The bar chart exposes a `top / inside / center / insideTop` picker so a reviewer can see position control without reading source. Formatter hook used on bar + histogram (`Intl.NumberFormat('en-US')`). | -| 8 | Toggle data labels on/off | ✅ | `primitives/LabelsToggle.tsx` per chart | Bar, line, histogram each have an independent toggle. Donut renders labels always (it's unreadable without them). | -| 9 | Period-over-period delta | ✅ | `primitives/periodOverPeriod.ts` + `kpi/DeltaBadge.tsx` | Pure helper, fully tested (positive, negative, flat, zero-prev, zero-curr, both-zero). Renders `▲ +N%` / `▼ -N%` / `— 0%` and gracefully degrades to absolute delta when there's no prior-period baseline. | -| 10 | Tooltips, legend visible, series toggle, responsive layout | ✅ | `` + `` + `hide` prop | Legend click toggles a local `Set` of hidden series; the corresponding `` / `` reads from it. Applied to line, histogram, dual-axis (bar and donut don't need it — single effective series). | +| # | Criterion | Status | Evidence | Notes | +| --- | ---------------------------------------------------------- | :----: | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Bar chart w/ rounded corners + custom colors | ✅ | `components/recharts-poc/charts/BarChartCard.tsx` | `radius={[8,8,0,0]}` on ``; per-bar ``; colors driven by `usePalette()` so the palette adapts to theme. | +| 2 | Line chart w/ splines | ✅ | `components/recharts-poc/charts/LineChartCard.tsx` | `` + `` fill overlay for the long-tail series. | +| 3 | Dual-axis different scales | ✅ | `components/recharts-poc/charts/DualAxisChartCard.tsx` | `` + two `` with independent orientations, tick formatters, and colours. | +| 4 | Histogram comparing 2 metrics | ⚠️ | `components/recharts-poc/charts/HistogramCard.tsx` | Recharts has no native histogram primitive — we pre-bucket data in `mocks/keywordLength.ts` and render two `` series side-by-side. Standard, idiomatic Recharts approach. | +| 5 | Pie/donut chart with %-labels | ✅ | `components/recharts-poc/charts/DonutCard.tsx` | `` + custom percent-label renderer positioned outside the slice. Hover-grow via `activeShape={{ outerRadius: 120 }}` (note: Recharts 3.x drives this through internal Redux tooltip state, not the deprecated `activeIndex` prop). | +| 6 | Table with metric breakdown | ✅ | `components/recharts-poc/table/BreakdownTable.tsx` | Plain HTML table; sortable column headers; heat-tinted "position now" cell scaled by `\|delta\|/maxDelta`; Δ column rendered via shared `DeltaBadge`. | +| 7 | Data labels with position control + format hooks | ✅ | `` children across charts; position picker on `BarChartCard` | The bar chart exposes a `top / inside / center / insideTop` picker so a reviewer can see position control without reading source. Formatter hook used on bar + histogram (`Intl.NumberFormat('en-US')`). | +| 8 | Toggle data labels on/off | ✅ | `primitives/LabelsToggle.tsx` per chart | Bar, line, histogram each have an independent toggle. Donut renders labels always (it's unreadable without them). | +| 9 | Period-over-period delta | ✅ | `primitives/periodOverPeriod.ts` + `kpi/DeltaBadge.tsx` | Pure helper, fully tested (positive, negative, flat, zero-prev, zero-curr, both-zero). Renders `▲ +N%` / `▼ -N%` / `— 0%` and gracefully degrades to absolute delta when there's no prior-period baseline. | +| 10 | Tooltips, legend visible, series toggle, responsive layout | ✅ | `` + `` + `hide` prop | Legend click toggles a local `Set` of hidden series; the corresponding `` / `` reads from it. Applied to line, histogram, dual-axis (bar and donut don't need it — single effective series). | ## Non-functional -| Criterion | Status | Evidence | -|-----------|:------:|----------| -| Last release ≤ 6 months | ✅ | `recharts@3.8.1` released **2026-03-25** (npm registry). Today is 2026-06-09 — about 2.5 months ago. Well within the window. | -| TypeScript types | ✅ | Recharts ships its own types — no `@types/recharts` needed. The POC's `pnpm typecheck` is clean across 21 new files. Two type widenings were needed in v3 strict types: `Tooltip.formatter` / `LabelList.formatter` accept `(v: unknown) => ...`, and `Pie.activeShape` needs a cast to attach a plain object — both documented inline in the chart files. | -| Documentation quality | ✅ | recharts.org + Storybook stories were sufficient for every chart attempted. The only "discoverability" pain point is the v3 API rewrite around `Pie` hover behaviour, which has migrated from props to internal state — but the new behaviour is correct and the types make the right shape obvious. | -| Bundle impact | ⚠️ | Measured from `next build` output via `.next/build-manifest.json`. **First Load JS (gzipped) — `/labs/recharts`: 315.0 kB vs homepage `/`: 206.0 kB → Δ ≈ 109 kB.** Raw uncompressed delta: ~393 kB (Recharts + d3-* family + Redux Toolkit + react-redux + immer, all from Recharts' transitive graph). Pages Router code-splits per route, so this is **route-local — the homepage and other pages are unaffected**. Reasonable for an analytics page; heavy for a marketing page. | +| Criterion | Status | Evidence | +| ----------------------- | :----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Last release ≤ 6 months | ✅ | `recharts@3.8.1` released **2026-03-25** (npm registry). Today is 2026-06-09 — about 2.5 months ago. Well within the window. | +| TypeScript types | ✅ | Recharts ships its own types — no `@types/recharts` needed. The POC's `pnpm typecheck` is clean across 21 new files. Two type widenings were needed in v3 strict types: `Tooltip.formatter` / `LabelList.formatter` accept `(v: unknown) => ...`, and `Pie.activeShape` needs a cast to attach a plain object — both documented inline in the chart files. | +| Documentation quality | ✅ | recharts.org + Storybook stories were sufficient for every chart attempted. The only "discoverability" pain point is the v3 API rewrite around `Pie` hover behaviour, which has migrated from props to internal state — but the new behaviour is correct and the types make the right shape obvious. | +| Bundle impact | ⚠️ | Measured from `next build` output via `.next/build-manifest.json`. **First Load JS (gzipped) — `/labs/recharts`: 315.0 kB vs homepage `/`: 206.0 kB → Δ ≈ 109 kB.** Raw uncompressed delta: ~393 kB (Recharts + d3-\* family + Redux Toolkit + react-redux + immer, all from Recharts' transitive graph). Pages Router code-splits per route, so this is **route-local — the homepage and other pages are unaffected**. Reasonable for an analytics page; heavy for a marketing page. | ## Weak-laptop test (Marta's Lenovo) @@ -44,13 +44,13 @@ To be filled in after running on the target hardware. Test instructions: 3. Re-open `http://shramko.dev/labs/recharts?perf=1` (animations off) and repeat. 4. Fill in the scores below (1 = unusable, 5 = smooth). -| Scenario | Animations on | Animations off | -|----------|---------------|----------------| -| Cold load to interactive | _ s | _ s | -| Scroll smoothness (1–5) | _ | _ | -| Tooltip lag (1–5) | _ | _ | -| Series toggle response (1–5) | _ | _ | -| Subjective verdict | _ | _ | +| Scenario | Animations on | Animations off | +| ---------------------------- | ------------- | -------------- | +| Cold load to interactive | \_ s | \_ s | +| Scroll smoothness (1–5) | \_ | \_ | +| Tooltip lag (1–5) | \_ | \_ | +| Series toggle response (1–5) | \_ | \_ | +| Subjective verdict | \_ | \_ | ## Known limitations / workarounds From 430c1fcc90764afd72a5d05f877fe4611296b03c Mon Sep 17 00:00:00 2001 From: shramko Date: Tue, 9 Jun 2026 08:55:16 -0400 Subject: [PATCH 22/22] chore: tmp --- components/recharts-poc/RechartsPocPage.tsx | 84 ++++--- .../recharts-poc/charts/BarChartCard.tsx | 128 ----------- components/recharts-poc/charts/DonutCard.tsx | 79 ------- .../recharts-poc/charts/DualAxisChartCard.tsx | 101 --------- .../charts/FeatureAdoptionCard.tsx | 74 +++++++ .../charts/FeatureRetentionCard.tsx | 38 ++++ .../recharts-poc/charts/HistogramCard.tsx | 113 ---------- .../recharts-poc/charts/LineChartCard.tsx | 94 -------- .../charts/MonthlyActiveUsersCard.tsx | 21 ++ .../charts/NewUsersCohortCard.tsx | 21 ++ .../charts/ProgramUtilizationCard.tsx | 100 +++++++++ .../recharts-poc/charts/SimpleLineCard.tsx | 120 ++++++++++ .../charts/UserActivitySegmentsCard.tsx | 109 ++++++++++ components/recharts-poc/data/engagement.ts | 205 ++++++++++++++++++ components/recharts-poc/kpi/DeltaBadge.tsx | 49 ----- .../recharts-poc/kpi/EngagementKpiCard.tsx | 34 +++ components/recharts-poc/kpi/KpiCard.tsx | 41 ---- .../recharts-poc/mocks/devicePerformance.ts | 9 - .../recharts-poc/mocks/improvedKeywords.ts | 26 --- .../recharts-poc/mocks/keywordLength.ts | 18 -- components/recharts-poc/mocks/keywords.ts | 22 -- components/recharts-poc/mocks/kpis.ts | 41 ---- components/recharts-poc/mocks/salesAsp.ts | 14 -- .../recharts-poc/primitives/ChartCard.tsx | 41 +++- .../recharts-poc/primitives/ChartLegend.tsx | 26 +++ .../primitives/FeatureDropdown.tsx | 91 ++++++++ .../recharts-poc/primitives/InfoTooltip.tsx | 18 ++ .../recharts-poc/primitives/LabelsToggle.tsx | 18 -- .../recharts-poc/primitives/SectionFrame.tsx | 16 ++ .../recharts-poc/primitives/colors.test.ts | 40 ---- components/recharts-poc/primitives/colors.ts | 56 ----- .../primitives/periodOverPeriod.test.ts | 51 ----- .../primitives/periodOverPeriod.ts | 23 -- components/recharts-poc/primitives/theme.ts | 35 +++ .../sections/EngagementSection.tsx | 44 ++++ .../recharts-poc/sections/TrendsSection.tsx | 60 +++++ .../recharts-poc/table/BreakdownTable.tsx | 146 ------------- next-env.d.ts | 2 +- 38 files changed, 1083 insertions(+), 1125 deletions(-) delete mode 100644 components/recharts-poc/charts/BarChartCard.tsx delete mode 100644 components/recharts-poc/charts/DonutCard.tsx delete mode 100644 components/recharts-poc/charts/DualAxisChartCard.tsx create mode 100644 components/recharts-poc/charts/FeatureAdoptionCard.tsx create mode 100644 components/recharts-poc/charts/FeatureRetentionCard.tsx delete mode 100644 components/recharts-poc/charts/HistogramCard.tsx delete mode 100644 components/recharts-poc/charts/LineChartCard.tsx create mode 100644 components/recharts-poc/charts/MonthlyActiveUsersCard.tsx create mode 100644 components/recharts-poc/charts/NewUsersCohortCard.tsx create mode 100644 components/recharts-poc/charts/ProgramUtilizationCard.tsx create mode 100644 components/recharts-poc/charts/SimpleLineCard.tsx create mode 100644 components/recharts-poc/charts/UserActivitySegmentsCard.tsx create mode 100644 components/recharts-poc/data/engagement.ts delete mode 100644 components/recharts-poc/kpi/DeltaBadge.tsx create mode 100644 components/recharts-poc/kpi/EngagementKpiCard.tsx delete mode 100644 components/recharts-poc/kpi/KpiCard.tsx delete mode 100644 components/recharts-poc/mocks/devicePerformance.ts delete mode 100644 components/recharts-poc/mocks/improvedKeywords.ts delete mode 100644 components/recharts-poc/mocks/keywordLength.ts delete mode 100644 components/recharts-poc/mocks/keywords.ts delete mode 100644 components/recharts-poc/mocks/kpis.ts delete mode 100644 components/recharts-poc/mocks/salesAsp.ts create mode 100644 components/recharts-poc/primitives/ChartLegend.tsx create mode 100644 components/recharts-poc/primitives/FeatureDropdown.tsx create mode 100644 components/recharts-poc/primitives/InfoTooltip.tsx delete mode 100644 components/recharts-poc/primitives/LabelsToggle.tsx create mode 100644 components/recharts-poc/primitives/SectionFrame.tsx delete mode 100644 components/recharts-poc/primitives/colors.test.ts delete mode 100644 components/recharts-poc/primitives/colors.ts delete mode 100644 components/recharts-poc/primitives/periodOverPeriod.test.ts delete mode 100644 components/recharts-poc/primitives/periodOverPeriod.ts create mode 100644 components/recharts-poc/primitives/theme.ts create mode 100644 components/recharts-poc/sections/EngagementSection.tsx create mode 100644 components/recharts-poc/sections/TrendsSection.tsx delete mode 100644 components/recharts-poc/table/BreakdownTable.tsx diff --git a/components/recharts-poc/RechartsPocPage.tsx b/components/recharts-poc/RechartsPocPage.tsx index 37a4e879..3f227ab3 100644 --- a/components/recharts-poc/RechartsPocPage.tsx +++ b/components/recharts-poc/RechartsPocPage.tsx @@ -1,54 +1,52 @@ import { useRouter } from 'next/router'; -import { BarChartCard } from './charts/BarChartCard'; -import { DonutCard } from './charts/DonutCard'; -import { DualAxisChartCard } from './charts/DualAxisChartCard'; -import { HistogramCard } from './charts/HistogramCard'; -import { LineChartCard } from './charts/LineChartCard'; -import { KpiCard } from './kpi/KpiCard'; -import { KPIS } from './mocks/kpis'; -import { BreakdownTable } from './table/BreakdownTable'; +import { theme } from './primitives/theme'; +import { EngagementSection } from './sections/EngagementSection'; +import { TrendsSection } from './sections/TrendsSection'; + +const CSS_VARS: Record = { + '--rcp-background': theme.background, + '--rcp-surface': theme.surface, + '--rcp-border': theme.border, + '--rcp-border-strong': theme.borderStrong, + '--rcp-text': theme.text, + '--rcp-text-muted': theme.textMuted, + '--rcp-text-subtle': theme.textSubtle, + '--rcp-tooltip-bg': theme.tooltipBackground, + '--rcp-tooltip-text': theme.tooltipText, +}; export function RechartsPocPage() { const router = useRouter(); const perfMode = router.query.perf === '1'; return ( -
-
-

- Recharts POC -

-

- Single-page validation of Recharts against our acceptance criteria. - Add{' '} - - ?perf=1 - {' '} - to disable animations. - {perfMode ? ( - - perf mode - - ) : null} -

-
- -
- {KPIS.map((kpi) => ( - - ))} -
- -
- - - - - -
+
+
+
+

+ Engagement Dashboard +

+

+ Recharts POC — engagement layout.{' '} + + ?perf=1 + {' '} + disables animations. + {perfMode ? ( + + perf mode + + ) : null} +

+
- -
+ + +
+ ); } diff --git a/components/recharts-poc/charts/BarChartCard.tsx b/components/recharts-poc/charts/BarChartCard.tsx deleted file mode 100644 index 248affa3..00000000 --- a/components/recharts-poc/charts/BarChartCard.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useState } from 'react'; -import { - Bar, - BarChart, - CartesianGrid, - Cell, - LabelList, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; - -import { KEYWORD_LENGTH } from '@/components/recharts-poc/mocks/keywordLength'; -import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; -import { usePalette } from '@/components/recharts-poc/primitives/colors'; -import { LabelsToggle } from '@/components/recharts-poc/primitives/LabelsToggle'; - -const POSITIONS = ['top', 'inside', 'center', 'insideTop'] as const; -type Position = (typeof POSITIONS)[number]; - -interface BarChartCardProps { - perfMode: boolean; -} - -const NUMBER_FMT = new Intl.NumberFormat('en-US'); - -export function BarChartCard(props: BarChartCardProps) { - const { perfMode } = props; - const [labelsOn, setLabelsOn] = useState(true); - const [position, setPosition] = useState('top'); - const palette = usePalette(); - - const colors = [ - palette.primary, - palette.primary, - palette.primaryStrong, - palette.primaryStrong, - palette.primaryStrong, - palette.primary, - palette.primary, - palette.primary, - palette.primary, - palette.primary, - ]; - - return ( - - - - - } - > - - - - - NUMBER_FMT.format(Number(v))} - /> - - {KEYWORD_LENGTH.map((_, idx) => ( - - ))} - {labelsOn ? ( - NUMBER_FMT.format(Number(v))} - fill={palette.textLabel} - fontSize={12} - /> - ) : null} - - - - ); -} - -function LabelPositionPicker(props: { - value: Position; - onChange: (next: Position) => void; -}) { - const { value, onChange } = props; - return ( -
- {POSITIONS.map((p) => ( - - ))} -
- ); -} diff --git a/components/recharts-poc/charts/DonutCard.tsx b/components/recharts-poc/charts/DonutCard.tsx deleted file mode 100644 index 5ebb5a60..00000000 --- a/components/recharts-poc/charts/DonutCard.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'; - -import { DEVICE_PERFORMANCE } from '@/components/recharts-poc/mocks/devicePerformance'; -import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; -import { usePalette } from '@/components/recharts-poc/primitives/colors'; - -interface DonutCardProps { - perfMode: boolean; -} - -interface PercentLabelProps { - cx: number; - cy: number; - midAngle: number; - innerRadius: number; - outerRadius: number; - percent: number; -} - -const renderPercentLabel = (props: PercentLabelProps) => { - const { cx, cy, midAngle, innerRadius, outerRadius, percent } = props; - const RADIAN = Math.PI / 180; - const r = innerRadius + (outerRadius - innerRadius) * 1.25; - const x = cx + r * Math.cos(-midAngle * RADIAN); - const y = cy + r * Math.sin(-midAngle * RADIAN); - return ( - cx ? 'start' : 'end'} - dominantBaseline="central" - fontSize={14} - fill="currentColor" - > - {`${(percent * 100).toFixed(1)}%`} - - ); -}; - -export function DonutCard(props: DonutCardProps) { - const { perfMode } = props; - const palette = usePalette(); - - return ( - - - - - renderPercentLabel(p as PercentLabelProps)} - labelLine={false} - isAnimationActive={!perfMode} - activeShape={ - { outerRadius: 120 } as unknown as React.SVGProps - } - > - {DEVICE_PERFORMANCE.map((entry, idx) => ( - - ))} - - - - ); -} diff --git a/components/recharts-poc/charts/DualAxisChartCard.tsx b/components/recharts-poc/charts/DualAxisChartCard.tsx deleted file mode 100644 index 7f107cb6..00000000 --- a/components/recharts-poc/charts/DualAxisChartCard.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useState } from 'react'; -import { - CartesianGrid, - ComposedChart, - Legend, - Line, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; - -import { SALES_ASP } from '@/components/recharts-poc/mocks/salesAsp'; -import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; -import { usePalette } from '@/components/recharts-poc/primitives/colors'; - -interface DualAxisChartCardProps { - perfMode: boolean; -} - -export function DualAxisChartCard(props: DualAxisChartCardProps) { - const { perfMode } = props; - const [hidden, setHidden] = useState>(new Set()); - const palette = usePalette(); - - const toggleSeries = (key: string) => { - setHidden((prev) => { - const next = new Set(prev); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - }; - - return ( - - - - - `$${v}`} - /> - - - { - if (entry && typeof entry.dataKey === 'string') - toggleSeries(entry.dataKey); - }} - /> - - - - - ); -} diff --git a/components/recharts-poc/charts/FeatureAdoptionCard.tsx b/components/recharts-poc/charts/FeatureAdoptionCard.tsx new file mode 100644 index 00000000..c1a58d3d --- /dev/null +++ b/components/recharts-poc/charts/FeatureAdoptionCard.tsx @@ -0,0 +1,74 @@ +import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts'; + +import { FEATURE_ADOPTION } from '../data/engagement'; +import { ChartCard } from '../primitives/ChartCard'; +import { theme } from '../primitives/theme'; + +interface FeatureAdoptionCardProps { + perfMode: boolean; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function FeatureAdoptionCard(props: FeatureAdoptionCardProps) { + const { perfMode } = props; + return ( + + + + NUMBER_FMT.format(v).replace(/,/g, ' ')} + /> + + [NUMBER_FMT.format(Number(v)), 'Users']} + /> + + + + ); +} diff --git a/components/recharts-poc/charts/FeatureRetentionCard.tsx b/components/recharts-poc/charts/FeatureRetentionCard.tsx new file mode 100644 index 00000000..060ecade --- /dev/null +++ b/components/recharts-poc/charts/FeatureRetentionCard.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; + +import { + FEATURE_RETENTION, + FEATURES, + type FeatureKey, +} from '../data/engagement'; +import { FeatureDropdown } from '../primitives/FeatureDropdown'; +import { SimpleLineCard } from './SimpleLineCard'; + +interface FeatureRetentionCardProps { + perfMode: boolean; +} + +export function FeatureRetentionCard(props: FeatureRetentionCardProps) { + const { perfMode } = props; + const [feature, setFeature] = useState('workout'); + + return ( + `${v}%`} + perfMode={perfMode} + actions={ + + } + /> + ); +} diff --git a/components/recharts-poc/charts/HistogramCard.tsx b/components/recharts-poc/charts/HistogramCard.tsx deleted file mode 100644 index 9a860a1c..00000000 --- a/components/recharts-poc/charts/HistogramCard.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useState } from 'react'; -import { - Bar, - BarChart, - CartesianGrid, - LabelList, - Legend, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; - -import { KEYWORD_LENGTH } from '@/components/recharts-poc/mocks/keywordLength'; -import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; -import { usePalette } from '@/components/recharts-poc/primitives/colors'; -import { LabelsToggle } from '@/components/recharts-poc/primitives/LabelsToggle'; - -interface HistogramCardProps { - perfMode: boolean; -} - -const NUMBER_FMT = new Intl.NumberFormat('en-US'); - -export function HistogramCard(props: HistogramCardProps) { - const { perfMode } = props; - const [labelsOn, setLabelsOn] = useState(false); - const [hidden, setHidden] = useState>(new Set()); - const palette = usePalette(); - - const toggle = (key: string) => { - setHidden((prev) => { - const next = new Set(prev); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - }; - - return ( - } - > - - - - - NUMBER_FMT.format(Number(v))} - /> - { - if (entry && typeof entry.dataKey === 'string') - toggle(entry.dataKey); - }} - /> - - {labelsOn ? ( - NUMBER_FMT.format(Number(v))} - fill={palette.textLabel} - fontSize={10} - /> - ) : null} - - - {labelsOn ? ( - NUMBER_FMT.format(Number(v))} - fill={palette.textLabel} - fontSize={10} - /> - ) : null} - - - - ); -} diff --git a/components/recharts-poc/charts/LineChartCard.tsx b/components/recharts-poc/charts/LineChartCard.tsx deleted file mode 100644 index cf0ff32e..00000000 --- a/components/recharts-poc/charts/LineChartCard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState } from 'react'; -import { - Area, - CartesianGrid, - ComposedChart, - Legend, - Line, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; - -import { KEYWORDS } from '@/components/recharts-poc/mocks/keywords'; -import { ChartCard } from '@/components/recharts-poc/primitives/ChartCard'; -import { usePalette } from '@/components/recharts-poc/primitives/colors'; -import { LabelsToggle } from '@/components/recharts-poc/primitives/LabelsToggle'; - -interface LineChartCardProps { - perfMode: boolean; -} - -export function LineChartCard(props: LineChartCardProps) { - const { perfMode } = props; - const [labelsOn, setLabelsOn] = useState(false); - const [hidden, setHidden] = useState>(new Set()); - const palette = usePalette(); - - const toggleSeries = (key: string) => { - setHidden((prev) => { - const next = new Set(prev); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - }; - - return ( - } - > - - - - - - { - if (entry && typeof entry.dataKey === 'string') - toggleSeries(entry.dataKey); - }} - /> - - - - - ); -} diff --git a/components/recharts-poc/charts/MonthlyActiveUsersCard.tsx b/components/recharts-poc/charts/MonthlyActiveUsersCard.tsx new file mode 100644 index 00000000..d38a2a4e --- /dev/null +++ b/components/recharts-poc/charts/MonthlyActiveUsersCard.tsx @@ -0,0 +1,21 @@ +import { MONTHLY_ACTIVE_USERS } from '../data/engagement'; +import { SimpleLineCard } from './SimpleLineCard'; + +interface MonthlyActiveUsersCardProps { + perfMode: boolean; +} + +export function MonthlyActiveUsersCard(props: MonthlyActiveUsersCardProps) { + const { perfMode } = props; + return ( + + ); +} diff --git a/components/recharts-poc/charts/NewUsersCohortCard.tsx b/components/recharts-poc/charts/NewUsersCohortCard.tsx new file mode 100644 index 00000000..2e163a1a --- /dev/null +++ b/components/recharts-poc/charts/NewUsersCohortCard.tsx @@ -0,0 +1,21 @@ +import { NEW_USERS_COHORT } from '../data/engagement'; +import { SimpleLineCard } from './SimpleLineCard'; + +interface NewUsersCohortCardProps { + perfMode: boolean; +} + +export function NewUsersCohortCard(props: NewUsersCohortCardProps) { + const { perfMode } = props; + return ( + + ); +} diff --git a/components/recharts-poc/charts/ProgramUtilizationCard.tsx b/components/recharts-poc/charts/ProgramUtilizationCard.tsx new file mode 100644 index 00000000..4973a929 --- /dev/null +++ b/components/recharts-poc/charts/ProgramUtilizationCard.tsx @@ -0,0 +1,100 @@ +import { + Area, + AreaChart, + CartesianGrid, + Label, + ReferenceLine, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { PROGRAM_UTILIZATION, UTILIZATION_TARGET } from '../data/engagement'; +import { ChartCard } from '../primitives/ChartCard'; +import { theme } from '../primitives/theme'; + +interface ProgramUtilizationCardProps { + perfMode: boolean; +} + +export function ProgramUtilizationCard(props: ProgramUtilizationCardProps) { + const { perfMode } = props; + return ( + + + + + + + + + + + `${v}%`} + /> + [`${Number(v)}%`, 'Utilization']} + /> + + + + + + ); +} diff --git a/components/recharts-poc/charts/SimpleLineCard.tsx b/components/recharts-poc/charts/SimpleLineCard.tsx new file mode 100644 index 00000000..e504b3a1 --- /dev/null +++ b/components/recharts-poc/charts/SimpleLineCard.tsx @@ -0,0 +1,120 @@ +import { type ReactNode } from 'react'; +import { + CartesianGrid, + Line, + LineChart, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import type { SeriesPoint } from '../data/engagement'; +import { ChartCard } from '../primitives/ChartCard'; +import { theme } from '../primitives/theme'; + +interface SimpleLineCardProps { + title: string; + description: string; + data: SeriesPoint[]; + seriesLabel: string; + yTicks: number[]; + yDomain: [number, number]; + yFormatter?: (v: number) => string; + perfMode: boolean; + actions?: ReactNode; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function SimpleLineCard(props: SimpleLineCardProps) { + const { + title, + description, + data, + seriesLabel, + yTicks, + yDomain, + yFormatter, + perfMode, + actions, + } = props; + + return ( + + + + + NUMBER_FMT.format(v))} + /> + { + const n = Number(v); + return [ + yFormatter ? yFormatter(n) : NUMBER_FMT.format(n), + seriesLabel, + ]; + }} + /> + + + + + ); +} diff --git a/components/recharts-poc/charts/UserActivitySegmentsCard.tsx b/components/recharts-poc/charts/UserActivitySegmentsCard.tsx new file mode 100644 index 00000000..8fcaaff3 --- /dev/null +++ b/components/recharts-poc/charts/UserActivitySegmentsCard.tsx @@ -0,0 +1,109 @@ +import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts'; + +import { + ACTIVITY_SEGMENTS, + type WeeklyActivity, + WEEKLY_ACTIVITY, +} from '../data/engagement'; +import { ChartCard } from '../primitives/ChartCard'; +import { SEGMENT_COLORS, theme } from '../primitives/theme'; + +interface UserActivitySegmentsCardProps { + perfMode: boolean; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function UserActivitySegmentsCard(props: UserActivitySegmentsCardProps) { + const { perfMode } = props; + return ( + + + + + `${v}%`} + /> + } + /> + + + + + + + ); +} + +interface StackTooltipProps { + active?: boolean; + payload?: Array<{ payload?: WeeklyActivity }>; +} + +function StackTooltip(props: StackTooltipProps) { + const { active, payload } = props; + if (!active || !payload?.length) return null; + const data = payload[0]?.payload; + if (!data) return null; + return ( +
+
100%
+
+ {NUMBER_FMT.format(data.total)} +
+
+ ); +} + +export const ACTIVITY_LEGEND = ACTIVITY_SEGMENTS; diff --git a/components/recharts-poc/data/engagement.ts b/components/recharts-poc/data/engagement.ts new file mode 100644 index 00000000..a9bfff34 --- /dev/null +++ b/components/recharts-poc/data/engagement.ts @@ -0,0 +1,205 @@ +export interface KpiTile { + label: string; + value: number; + format: 'count' | 'percent'; + info: string; +} + +export const KPIS: KpiTile[] = [ + { + label: 'Utilization rate, %', + value: 36, + format: 'percent', + info: 'Share of MAU who actively used the program during the selected period.', + }, + { + label: 'MAU', + value: 200, + format: 'count', + info: 'Monthly active users — unique users with at least one tracked action this month.', + }, +]; + +export interface WeeklyActivity { + week: string; + high: number; + moderate: number; + low: number; + inactive: number; + total: number; +} + +export const WEEKLY_ACTIVITY: WeeklyActivity[] = [ + { + week: 'Week 1', + high: 22, + moderate: 31, + low: 27, + inactive: 20, + total: 463_120, + }, + { + week: 'Week 2', + high: 28, + moderate: 34, + low: 23, + inactive: 15, + total: 478_523, + }, + { + week: 'Week 3', + high: 24, + moderate: 32, + low: 26, + inactive: 18, + total: 471_044, + }, + { + week: 'Week 4', + high: 26, + moderate: 30, + low: 25, + inactive: 19, + total: 469_812, + }, +]; + +export const ACTIVITY_SEGMENTS = [ + { key: 'high', label: 'High activity' }, + { key: 'moderate', label: 'Moderate activity' }, + { key: 'low', label: 'Low activity' }, + { key: 'inactive', label: 'Inactive' }, +] as const; + +export type ActivitySegmentKey = (typeof ACTIVITY_SEGMENTS)[number]['key']; + +export const FEATURE_KEYS = [ + 'meal-logging', + 'workout', + 'water-intake', + 'habit-check-in', + 'sleep-log', + 'step-challenge', +] as const; + +export type FeatureKey = (typeof FEATURE_KEYS)[number]; + +export interface FeatureMeta { + key: FeatureKey; + label: string; +} + +export const FEATURES: FeatureMeta[] = [ + { key: 'meal-logging', label: 'Meal Logging' }, + { key: 'workout', label: 'Workout' }, + { key: 'water-intake', label: 'Water Intake' }, + { key: 'habit-check-in', label: 'Habit Check-in' }, + { key: 'sleep-log', label: 'Sleep Log' }, + { key: 'step-challenge', label: 'Step Challenge' }, +]; + +export interface FeatureAdoption { + key: FeatureKey; + label: string; + users: number; +} + +export const FEATURE_ADOPTION: FeatureAdoption[] = [ + { key: 'meal-logging', label: 'Meal Logging', users: 8_400 }, + { key: 'workout', label: 'Workout', users: 6_200 }, + { key: 'water-intake', label: 'Water Intake', users: 4_900 }, + { key: 'habit-check-in', label: 'Habit Check-in', users: 3_700 }, + { key: 'sleep-log', label: 'Sleep Log', users: 2_400 }, + { key: 'step-challenge', label: 'Step Challenge', users: 1_900 }, +]; + +export const MONTHS = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +] as const; + +export type Month = (typeof MONTHS)[number]; + +export interface SeriesPoint { + month: Month; + actual: number | null; + projected: number | null; +} + +function buildSeries( + values: Array, + projectedFrom: number, +): SeriesPoint[] { + return MONTHS.map((month, idx) => { + const v = values[idx] ?? null; + if (idx < projectedFrom) { + return { month, actual: v, projected: null }; + } + if (idx === projectedFrom) { + return { month, actual: v, projected: v }; + } + return { month, actual: null, projected: v }; + }); +} + +export const MONTHLY_ACTIVE_USERS: SeriesPoint[] = buildSeries( + [1_000, 460, 360, 320, 290, 260, 240, 230, 220, 215, 195, 180], + 9, +); + +export const NEW_USERS_COHORT: SeriesPoint[] = buildSeries( + [80, 400, 820, 720, 600, 480, 380, 310, 230, 220, 210, 200], + 9, +); + +export type RetentionByFeature = Record; + +export const FEATURE_RETENTION: RetentionByFeature = { + 'meal-logging': buildSeries( + [100, 62, 48, 41, 37, 34, 32, 31, 29, 28, 27, 26], + 9, + ), + workout: buildSeries([100, 38, 31, 27, 25, 24, 23, 22, 22, 21, 20, 19], 9), + 'water-intake': buildSeries( + [100, 55, 44, 38, 34, 32, 30, 28, 27, 26, 25, 24], + 9, + ), + 'habit-check-in': buildSeries( + [100, 48, 37, 31, 28, 26, 24, 23, 22, 21, 20, 19], + 9, + ), + 'sleep-log': buildSeries( + [100, 42, 32, 27, 24, 22, 21, 20, 19, 18, 17, 16], + 9, + ), + 'step-challenge': buildSeries( + [100, 33, 24, 19, 16, 14, 13, 12, 11, 10, 10, 9], + 9, + ), +}; + +export interface UtilizationPoint { + month: Month; + utilization: number; +} + +export const PROGRAM_UTILIZATION: UtilizationPoint[] = [ + { month: 'Jan', utilization: 10 }, + { month: 'Feb', utilization: 13 }, + { month: 'Mar', utilization: 11 }, + { month: 'Apr', utilization: 8 }, + { month: 'May', utilization: 12 }, + { month: 'Jun', utilization: 18 }, +]; + +export const UTILIZATION_TARGET = 40; diff --git a/components/recharts-poc/kpi/DeltaBadge.tsx b/components/recharts-poc/kpi/DeltaBadge.tsx deleted file mode 100644 index 49d3f455..00000000 --- a/components/recharts-poc/kpi/DeltaBadge.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ArrowDown, ArrowUp, Minus } from 'lucide-react'; - -import type { PoPResult } from '@/components/recharts-poc/primitives/periodOverPeriod'; - -interface DeltaBadgeProps { - result: PoPResult; - format?: 'count' | 'percent'; -} - -const NUMBER_FMT = new Intl.NumberFormat('en-US'); - -export function DeltaBadge(props: DeltaBadgeProps) { - const { result, format = 'count' } = props; - const { absDelta, pctDelta, direction } = result; - - if (direction === 'flat') { - return ( - - - ); - } - - const isUp = direction === 'up'; - const Icon = isUp ? ArrowUp : ArrowDown; - const color = isUp - ? 'text-green-600 dark:text-green-400' - : 'text-red-600 dark:text-red-400'; - const sign = isUp ? '+' : ''; - const baseline = pctDelta === null ? ' (no baseline)' : ''; - const numeric = - pctDelta === null - ? `${sign}${NUMBER_FMT.format(absDelta)}${format === 'percent' ? '%' : ''}` - : `${sign}${pctDelta.toFixed(1)}%`; - - return ( - - - ); -} diff --git a/components/recharts-poc/kpi/EngagementKpiCard.tsx b/components/recharts-poc/kpi/EngagementKpiCard.tsx new file mode 100644 index 00000000..7c915fec --- /dev/null +++ b/components/recharts-poc/kpi/EngagementKpiCard.tsx @@ -0,0 +1,34 @@ +import type { LucideIcon } from 'lucide-react'; + +import { InfoTooltip } from '../primitives/InfoTooltip'; + +interface EngagementKpiCardProps { + label: string; + value: number; + format: 'count' | 'percent'; + info: string; + icon: LucideIcon; +} + +const NUMBER_FMT = new Intl.NumberFormat('en-US'); + +export function EngagementKpiCard(props: EngagementKpiCardProps) { + const { label, value, format, info, icon: Icon } = props; + const formatted = + format === 'percent' ? String(value) : NUMBER_FMT.format(value); + + return ( +
+
+
+
+ + {formatted} + +
+ +
+ ); +} diff --git a/components/recharts-poc/kpi/KpiCard.tsx b/components/recharts-poc/kpi/KpiCard.tsx deleted file mode 100644 index 50197692..00000000 --- a/components/recharts-poc/kpi/KpiCard.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { LucideIcon } from 'lucide-react'; - -import { periodOverPeriod } from '@/components/recharts-poc/primitives/periodOverPeriod'; - -import { DeltaBadge } from './DeltaBadge'; - -interface KpiCardProps { - label: string; - current: number; - previous: number; - format: 'count' | 'percent'; - icon: LucideIcon; -} - -const NUMBER_FMT = new Intl.NumberFormat('en-US'); - -export function KpiCard(props: KpiCardProps) { - const { label, current, previous, format, icon: Icon } = props; - const result = periodOverPeriod(current, previous); - const formatted = - format === 'percent' - ? `${current.toFixed(1)}%` - : NUMBER_FMT.format(current); - - return ( -
-
-
-
- - {label} - - - {formatted} - - -
-
- ); -} diff --git a/components/recharts-poc/mocks/devicePerformance.ts b/components/recharts-poc/mocks/devicePerformance.ts deleted file mode 100644 index 035e8240..00000000 --- a/components/recharts-poc/mocks/devicePerformance.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface DeviceSlice { - name: string; - value: number; -} - -export const DEVICE_PERFORMANCE: DeviceSlice[] = [ - { name: 'Desktop', value: 89.7 }, - { name: 'Mobile', value: 10.3 }, -]; diff --git a/components/recharts-poc/mocks/improvedKeywords.ts b/components/recharts-poc/mocks/improvedKeywords.ts deleted file mode 100644 index ef52881e..00000000 --- a/components/recharts-poc/mocks/improvedKeywords.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface ImprovedKeywordRow { - keyword: string; - positionNow: number; - positionBefore: number; -} - -export const IMPROVED_KEYWORDS: ImprovedKeywordRow[] = [ - { keyword: 'technical seo examples', positionNow: 1, positionBefore: 57 }, - { keyword: 'items ids', positionNow: 15, positionBefore: 39 }, - { keyword: 'seo report how to', positionNow: 45, positionBefore: 69 }, - { keyword: 'agencydashboard', positionNow: 9, positionBefore: 30 }, - { keyword: 'marketing report writing', positionNow: 23, positionBefore: 42 }, - { - keyword: 'how to access google analytics', - positionNow: 2, - positionBefore: 19, - }, - { - keyword: 'seo analysis report example', - positionNow: 6, - positionBefore: 19, - }, - { keyword: 'best google ads software', positionNow: 10, positionBefore: 22 }, - { keyword: 'supermetrics alternative', positionNow: 13, positionBefore: 25 }, - { keyword: 'example of audience name', positionNow: 23, positionBefore: 35 }, -]; diff --git a/components/recharts-poc/mocks/keywordLength.ts b/components/recharts-poc/mocks/keywordLength.ts deleted file mode 100644 index a98c6be8..00000000 --- a/components/recharts-poc/mocks/keywordLength.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface KeywordLengthBucket { - length: number; - currentPeriod: number; - previousPeriod: number; -} - -export const KEYWORD_LENGTH: KeywordLengthBucket[] = [ - { length: 1, currentPeriod: 300, previousPeriod: 280 }, - { length: 2, currentPeriod: 1559, previousPeriod: 1400 }, - { length: 3, currentPeriod: 4183, previousPeriod: 3900 }, - { length: 4, currentPeriod: 4478, previousPeriod: 4100 }, - { length: 5, currentPeriod: 3112, previousPeriod: 3200 }, - { length: 6, currentPeriod: 1466, previousPeriod: 1500 }, - { length: 7, currentPeriod: 732, previousPeriod: 690 }, - { length: 8, currentPeriod: 414, previousPeriod: 450 }, - { length: 9, currentPeriod: 261, previousPeriod: 240 }, - { length: 10, currentPeriod: 179, previousPeriod: 200 }, -]; diff --git a/components/recharts-poc/mocks/keywords.ts b/components/recharts-poc/mocks/keywords.ts deleted file mode 100644 index 95e3f986..00000000 --- a/components/recharts-poc/mocks/keywords.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface KeywordPoint { - date: string; - shortTail: number; - longTail: number; -} - -export const KEYWORDS: KeywordPoint[] = [ - { date: 'Aug 25', shortTail: 35, longTail: 62 }, - { date: 'Aug 27', shortTail: 50, longTail: 68 }, - { date: 'Aug 29', shortTail: 42, longTail: 55 }, - { date: 'Aug 31', shortTail: 22, longTail: 35 }, - { date: 'Sep 2', shortTail: 48, longTail: 60 }, - { date: 'Sep 4', shortTail: 38, longTail: 50 }, - { date: 'Sep 6', shortTail: 20, longTail: 30 }, - { date: 'Sep 8', shortTail: 70, longTail: 92 }, - { date: 'Sep 10', shortTail: 55, longTail: 70 }, - { date: 'Sep 12', shortTail: 38, longTail: 48 }, - { date: 'Sep 14', shortTail: 18, longTail: 28 }, - { date: 'Sep 16', shortTail: 45, longTail: 62 }, - { date: 'Sep 18', shortTail: 42, longTail: 55 }, - { date: 'Sep 20', shortTail: 30, longTail: 38 }, -]; diff --git a/components/recharts-poc/mocks/kpis.ts b/components/recharts-poc/mocks/kpis.ts deleted file mode 100644 index 953b98cc..00000000 --- a/components/recharts-poc/mocks/kpis.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { LucideIcon } from 'lucide-react'; -import { Eye, FileText, MousePointerClick, Users } from 'lucide-react'; - -export interface KpiTile { - label: string; - current: number; - previous: number; - format: 'count' | 'percent'; - icon: LucideIcon; -} - -export const KPIS: KpiTile[] = [ - { - label: 'Unique Pages', - current: 570, - previous: 555, - format: 'count', - icon: FileText, - }, - { - label: 'Page Views', - current: 12480, - previous: 11200, - format: 'count', - icon: Eye, - }, - { - label: 'CTR', - current: 4.3, - previous: 4.6, - format: 'percent', - icon: MousePointerClick, - }, - { - label: 'Active Users', - current: 1820, - previous: 1900, - format: 'count', - icon: Users, - }, -]; diff --git a/components/recharts-poc/mocks/salesAsp.ts b/components/recharts-poc/mocks/salesAsp.ts deleted file mode 100644 index aa687385..00000000 --- a/components/recharts-poc/mocks/salesAsp.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface SalesAspPoint { - month: string; - asp: number; - sales: number; -} - -export const SALES_ASP: SalesAspPoint[] = [ - { month: 'May', asp: 24, sales: 55 }, - { month: 'Jun', asp: 27, sales: 60 }, - { month: 'Jul', asp: 25, sales: 70 }, - { month: 'Aug', asp: 20, sales: 130 }, - { month: 'Sep', asp: 17, sales: 200 }, - { month: 'Oct', asp: 15, sales: 245 }, -]; diff --git a/components/recharts-poc/primitives/ChartCard.tsx b/components/recharts-poc/primitives/ChartCard.tsx index b209de2c..809cf5a0 100644 --- a/components/recharts-poc/primitives/ChartCard.tsx +++ b/components/recharts-poc/primitives/ChartCard.tsx @@ -5,33 +5,50 @@ import { useIsMounted } from './useIsMounted'; interface ChartCardProps { title: string; + description?: string; + icon?: ReactNode; actions?: ReactNode; height: number; - children: ReactNode; + children: ReactElement; } export function ChartCard(props: ChartCardProps) { - const { title, actions, height, children } = props; + const { title, description, icon, actions, height, children } = props; const mounted = useIsMounted(); return ( -
-
-

- {title} -

- {actions ? ( -
{actions}
- ) : null} +
+
+
+
+ {icon ? ( + + ) : null} +

+ {title} +

+
+ {description ? ( +

+ {description} +

+ ) : null} +
+ {actions ?
{actions}
: null}
{mounted ? ( - {children as ReactElement} + {children} ) : (