From b59e04b3a3a4243194f6b641a851cde3e29466b6 Mon Sep 17 00:00:00 2001 From: Andrey Khasanov <7570386+Aliamondo@users.noreply.github.com> Date: Mon, 4 May 2026 16:47:07 +0200 Subject: [PATCH 1/4] Add notes view --- package.json | 3 +- pnpm-lock.yaml | 673 ++++++++++++++++++++++ src/components/PrNoteButton.tsx | 141 +++++ src/components/PullRequestFilterBar.tsx | 26 +- src/helpers/notesHandler.ts | 335 +++++++++++ src/helpers/settingsHandler.ts | 4 +- src/types/notes.d.ts | 36 ++ src/types/settings.d.ts | 2 +- src/views/DailyHelper/DailyHelper.tsx | 94 +-- src/views/DailyHelper/PullRequest.tsx | 82 ++- src/views/Notes/NoteEditor.tsx | 130 +++++ src/views/Notes/NoteGroup.tsx | 727 ++++++++++++++++++++++++ src/views/Notes/NotesView.tsx | 189 ++++++ 13 files changed, 2363 insertions(+), 79 deletions(-) create mode 100644 src/components/PrNoteButton.tsx create mode 100644 src/helpers/notesHandler.ts create mode 100644 src/types/notes.d.ts create mode 100644 src/views/Notes/NoteEditor.tsx create mode 100644 src/views/Notes/NoteGroup.tsx create mode 100644 src/views/Notes/NotesView.tsx diff --git a/package.json b/package.json index bdde7db..60f55fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "daily-helper", - "version": "2.2.0", + "version": "2.3.0", "private": true, "scripts": { "start": "vite", @@ -20,6 +20,7 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "motion": "^12.38.0", + "react-markdown": "^9.0.1", "react": "^18.1.0", "react-dom": "^18.1.0", "typescript": "^5.9.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2ff570..b0dfd66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: react-dom: specifier: ^18.1.0 version: 18.3.1(react@18.3.1) + react-markdown: + specifier: ^9.0.1 + version: 9.1.0(@types/react@18.3.28)(react@18.3.1) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -519,12 +522,21 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -537,6 +549,12 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@16.18.126': resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} @@ -565,12 +583,21 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -669,6 +696,9 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -692,6 +722,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -700,6 +733,18 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -726,6 +771,9 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -769,6 +817,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-equal@2.2.3: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} @@ -781,10 +832,17 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -842,6 +900,9 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -856,6 +917,9 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -946,6 +1010,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -953,6 +1023,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -978,10 +1051,19 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -1013,10 +1095,16 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -1029,6 +1117,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1200,6 +1292,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1218,6 +1313,93 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1292,6 +1474,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -1344,6 +1529,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1365,6 +1553,12 @@ packages: react-is@19.2.4: resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + react-markdown@9.1.0: + resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -1383,6 +1577,12 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1479,6 +1679,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -1505,6 +1708,9 @@ packages: resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} @@ -1513,6 +1719,12 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -1566,6 +1778,12 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1577,9 +1795,33 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1734,6 +1976,9 @@ packages: engines: {node: '>= 14.6'} hasBin: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -2193,10 +2438,22 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -2212,6 +2469,12 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@16.18.126': {} '@types/node@22.19.17': @@ -2237,12 +2500,18 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': dependencies: '@types/yargs-parser': 21.0.3 + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 @@ -2333,6 +2602,8 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.11 + bail@2.0.2: {} + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -2359,6 +2630,8 @@ snapshots: callsites@3.1.0: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk@4.1.2: @@ -2366,6 +2639,14 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + ci-info@3.9.0: {} cli-cursor@5.0.0: @@ -2387,6 +2668,8 @@ snapshots: colorette@2.0.20: {} + comma-separated-tokens@2.0.3: {} + commander@14.0.3: {} commander@2.20.3: @@ -2424,6 +2707,10 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-equal@2.2.3: dependencies: array-buffer-byte-length: 1.0.2 @@ -2457,8 +2744,14 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff-sequences@29.6.3: {} dom-accessibility-api@0.5.16: {} @@ -2512,6 +2805,8 @@ snapshots: escape-string-regexp@4.0.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -2528,6 +2823,8 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + extend@3.0.2: {} + fast-content-type-parse@3.0.0: {} fdir@6.5.0(picomatch@4.0.4): @@ -2603,6 +2900,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -2611,6 +2932,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-url-attributes@3.0.1: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -2638,12 +2961,21 @@ snapshots: indent-string@4.0.0: {} + inline-style-parser@0.2.7: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.2 side-channel: 1.1.0 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -2677,10 +3009,14 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.5.0 + is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} is-number-object@1.1.1: @@ -2690,6 +3026,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-regex@1.2.1: @@ -2877,6 +3215,8 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -2891,6 +3231,228 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -2951,6 +3513,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -3002,6 +3574,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + punycode@2.3.1: {} react-dom@18.3.1(react@18.3.1): @@ -3018,6 +3592,24 @@ snapshots: react-is@19.2.4: {} + react-markdown@9.1.0(@types/react@18.3.28)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.28 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.29.2 @@ -3045,6 +3637,23 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + resolve-from@4.0.0: {} resolve@1.22.11: @@ -3172,6 +3781,8 @@ snapshots: source-map@0.6.1: optional: true + space-separated-tokens@2.0.2: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -3198,6 +3809,11 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -3206,6 +3822,14 @@ snapshots: dependencies: min-indent: 1.0.1 + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + stylis@4.2.0: {} supports-color@7.2.0: @@ -3253,14 +3877,61 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + tslib@2.8.1: {} typescript@5.9.3: {} undici-types@6.21.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universal-user-agent@7.0.3: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -3365,3 +4036,5 @@ snapshots: yaml@1.10.3: {} yaml@2.8.3: {} + + zwitch@2.0.4: {} diff --git a/src/components/PrNoteButton.tsx b/src/components/PrNoteButton.tsx new file mode 100644 index 0000000..b3e645a --- /dev/null +++ b/src/components/PrNoteButton.tsx @@ -0,0 +1,141 @@ +import { useState, useRef } from 'react' +import NoteAltIcon from '@mui/icons-material/NoteAlt' +import NoteAltOutlinedIcon from '@mui/icons-material/NoteAltOutlined' +import Box from '@mui/material/Box' +import IconButton from '@mui/material/IconButton' +import Popover from '@mui/material/Popover' +import Stack from '@mui/material/Stack' +import Tooltip from '@mui/material/Tooltip' +import Typography from '@mui/material/Typography' +import NoteEditor from '../views/Notes/NoteEditor' +import { NoteCard, TodoNoteCard } from '../views/Notes/NoteGroup' +import { notesHandler } from '../helpers/notesHandler' + +type PrNoteButtonProps = { + prId: string + prNumber: number + prTitle: string + prUrl: string + repositoryName: string + size?: 'small' | 'medium' +} + +export default function PrNoteButton({ + prId, + prNumber, + prTitle, + prUrl, + repositoryName, + size = 'medium', +}: PrNoteButtonProps) { + const [note, setNote] = useState(() => + notesHandler.getPrNote(repositoryName, prId, prNumber, prTitle), + ) + const [anchorEl, setAnchorEl] = useState(null) + const buttonRef = useRef(null) + + const hasNote = + note !== null && + (note.content?.trim() !== '' || (note.items?.length ?? 0) > 0) + + const groupId = note ? (notesHandler.findNoteGroupId(note.id) ?? '') : '' + + const refreshNote = () => + setNote(notesHandler.getPrNote(repositoryName, prId, prNumber, prTitle)) + + // Creating a brand-new markdown note via the popover + const handleCreate = (content: string) => { + notesHandler.savePrNote({ + prId, + prNumber, + prTitle, + prUrl, + repositoryName, + content, + }) + refreshNote() + } + + return ( + <> + + setAnchorEl(buttonRef.current)} + sx={{ + color: hasNote ? 'primary.main' : 'text.disabled', + p: size === 'small' ? 0.25 : 0.5, + }} + > + {hasNote ? ( + + ) : ( + + )} + + + + setAnchorEl(null)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + disableRestoreFocus + > + + + + + Note for + + + {repositoryName} + + + #{prNumber} + + + + {note === null ? ( + + ) : note.type === 'todo' ? ( + { + notesHandler.deleteNote(groupId, note.id) + setNote(null) + setAnchorEl(null) + }} + onTodoChange={refreshNote} + /> + ) : ( + { + notesHandler.updateNote(groupId, note.id, content) + refreshNote() + }} + onDelete={() => { + notesHandler.deleteNote(groupId, note.id) + setNote(null) + setAnchorEl(null) + }} + onReload={refreshNote} + /> + )} + + + + + ) +} diff --git a/src/components/PullRequestFilterBar.tsx b/src/components/PullRequestFilterBar.tsx index 1a86f5c..4a57633 100644 --- a/src/components/PullRequestFilterBar.tsx +++ b/src/components/PullRequestFilterBar.tsx @@ -1,4 +1,5 @@ import GroupIcon from '@mui/icons-material/Group' +import NoteAltOutlinedIcon from '@mui/icons-material/NoteAltOutlined' import PersonIcon from '@mui/icons-material/Person' import Stack from '@mui/material/Stack' import StarRate from '@mui/icons-material/StarRate' @@ -49,8 +50,8 @@ function FilterButton({ } type ViewToggleProps = { - activeView: 'list' | 'kanban' - onViewToggle: (view: 'list' | 'kanban') => void + activeView: 'list' | 'kanban' | 'notes' + onViewToggle: (view: 'list' | 'kanban' | 'notes') => void } function ViewToggle({ activeView, onViewToggle }: ViewToggleProps) { return ( @@ -99,6 +100,23 @@ function ViewToggle({ activeView, onViewToggle }: ViewToggleProps) { + + onViewToggle('notes')} + sx={{ + px: 0.75, + py: 0.5, + cursor: 'pointer', + color: activeView === 'notes' ? 'primary.main' : 'text.secondary', + bgcolor: activeView === 'notes' ? 'action.selected' : 'transparent', + '&:hover': { bgcolor: 'action.hover' }, + }} + > + + + ) } @@ -110,8 +128,8 @@ type ReviewFilterBarProps = { onMyPrsToggle: VoidFunction isMyWorkActive: boolean onMyWorkToggle: VoidFunction - activeView: 'list' | 'kanban' - onViewToggle: (view: 'list' | 'kanban') => void + activeView: 'list' | 'kanban' | 'notes' + onViewToggle: (view: 'list' | 'kanban' | 'notes') => void } export default function PullRequestFilterBar({ isMustReviewActive, diff --git a/src/helpers/notesHandler.ts b/src/helpers/notesHandler.ts new file mode 100644 index 0000000..bc446e7 --- /dev/null +++ b/src/helpers/notesHandler.ts @@ -0,0 +1,335 @@ +const NOTES_KEY = 'notes' + +const defaultData = (): NotesData => ({ groups: [] }) + +function load(): NotesData { + try { + const raw = localStorage.getItem(NOTES_KEY) + if (!raw) return defaultData() + const parsed = JSON.parse(raw) as NotesData + return { + groups: (parsed.groups ?? []).map(g => ({ + ...g, + notes: (g.notes ?? []).map(n => ({ + type: 'note' as const, + ...n, + items: n.items ?? [], + })), + })), + } + } catch { + return defaultData() + } +} + +function saveAll(data: NotesData): void { + localStorage.setItem(NOTES_KEY, JSON.stringify(data)) +} + +function makeRepoGroup(repositoryName: string): NoteGroup { + return { + id: `repo:${repositoryName}`, + title: repositoryName, + notes: [], + hidden: false, + isRepoPanel: true, + repositoryName, + } +} + +function findGroup(data: NotesData, groupId: string): NoteGroup | undefined { + return data.groups.find(g => g.id === groupId) +} + +function findNote( + data: NotesData, + groupId: string, + noteId: string, +): NoteItem | undefined { + return findGroup(data, groupId)?.notes.find(n => n.id === noteId) +} + +const notesHandler = { + load, + saveAll, + + // ── Groups ────────────────────────────────────────────────────────────── + + ensureRepoGroup(repositoryName: string): void { + const data = load() + const has = data.groups.some( + g => g.isRepoPanel && g.repositoryName === repositoryName, + ) + if (!has) { + data.groups.push(makeRepoGroup(repositoryName)) + saveAll(data) + } + }, + + ensureRepoGroups(repositoryNames: string[]): void { + const data = load() + let changed = false + for (const name of repositoryNames) { + if (!data.groups.some(g => g.isRepoPanel && g.repositoryName === name)) { + data.groups.push(makeRepoGroup(name)) + changed = true + } + } + if (changed) saveAll(data) + }, + + addCustomGroup(title: string): NoteGroup { + const data = load() + const group: NoteGroup = { + id: Date.now().toString(), + title, + notes: [], + hidden: false, + } + data.groups.push(group) + saveAll(data) + return group + }, + + updateGroup( + id: string, + partial: Partial>, + ): void { + const data = load() + const i = data.groups.findIndex(g => g.id === id) + if (i === -1) return + data.groups[i] = { ...data.groups[i], ...partial } + saveAll(data) + }, + + hideGroup(id: string): void { + this.updateGroup(id, { hidden: true }) + }, + + deleteGroup(id: string): void { + const data = load() + data.groups = data.groups.filter(g => g.id !== id) + saveAll(data) + }, + + // ── Notes ──────────────────────────────────────────────────────────────── + + addNote( + groupId: string, + content: string, + prRef?: NoteItem['prRef'], + ): NoteItem | null { + const data = load() + const group = findGroup(data, groupId) + if (!group) return null + const note: NoteItem = { + id: Date.now().toString(), + type: 'note', + content, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...(prRef ? { prRef } : {}), + } + group.notes.push(note) + saveAll(data) + return note + }, + + addTodoNote(groupId: string): NoteItem | null { + const data = load() + const group = findGroup(data, groupId) + if (!group) return null + const note: NoteItem = { + id: Date.now().toString(), + type: 'todo', + items: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + group.notes.push(note) + saveAll(data) + return note + }, + + updateNote(groupId: string, noteId: string, content: string): void { + const data = load() + const note = findNote(data, groupId, noteId) + if (!note) return + note.content = content + note.updatedAt = new Date().toISOString() + saveAll(data) + }, + + setNotePrRef( + groupId: string, + noteId: string, + prRef: NoteItem['prRef'], + ): void { + const data = load() + const note = findNote(data, groupId, noteId) + if (!note) return + note.prRef = prRef + note.updatedAt = new Date().toISOString() + saveAll(data) + }, + + deleteNote(groupId: string, noteId: string): void { + const data = load() + const group = findGroup(data, groupId) + if (!group) return + group.notes = group.notes.filter(n => n.id !== noteId) + saveAll(data) + }, + + // ── Todo items ──────────────────────────────────────────────────────────── + + addTodoItem(groupId: string, noteId: string, text: string): void { + const data = load() + const note = findNote(data, groupId, noteId) + if (!note?.items) return + note.items.push({ id: Date.now().toString(), text, done: false }) + note.updatedAt = new Date().toISOString() + saveAll(data) + }, + + toggleTodoItem(groupId: string, noteId: string, itemId: string): void { + const data = load() + const note = findNote(data, groupId, noteId) + const item = note?.items?.find(i => i.id === itemId) + if (!item || !note) return + item.done = !item.done + note.updatedAt = new Date().toISOString() + saveAll(data) + }, + + updateTodoItemText( + groupId: string, + noteId: string, + itemId: string, + text: string, + ): void { + const data = load() + const note = findNote(data, groupId, noteId) + const item = note?.items?.find(i => i.id === itemId) + if (!item || !note) return + item.text = text + note.updatedAt = new Date().toISOString() + saveAll(data) + }, + + deleteTodoItem(groupId: string, noteId: string, itemId: string): void { + const data = load() + const note = findNote(data, groupId, noteId) + if (!note?.items) return + note.items = note.items.filter(i => i.id !== itemId) + note.updatedAt = new Date().toISOString() + saveAll(data) + }, + + // ── PR-note helpers ──────────────────────────────────────────────────────── + + getPrNote( + repositoryName: string, + prId: string, + prNumber?: number, + prTitle?: string, + ): NoteItem | null { + const data = load() + for (const group of data.groups) { + const note = group.notes.find( + n => + n.prRef?.prId === prId || + (prNumber !== undefined && + n.prRef?.prNumber === prNumber && + n.prRef.prUrl.includes(`/${repositoryName}/pull/`)), + ) + if (note) { + // Back-fill title + canonical prId when found via number-fallback + if ( + note.prRef && + prTitle && + prNumber !== undefined && + (!note.prRef.prTitle || note.prRef.prId !== prId) + ) { + note.prRef = { prId, prNumber, prTitle, prUrl: note.prRef.prUrl } + note.updatedAt = new Date().toISOString() + saveAll(data) + } + return note + } + } + return null + }, + + findNoteGroupId(noteId: string): string | undefined { + const data = load() + return data.groups.find(g => g.notes.some(n => n.id === noteId))?.id + }, + + savePrNote(props: { + prId: string + prNumber: number + prTitle: string + prUrl: string + repositoryName: string + content: string + }): void { + this.ensureRepoGroup(props.repositoryName) + const data = load() + // Find existing note by prId OR prNumber+URL across ALL groups (handles manually linked notes) + let targetGroup: NoteGroup | undefined + let existingIdx = -1 + for (const group of data.groups) { + const idx = group.notes.findIndex( + n => + n.prRef?.prId === props.prId || + (n.prRef?.prNumber === props.prNumber && + n.prRef.prUrl.includes(`/${props.repositoryName}/pull/`)), + ) + if (idx !== -1) { + targetGroup = group + existingIdx = idx + break + } + } + if (props.content.trim() === '') { + if (targetGroup && existingIdx !== -1) + targetGroup.notes.splice(existingIdx, 1) + } else if (targetGroup && existingIdx !== -1) { + targetGroup.notes[existingIdx] = { + ...targetGroup.notes[existingIdx], + content: props.content, + // Upgrade prRef to include the canonical GitHub prId if it was missing + prRef: { + prId: props.prId, + prNumber: props.prNumber, + prTitle: props.prTitle, + prUrl: props.prUrl, + }, + updatedAt: new Date().toISOString(), + } + } else { + const group = data.groups.find( + g => g.isRepoPanel && g.repositoryName === props.repositoryName, + ) + if (!group) return + group.notes.push({ + id: Date.now().toString(), + type: 'note', + content: props.content, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + prRef: { + prId: props.prId, + prNumber: props.prNumber, + prTitle: props.prTitle, + prUrl: props.prUrl, + }, + }) + } + saveAll(data) + }, +} + +Object.freeze(notesHandler) +export { notesHandler } diff --git a/src/helpers/settingsHandler.ts b/src/helpers/settingsHandler.ts index 3c980c0..3f720e8 100644 --- a/src/helpers/settingsHandler.ts +++ b/src/helpers/settingsHandler.ts @@ -67,11 +67,11 @@ const settingsHandler = { ) }, - loadView(): 'list' | 'kanban' { + loadView(): 'list' | 'kanban' | 'notes' { return this.load().view ?? 'list' }, - saveView(view: 'list' | 'kanban'): void { + saveView(view: 'list' | 'kanban' | 'notes'): void { const settings = this.load() this.saveAll({ ...settings, view }) }, diff --git a/src/types/notes.d.ts b/src/types/notes.d.ts new file mode 100644 index 0000000..2c4a5bb --- /dev/null +++ b/src/types/notes.d.ts @@ -0,0 +1,36 @@ +type TodoItem = { + id: string + text: string + done: boolean +} + +type NoteItem = { + id: string + type: 'note' | 'todo' + /** Markdown content — present when type is 'note' */ + content?: string + /** Checklist items — present when type is 'todo' */ + items?: TodoItem[] + createdAt: string + updatedAt: string + /** Set when this note was created from a PR card */ + prRef?: { + prId: string + prNumber: number + prTitle: string + prUrl: string + } +} + +type NoteGroup = { + id: string + title: string + notes: NoteItem[] + hidden: boolean + isRepoPanel?: true + repositoryName?: string +} + +type NotesData = { + groups: NoteGroup[] +} diff --git a/src/types/settings.d.ts b/src/types/settings.d.ts index 53003b1..995938b 100644 --- a/src/types/settings.d.ts +++ b/src/types/settings.d.ts @@ -19,7 +19,7 @@ type Settings_All = { loadPipelineStatus?: boolean aliases?: Record filters?: Settings_Filters - view?: 'list' | 'kanban' + view?: 'list' | 'kanban' | 'notes' sort?: { field: import('../components/SortControl').SortField dirs: Record< diff --git a/src/views/DailyHelper/DailyHelper.tsx b/src/views/DailyHelper/DailyHelper.tsx index 6b123c4..963f38b 100644 --- a/src/views/DailyHelper/DailyHelper.tsx +++ b/src/views/DailyHelper/DailyHelper.tsx @@ -28,6 +28,7 @@ import { dataFetcher } from '../../helpers/dataFetcher' import { queryCache } from '../../helpers/queryCache' import { settingsHandler } from '../../helpers/settingsHandler' import KanbanBoard from './KanbanBoard' +import NotesView from '../Notes/NotesView' import PullRequestFilterBar from '../../components/PullRequestFilterBar' import SortControl, { SortDir, SortField } from '../../components/SortControl' @@ -44,6 +45,7 @@ export default function DailyHelper() { ) const [orgName, setOrgName] = useState(settingsHandler.loadOrgName()) const [teamName, setTeamName] = useState(teamNames[0]) + const trackedRepos = settingsHandler.loadTeam(teamName)?.repositories ?? [] const [shouldLoad, setShouldLoad] = useState(true) const [loadingProgress, setLoadingProgress] = useState(0) const [pullRequests, setPullRequests] = useState( @@ -66,11 +68,11 @@ export default function DailyHelper() { const isMyPrsFilterActive = activeFilter === 'myPrs' const isMyWorkFilterActive = activeFilter === 'myWork' const [viewerLogin, setViewerLogin] = useState(null) - const [activeView, setActiveView] = useState<'list' | 'kanban'>( + const [activeView, setActiveView] = useState<'list' | 'kanban' | 'notes'>( settingsHandler.loadView(), ) - const handleViewToggle = (view: 'list' | 'kanban') => { + const handleViewToggle = (view: 'list' | 'kanban' | 'notes') => { setActiveView(view) settingsHandler.saveView(view) } @@ -284,7 +286,9 @@ export default function DailyHelper() { > {isInvalidToken && ( @@ -351,45 +355,47 @@ export default function DailyHelper() { activeView={activeView} onViewToggle={handleViewToggle} /> - - {activeView === 'list' && !isLoadingAnimationPlaying && ( - - - {visiblePullRequests.length === pullRequests.length - ? pullRequests.length - : `${visiblePullRequests.length} / ${pullRequests.length}`} - - - total - - - )} - { - const newDir = f === sortField ? d : sortDirs[f] - const newDirs = { ...sortDirs, [f]: newDir } - setSortField(f) - setSortDirs(newDirs) - settingsHandler.saveSort({ field: f, dirs: newDirs }) - }} - /> - + {activeView !== 'notes' && ( + + {activeView === 'list' && !isLoadingAnimationPlaying && ( + + + {visiblePullRequests.length === pullRequests.length + ? pullRequests.length + : `${visiblePullRequests.length} / ${pullRequests.length}`} + + + total + + + )} + { + const newDir = f === sortField ? d : sortDirs[f] + const newDirs = { ...sortDirs, [f]: newDir } + setSortField(f) + setSortDirs(newDirs) + settingsHandler.saveSort({ field: f, dirs: newDirs }) + }} + /> + + )} {activeView === 'list' ? ( @@ -434,13 +440,15 @@ export default function DailyHelper() { )} - ) : ( + ) : activeView === 'kanban' ? ( + ) : ( + )} )} diff --git a/src/views/DailyHelper/PullRequest.tsx b/src/views/DailyHelper/PullRequest.tsx index 281afd6..2482bb1 100644 --- a/src/views/DailyHelper/PullRequest.tsx +++ b/src/views/DailyHelper/PullRequest.tsx @@ -12,6 +12,7 @@ import { ICON_BUTTON_SIZE } from './DailyHelper' import Label from '../../components/Label' import Link from '@mui/material/Link' import NoCommentsIcon from '@mui/icons-material/ChatBubbleOutline' +import PrNoteButton from '../../components/PrNoteButton' import PullRequestStateIcon from '../../components/PullRequestStateIcon' import PullRequestStatus from './PullRequestStatus' import Skeleton from '@mui/material/Skeleton' @@ -140,27 +141,37 @@ export default function PullRequest({ {fromNow(createdAt)} - {hasReviewers && ( - - {activeReviews.slice(0, 3).map(review => ( - - ))} - {requestedReviewers - .slice(0, 3 - activeReviews.length) - .map(u => ( + + + {hasReviewers && ( + + {activeReviews.slice(0, 3).map(review => ( ))} - - )} + {requestedReviewers + .slice(0, 3 - activeReviews.length) + .map(u => ( + + ))} + + )} + )} @@ -282,17 +293,32 @@ export default function PullRequest({ height={ICON_BUTTON_SIZE} /> ) : ( - - + - {comments ? : } - {comments} - - + + {comments ? : } + {comments} + + + + )} void + placeholder?: string + minRows?: number + /** Removes border/padding in preview mode — use when the parent card provides the container */ + bare?: boolean +} + +export default function NoteEditor({ + value, + onSave, + placeholder = 'Click to add a note…', + minRows = 4, + bare = false, +}: NoteEditorProps) { + const [editing, setEditing] = useState(false) + const [draft, setDraft] = useState(value) + + // Sync incoming value changes (e.g. after parent reloads) only when not actively editing + useEffect(() => { + if (!editing) setDraft(value) + }, [value, editing]) + + const commit = (text: string) => { + onSave(text) + setEditing(false) + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + commit(draft) + } + if (e.key === 'Escape') { + setDraft(value) // discard + setEditing(false) + } + } + + if (editing) { + return ( + setDraft(e.target.value)} + onBlur={() => commit(draft)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + variant="outlined" + size="small" + inputProps={{ style: { fontFamily: 'monospace', fontSize: '0.85rem' } }} + /> + ) + } + + return ( + setEditing(true)} + sx={{ + minHeight: minRows * 22, + cursor: 'text', + borderRadius: 1, + ...(bare + ? {} + : { + p: 1, + border: 1, + borderColor: 'transparent', + '&:hover': { borderColor: 'divider', bgcolor: 'action.hover' }, + }), + ...markdownSx, + }} + > + {draft ? ( + {draft} + ) : ( + + {placeholder} + + )} + + ) +} diff --git a/src/views/Notes/NoteGroup.tsx b/src/views/Notes/NoteGroup.tsx new file mode 100644 index 0000000..869086c --- /dev/null +++ b/src/views/Notes/NoteGroup.tsx @@ -0,0 +1,727 @@ +import { useState, KeyboardEvent } from 'react' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import Checkbox from '@mui/material/Checkbox' +import Divider from '@mui/material/Divider' +import CloseIcon from '@mui/icons-material/Close' +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' +import IconButton from '@mui/material/IconButton' +import Link from '@mui/material/Link' +import LinkIcon from '@mui/icons-material/Link' +import LinkOffIcon from '@mui/icons-material/LinkOff' +import OpenInNewIcon from '@mui/icons-material/OpenInNew' +import ReactMarkdown from 'react-markdown' +import Stack from '@mui/material/Stack' +import TextField from '@mui/material/TextField' +import Tooltip from '@mui/material/Tooltip' +import Typography from '@mui/material/Typography' +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff' +import NoteEditor, { markdownSx } from './NoteEditor' +import { notesHandler } from '../../helpers/notesHandler' +import { settingsHandler } from '../../helpers/settingsHandler' + +// ── Shared hover-reveal style for action buttons ────────────────────────────── +const actionSx = { + opacity: 0, + transition: 'opacity 0.15s', +} as const + +// ── Shared card shell — PR linking, hover-reveal buttons, outer box ─────────── + +type NoteCardShellProps = { + note: NoteItem + groupId: string + onDelete: () => void + onReload: () => void + deleteTooltip: string + children: React.ReactNode +} + +function NoteCardShell({ + note, + groupId, + onDelete, + onReload, + deleteTooltip, + children, +}: NoteCardShellProps) { + const [linkingPr, setLinkingPr] = useState(false) + const [prUrl, setPrUrl] = useState('') + const [prTitle, setPrTitle] = useState('') + + const parsePrNumber = (url: string) => { + const m = url.match(/\/pull\/(\d+)/) + return m ? parseInt(m[1], 10) : null + } + + const cancelLink = () => { + setLinkingPr(false) + setPrUrl('') + setPrTitle('') + } + + const fetchPrTitle = async (url: string) => { + const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/) + if (!match) return + const [, owner, repo, number] = match + const token = settingsHandler.loadGithubToken() + if (!token) return + try { + const res = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${number}`, + { + headers: { Authorization: `token ${token}` }, + }, + ) + if (!res.ok) return + const data = (await res.json()) as { title?: string } + if (data.title) setPrTitle(t => (t.trim() ? t : data.title!)) + } catch { + /* silently ignore */ + } + } + + const commitLink = () => { + const url = prUrl.trim() + if (!url) { + cancelLink() + return + } + const num = parsePrNumber(url) + notesHandler.setNotePrRef(groupId, note.id, { + prId: num?.toString() ?? url, + prNumber: num ?? 0, + prTitle: prTitle.trim(), + prUrl: url, + }) + onReload() + cancelLink() + } + + const unlink = () => { + notesHandler.setNotePrRef(groupId, note.id, undefined) + onReload() + } + + return ( + + {/* PR link header — shown when linked */} + {note.prRef && ( + + + {note.prRef.prTitle + ? `#${note.prRef.prNumber}: ${note.prRef.prTitle}` + : `#${note.prRef.prNumber}`} + + + + + + + + + )} + + {/* Inline link-to-PR form */} + {linkingPr && ( + + { + const val = e.target.value + setPrUrl(val) + if (/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(val)) + fetchPrTitle(val) + }} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter') commitLink() + if (e.key === 'Escape') cancelLink() + }} + inputProps={{ style: { fontSize: '0.8rem' } }} + /> + setPrTitle(e.target.value)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter') commitLink() + if (e.key === 'Escape') cancelLink() + }} + inputProps={{ style: { fontSize: '0.8rem' } }} + /> + + + + + + )} + + {/* Card-specific content — right padding reserves space for action buttons */} + {children} + + {/* Link button — only when not linked and form is closed */} + {!note.prRef && !linkingPr && ( + + setLinkingPr(true)} + sx={{ + position: 'absolute', + top: 4, + right: 28, + p: 0.25, + color: 'text.disabled', + '&:hover': { color: 'primary.main' }, + }} + > + + + + )} + + + + + + + + ) +} + +// ── Note card (markdown) ────────────────────────────────────────────────────── + +type NoteCardProps = { + note: NoteItem + groupId: string + onUpdate: (content: string) => void + onDelete: () => void + onReload: () => void +} + +export function NoteCard({ + note, + groupId, + onUpdate, + onDelete, + onReload, +}: NoteCardProps) { + return ( + + + + ) +} + +// ── Todo item row ───────────────────────────────────────────────────────────── + +type TodoItemRowProps = { + item: TodoItem + groupId: string + noteId: string + onTodoChange: () => void +} + +function TodoItemRow({ + item, + groupId, + noteId, + onTodoChange, +}: TodoItemRowProps) { + const [editing, setEditing] = useState(false) + const [draft, setDraft] = useState(item.text) + + const commitText = () => { + const trimmed = draft.trim() + if (trimmed && trimmed !== item.text) { + notesHandler.updateTodoItemText(groupId, noteId, item.id, trimmed) + onTodoChange() + } else { + setDraft(item.text) + } + setEditing(false) + } + + return ( + + { + notesHandler.toggleTodoItem(groupId, noteId, item.id) + onTodoChange() + }} + size="small" + sx={{ p: 0, mt: 0.2, flexShrink: 0 }} + /> + + {editing ? ( + setDraft(e.target.value)} + onBlur={commitText} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter') commitText() + if (e.key === 'Escape') { + setDraft(item.text) + setEditing(false) + } + }} + inputProps={{ style: { fontSize: '0.875rem' } }} + /> + ) : ( + setEditing(true)} + sx={{ + flexGrow: 1, + cursor: 'text', + textDecoration: item.done ? 'line-through' : 'none', + color: item.done ? 'text.disabled' : 'text.primary', + lineHeight: 1.5, + ...markdownSx, + // inline-only: suppress block spacing + '& p': { m: 0, display: 'inline' }, + }} + > + {children} }} + > + {item.text} + + + )} + + + { + notesHandler.deleteTodoItem(groupId, noteId, item.id) + onTodoChange() + }} + sx={{ + p: 0.25, + flexShrink: 0, + color: 'text.disabled', + '&:hover': { color: 'error.main' }, + }} + > + + + + + ) +} + +// ── Todo note card ──────────────────────────────────────────────────────────── + +type TodoNoteCardProps = { + note: NoteItem + groupId: string + onDelete: () => void + onTodoChange: () => void +} + +export function TodoNoteCard({ + note, + groupId, + onDelete, + onTodoChange, +}: TodoNoteCardProps) { + const [addingItem, setAddingItem] = useState(() => note.items?.length === 0) + const [itemDraft, setItemDraft] = useState('') + + const saveItem = (andClose: boolean) => { + const trimmed = itemDraft.trim() + if (trimmed) { + notesHandler.addTodoItem(groupId, note.id, trimmed) + onTodoChange() + setItemDraft('') + } + if (andClose || !trimmed) setAddingItem(false) + } + + return ( + + + {[...(note.items ?? [])] + .sort((a, b) => Number(a.done) - Number(b.done)) + .map(item => ( + + ))} + + {addingItem ? ( + + + setItemDraft(e.target.value)} + onBlur={() => saveItem(true)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + saveItem(false) + } + if (e.key === 'Escape') { + setItemDraft('') + setAddingItem(false) + } + }} + inputProps={{ style: { fontSize: '0.875rem' } }} + /> + + ) : ( + setAddingItem(true)} + sx={{ + cursor: 'pointer', + pl: 3.25, + '&:hover': { color: 'text.secondary' }, + }} + > + + Add item + + )} + + + ) +} + +// ── Inline new-note form ────────────────────────────────────────────────────── + +type AddNoteFormProps = { + onCommit: (content: string) => void + onCancel: () => void +} + +function AddNoteForm({ onCommit, onCancel }: AddNoteFormProps) { + const [draft, setDraft] = useState('') + + const commit = () => { + const t = draft.trim() + if (t) onCommit(t) + else onCancel() + } + + return ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) commit() + if (e.key === 'Escape') onCancel() + }} + placeholder="Write a note in markdown… (Ctrl+Enter to save, Esc to cancel)" + variant="outlined" + size="small" + inputProps={{ style: { fontFamily: 'monospace', fontSize: '0.85rem' } }} + /> + ) +} + +// ── Group panel ─────────────────────────────────────────────────────────────── + +type NoteGroupProps = { + group: NoteGroup + onRenameGroup?: (title: string) => void + onHideGroup: () => void + onAddNote: (content: string) => void + onAddTodoNote: () => void + onUpdateNote: (noteId: string, content: string) => void + onDeleteNote: (noteId: string) => void + onTodoChange: () => void +} + +export default function NoteGroup({ + group, + onRenameGroup, + onHideGroup, + onAddNote, + onAddTodoNote, + onUpdateNote, + onDeleteNote, + onTodoChange, +}: NoteGroupProps) { + const [editingTitle, setEditingTitle] = useState(false) + const [titleDraft, setTitleDraft] = useState(group.title) + const [addingNote, setAddingNote] = useState(false) + + const isRepo = group.isRepoPanel === true + + const handleTitleCommit = () => { + setEditingTitle(false) + const t = titleDraft.trim() + if (t && t !== group.title) onRenameGroup?.(t) + else setTitleDraft(group.title) + } + + return ( + + {/* Title row */} + + {isRepo ? ( + + {group.title} + + ) : editingTitle ? ( + setTitleDraft(e.target.value)} + onBlur={handleTitleCommit} + onKeyDown={e => { + if (e.key === 'Enter') handleTitleCommit() + if (e.key === 'Escape') { + setTitleDraft(group.title) + setEditingTitle(false) + } + }} + inputProps={{ style: { fontSize: '0.875rem', fontWeight: 600 } }} + sx={{ flexGrow: 1 }} + /> + ) : ( + setEditingTitle(true)} + sx={{ flexGrow: 1, cursor: 'text', userSelect: 'none' }} + > + {group.title} + + )} + + + + + + + + + {/* Notes — divider between each item */} + {group.notes.length > 0 && ( + } + > + {[...group.notes] + .sort((a, b) => { + const done = (n: NoteItem) => + n.type === 'todo' && + (n.items?.length ?? 0) > 0 && + n.items!.every(i => i.done) + return Number(done(a)) - Number(done(b)) + }) + .map(note => + note.type === 'todo' ? ( + onDeleteNote(note.id)} + onTodoChange={onTodoChange} + /> + ) : ( + onUpdateNote(note.id, content)} + onDelete={() => onDeleteNote(note.id)} + onReload={onTodoChange} + /> + ), + )} + + )} + + {/* Add-note form sits outside the divider stack */} + {addingNote && ( + + { + onAddNote(content) + setAddingNote(false) + }} + onCancel={() => setAddingNote(false)} + /> + + )} + + {/* Footer: add buttons */} + + { + setAddingNote(true) + }} + sx={{ cursor: 'pointer', '&:hover': { color: 'text.secondary' } }} + > + + Note + + + · + + { + onAddTodoNote() + setAddingNote(false) + }} + sx={{ cursor: 'pointer', '&:hover': { color: 'text.secondary' } }} + > + + Todo + + + + ) +} diff --git a/src/views/Notes/NotesView.tsx b/src/views/Notes/NotesView.tsx new file mode 100644 index 0000000..9b6d2cf --- /dev/null +++ b/src/views/Notes/NotesView.tsx @@ -0,0 +1,189 @@ +import { useState, useEffect } from 'react' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import Chip from '@mui/material/Chip' +import Divider from '@mui/material/Divider' +import RestoreIcon from '@mui/icons-material/Restore' +import Stack from '@mui/material/Stack' +import TextField from '@mui/material/TextField' +import Typography from '@mui/material/Typography' +import NoteGroup from './NoteGroup' +import { notesHandler } from '../../helpers/notesHandler' + +type NotesViewProps = { + trackedRepos: string[] +} + +export default function NotesView({ trackedRepos }: NotesViewProps) { + const [notesData, setNotesData] = useState(() => + notesHandler.load(), + ) + const [addingGroup, setAddingGroup] = useState(false) + const [newGroupTitle, setNewGroupTitle] = useState('') + + const reload = () => setNotesData(notesHandler.load()) + + useEffect(() => { + if (trackedRepos.length > 0) { + notesHandler.ensureRepoGroups(trackedRepos) + reload() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [trackedRepos.join(',')]) + + const visibleGroups = notesData.groups.filter(g => !g.hidden) + const hiddenGroups = notesData.groups.filter(g => g.hidden) + + const handleAddGroup = () => { + const title = newGroupTitle.trim() + if (!title) return + notesHandler.addCustomGroup(title) + setNewGroupTitle('') + setAddingGroup(false) + reload() + } + + return ( + + {/* Header */} + + + Notes + + {addingGroup ? ( + + setNewGroupTitle(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') handleAddGroup() + if (e.key === 'Escape') { + setAddingGroup(false) + setNewGroupTitle('') + } + }} + sx={{ width: 200 }} + /> + + + + ) : ( + + )} + + + {visibleGroups.length === 0 && ( + + No repos tracked yet. Configure your team in Settings to get started. + + )} + + {/* Dashboard grid */} + + {visibleGroups.map(group => ( + { + notesHandler.updateGroup(group.id, { title }) + reload() + }} + onHideGroup={() => { + notesHandler.hideGroup(group.id) + reload() + }} + onAddNote={content => { + notesHandler.addNote(group.id, content) + reload() + }} + onAddTodoNote={() => { + notesHandler.addTodoNote(group.id) + reload() + }} + onUpdateNote={(noteId, content) => { + notesHandler.updateNote(group.id, noteId, content) + reload() + }} + onDeleteNote={noteId => { + notesHandler.deleteNote(group.id, noteId) + reload() + }} + onTodoChange={reload} + /> + ))} + + + {/* Hidden groups restore row */} + {hiddenGroups.length > 0 && ( + <> + + + Hidden + + + {hiddenGroups.map(group => ( + 0 + ? `${group.title} (${group.notes.length})` + : group.title + } + size="small" + icon={} + onClick={() => { + notesHandler.updateGroup(group.id, { hidden: false }) + reload() + }} + variant="outlined" + clickable + /> + ))} + + + )} + + ) +} From 937456d871053fb966f7ec9534afee0855c6fdc4 Mon Sep 17 00:00:00 2001 From: Andrey Khasanov Date: Tue, 12 May 2026 10:44:23 +0200 Subject: [PATCH 2/4] Order notes nicely --- src/views/Notes/NoteGroup.tsx | 4 +- src/views/Notes/NotesView.tsx | 123 +++++++++++++++++++++++----------- 2 files changed, 87 insertions(+), 40 deletions(-) diff --git a/src/views/Notes/NoteGroup.tsx b/src/views/Notes/NoteGroup.tsx index 869086c..d323272 100644 --- a/src/views/Notes/NoteGroup.tsx +++ b/src/views/Notes/NoteGroup.tsx @@ -321,7 +321,7 @@ function TodoItemRow({ return ( {editing ? ( diff --git a/src/views/Notes/NotesView.tsx b/src/views/Notes/NotesView.tsx index 9b6d2cf..761cbbb 100644 --- a/src/views/Notes/NotesView.tsx +++ b/src/views/Notes/NotesView.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef, useCallback } from 'react' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Chip from '@mui/material/Chip' @@ -10,6 +10,30 @@ import Typography from '@mui/material/Typography' import NoteGroup from './NoteGroup' import { notesHandler } from '../../helpers/notesHandler' +const COLUMN_MIN_WIDTH = 360 +const GAP = 16 + +function estimateGroupHeight(group: NoteGroup): number { + return group.notes.reduce((sum, note) => { + if (note.type === 'todo') return sum + 1 + (note.items?.length ?? 0) + return sum + 2 + }, 2) +} + +function distributeIntoColumns( + groups: NoteGroup[], + colCount: number, +): NoteGroup[][] { + const columns: NoteGroup[][] = Array.from({ length: colCount }, () => []) + const heights: number[] = Array(colCount).fill(0) + for (const group of groups) { + const shortest = heights.indexOf(Math.min(...heights)) + columns[shortest].push(group) + heights[shortest] += estimateGroupHeight(group) + } + return columns +} + type NotesViewProps = { trackedRepos: string[] } @@ -20,6 +44,8 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { ) const [addingGroup, setAddingGroup] = useState(false) const [newGroupTitle, setNewGroupTitle] = useState('') + const [containerWidth, setContainerWidth] = useState(0) + const containerRef = useRef(null) const reload = () => setNotesData(notesHandler.load()) @@ -31,9 +57,28 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [trackedRepos.join(',')]) + const measureRef = useCallback((node: HTMLDivElement | null) => { + if (!node) return + ;(containerRef as React.MutableRefObject).current = + node + const observer = new ResizeObserver(entries => { + setContainerWidth(entries[0].contentRect.width) + }) + observer.observe(node) + }, []) + const visibleGroups = notesData.groups.filter(g => !g.hidden) const hiddenGroups = notesData.groups.filter(g => g.hidden) + const colCount = + containerWidth > 0 + ? Math.max( + 1, + Math.floor((containerWidth + GAP) / (COLUMN_MIN_WIDTH + GAP)), + ) + : 1 + const columns = distributeIntoColumns(visibleGroups, colCount) + const handleAddGroup = () => { const title = newGroupTitle.trim() if (!title) return @@ -43,6 +88,38 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { reload() } + const renderGroup = (group: NoteGroup) => ( + { + notesHandler.updateGroup(group.id, { title }) + reload() + }} + onHideGroup={() => { + notesHandler.hideGroup(group.id) + reload() + }} + onAddNote={content => { + notesHandler.addNote(group.id, content) + reload() + }} + onAddTodoNote={() => { + notesHandler.addTodoNote(group.id) + reload() + }} + onUpdateNote={(noteId, content) => { + notesHandler.updateNote(group.id, noteId, content) + reload() + }} + onDeleteNote={noteId => { + notesHandler.deleteNote(group.id, noteId) + reload() + }} + onTodoChange={reload} + /> + ) + return ( {/* Header */} @@ -107,45 +184,15 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { )} - {/* Dashboard grid */} + {/* Masonry board */} - {visibleGroups.map(group => ( - { - notesHandler.updateGroup(group.id, { title }) - reload() - }} - onHideGroup={() => { - notesHandler.hideGroup(group.id) - reload() - }} - onAddNote={content => { - notesHandler.addNote(group.id, content) - reload() - }} - onAddTodoNote={() => { - notesHandler.addTodoNote(group.id) - reload() - }} - onUpdateNote={(noteId, content) => { - notesHandler.updateNote(group.id, noteId, content) - reload() - }} - onDeleteNote={noteId => { - notesHandler.deleteNote(group.id, noteId) - reload() - }} - onTodoChange={reload} - /> + {columns.map((colGroups, ci) => ( + + {colGroups.map(renderGroup)} + ))} From f94977611ed15f51232ec6abe1d675f612e056f1 Mon Sep 17 00:00:00 2001 From: Andrey Khasanov Date: Wed, 13 May 2026 18:46:04 +0200 Subject: [PATCH 3/4] Align groups correctly --- src/helpers/notesHandler.ts | 3 +- src/views/Notes/NoteGroup.tsx | 19 ++++++ src/views/Notes/NotesView.tsx | 106 +++++++++++++++++++++++----------- 3 files changed, 91 insertions(+), 37 deletions(-) diff --git a/src/helpers/notesHandler.ts b/src/helpers/notesHandler.ts index bc446e7..42f0fe3 100644 --- a/src/helpers/notesHandler.ts +++ b/src/helpers/notesHandler.ts @@ -11,8 +11,8 @@ function load(): NotesData { groups: (parsed.groups ?? []).map(g => ({ ...g, notes: (g.notes ?? []).map(n => ({ - type: 'note' as const, ...n, + type: n.type ?? ('note' as const), items: n.items ?? [], })), })), @@ -51,7 +51,6 @@ function findNote( const notesHandler = { load, - saveAll, // ── Groups ────────────────────────────────────────────────────────────── diff --git a/src/views/Notes/NoteGroup.tsx b/src/views/Notes/NoteGroup.tsx index d323272..e97ae5f 100644 --- a/src/views/Notes/NoteGroup.tsx +++ b/src/views/Notes/NoteGroup.tsx @@ -537,6 +537,7 @@ type NoteGroupProps = { group: NoteGroup onRenameGroup?: (title: string) => void onHideGroup: () => void + onDeleteGroup?: () => void onAddNote: (content: string) => void onAddTodoNote: () => void onUpdateNote: (noteId: string, content: string) => void @@ -548,6 +549,7 @@ export default function NoteGroup({ group, onRenameGroup, onHideGroup, + onDeleteGroup, onAddNote, onAddTodoNote, onUpdateNote, @@ -631,6 +633,23 @@ export default function NoteGroup({ + + {!isRepo && onDeleteGroup && ( + + + + + + )} {/* Notes — divider between each item */} diff --git a/src/views/Notes/NotesView.tsx b/src/views/Notes/NotesView.tsx index 761cbbb..b7e5f29 100644 --- a/src/views/Notes/NotesView.tsx +++ b/src/views/Notes/NotesView.tsx @@ -23,17 +23,43 @@ function estimateGroupHeight(group: NoteGroup): number { function distributeIntoColumns( groups: NoteGroup[], colCount: number, + heightMap: Record, ): NoteGroup[][] { const columns: NoteGroup[][] = Array.from({ length: colCount }, () => []) const heights: number[] = Array(colCount).fill(0) for (const group of groups) { + const h = heightMap[group.id] ?? estimateGroupHeight(group) const shortest = heights.indexOf(Math.min(...heights)) columns[shortest].push(group) - heights[shortest] += estimateGroupHeight(group) + heights[shortest] += h } return columns } +function useElementHeights(): [ + Record, + (id: string, el: HTMLDivElement | null) => void, +] { + const [heights, setHeights] = useState>({}) + const observersRef = useRef>(new Map()) + + const measureRef = useCallback((id: string, el: HTMLDivElement | null) => { + observersRef.current.get(id)?.disconnect() + observersRef.current.delete(id) + if (!el) return + const observer = new ResizeObserver(entries => { + const height = entries[0].contentRect.height + setHeights(prev => + prev[id] === height ? prev : { ...prev, [id]: height }, + ) + }) + observer.observe(el) + observersRef.current.set(id, observer) + }, []) + + return [heights, measureRef] +} + type NotesViewProps = { trackedRepos: string[] } @@ -45,7 +71,7 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { const [addingGroup, setAddingGroup] = useState(false) const [newGroupTitle, setNewGroupTitle] = useState('') const [containerWidth, setContainerWidth] = useState(0) - const containerRef = useRef(null) + const [heightMap, measureGroupRef] = useElementHeights() const reload = () => setNotesData(notesHandler.load()) @@ -57,10 +83,8 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [trackedRepos.join(',')]) - const measureRef = useCallback((node: HTMLDivElement | null) => { + const measureContainerRef = useCallback((node: HTMLDivElement | null) => { if (!node) return - ;(containerRef as React.MutableRefObject).current = - node const observer = new ResizeObserver(entries => { setContainerWidth(entries[0].contentRect.width) }) @@ -77,7 +101,7 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { Math.floor((containerWidth + GAP) / (COLUMN_MIN_WIDTH + GAP)), ) : 1 - const columns = distributeIntoColumns(visibleGroups, colCount) + const columns = distributeIntoColumns(visibleGroups, colCount, heightMap) const handleAddGroup = () => { const title = newGroupTitle.trim() @@ -89,35 +113,47 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { } const renderGroup = (group: NoteGroup) => ( - { - notesHandler.updateGroup(group.id, { title }) - reload() - }} - onHideGroup={() => { - notesHandler.hideGroup(group.id) - reload() - }} - onAddNote={content => { - notesHandler.addNote(group.id, content) - reload() - }} - onAddTodoNote={() => { - notesHandler.addTodoNote(group.id) - reload() - }} - onUpdateNote={(noteId, content) => { - notesHandler.updateNote(group.id, noteId, content) - reload() - }} - onDeleteNote={noteId => { - notesHandler.deleteNote(group.id, noteId) - reload() - }} - onTodoChange={reload} - /> + ref={(el: HTMLDivElement | null) => measureGroupRef(group.id, el)} + > + { + notesHandler.updateGroup(group.id, { title }) + reload() + }} + onHideGroup={() => { + notesHandler.hideGroup(group.id) + reload() + }} + onDeleteGroup={ + group.isRepoPanel + ? undefined + : () => { + notesHandler.deleteGroup(group.id) + reload() + } + } + onAddNote={content => { + notesHandler.addNote(group.id, content) + reload() + }} + onAddTodoNote={() => { + notesHandler.addTodoNote(group.id) + reload() + }} + onUpdateNote={(noteId, content) => { + notesHandler.updateNote(group.id, noteId, content) + reload() + }} + onDeleteNote={noteId => { + notesHandler.deleteNote(group.id, noteId) + reload() + }} + onTodoChange={reload} + /> + ) return ( @@ -186,7 +222,7 @@ export default function NotesView({ trackedRepos }: NotesViewProps) { {/* Masonry board */} {columns.map((colGroups, ci) => ( From c44f265b790b9d266b5de45e29b3d87325458ba4 Mon Sep 17 00:00:00 2001 From: Andrey Khasanov Date: Wed, 13 May 2026 19:01:10 +0200 Subject: [PATCH 4/4] Override postcss v8.5.9 to v8.5.14 --- pnpm-lock.yaml | 100 ++++++++------------------------------------ pnpm-workspace.yaml | 2 + 2 files changed, 20 insertions(+), 82 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0dfd66..31b80da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + postcss: 8.5.14 + importers: .: @@ -65,7 +68,7 @@ importers: version: 29.5.14 '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3)) + version: 6.0.1(vite@8.0.8(@types/node@22.19.17)(yaml@2.8.3)) husky: specifier: ^9.1.7 version: 9.1.7 @@ -80,10 +83,10 @@ importers: version: 3.8.1 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3) + version: 8.0.8(@types/node@22.19.17)(yaml@2.8.3) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3)) + version: 4.1.4(@types/node@22.19.17)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(yaml@2.8.3)) packages: @@ -248,9 +251,6 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.11': - resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -640,11 +640,6 @@ packages: '@vitest/utils@4.1.4': resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -703,9 +698,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -778,9 +770,6 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -1175,10 +1164,6 @@ packages: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1509,8 +1494,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} prettier@3.8.1: @@ -1668,17 +1653,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -1739,11 +1717,6 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - terser@5.46.1: - resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} - engines: {node: '>=10'} - hasBin: true - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2190,12 +2163,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/source-map@0.3.11': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -2512,10 +2479,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@22.19.17)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3) + vite: 8.0.8(@types/node@22.19.17)(yaml@2.8.3) '@vitest/expect@4.1.4': dependencies: @@ -2526,13 +2493,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3))': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@22.19.17)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3) + vite: 8.0.8(@types/node@22.19.17)(yaml@2.8.3) '@vitest/pretty-format@4.1.4': dependencies: @@ -2558,9 +2525,6 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - acorn@8.16.0: - optional: true - agent-base@7.1.4: {} ansi-escapes@7.3.0: @@ -2608,9 +2572,6 @@ snapshots: dependencies: fill-range: 7.1.1 - buffer-from@1.1.2: - optional: true - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2672,9 +2633,6 @@ snapshots: commander@14.0.3: {} - commander@2.20.3: - optional: true - convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -3100,9 +3058,6 @@ snapshots: graceful-fs: 4.2.11 picomatch: 2.3.2 - jiti@1.21.7: - optional: true - js-tokens@4.0.0: {} jsdom@26.1.0: @@ -3548,7 +3503,7 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.9: + postcss@8.5.14: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -3770,17 +3725,8 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - optional: true - source-map@0.5.7: {} - source-map@0.6.1: - optional: true - space-separated-tokens@2.0.2: {} stack-utils@2.0.6: @@ -3840,14 +3786,6 @@ snapshots: symbol-tree@3.2.4: {} - terser@5.46.1: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 - commander: 2.20.3 - source-map-support: 0.5.21 - optional: true - tinybench@2.9.0: {} tinyexec@1.1.1: {} @@ -3932,24 +3870,22 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3): + vite@8.0.8(@types/node@22.19.17)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.9 + postcss: 8.5.14 rolldown: 1.0.0-rc.15 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 22.19.17 fsevents: 2.3.3 - jiti: 1.21.7 - terser: 5.46.1 yaml: 2.8.3 - vitest@4.1.4(@types/node@22.19.17)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3)): + vitest@4.1.4(@types/node@22.19.17)(jsdom@26.1.0)(vite@8.0.8(@types/node@22.19.17)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -3966,7 +3902,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@22.19.17)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3) + vite: 8.0.8(@types/node@22.19.17)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.17 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..1488dcc --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +overrides: + postcss: "8.5.14"