diff --git a/package.json b/package.json index c5b8ddb..9fbcdeb 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,15 @@ "@radix-ui/react-dialog": "^1.1.17", "@radix-ui/react-dropdown-menu": "^2.1.18", "@radix-ui/react-label": "^2.1.10", + "@radix-ui/react-popover": "^1.1.23", "@radix-ui/react-select": "^2.3.1", "@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-tooltip": "^1.2.10", + "@zip.js/zip.js": "^2.8.34", "ansis": "^4.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dexie": "^4.4.4", "jotai": "^2.20.1", "jszip": "^3.10.1", "lodash-es": "^4.18.1", @@ -39,7 +42,8 @@ "source-map-js": "^1.2.1", "tailwind-merge": "^3.6.0", "terser-webpack-plugin": "^5.6.1", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2" }, "devDependencies": { "@rsbuild/core": "^2.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f8dfa4..d2b82f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@radix-ui/react-label': specifier: ^2.1.10 version: 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': + specifier: ^1.1.23 + version: 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-select': specifier: ^2.3.1 version: 2.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -35,6 +38,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.10 version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@zip.js/zip.js': + specifier: ^2.8.34 + version: 2.8.34 ansis: specifier: ^4.3.1 version: 4.3.1 @@ -44,6 +50,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dexie: + specifier: ^4.4.4 + version: 4.4.4 jotai: specifier: ^2.20.1 version: 2.20.1(@types/react@19.2.17)(react@19.2.7) @@ -83,6 +92,9 @@ importers: tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) devDependencies: '@rsbuild/core': specifier: ^2.0.15 @@ -508,6 +520,9 @@ packages: '@radix-ui/primitive@1.1.4': resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} + '@radix-ui/react-alert-dialog@1.1.17': resolution: {integrity: sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg==} peerDependencies: @@ -534,6 +549,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-checkbox@1.3.5': resolution: {integrity: sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==} peerDependencies: @@ -569,6 +597,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.4': resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} peerDependencies: @@ -578,6 +615,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.17': resolution: {integrity: sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==} peerDependencies: @@ -613,6 +659,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.19': + resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dropdown-menu@2.1.18': resolution: {integrity: sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==} peerDependencies: @@ -635,6 +694,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-guards@1.1.6': + resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-scope@1.1.10': resolution: {integrity: sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==} peerDependencies: @@ -648,6 +716,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.16': + resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.2': resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} peerDependencies: @@ -657,6 +738,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-label@2.1.10': resolution: {integrity: sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==} peerDependencies: @@ -683,6 +773,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.23': + resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.3.1': resolution: {integrity: sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==} peerDependencies: @@ -696,6 +799,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.12': resolution: {integrity: sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==} peerDependencies: @@ -709,6 +825,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.10': + resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.6': resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} peerDependencies: @@ -722,6 +864,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.6': resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==} peerDependencies: @@ -770,6 +925,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.3.3': + resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tooltip@1.2.10': resolution: {integrity: sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==} peerDependencies: @@ -792,6 +956,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.4': + resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.2.3': resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} peerDependencies: @@ -801,6 +974,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-effect-event@0.0.3': resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} peerDependencies: @@ -810,6 +992,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.2': resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} peerDependencies: @@ -828,6 +1019,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-previous@1.1.2': resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} peerDependencies: @@ -846,6 +1046,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-size@1.1.2': resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} peerDependencies: @@ -855,6 +1064,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-visually-hidden@1.2.6': resolution: {integrity: sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==} peerDependencies: @@ -871,6 +1089,9 @@ packages: '@radix-ui/rect@1.1.2': resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} + '@rsbuild/core@2.0.15': resolution: {integrity: sha512-O8vmMhZu1YImO6jOqt/K/vlJSvkq7UtSq5YM1DIlcEd9LW8Gf6/dkQ1B2KPI6F+hSMFBnTTTumdcIowSLCw97g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1230,6 +1451,10 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@zip.js/zip.js@2.8.34': + resolution: {integrity: sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==} + engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} + acorn-import-phases@1.0.4: resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} engines: {node: '>=10.13.0'} @@ -1328,6 +1553,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dexie@4.4.4: + resolution: {integrity: sha512-jIwsYI8Os2hgnqc6O49YwFDKGc5v5QjGx0wPVp543ip1F53VFAKMLthV2pQosQcVTv3eAskTWYspOx195PM0FQ==} + dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} @@ -1337,10 +1565,6 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - enhanced-resolve@5.21.4: - resolution: {integrity: sha512-wE4fDO8OjJhrPFH69HUQStq5oKvGRTNXEyW+k5C/pUQLASSsTu7obd2V3GvCDgPcY9AWjhJ4jz9Kh7iRvrxhJg==} - engines: {node: '>=10.13.0'} - enhanced-resolve@5.21.6: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} @@ -1921,6 +2145,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -2254,6 +2484,8 @@ snapshots: '@radix-ui/primitive@1.1.4': {} + '@radix-ui/primitive@1.1.7': {} + '@radix-ui/react-alert-dialog@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -2276,6 +2508,15 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-checkbox@1.3.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -2310,12 +2551,24 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-context@1.2.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-dialog@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -2357,6 +2610,19 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-dropdown-menu@2.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -2378,6 +2644,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-focus-scope@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) @@ -2389,6 +2661,17 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) @@ -2396,6 +2679,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-id@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-label@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -2431,6 +2721,29 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-popper@1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -2449,6 +2762,24 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-portal@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -2459,6 +2790,25 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) @@ -2468,6 +2818,15 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) @@ -2531,6 +2890,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-slot@1.3.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-tooltip@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -2557,6 +2923,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': dependencies: '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) @@ -2565,6 +2937,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) @@ -2572,6 +2953,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) @@ -2585,6 +2973,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 @@ -2598,6 +2992,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/rect': 1.1.3 + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) @@ -2605,6 +3006,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@radix-ui/react-visually-hidden@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -2616,6 +3024,8 @@ snapshots: '@radix-ui/rect@1.1.2': {} + '@radix-ui/rect@1.1.3': {} + '@rsbuild/core@2.0.15': dependencies: '@rspack/core': 2.0.8(@swc/helpers@0.5.23) @@ -2954,6 +3364,8 @@ snapshots: '@xtuc/long@4.2.2': {} + '@zip.js/zip.js@2.8.34': {} + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -3031,6 +3443,8 @@ snapshots: detect-node-es@1.1.0: {} + dexie@4.4.4: {} + dompurify@3.2.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -3039,11 +3453,6 @@ snapshots: emoji-regex@10.6.0: {} - enhanced-resolve@5.21.4: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 @@ -3501,6 +3910,15 @@ snapshots: util-deprecate@1.0.2: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -3520,7 +3938,7 @@ snapshots: acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.21.4 + enhanced-resolve: 5.21.6 es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index 020b54a..f94b21a 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -1,5 +1,5 @@ import ansis from "ansis"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { debounce } from "lodash-es"; import { Check, Settings2, X } from "lucide-react"; import type * as Monaco from "monaco-editor"; @@ -20,6 +20,7 @@ import { bindingLoadingAtom, bundleResultAtom, enableFormatCode, + initializeProjectAtom, inputFilesAtom, } from "@/store/bundler"; import { activeInputFileAtom, activeOutputFileAtom, enableDependenciesAtom } from "@/store/editor"; @@ -234,13 +235,15 @@ function OutputPanel({ } function Editor() { - const [inputFiles, _setInputFiles] = useAtom(inputFilesAtom); + const inputFiles = useAtomValue(inputFilesAtom); + const setInputFiles = useSetAtom(inputFilesAtom); const [activeInputFile, setActiveInputFile] = useAtom(activeInputFileAtom); const [activeOutputFile, setActiveOutputFile] = useAtom(activeOutputFileAtom); const [enableDeps, setEnableDeps] = useAtom(enableDependenciesAtom); const isLoadingBinding = useAtomValue(bindingLoadingAtom); const bundleResult = useAtomValue(bundleResultAtom); const handleBundle = useBundle(); + const initializeProject = useSetAtom(initializeProjectAtom); const editorContainerRef = useRef(null); const isMobile = useIsMobile(); @@ -272,13 +275,12 @@ function Editor() { const debouncedHandleBundle = useMemo(() => debounce(handleBundle, 300), [handleBundle]); - // biome-ignore lint/correctness/useExhaustiveDependencies: initialize bundle on mount useEffect(() => { - handleBundle(inputFiles); - }, []); + void initializeProject(); + }, [initializeProject]); - const setInputFiles = (files: SourceFile[]) => { - _setInputFiles(files); + const updateInputFiles = (files: SourceFile[]) => { + setInputFiles(files); debouncedHandleBundle(files); }; @@ -287,7 +289,7 @@ function Editor() { filename, text: "", }; - setInputFiles([...inputFiles, newFile]); + updateInputFiles([...inputFiles, newFile]); setActiveInputFile(inputFiles.length); }; @@ -295,7 +297,7 @@ function Editor() { if (inputFiles.length <= 1) return; const newFiles = inputFiles.filter((_, i) => i !== index); - setInputFiles(newFiles); + updateInputFiles(newFiles); if (activeInputFile >= newFiles.length) { setActiveInputFile(newFiles.length - 1); @@ -307,13 +309,13 @@ function Editor() { const handleInputFileRename = (index: number, newName: string) => { const newFiles = [...inputFiles]; newFiles[index] = { ...newFiles[index], filename: newName }; - setInputFiles(newFiles); + updateInputFiles(newFiles); }; const handleInputContentChange = (index: number, content: string) => { const newFiles = [...inputFiles]; newFiles[index] = { ...newFiles[index], text: content }; - setInputFiles(newFiles); + updateInputFiles(newFiles); }; const handleInputEditorMount = useCallback((editor: Monaco.editor.IStandaloneCodeEditor) => { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 565c8cb..169d7b1 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,9 +1,10 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { Clock, Download, RotateCcw, Share2 } from "lucide-react"; +import { Clock, Download, History, RotateCcw, Share2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import Github from "@/components/icon/Github"; import Logo from "@/components/icon/Rspack"; +import HistoryDrawer from "@/components/HistoryDrawer"; import { ModeToggle } from "@/components/ModeToggle"; import Preview from "@/components/Preview"; import { @@ -88,6 +89,7 @@ export default function Header() { const downloadProject = useDownloadProject(); const [selectedPreset, setSelectedPreset] = useState(presets[0].name); + const [historyOpen, setHistoryOpen] = useState(false); const handleReset = () => { const preset = getPresetByName(selectedPreset); @@ -223,6 +225,17 @@ export default function Header() {
+
+ ); } diff --git a/src/components/HistoryDrawer.tsx b/src/components/HistoryDrawer.tsx new file mode 100644 index 0000000..f6147c3 --- /dev/null +++ b/src/components/HistoryDrawer.tsx @@ -0,0 +1,474 @@ +import { useAtomValue, useSetAtom } from "jotai"; +import { + Check, + Copy, + History, + LoaderCircle, + Pencil, + Plus, + RotateCcw, + Trash2, + X, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, +} from "@/components/ui/drawer"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverDescription, + PopoverFooter, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} from "@/components/ui/popover"; +import useBundle from "@/hooks/use-bundle"; +import { getShareUrl } from "@/lib/share"; +import { + deleteHistory, + duplicateHistory, + historyObservable, + renameHistory, + restoreHistory, + type HistorySnapshot, +} from "@/lib/history"; +import { currentProjectIdAtom, inputFilesAtom, latestBundleRequestIdAtom } from "@/store/bundler"; +import { activeInputFileAtom } from "@/store/editor"; +import { rspackVersionAtom } from "@/store/version"; + +interface HistoryDrawerProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const dateFormatter = new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", +}); + +function formatSnapshotDate(timestamp: number) { + return dateFormatter.format(new Date(timestamp)); +} + +export default function HistoryDrawer({ open, onOpenChange }: HistoryDrawerProps) { + const [snapshots, setSnapshots] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [busyId, setBusyId] = useState(null); + const [snapshotToDelete, setSnapshotToDelete] = useState(null); + const [retryNonce, setRetryNonce] = useState(0); + const [editingSnapshotId, setEditingSnapshotId] = useState(null); + const [editingTitle, setEditingTitle] = useState(""); + const [titleError, setTitleError] = useState(null); + const [isStartingNewProject, setIsStartingNewProject] = useState(false); + const inputFiles = useAtomValue(inputFilesAtom); + const setInputFiles = useSetAtom(inputFilesAtom); + const setActiveInputFile = useSetAtom(activeInputFileAtom); + const currentProjectId = useAtomValue(currentProjectIdAtom); + const setCurrentProjectId = useSetAtom(currentProjectIdAtom); + const setLatestBundleRequestId = useSetAtom(latestBundleRequestIdAtom); + const rspackVersion = useAtomValue(rspackVersionAtom); + const setRspackVersion = useSetAtom(rspackVersionAtom); + const handleBundle = useBundle(); + + useEffect(() => { + if (!open) { + return; + } + + setIsLoading(true); + setError(null); + const subscription = historyObservable.subscribe({ + next: (nextSnapshots) => { + setSnapshots(nextSnapshots); + setIsLoading(false); + setError(null); + }, + error: (loadError) => { + console.error("Failed to load project history:", loadError); + setIsLoading(false); + setError("History could not be loaded. Please try again."); + }, + }); + + return () => subscription.unsubscribe(); + }, [open, retryNonce]); + + const handleRestore = async (snapshot: HistorySnapshot) => { + setBusyId(snapshot.id); + try { + const restored = await restoreHistory(snapshot.id); + setActiveInputFile(0); + setInputFiles(restored.files); + setCurrentProjectId(snapshot.id); + setRspackVersion(restored.rspackVersion); + window.history.replaceState( + null, + "", + getShareUrl({ + rspackVersion: restored.rspackVersion, + inputFiles: restored.files, + }), + ); + await handleBundle(restored.files, restored.rspackVersion); + toast.success("Project restored from history"); + onOpenChange(false); + } catch (restoreError) { + console.error("Failed to restore project history:", restoreError); + toast.error("Failed to restore project history"); + } finally { + setBusyId(null); + } + }; + + const handleDelete = async (snapshot: HistorySnapshot) => { + if (busyId !== null || isStartingNewProject) { + return; + } + + const wasCurrentProject = snapshot.id === currentProjectId; + if (wasCurrentProject) { + setLatestBundleRequestId((value) => value + 1); + setCurrentProjectId(null); + } + + setBusyId(snapshot.id); + try { + await deleteHistory(snapshot.id); + toast.success("History entry deleted"); + } catch (deleteError) { + if (wasCurrentProject) { + setCurrentProjectId(snapshot.id); + } + console.error("Failed to delete project history:", deleteError); + toast.error("Failed to delete history entry"); + } finally { + setBusyId(null); + setSnapshotToDelete(null); + } + }; + + const handleCopy = async (snapshot: HistorySnapshot) => { + setBusyId(snapshot.id); + try { + const copiedSnapshot = await duplicateHistory(snapshot.id); + const copiedProject = await restoreHistory(copiedSnapshot.id); + setActiveInputFile(0); + setInputFiles(copiedProject.files); + setCurrentProjectId(copiedSnapshot.id); + setRspackVersion(copiedProject.rspackVersion); + window.history.replaceState( + null, + "", + getShareUrl({ + rspackVersion: copiedProject.rspackVersion, + inputFiles: copiedProject.files, + }), + ); + await handleBundle(copiedProject.files, copiedProject.rspackVersion); + toast.success("Project copied to a new project"); + onOpenChange(false); + } catch (copyError) { + console.error("Failed to copy project history:", copyError); + toast.error("Failed to copy project history"); + } finally { + setBusyId(null); + } + }; + + const handleStartNewProject = async () => { + if (busyId !== null || isStartingNewProject) { + return; + } + + setIsStartingNewProject(true); + setCurrentProjectId(null); + setSnapshotToDelete(null); + setEditingSnapshotId(null); + try { + await handleBundle(inputFiles, rspackVersion); + onOpenChange(false); + } catch (bundleError) { + console.error("Failed to start a new project:", bundleError); + toast.error("Failed to start a new project"); + } finally { + setIsStartingNewProject(false); + } + }; + + const handleStartEditing = (snapshot: HistorySnapshot) => { + setSnapshotToDelete(null); + setEditingSnapshotId(snapshot.id); + setEditingTitle(snapshot.title); + setTitleError(null); + }; + + const handleCancelEditing = () => { + setEditingSnapshotId(null); + setEditingTitle(""); + setTitleError(null); + }; + + const handleRename = async (snapshot: HistorySnapshot) => { + const trimmedTitle = editingTitle.trim(); + if (!trimmedTitle) { + setTitleError("Project title cannot be empty"); + toast.error("Project title cannot be empty"); + return; + } + + setBusyId(snapshot.id); + try { + await renameHistory(snapshot.id, trimmedTitle); + handleCancelEditing(); + } catch (renameError) { + console.error("Failed to rename project history:", renameError); + setTitleError("Failed to rename project"); + toast.error("Failed to rename project"); + } finally { + setBusyId(null); + } + }; + + return ( + + + +
+
+ Project History + + Restore, copy, or remove saved project snapshots. + +
+ +
+
+
+ {isLoading ? ( +
+ + Loading history… +
+ ) : error ? ( +
+

{error}

+ +
+ ) : snapshots.length === 0 ? ( +
+ +
+

No history yet

+

Compile to save the current project here.

+
+
+ ) : ( +
+ {snapshots.map((snapshot) => { + const isBusy = busyId === snapshot.id; + return ( +
+
+
+ {editingSnapshotId === snapshot.id ? ( +
+
+ { + setEditingTitle(event.target.value); + if (titleError) { + setTitleError(null); + } + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void handleRename(snapshot); + } else if (event.key === "Escape") { + event.preventDefault(); + handleCancelEditing(); + } + }} + className="h-7 text-sm" + aria-label="Project title" + autoFocus + disabled={busyId !== null} + /> + + +
+ {titleError &&

{titleError}

} +
+ ) : ( +
+

+ {snapshot.title} +

+ +
+ )} +

+ {formatSnapshotDate(snapshot.updatedAt)} +

+

+ Rspack v{snapshot.rspackVersion} · {snapshot.fileCount} files +

+
+
+ + { + if (busyId === null) { + setSnapshotToDelete(nextOpen ? snapshot : null); + } + }} + > + + + + + + Delete history entry? + + This snapshot will be permanently removed from this browser. + + + + + + + + +
+
+ +
+ ); + })} +
+ )} +
+
+
+ ); +} diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..74e67c2 --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -0,0 +1,122 @@ +import { Drawer as DrawerPrimitive } from "vaul"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Drawer({ ...props }: React.ComponentProps) { + return ; +} + +function DrawerTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function DrawerPortal({ ...props }: React.ComponentProps) { + return ; +} + +function DrawerClose({ ...props }: React.ComponentProps) { + return ; +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + ); +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DrawerTitle({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + DrawerPortal, + DrawerTitle, + DrawerTrigger, +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..73ea867 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..dc8d52e --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,79 @@ +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Popover({ ...props }: React.ComponentProps) { + return ; +} + +function PopoverTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function PopoverAnchor({ ...props }: React.ComponentProps) { + return ; +} + +function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function PopoverFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function PopoverTitle({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function PopoverDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Popover, + PopoverAnchor, + PopoverContent, + PopoverDescription, + PopoverFooter, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +}; diff --git a/src/lib/history.ts b/src/lib/history.ts new file mode 100644 index 0000000..65b0c07 --- /dev/null +++ b/src/lib/history.ts @@ -0,0 +1,284 @@ +import Dexie, { liveQuery, type Table } from "dexie"; +import { + BlobReader, + BlobWriter, + TextReader, + TextWriter, + ZipReader, + ZipWriter, +} from "@zip.js/zip.js"; +import type { SourceFile } from "@/store/bundler"; + +const defaultProjectTitle = "Untitled project"; + +export interface HistorySnapshot { + id: number; + createdAt: number; + updatedAt: number; + title: string; + rspackVersion: string; + fileCount: number; +} + +interface StoredHistorySnapshot extends Omit { + id?: number; + archive: Blob; +} + +class HistoryDatabase extends Dexie { + history!: Table; + + constructor() { + super("rspack-playground-history"); + this.version(1).stores({ + history: "++id, createdAt", + }); + this.version(2) + .stores({ + history: "++id, createdAt, updatedAt", + }) + .upgrade((transaction) => + transaction + .table("history") + .toCollection() + .modify((record: StoredHistorySnapshot) => { + record.updatedAt ??= record.createdAt; + }), + ); + this.version(3) + .stores({ + history: "++id, createdAt, updatedAt", + }) + .upgrade((transaction) => + transaction + .table("history") + .toCollection() + .modify((record: StoredHistorySnapshot) => { + record.title ??= defaultProjectTitle; + }), + ); + } +} + +const database = new HistoryDatabase(); +let pendingProjectCreation: Promise | null = null; +let historyOperationTail = Promise.resolve(); + +function enqueueHistoryOperation(operation: () => Promise): Promise { + const previousOperation = historyOperationTail; + let release!: () => void; + historyOperationTail = new Promise((resolve) => { + release = resolve; + }); + + return previousOperation.then(operation).finally(release); +} + +async function createArchive(files: SourceFile[]) { + const blobWriter = new BlobWriter("application/zip"); + const zipWriter = new ZipWriter(blobWriter); + + try { + for (const file of files) { + await zipWriter.add(file.filename, new TextReader(file.text)); + } + + await zipWriter.close(); + return blobWriter.getData(); + } catch (error) { + await zipWriter.close().catch(() => {}); + throw error; + } +} + +function getSnapshotMetadata(record: StoredHistorySnapshot): HistorySnapshot { + if (record.id === undefined) { + throw new Error("History snapshot is missing its id"); + } + + return { + id: record.id, + createdAt: record.createdAt, + updatedAt: record.updatedAt ?? record.createdAt, + title: record.title ?? defaultProjectTitle, + rspackVersion: record.rspackVersion, + fileCount: record.fileCount, + }; +} + +export async function listHistory(): Promise { + const records = await database.history.orderBy("createdAt").reverse().toArray(); + return records.map(getSnapshotMetadata); +} + +export const historyObservable = liveQuery(() => listHistory()); + +export async function saveHistory( + projectId: number | null, + files: SourceFile[], + rspackVersion: string, +): Promise { + if (projectId === null) { + if (pendingProjectCreation) { + const snapshot = await pendingProjectCreation; + return saveHistory(snapshot.id, files, rspackVersion); + } + + const creation = enqueueHistoryOperation(() => createProjectRecord(files, rspackVersion)); + pendingProjectCreation = creation; + try { + const snapshot = await creation; + return snapshot; + } finally { + if (pendingProjectCreation === creation) { + pendingProjectCreation = null; + } + } + } + + return enqueueHistoryOperation(() => saveExistingProject(projectId, files, rspackVersion)); +} + +async function saveExistingProject( + projectId: number, + files: SourceFile[], + rspackVersion: string, +): Promise { + const archive = await createArchive(files); + const updatedAt = Date.now(); + const existing = await database.history.get(projectId); + let snapshot: HistorySnapshot; + + if (existing) { + const updated = await database.history.update(projectId, { + archive, + rspackVersion, + fileCount: files.length, + updatedAt, + }); + if (updated > 0) { + snapshot = { + id: projectId, + createdAt: existing.createdAt, + updatedAt, + title: existing.title ?? defaultProjectTitle, + rspackVersion, + fileCount: files.length, + }; + } else { + snapshot = await createHistoryRecord(archive, updatedAt, rspackVersion, files.length); + } + } else { + snapshot = await createHistoryRecord(archive, updatedAt, rspackVersion, files.length); + } + + return snapshot; +} + +async function createProjectRecord( + files: SourceFile[], + rspackVersion: string, +): Promise { + const archive = await createArchive(files); + const timestamp = Date.now(); + return createHistoryRecord(archive, timestamp, rspackVersion, files.length); +} + +async function createHistoryRecord( + archive: Blob, + timestamp: number, + rspackVersion: string, + fileCount: number, +): Promise { + const id = await database.history.add({ + archive, + createdAt: timestamp, + updatedAt: timestamp, + title: defaultProjectTitle, + rspackVersion, + fileCount, + }); + return { + id, + createdAt: timestamp, + updatedAt: timestamp, + title: defaultProjectTitle, + rspackVersion, + fileCount, + }; +} + +export async function restoreHistory( + id: number, +): Promise<{ files: SourceFile[]; rspackVersion: string }> { + const record = await database.history.get(id); + if (!record) { + throw new Error("History snapshot not found"); + } + + const zipReader = new ZipReader(new BlobReader(record.archive)); + try { + const entries = await zipReader.getEntries(); + const files: SourceFile[] = []; + for (const entry of entries) { + if (entry.directory || !entry.getData) { + continue; + } + files.push({ + filename: entry.filename, + text: await entry.getData(new TextWriter()), + }); + } + + return { files, rspackVersion: record.rspackVersion }; + } finally { + await zipReader.close(); + } +} + +export async function deleteHistory(id: number): Promise { + await enqueueHistoryOperation(() => database.history.delete(id).then(() => undefined)); +} + +export async function duplicateHistory(id: number): Promise { + return enqueueHistoryOperation(async () => { + const record = await database.history.get(id); + if (!record) { + throw new Error("History snapshot not found"); + } + + const timestamp = Date.now(); + const title = `Copy of ${record.title ?? defaultProjectTitle}`; + const newId = await database.history.add({ + archive: record.archive, + createdAt: timestamp, + updatedAt: timestamp, + title, + rspackVersion: record.rspackVersion, + fileCount: record.fileCount, + }); + + return { + id: newId, + createdAt: timestamp, + updatedAt: timestamp, + title, + rspackVersion: record.rspackVersion, + fileCount: record.fileCount, + }; + }); +} + +export async function renameHistory(id: number, title: string): Promise { + const trimmedTitle = title.trim(); + if (!trimmedTitle) { + throw new Error("Project title cannot be empty"); + } + + await enqueueHistoryOperation(async () => { + const updated = await database.history.update(id, { title: trimmedTitle }); + if (updated === 0) { + throw new Error("History snapshot not found"); + } + }); +} diff --git a/src/store/bundler.ts b/src/store/bundler.ts index 8ab9e91..d1ddc3d 100644 --- a/src/store/bundler.ts +++ b/src/store/bundler.ts @@ -1,12 +1,15 @@ import { atom } from "jotai"; +import { atomWithStorage, createJSONStorage } from "jotai/utils"; +import { toast } from "sonner"; import { bundle } from "@/lib/bundle"; +import { restoreHistory, saveHistory } from "@/lib/history"; import type { RspackChunkGroupInfo, RspackChunkInfo, RspackModuleDeps, } from "@/lib/bundle/dependency"; import { deserializeShareData } from "@/lib/share"; -import { activeOutputFileAtom } from "./editor"; +import { activeInputFileAtom, activeOutputFileAtom } from "./editor"; import { getPresetFiles, PresetBasicLibrary } from "./presets"; import { getSafeInitRspackVersion, rspackVersionAtom } from "./version"; @@ -54,12 +57,35 @@ function getInitFiles() { return getPresetFiles(PresetBasicLibrary, getSafeInitRspackVersion()); } +function areSourceFilesEqual(left: SourceFile[], right: SourceFile[]) { + return ( + left.length === right.length && + left.every( + (file, index) => file.filename === right[index]?.filename && file.text === right[index]?.text, + ) + ); +} + // Bundle export const bindingLoadedAtom = atom(null); export const bindingLoadingAtom = atom(false); export const isBundlingAtom = atom(false); export const latestBundleRequestIdAtom = atom(0); export const inputFilesAtom = atom(getInitFiles()); +const currentProjectIdJsonStorage = createJSONStorage(); +// Another tab must not switch this tab's active project without restoring its files. +const currentProjectIdStorage = { + getItem: currentProjectIdJsonStorage.getItem, + setItem: currentProjectIdJsonStorage.setItem, + removeItem: currentProjectIdJsonStorage.removeItem, +}; +export const currentProjectIdAtom = atomWithStorage( + "rspack-playground-current-project-id", + null, + currentProjectIdStorage, + { getOnInit: true }, +); +const projectInitializedAtom = atom(false); export const bundleResultAtom = atom(null); export const enableFormatCode = atom(true); @@ -70,9 +96,14 @@ export const bundleActionAtom = atom( set, { files, versionOverride }: { files: SourceFile[]; versionOverride?: string }, ) => { + if (files !== get(inputFilesAtom)) { + return; + } + const requestId = get(latestBundleRequestIdAtom) + 1; set(latestBundleRequestIdAtom, requestId); + const projectId = get(currentProjectIdAtom); const targetVersion = versionOverride ?? (await get(rspackVersionAtom)); const shouldLoadBinding = get(bindingLoadedAtom) !== targetVersion; const isLatestRequest = () => requestId === get(latestBundleRequestIdAtom); @@ -82,6 +113,7 @@ export const bundleActionAtom = atom( set(bindingLoadingAtom, true); } + let bundleResultPublished = false; try { const result = await bundle(files, targetVersion); if (!isLatestRequest()) { @@ -94,6 +126,28 @@ export const bundleActionAtom = atom( set(bindingLoadedAtom, targetVersion); } + if (!isLatestRequest()) { + return; + } + + set(bindingLoadingAtom, false); + set(isBundlingAtom, false); + bundleResultPublished = true; + + const liveProjectId = get(currentProjectIdAtom); + if (projectId === null || liveProjectId === projectId) { + const projectIdForSave = projectId === null ? liveProjectId : projectId; + try { + const snapshot = await saveHistory(projectIdForSave, files, targetVersion); + if (get(currentProjectIdAtom) === projectIdForSave) { + set(currentProjectIdAtom, snapshot.id); + } + } catch (error) { + console.error("Failed to save project history:", error); + toast.error("Failed to save project history"); + } + } + const activeOutputFile = get(activeOutputFileAtom); if (result.output.length > 0 && activeOutputFile >= result.output.length) { set(activeOutputFileAtom, 0); @@ -106,10 +160,63 @@ export const bundleActionAtom = atom( const message = error instanceof Error ? error.message : "Failed to load rspack binding"; set(bundleResultAtom, createBundleFailure(message)); } finally { - if (isLatestRequest()) { + if (isLatestRequest() && !bundleResultPublished) { set(bindingLoadingAtom, false); set(isBundlingAtom, false); } } }, ); + +export const initializeProjectAtom = atom(null, async (get, set) => { + if (get(projectInitializedAtom)) { + return; + } + set(projectInitializedAtom, true); + + let files = get(inputFilesAtom); + let versionOverride: string | undefined; + const projectId = get(currentProjectIdAtom); + const initialRequestId = get(latestBundleRequestIdAtom); + const hash = window.location.hash.slice(1); + const shareData = hash ? deserializeShareData(hash) : null; + + if (projectId !== null) { + try { + const restored = await restoreHistory(projectId); + if ( + get(latestBundleRequestIdAtom) !== initialRequestId || + get(currentProjectIdAtom) !== projectId + ) { + return; + } + + const shareDataMatchesProject = + !shareData || + (shareData.rspackVersion === restored.rspackVersion && + areSourceFilesEqual(shareData.inputFiles, restored.files)); + + if (shareDataMatchesProject) { + files = restored.files; + versionOverride = restored.rspackVersion; + set(activeInputFileAtom, 0); + set(inputFilesAtom, restored.files); + set(rspackVersionAtom, restored.rspackVersion); + } else { + set(currentProjectIdAtom, null); + } + } catch (error) { + if ( + get(latestBundleRequestIdAtom) !== initialRequestId || + get(currentProjectIdAtom) !== projectId + ) { + return; + } + + console.warn("Failed to restore the persisted project; starting a new project:", error); + set(currentProjectIdAtom, null); + } + } + + await set(bundleActionAtom, { files, versionOverride }); +});