diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3011eeb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + cache-dependency-path: "**/pnpm-lock.yaml" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: test + run: pnpm test diff --git a/.github/workflows/publish_app.yml b/.github/workflows/publish_app.yml new file mode 100644 index 0000000..6648c14 --- /dev/null +++ b/.github/workflows/publish_app.yml @@ -0,0 +1,58 @@ +name: App Publisher + +on: + workflow_call: + inputs: + app: + required: true + type: string + image: + required: true + type: string + branch: + required: false + type: string + default: main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.image }} + tags: | + ${{ github.event_name == 'release' && 'type=semver,pattern={{version}}' || github.event.inputs.tag }} + + - name: Build & push image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.web-app + build-args: | + APP_NAME=${{ inputs.app }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate attestation + if: startsWith(github.ref, 'refs/tags/') + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ inputs.image }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/publish_visr.yml b/.github/workflows/publish_visr.yml new file mode 100644 index 0000000..f77b5c7 --- /dev/null +++ b/.github/workflows/publish_visr.yml @@ -0,0 +1,20 @@ +name: Publish ViSR Docker Image + +on: + release: + types: [published] + + workflow_dispatch: + inputs: + branch: + description: "Branch to build from" + default: "main" + required: true + +jobs: + publish: + uses: ./.github/workflows/publish_app.yml + with: + app: visr + image: ghcr.io/DiamondLightSource/atlas/visr + branch: ${{ github.event.inputs.branch || 'main' }} diff --git a/.gitignore b/.gitignore index 51120e7..64718f1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Turborepo +.turbo diff --git a/apps/visr/.dockerignore b/apps/visr/.dockerignore new file mode 100644 index 0000000..3d938ea --- /dev/null +++ b/apps/visr/.dockerignore @@ -0,0 +1,6 @@ +.dockerignore +coverage +node_modules +dist +README.md +.git \ No newline at end of file diff --git a/apps/visr/.gitignore b/apps/visr/.gitignore new file mode 100644 index 0000000..8b7e502 --- /dev/null +++ b/apps/visr/.gitignore @@ -0,0 +1,22 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/visr/.prettierignore b/apps/visr/.prettierignore new file mode 100644 index 0000000..25983d8 --- /dev/null +++ b/apps/visr/.prettierignore @@ -0,0 +1,5 @@ +.github +.yarn +dist +node_modules +yarn.lock diff --git a/apps/visr/.prettierrc b/apps/visr/.prettierrc new file mode 100644 index 0000000..2aecfbd --- /dev/null +++ b/apps/visr/.prettierrc @@ -0,0 +1,6 @@ +{ + "arrowParens": "avoid", + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} \ No newline at end of file diff --git a/apps/visr/.vscode/extensions.json b/apps/visr/.vscode/extensions.json new file mode 100644 index 0000000..9edd4f1 --- /dev/null +++ b/apps/visr/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode"] +} \ No newline at end of file diff --git a/apps/visr/.vscode/settings.json b/apps/visr/.vscode/settings.json new file mode 100644 index 0000000..66dd16e --- /dev/null +++ b/apps/visr/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "prettier.requireConfig": true, + "prettier.configPath": "./.prettierrc", + "prettier.prettierPath": "./node_modules/prettier" +} diff --git a/apps/visr/LICENSE b/apps/visr/LICENSE new file mode 100644 index 0000000..9c8f3ea --- /dev/null +++ b/apps/visr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/apps/visr/README.md b/apps/visr/README.md new file mode 100644 index 0000000..6421932 --- /dev/null +++ b/apps/visr/README.md @@ -0,0 +1 @@ +# ViSR UI diff --git a/apps/visr/eslint.config.js b/apps/visr/eslint.config.js new file mode 100644 index 0000000..d94e7de --- /dev/null +++ b/apps/visr/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/apps/visr/index.html b/apps/visr/index.html new file mode 100644 index 0000000..8668e6d --- /dev/null +++ b/apps/visr/index.html @@ -0,0 +1,13 @@ + + + + + + + ViSR + + +
+ + + diff --git a/apps/visr/package.json b/apps/visr/package.json new file mode 100644 index 0000000..db8db1d --- /dev/null +++ b/apps/visr/package.json @@ -0,0 +1,60 @@ +{ + "name": "@atlas/visr", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -b && vite build", + "dev": "vite", + "lint": "eslint .", + "preview": "vite preview", + "relay": "relay-compiler", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@diamondlightsource/davidia": "^1.0.3", + "@diamondlightsource/sci-react-ui": "^0.2.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@jsonforms/core": "3.6.0", + "@jsonforms/material-renderers": "3.6.0", + "@jsonforms/react": "3.6.0", + "@mui/icons-material": "^6.5.0", + "@mui/lab": "6.0.0-beta.22", + "@mui/material": "<7.0.0", + "@mui/x-date-pickers": "^7.17.0", + "ndarray": "^1.0.19", + "react-error-boundary": "^6.0.0", + "react-router-dom": "^7.7.1" + }, + "devDependencies": { + "@atlas/vitest-conf": "workspace:*", + "@eslint/js": "^9.33.0", + "@mui/material": "^6.5.0", + "@mui/x-date-pickers": "^7.29.4", + "@types/ndarray": "^1.0.14", + "@types/react-relay": "^18.2.1", + "@types/relay-runtime": "^19.0.2", + "@vitejs/plugin-react-swc": "^3.11.0", + "ajv": "^8.17.1", + "babel-plugin-relay": "^20.1.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "msw": "^2.10.4", + "react-relay": "^20.1.1", + "react-router-dom": "^7.8.0", + "relay-compiler": "^20.1.1", + "relay-runtime": "^20.1.1", + "vite": "^7.1.2", + "vite-plugin-relay": "^2.1.0", + "vitest": "*" + }, + "packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417", + "msw": { + "workerDirectory": [ + "public" + ] + } +} diff --git a/apps/visr/public/diamond.svg b/apps/visr/public/diamond.svg new file mode 100644 index 0000000..0af1a17 --- /dev/null +++ b/apps/visr/public/diamond.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/apps/visr/public/mockServiceWorker.js b/apps/visr/public/mockServiceWorker.js new file mode 100644 index 0000000..f22ebf3 --- /dev/null +++ b/apps/visr/public/mockServiceWorker.js @@ -0,0 +1,343 @@ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = "2.10.4"; +const INTEGRITY_CHECKSUM = "f5825c521429caf22a4dd13b66e243af"; +const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); +const activeClientIds = new Set(); + +addEventListener("install", function () { + self.skipWaiting(); +}); + +addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); + +addEventListener("message", async function (event) { + const clientId = Reflect.get(event.source || {}, "id"); + + if (!clientId || !self.clients) { + return; + } + + const client = await self.clients.get(clientId); + + if (!client) { + return; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + switch (event.data) { + case "KEEPALIVE_REQUEST": { + sendToClient(client, { + type: "KEEPALIVE_RESPONSE", + }); + break; + } + + case "INTEGRITY_CHECK_REQUEST": { + sendToClient(client, { + type: "INTEGRITY_CHECK_RESPONSE", + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }); + break; + } + + case "MOCK_ACTIVATE": { + activeClientIds.add(clientId); + + sendToClient(client, { + type: "MOCKING_ENABLED", + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }); + break; + } + + case "MOCK_DEACTIVATE": { + activeClientIds.delete(clientId); + break; + } + + case "CLIENT_CLOSED": { + activeClientIds.delete(clientId); + + const remainingClients = allClients.filter(client => { + return client.id !== clientId; + }); + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister(); + } + + break; + } + } +}); + +addEventListener("fetch", function (event) { + // Bypass navigation requests. + if (event.request.mode === "navigate") { + return; + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === "only-if-cached" && + event.request.mode !== "same-origin" + ) { + return; + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return; + } + + const requestId = crypto.randomUUID(); + event.respondWith(handleRequest(event, requestId)); +}); + +/** + * @param {FetchEvent} event + * @param {string} requestId + */ +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event); + const requestCloneForEvents = event.request.clone(); + const response = await getResponse(event, client, requestId); + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents); + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone(); + + sendToClient( + client, + { + type: "RESPONSE", + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ); + } + + return response; +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId); + + if (activeClientIds.has(event.clientId)) { + return client; + } + + if (client?.frameType === "top-level") { + return client; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + return allClients + .filter(client => { + // Get only those clients that are currently visible. + return client.visibilityState === "visible"; + }) + .find(client => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id); + }); +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @returns {Promise} + */ +async function getResponse(event, client, requestId) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone(); + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers); + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get("accept"); + if (acceptHeader) { + const values = acceptHeader.split(",").map(value => value.trim()); + const filteredValues = values.filter( + value => value !== "msw/passthrough", + ); + + if (filteredValues.length > 0) { + headers.set("accept", filteredValues.join(", ")); + } else { + headers.delete("accept"); + } + } + + return fetch(requestClone, { headers }); + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough(); + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough(); + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request); + const clientMessage = await sendToClient( + client, + { + type: "REQUEST", + payload: { + id: requestId, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ); + + switch (clientMessage.type) { + case "MOCK_RESPONSE": { + return respondWithMock(clientMessage.data); + } + + case "PASSTHROUGH": { + return passthrough(); + } + } + + return passthrough(); +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + + channel.port1.onmessage = event => { + if (event.data && event.data.error) { + return reject(event.data.error); + } + + resolve(event.data); + }; + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]); + }); +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error(); + } + + const mockedResponse = new Response(response.body, response); + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }); + + return mockedResponse; +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + }; +} diff --git a/apps/visr/relay.config.json b/apps/visr/relay.config.json new file mode 100644 index 0000000..cb1be38 --- /dev/null +++ b/apps/visr/relay.config.json @@ -0,0 +1,7 @@ +{ + "src": "./src", + "language": "typescript", + "schema": "./supergraph.graphql", + "exclude": ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"], + "eagerEsModules": true +} diff --git a/apps/visr/src/RelayEnvironment.ts b/apps/visr/src/RelayEnvironment.ts new file mode 100644 index 0000000..3e85e88 --- /dev/null +++ b/apps/visr/src/RelayEnvironment.ts @@ -0,0 +1,34 @@ +import { + Environment, + Network, + RecordSource, + Store, + type FetchFunction, +} from "relay-runtime"; + +const HTTP_ENDPOINT = "/api/graphql"; +const fetchFn: FetchFunction = async (request, variables) => { + const resp = await fetch(HTTP_ENDPOINT, { + method: "POST", + headers: { + Accept: + "application/graphql-response+json; charset=utf-8, application/json; charset=utf-8", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: request.text, + variables, + }), + }); + + return await resp.json(); +}; + +function createRelayEnvironment() { + return new Environment({ + network: Network.create(fetchFn), + store: new Store(new RecordSource()), + }); +} + +export const RelayEnvironment = createRelayEnvironment(); diff --git a/apps/visr/src/components/InstrumentSessionSelection/InstrumentSession.tsx b/apps/visr/src/components/InstrumentSessionSelection/InstrumentSession.tsx new file mode 100644 index 0000000..d0f854f --- /dev/null +++ b/apps/visr/src/components/InstrumentSessionSelection/InstrumentSession.tsx @@ -0,0 +1,46 @@ +import { visitToText, type Visit } from "@diamondlightsource/sci-react-ui"; +import { useLazyLoadQuery } from "react-relay/hooks"; +import { graphql } from "relay-runtime"; +import type { InstrumentSessionQuery as InstrumentSessionQueryType } from "./__generated__/InstrumentSessionQuery.graphql"; + +const instrumentSessionQuery = graphql` + query InstrumentSessionQuery($instrumentName: String!) { + instrument(instrumentName: $instrumentName) { + instrumentSessions { + instrumentSessionNumber + proposal { + proposalCategory + proposalNumber + } + } + } + } +`; + +function GetInstrumentSessions() { + const data = useLazyLoadQuery( + instrumentSessionQuery, + { instrumentName: "ViSR" }, + ); + + const sessionListLen = data.instrument?.instrumentSessions.length ?? 1; + const sessionsList = []; + + for (let i = 0; i < sessionListLen; i++) { + const visit: Visit = { + proposalCode: + data.instrument?.instrumentSessions[ + i + ].proposal?.proposalCategory?.toLowerCase() ?? "cm", + proposalNumber: + data.instrument?.instrumentSessions[i].proposal?.proposalNumber ?? + 12345, + number: + data.instrument?.instrumentSessions[i].instrumentSessionNumber ?? 1, + }; + sessionsList.push(visitToText(visit)); + } + return sessionsList; +} + +export default GetInstrumentSessions; diff --git a/apps/visr/src/components/InstrumentSessionSelection/InstrumentSessionView.tsx b/apps/visr/src/components/InstrumentSessionSelection/InstrumentSessionView.tsx new file mode 100644 index 0000000..c10225d --- /dev/null +++ b/apps/visr/src/components/InstrumentSessionSelection/InstrumentSessionView.tsx @@ -0,0 +1,93 @@ +import { useState } from "react"; +import { visitToText, VisitInput } from "@diamondlightsource/sci-react-ui"; +import { useInstrumentSession } from "../../context/instrumentSession/useInstrumentSession"; +import { + Divider, + List, + ListItemButton, + ListItemText, + Menu, + MenuItem, +} from "@mui/material"; + +function InstrumentSessionView({ sessionsList }: { sessionsList: string[] }) { + const { instrumentSession, setInstrumentSession } = useInstrumentSession(); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedIndex, setSelectedIndex] = useState(1); + const open = Boolean(anchorEl); + const handleClickListItem = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuItemClick = ( + event: React.MouseEvent, + index: number, + ) => { + setSelectedIndex(index); + setAnchorEl(null); + setInstrumentSession(event.currentTarget.textContent ?? ""); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( +
+ + + + + + + e.stopPropagation()}> + { + setInstrumentSession(visitToText(visit)); + setAnchorEl(null); + }} + /> + + + {sessionsList.map((option, index) => ( + handleMenuItemClick(event, index)} + > + {option} + + ))} + +
+ ); +} + +export default InstrumentSessionView; diff --git a/apps/visr/src/components/InstrumentSessionSelection/__generated__/InstrumentSessionQuery.graphql.ts b/apps/visr/src/components/InstrumentSessionSelection/__generated__/InstrumentSessionQuery.graphql.ts new file mode 100644 index 0000000..89ed37f --- /dev/null +++ b/apps/visr/src/components/InstrumentSessionSelection/__generated__/InstrumentSessionQuery.graphql.ts @@ -0,0 +1,131 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type InstrumentSessionQuery$variables = { + instrumentName: string; +}; +export type InstrumentSessionQuery$data = { + readonly instrument: { + readonly instrumentSessions: ReadonlyArray<{ + readonly instrumentSessionNumber: number; + readonly proposal: { + readonly proposalCategory: string | null | undefined; + readonly proposalNumber: number; + } | null | undefined; + }>; + } | null | undefined; +}; +export type InstrumentSessionQuery = { + response: InstrumentSessionQuery$data; + variables: InstrumentSessionQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "instrumentName" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "instrumentName", + "variableName": "instrumentName" + } + ], + "concreteType": "Instrument", + "kind": "LinkedField", + "name": "instrument", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "InstrumentSession", + "kind": "LinkedField", + "name": "instrumentSessions", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "instrumentSessionNumber", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Proposal", + "kind": "LinkedField", + "name": "proposal", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "proposalCategory", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "proposalNumber", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "InstrumentSessionQuery", + "selections": (v1/*: any*/), + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "InstrumentSessionQuery", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "625bde94466c698bcacfb6d41e195cbe", + "id": null, + "metadata": {}, + "name": "InstrumentSessionQuery", + "operationKind": "query", + "text": "query InstrumentSessionQuery(\n $instrumentName: String!\n) {\n instrument(instrumentName: $instrumentName) {\n instrumentSessions {\n instrumentSessionNumber\n proposal {\n proposalCategory\n proposalNumber\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "3cb865e4a1f45b6db5b70c2cb201f4e7"; + +export default node; diff --git a/apps/visr/src/components/NumberTextField.tsx b/apps/visr/src/components/NumberTextField.tsx new file mode 100644 index 0000000..ddaf0be --- /dev/null +++ b/apps/visr/src/components/NumberTextField.tsx @@ -0,0 +1,48 @@ +import { TextField } from "@mui/material"; +import type { SpectroscopyFormData } from "./SpectroscopyForm"; + +type NumberTextFieldProps = { + formData: SpectroscopyFormData; + setFormData: (f: SpectroscopyFormData) => void; + field: keyof SpectroscopyFormData; + step: number; + label: string; +}; + +const NumberTextField = ({ + formData, + setFormData, + field, + step = 1, + label = field, +}: NumberTextFieldProps) => { + const handleChange = ( + e: React.ChangeEvent, + ) => { + const value = e.target.value; + const parsedValue = + value === "" + ? 0 + : Number.isInteger(step) // parse to Int or Float depending on step + ? parseInt(value, 10) + : parseFloat(value); + + setFormData({ + ...formData, + [field]: parsedValue, + }); + }; + + return ( + + ); +}; + +export default NumberTextField; diff --git a/apps/visr/src/components/PlanBrowser/PlanBrowser.test.tsx b/apps/visr/src/components/PlanBrowser/PlanBrowser.test.tsx new file mode 100644 index 0000000..7ec51a4 --- /dev/null +++ b/apps/visr/src/components/PlanBrowser/PlanBrowser.test.tsx @@ -0,0 +1,83 @@ +import type { Plan } from "../../utils/api"; +import { render, screen, userEvent } from "@atlas/vitest-conf"; +import PlanBrowser from "./PlanBrowser"; + +const plans: Plan[] = [ + { name: "Plan 1", schema: {}, description: "" }, + { name: "Plan 2", schema: {}, description: "" }, + { name: "Plan 3", schema: {}, description: "" }, +]; + +const renderPlan = (plan: Plan) => ( +
{plan.name}
+); + +function renderBrowser() { + return render(); +} + +describe("PlanBrowser", () => { + it("shows a placeholder before initial plan selection", () => { + renderBrowser(); + + expect(screen.getByText("Select a plan")).toBeInTheDocument(); + expect( + screen.getByText("Choose from the list on the left to see details."), + ).toBeInTheDocument(); + }); + + it("does not invoke renderPlan before selection", () => { + const mockRender = vi.fn(); + render(); + expect(mockRender).not.toBeCalled(); + }); + + it("renders plan details when a plan is selected", async () => { + renderBrowser(); + + const selectedPlan = screen.getByRole("button", { name: "Plan 2" }); + const user = userEvent.setup(); + await user.click(selectedPlan); + + // placeholder disappears... + expect(screen.queryByText("Select a plan")).not.toBeInTheDocument(); + + // ...plan details appear + const planDetails = screen.getByTestId("plan-view"); + expect(planDetails).toBeInTheDocument(); + expect(planDetails).toHaveTextContent("Plan 2"); + }); + + it("renders plan details with every selection", async () => { + renderBrowser(); + + const user = userEvent.setup(); + await user.click(screen.getByRole("button", { name: "Plan 3" })); + + const planDetails = screen.getByTestId("plan-view"); + expect(planDetails).toBeInTheDocument(); + expect(planDetails).toHaveTextContent("Plan 3"); + + await user.click(screen.getByRole("button", { name: "Plan 1" })); + expect(planDetails).toHaveTextContent("Plan 1"); + }); + + it("persists plan details through search/filtering", async () => { + renderBrowser(); + const user = userEvent.setup(); + + // select plan 1 + await user.click(screen.getByRole("button", { name: "Plan 1" })); + + // plan 1 details appear + const planDetails = screen.getByTestId("plan-view"); + expect(planDetails).toHaveTextContent("Plan 1"); + + // search for a different plan + const searchbox = screen.getByRole("textbox", { name: /search plans/i }); + await user.type(searchbox, "Plan 3"); + + // but user has not selected it, so plan 1 details remain + expect(planDetails).toHaveTextContent("Plan 1"); + }); +}); diff --git a/apps/visr/src/components/PlanBrowser/PlanBrowser.tsx b/apps/visr/src/components/PlanBrowser/PlanBrowser.tsx new file mode 100644 index 0000000..d5a1174 --- /dev/null +++ b/apps/visr/src/components/PlanBrowser/PlanBrowser.tsx @@ -0,0 +1,60 @@ +import { useState, type ReactNode } from "react"; +import type { Plan } from "../../utils/api"; +import { + Box, + Container, + Grid2 as Grid, + Paper, + Typography, +} from "@mui/material"; +import SearchablePlanList from "./SearchablePlanList"; + +export type PlanBrowserProps = { + plans: Plan[]; + renderPlan: (plan: Plan) => ReactNode; +}; + +export default function PlanBrowser({ plans, renderPlan }: PlanBrowserProps) { + const [selectedPlan, setSelectedPlan] = useState(null); + + return ( + + + + + + + + + + {selectedPlan ? ( + + {renderPlan(selectedPlan)} + + ) : ( + + + Select a plan + + + Choose from the list on the left to see details. + + + )} + + + + + ); +} diff --git a/apps/visr/src/components/PlanBrowser/PlanParameters.test.tsx b/apps/visr/src/components/PlanBrowser/PlanParameters.test.tsx new file mode 100644 index 0000000..cb7f3e7 --- /dev/null +++ b/apps/visr/src/components/PlanBrowser/PlanParameters.test.tsx @@ -0,0 +1,54 @@ +import type { Plan } from "../../utils/api"; +import { render, screen } from "@atlas/vitest-conf"; + +import PlanParameters from "./PlanParameters"; +import { InstrumentSessionProvider } from "../../context/instrumentSession/InstrumentSessionProvider"; + +const mockJsonFormsImpl = vi.fn(() => { + return
; +}); + +vi.mock("@jsonforms/react", () => { + return { + JsonForms: () => mockJsonFormsImpl(), + }; +}); + +const plan: Plan = { + name: "hi_plan", + description: "Says hi to you", + schema: { + properties: { name: { title: "Name", type: "string" } }, + }, +}; + +describe("PlanParameters", () => { + it("renders a plan's name, description, parameters, session, and run button", () => { + render( + + + , + ); + + expect(screen.getByText(plan.name)).toBeInTheDocument(); + expect(screen.getByText(plan.description!)).toBeInTheDocument(); + expect(screen.getByTestId("jsonforms-sentinel")).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: "Instrument Session" })); + expect(screen.getByRole("button", { name: "Run" })); + }); + + it("renders fallback UI if JSON Forms component fails", async () => { + // this time JsonForms will throw + mockJsonFormsImpl.mockImplementation(() => { + throw new Error("I can't do it!"); + }); + + render( + + + , + ); + + expect(screen.getByText("UI unavailable")).toBeInTheDocument(); + }); +}); diff --git a/apps/visr/src/components/PlanBrowser/PlanParameters.tsx b/apps/visr/src/components/PlanBrowser/PlanParameters.tsx new file mode 100644 index 0000000..db50577 --- /dev/null +++ b/apps/visr/src/components/PlanBrowser/PlanParameters.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { Box, TextField, Typography } from "@mui/material"; +import { JsonForms } from "@jsonforms/react"; +import { + materialRenderers, + materialCells, +} from "@jsonforms/material-renderers"; +import sanitizeSchema from "../../utils/schema"; +import type { Plan } from "../../utils/api"; +import RunPlanButton from "../RunPlanButton"; +import { useInstrumentSession } from "../../context/instrumentSession/useInstrumentSession"; + +import { ErrorBoundary } from "react-error-boundary"; + +/** + * If the UI generation fails, we show a simple apology + */ +function UIFallback() { + return ( + + UI unavailable + + ); +} +type PlanParametersProps = { + plan: Plan; +}; + +const PlanParameters: React.FC = ( + props: PlanParametersProps, +) => { + const schema = sanitizeSchema(props.plan.schema); + + // const renderers = materialRenderers; + const [planParameters, setPlanParameters] = useState({}); + const { instrumentSession, setInstrumentSession } = useInstrumentSession(); + + return ( + + + {props.plan.name} + + {props.plan.description && ( + + {props.plan.description} + + )} + setPlanParameters(data)} + /> + setInstrumentSession(e.target.value)} + > + + + + + ); +}; + +export default PlanParameters; diff --git a/apps/visr/src/components/PlanBrowser/SearchablePlanList.test.tsx b/apps/visr/src/components/PlanBrowser/SearchablePlanList.test.tsx new file mode 100644 index 0000000..2ac9fd2 --- /dev/null +++ b/apps/visr/src/components/PlanBrowser/SearchablePlanList.test.tsx @@ -0,0 +1,161 @@ +import * as React from "react"; +import { render, screen, within, userEvent } from "@atlas/vitest-conf"; +import SearchablePlanList from "./SearchablePlanList"; +import type { Plan } from "../../utils/api"; + +const plans: Plan[] = [ + { name: "Align Beam", description: "", schema: {} }, + { name: "Dark Current", description: "", schema: {} }, + { name: "Flat Field", description: "", schema: {} }, +]; + +function renderList( + overrides?: Partial>, +) { + const updateSelection = vi.fn(); + const props: React.ComponentProps = { + plans, + selectedPlan: null, + updateSelection, + ...overrides, + }; + const utils = render(); + + // merge useful things for the test + return { updateSelection, props, ...utils }; +} + +describe("SearchablePlanList", () => { + it("renders search, heading, and all plans initially", () => { + renderList(); + + // search field + expect( + screen.getByRole("textbox", { name: /search plans/i }), + ).toBeInTheDocument(); + + // heading + expect(screen.getByText("Plans")).toBeInTheDocument(); + + // all items + for (const p of plans) { + expect(screen.getByRole("button", { name: p.name })).toBeInTheDocument(); + } + + // no selection initially + for (const p of plans) { + expect(screen.getByRole("button", { name: p.name })).toHaveAttribute( + "aria-selected", + "false", + ); + } + }); + + it("calls updateSelection with the clicked plan", async () => { + const user = userEvent.setup(); + const { updateSelection } = renderList(); + + const item = screen.getByRole("button", { name: "Flat Field" }); + await user.click(item); + + expect(updateSelection).toHaveBeenCalledTimes(1); + expect(updateSelection).toHaveBeenCalledWith({ + name: "Flat Field", + schema: {}, + description: "", + }); + }); + + it("reflects controlled selection via selectedPlan prop", async () => { + const { props, rerender } = renderList(); + + const selectedButton = screen.getByRole("button", { name: "Dark Current" }); + + // none selected initially + expect(selectedButton).toHaveAttribute("aria-selected", "false"); + + // re-render with selected plan + const selected = { name: "Dark Current", schema: {}, description: "" }; + rerender(); + // selected plan marked so with aria-selected + expect(selectedButton).toHaveAttribute("aria-selected", "true"); + // others remain unselected + expect(screen.getByRole("button", { name: "Align Beam" })).toHaveAttribute( + "aria-selected", + "false", + ); + }); + + it("filters plans by case-insensitive search", async () => { + const user = userEvent.setup(); + renderList(); + + const search = screen.getByRole("textbox", { name: /search plans/i }); + await user.type(search, "dark"); // lower-case query + + expect( + screen.getByRole("button", { name: "Dark Current" }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Align Beam" }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Flat Field" }), + ).not.toBeInTheDocument(); + + // Case-insensitive check + await user.clear(search); + await user.type(search, "ALIG"); + expect( + screen.getByRole("button", { name: "Align Beam" }), + ).toBeInTheDocument(); + }); + + it("shows empty-state message when no plans match search", async () => { + const user = userEvent.setup(); + renderList(); + + const search = screen.getByRole("textbox", { name: /search plans/i }); + await user.type(search, "zzz"); + + expect(screen.getByText(/no plans match/i)).toBeInTheDocument(); + }); + + it("handles empty plans array", () => { + renderList({ plans: [] }); + // List is empty, but the heading and search are present + expect(screen.getByText("Plans")).toBeInTheDocument(); + expect( + screen.getByRole("textbox", { name: /search plans/i }), + ).toBeInTheDocument(); + }); + + it("shows all items after clearing a search", async () => { + renderList(); + const user = userEvent.setup(); + + const planList = screen.getByRole("list"); + const { getAllByRole, queryAllByRole } = within(planList); + + // three initial plans + expect(getAllByRole("button")).toHaveLength(3); + + // search + const search = screen.getByRole("textbox", { name: /search plans/i }); + await user.type(search, "zzz"); + + // no matches + expect(screen.getByText(/no plans match/i)).toBeInTheDocument(); + // no plans shown + expect(queryAllByRole("button")).toHaveLength(0); + + // clear search + await user.clear(search); + + // no matches label disappears + expect(screen.queryByText(/no plans match/i)).not.toBeInTheDocument(); + + // three plans again + expect(getAllByRole("button")).toHaveLength(3); + }); +}); diff --git a/apps/visr/src/components/PlanBrowser/SearchablePlanList.tsx b/apps/visr/src/components/PlanBrowser/SearchablePlanList.tsx new file mode 100644 index 0000000..5a9873c --- /dev/null +++ b/apps/visr/src/components/PlanBrowser/SearchablePlanList.tsx @@ -0,0 +1,71 @@ +import { + Box, + Divider, + List, + ListItemButton, + TextField, + Typography, +} from "@mui/material"; +import type { Plan } from "../../utils/api"; +import { useMemo, useState } from "react"; + +type Props = { + plans: Plan[]; + selectedPlan: Plan | null; + updateSelection: (plan: Plan) => void; +}; +export default function SearchablePlanList({ + plans, + selectedPlan, + updateSelection, +}: Props) { + const [query, setQuery] = useState(""); + + const matchingPlans = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return plans; + return plans.filter(plan => plan.name.toLowerCase().includes(q)); + }, [plans, query]); + + return ( + <> + + setQuery(e.target.value)} + /> + + + + + Plans + + + + {matchingPlans.map(plan => { + const selected = selectedPlan?.name === plan.name; + return ( + updateSelection(plan)} + > + {plan.name} + + ); + })} + {matchingPlans.length === 0 && plans.length > 0 && ( + + + No plans match “{query}”. + + + )} + + + ); +} diff --git a/apps/visr/src/components/RawSpectroscopyData.tsx b/apps/visr/src/components/RawSpectroscopyData.tsx new file mode 100644 index 0000000..517fd3b --- /dev/null +++ b/apps/visr/src/components/RawSpectroscopyData.tsx @@ -0,0 +1,113 @@ +import { ImagePlot, type NDT } from "@diamondlightsource/davidia"; +import Box from "@mui/material/Box"; +import ndarray from "ndarray"; +import { useSpectroscopyData, type RGBColour } from "./useSpectroscopyData"; + +type RGBColor = "red" | "green" | "blue" | "gray"; + +function toNDT(matrix: (number | null)[][], colour: RGBColor): NDT { + if (!matrix?.length || !matrix[0]?.length) { + return EMPTY_NDT; // skip invalid input + } + const height = matrix.length; + const width = matrix[0].length; + + // Flatten and filter out nulls for normalisation + const flat = matrix.flat(); + const valid = flat.filter((v): v is number => v !== null && !isNaN(v)); + + // Avoid crashes when no valid values + const min = valid.length ? Math.min(...valid) : 0; + const max = valid.length ? Math.max(...valid) : 1; + const scale = max > min ? 255 / (max - min) : 1; + + const rgb = new Uint8Array(width * height * 3); + + for (let i = 0; i < flat.length; i++) { + const v = flat[i]; + let scaled = 0; + if (v !== null && !isNaN(v)) { + scaled = Math.round((v - min) * scale); + } // else stays 0 (black) + + switch (colour) { + case "red": + rgb[i * 3] = scaled; + break; + case "green": + rgb[i * 3 + 1] = scaled; + break; + case "blue": + rgb[i * 3 + 2] = scaled; + break; + case "gray": + rgb[i * 3] = scaled; + rgb[i * 3 + 1] = scaled; + rgb[i * 3 + 2] = scaled; + break; + } + } + + return ndarray(rgb, [height, width, 3]) as NDT; +} +/** Placeholder empty gray dataset */ +const EMPTY_NDT = toNDT([[0]], "gray"); + +/** Return type of `/api/data/map` */ +interface MapResponse { + values: (number | null)[][]; +} + +async function fetchMap(filepath: string, datapath: string, colour: RGBColour) { + const url = `/api/data/map?filepath=${encodeURIComponent(filepath)}&datapath=${encodeURIComponent(datapath)}`; + const resp = await fetch(url); + if (!resp.ok) throw new Error(resp.statusText); + const mapResponse: MapResponse = await resp.json(); + return toNDT(mapResponse.values, colour); +} + +function RawSpectroscopyData() { + const { data } = useSpectroscopyData(fetchMap); + return ( + + + + + + + + ); +} + +export default RawSpectroscopyData; diff --git a/apps/visr/src/components/RunPlanButton.tsx b/apps/visr/src/components/RunPlanButton.tsx new file mode 100644 index 0000000..f4f477d --- /dev/null +++ b/apps/visr/src/components/RunPlanButton.tsx @@ -0,0 +1,34 @@ +import { Button } from "@mui/material"; + +import { createAndStartTask, type TaskRequest } from "../utils/api"; + +type RunPlanButtonProps = { + name: string; + params: object; + instrumentSession: string; +}; + +const RunPlanButton = ({ + name, + params, + instrumentSession, +}: RunPlanButtonProps) => { + return ( + + ); +}; + +export default RunPlanButton; diff --git a/apps/visr/src/components/SpectroscopyForm.tsx b/apps/visr/src/components/SpectroscopyForm.tsx new file mode 100644 index 0000000..8bf2d40 --- /dev/null +++ b/apps/visr/src/components/SpectroscopyForm.tsx @@ -0,0 +1,102 @@ +import { useState } from "react"; +import { Box, TextField } from "@mui/material"; +import NumberTextField from "./NumberTextField"; +import RunPlanButton from "./RunPlanButton"; +import RawSpectroscopyData from "./RawSpectroscopyData"; +import { useInstrumentSession } from "../context/instrumentSession/useInstrumentSession"; + +export type SpectroscopyFormData = { + total_number_of_scan_points: number; + grid_size: number; + grid_origin_x: number; + grid_origin_y: number; + exposure_time: number; +}; + +function SpectroscopyForm() { + const [formData, setFormData] = useState({ + total_number_of_scan_points: 25, + grid_size: 5.0, + grid_origin_x: 0.0, + grid_origin_y: 0.0, + exposure_time: 0.1, + }); + + const { instrumentSession, setInstrumentSession } = useInstrumentSession(); + + return ( + + + + + + + + + setInstrumentSession(e.target.value)} + /> + + + + + + ); +} + +export default SpectroscopyForm; diff --git a/apps/visr/src/components/SubmissionForm.tsx b/apps/visr/src/components/SubmissionForm.tsx new file mode 100644 index 0000000..5500cc0 --- /dev/null +++ b/apps/visr/src/components/SubmissionForm.tsx @@ -0,0 +1,30 @@ +import { useFragment } from "react-relay"; +import { type JSONObject, type Visit } from "../utils/types"; +import type { JsonSchema, UISchemaElement } from "@jsonforms/core"; +import type { workflowTemplateFragment$key } from "../graphql/__generated__/workflowTemplateFragment.graphql"; +import { workflowTemplateFragment } from "../graphql/workflowTemplateFragment"; +import TemplateSubmissionForm from "./TemplateSubmissionForm"; + +const SubmissionForm = (props: { + template: workflowTemplateFragment$key; + prepopulatedParameters?: JSONObject; + visit?: Visit; + onSubmit: (visit: Visit, parameters: object) => void; +}) => { + const data = useFragment(workflowTemplateFragment, props.template); + return ( + + ); +}; + +export default SubmissionForm; diff --git a/apps/visr/src/components/SubmittedMessagesList.tsx b/apps/visr/src/components/SubmittedMessagesList.tsx new file mode 100644 index 0000000..1b68e49 --- /dev/null +++ b/apps/visr/src/components/SubmittedMessagesList.tsx @@ -0,0 +1,124 @@ +import React from "react"; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Box, + Divider, + Paper, + Typography, +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { Link } from "react-router-dom"; +import type { + SubmissionGraphQLErrorMessage, + SubmissionNetworkErrorMessage, + SubmissionSuccessMessage, +} from "../utils/types"; + +const renderSubmittedMessage = ( + r: + | SubmissionGraphQLErrorMessage + | SubmissionNetworkErrorMessage + | SubmissionSuccessMessage, + i: number, +) => { + switch (r.type) { + case "success": + return ( + {}} + > + + + Successfully submitted{" "} + + {r.message} + + + + + ); + + case "networkError": + return ( + + }> + + Submission error type {r.error.name} + + + + Submission error message {r.error.message} + + + ); + case "graphQLError": + default: + return ( + + }> + + Submission error type GraphQL + + + + {r.errors.map((e, j) => { + return ( + + Error {j} {e.message} + + ); + })} + + + ); + } +}; + +interface SubmittedMessagesListProps { + submissionResults: ( + | SubmissionGraphQLErrorMessage + | SubmissionNetworkErrorMessage + | SubmissionSuccessMessage + )[]; +} + +const SubmittedMessagesList: React.FC = ({ + submissionResults, +}) => { + return ( + + + {submissionResults.length > 0 && ( + <> + + + Submissions + + + {submissionResults.map((r, i) => renderSubmittedMessage(r, i))} + + )} + + ); +}; + +export default SubmittedMessagesList; diff --git a/apps/visr/src/components/TemplateSubmissionForm.tsx b/apps/visr/src/components/TemplateSubmissionForm.tsx new file mode 100644 index 0000000..5296979 --- /dev/null +++ b/apps/visr/src/components/TemplateSubmissionForm.tsx @@ -0,0 +1,112 @@ +import { + materialCells, + materialRenderers, +} from "@jsonforms/material-renderers"; +import { + type JsonSchema, + type UISchemaElement, + createAjv, +} from "@jsonforms/core"; +import { JsonForms } from "@jsonforms/react"; +import React, { useState } from "react"; +import { Divider, Snackbar, Stack, Typography, useTheme } from "@mui/material"; +import type { ErrorObject } from "ajv"; +import type { JSONObject, Visit } from "../utils/types"; +import { VisitInput } from "@diamondlightsource/sci-react-ui"; + +interface TemplateSubmissionFormProps { + title: string; + maintainer: string; + repository?: string | null; + description?: string; + parametersSchema: JsonSchema; + parametersUISchema?: UISchemaElement; + prepopulatedParameters?: JSONObject; + visit?: Visit; + onSubmit: (visit: Visit, parameters: object) => void; +} + +const TemplateSubmissionForm: React.FC = ({ + title, + maintainer, + repository, + description, + parametersSchema, + parametersUISchema, + prepopulatedParameters, + visit, + onSubmit, +}) => { + const theme = useTheme(); + const validator = createAjv({ useDefaults: true, coerceTypes: true }); + + const [parameters, setParameters] = useState(prepopulatedParameters ?? {}); + const [errors, setErrors] = useState([]); + + const [submitted, setSubmitted] = useState(false); + + const onClick = (visit: Visit, parameters?: object) => { + if (errors.length === 0) { + onSubmit(visit, parameters ?? {}); + setSubmitted(true); + } + }; + + const handleCloseSnackbar = () => { + setSubmitted(false); + }; + const formWidth = + (parametersUISchema?.options?.formWidth as string | undefined) ?? "100%"; + + return ( + + + {title} + + + {description} + + + Maintainer: {maintainer} + + {repository && ( + + Repository: {repository} + + )} + + { + setParameters(data as JSONObject); + setErrors(errors ? errors : []); + }} + data-testid="paramters-form" + /> + + + + + ); +}; + +export default TemplateSubmissionForm; diff --git a/apps/visr/src/components/TemplateView.tsx b/apps/visr/src/components/TemplateView.tsx new file mode 100644 index 0000000..c0d529b --- /dev/null +++ b/apps/visr/src/components/TemplateView.tsx @@ -0,0 +1,122 @@ +import { useState } from "react"; +import { useLazyLoadQuery, useMutation } from "react-relay/hooks"; +import { graphql } from "relay-runtime"; +import type { + JSONObject, + SubmissionGraphQLErrorMessage, + SubmissionNetworkErrorMessage, + SubmissionSuccessMessage, +} from "../utils/types"; +import { type Visit, visitToText } from "@diamondlightsource/sci-react-ui"; +import SubmissionForm from "./SubmissionForm"; +import type { TemplateViewQuery as TemplateViewQueryType } from "./__generated__/TemplateViewQuery.graphql"; +import type { TemplateViewMutation as TemplateViewMutationType } from "./__generated__/TemplateViewMutation.graphql"; +import { visitTextToVisit } from "../utils/common"; +import { Box } from "@mui/material"; +import SubmittedMessagesList from "./SubmittedMessagesList"; + +const templateViewQuery = graphql` + query TemplateViewQuery($templateName: String!) { + workflowTemplate(name: $templateName) { + ...workflowTemplateFragment + } + } +`; + +const templateViewMutation = graphql` + mutation TemplateViewMutation( + $templateName: String! + $visit: VisitInput! + $parameters: JSON! + ) { + submitWorkflowTemplate( + name: $templateName + visit: $visit + parameters: $parameters + ) { + name + } + } +`; + +export default function TemplateView({ + templateName, + visit, + prepopulatedParameters, +}: { + templateName: string; + visit?: Visit; + prepopulatedParameters?: JSONObject; +}) { + const data = useLazyLoadQuery(templateViewQuery, { + templateName, + }); + const [submissionResults, setSubmissionResults] = useState< + ( + | SubmissionSuccessMessage + | SubmissionNetworkErrorMessage + | SubmissionGraphQLErrorMessage + )[] + >([]); + + const storedVisit = visitTextToVisit( + localStorage.getItem("instrumentSessionID") ?? "", + ); + + const [commitMutation] = + useMutation(templateViewMutation); + + function submitWorkflow(visit: Visit, parameters: object) { + commitMutation({ + variables: { + templateName: templateName, + visit: visit, + parameters: parameters, + }, + onCompleted: (response, errors) => { + if (errors?.length) { + console.error("GraphQL errors:", errors); + setSubmissionResults(prev => [ + { + type: "graphQLError", + errors: errors, + }, + ...prev, + ]); + } else { + const submittedName = response.submitWorkflowTemplate.name; + console.log("Successfully submitted:", submittedName); + setSubmissionResults(prev => [ + { + type: "success", + message: `${visitToText(visit)}/${submittedName}`, + }, + ...prev, + ]); + localStorage.setItem("instrumentSessionID", visitToText(visit)); + } + }, + onError: err => { + console.error("Submission failed:", err); + setSubmissionResults(prev => [ + { + type: "networkError", + error: err, + }, + ...prev, + ]); + }, + }); + } + return ( + + + + + ); +} diff --git a/apps/visr/src/components/VisrNavbar.tsx b/apps/visr/src/components/VisrNavbar.tsx new file mode 100644 index 0000000..12df262 --- /dev/null +++ b/apps/visr/src/components/VisrNavbar.tsx @@ -0,0 +1,48 @@ +import { Box } from "@mui/material"; +import { Link } from "react-router-dom"; +import { + ColourSchemeButton, + Navbar, + NavLink, + NavLinks, +} from "@diamondlightsource/sci-react-ui"; + +function VisrNavbar() { + return ( + + + + Spectroscopy + + + Plans + + + Workflows + + + + } + rightSlot={ + + + + } + /> + ); +} + +export default VisrNavbar; diff --git a/apps/visr/src/components/__generated__/TemplateViewMutation.graphql.ts b/apps/visr/src/components/__generated__/TemplateViewMutation.graphql.ts new file mode 100644 index 0000000..0a92c0c --- /dev/null +++ b/apps/visr/src/components/__generated__/TemplateViewMutation.graphql.ts @@ -0,0 +1,122 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type VisitInput = { + number: number; + proposalCode: string; + proposalNumber: number; +}; +export type TemplateViewMutation$variables = { + parameters: any; + templateName: string; + visit: VisitInput; +}; +export type TemplateViewMutation$data = { + readonly submitWorkflowTemplate: { + readonly name: string; + }; +}; +export type TemplateViewMutation = { + response: TemplateViewMutation$data; + variables: TemplateViewMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "parameters" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "templateName" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "visit" +}, +v3 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "name", + "variableName": "templateName" + }, + { + "kind": "Variable", + "name": "parameters", + "variableName": "parameters" + }, + { + "kind": "Variable", + "name": "visit", + "variableName": "visit" + } + ], + "concreteType": "Workflow", + "kind": "LinkedField", + "name": "submitWorkflowTemplate", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "TemplateViewMutation", + "selections": (v3/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v2/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "TemplateViewMutation", + "selections": (v3/*: any*/) + }, + "params": { + "cacheID": "b9ea26832024cbe126ef60c2639577e9", + "id": null, + "metadata": {}, + "name": "TemplateViewMutation", + "operationKind": "mutation", + "text": "mutation TemplateViewMutation(\n $templateName: String!\n $visit: VisitInput!\n $parameters: JSON!\n) {\n submitWorkflowTemplate(name: $templateName, visit: $visit, parameters: $parameters) {\n name\n }\n}\n" + } +}; +})(); + +(node as any).hash = "660c30e6a917a69df3bd2b5cbf6a3379"; + +export default node; diff --git a/apps/visr/src/components/__generated__/TemplateViewQuery.graphql.ts b/apps/visr/src/components/__generated__/TemplateViewQuery.graphql.ts new file mode 100644 index 0000000..32f7d5a --- /dev/null +++ b/apps/visr/src/components/__generated__/TemplateViewQuery.graphql.ts @@ -0,0 +1,149 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type TemplateViewQuery$variables = { + templateName: string; +}; +export type TemplateViewQuery$data = { + readonly workflowTemplate: { + readonly " $fragmentSpreads": FragmentRefs<"workflowTemplateFragment">; + }; +}; +export type TemplateViewQuery = { + response: TemplateViewQuery$data; + variables: TemplateViewQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "templateName" + } +], +v1 = [ + { + "kind": "Variable", + "name": "name", + "variableName": "templateName" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "TemplateViewQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "WorkflowTemplate", + "kind": "LinkedField", + "name": "workflowTemplate", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "workflowTemplateFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "TemplateViewQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "WorkflowTemplate", + "kind": "LinkedField", + "name": "workflowTemplate", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "maintainer", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "arguments", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "uiSchema", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "repository", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9fb9e57ce71efced848172180274ed2a", + "id": null, + "metadata": {}, + "name": "TemplateViewQuery", + "operationKind": "query", + "text": "query TemplateViewQuery(\n $templateName: String!\n) {\n workflowTemplate(name: $templateName) {\n ...workflowTemplateFragment\n }\n}\n\nfragment workflowTemplateFragment on WorkflowTemplate {\n name\n maintainer\n title\n description\n arguments\n uiSchema\n repository\n}\n" + } +}; +})(); + +(node as any).hash = "907837f3f9cf4408bf964271f9a981d1"; + +export default node; diff --git a/apps/visr/src/components/useSpectroscopyData.ts b/apps/visr/src/components/useSpectroscopyData.ts new file mode 100644 index 0000000..3090a3c --- /dev/null +++ b/apps/visr/src/components/useSpectroscopyData.ts @@ -0,0 +1,104 @@ +import { useEffect, useRef, useState } from "react"; +import { type NDT } from "@diamondlightsource/davidia"; + +export type RGBColour = "red" | "green" | "blue" | "gray"; + +export type FetchMapFunction = ( + filepath: string, + datapath: string, + colour: RGBColour, +) => Promise; + +export interface DataChannels { + red: NDT | null; + green: NDT | null; + blue: NDT | null; +} + +export interface ScanEventMessage { + status: "running" | "finished" | "failed"; + filepath: string; + uuid: string; +} + +export function useSpectroscopyData(fetchMap: FetchMapFunction) { + const [running, setRunning] = useState(false); + const [filepath, setFilepath] = useState(null); + const [data, setData] = useState({ + red: null, + green: null, + blue: null, + }); + + /** Cached interval id */ + const pollInterval = useRef | null>(null); + + // Subscribe to SSE + useEffect(() => { + const evtSource = new EventSource("/api/data/events"); + + evtSource.onmessage = event => { + try { + const msg = JSON.parse(event.data) as ScanEventMessage; + console.log("SSE message:", msg); + + if (msg.status === "running") { + setRunning(true); + setFilepath(msg.filepath); + } else if (msg.status === "finished" || msg.status === "failed") { + setRunning(false); // triggers final poll below + } + } catch (err) { + console.error("Error parsing SSE:", err); + } + }; + + evtSource.onerror = err => { + console.warn("Temporary SSE connection error:", err); + }; + + evtSource.onopen = () => { + console.log("SSE connection opened or re-established"); + }; + + return () => evtSource.close(); + }, []); + + // Poll during scan + once more afterwards + useEffect(() => { + async function poll() { + if (!filepath) return; + try { + const basePath = "/entry/instrument/spectroscopy_detector/"; + const [red, green, blue] = await Promise.all([ + fetchMap(filepath, basePath + "RedTotal", "red"), + fetchMap(filepath, basePath + "GreenTotal", "green"), + fetchMap(filepath, basePath + "BlueTotal", "blue"), + ]); + setData({ red, green, blue }); + } catch (err) { + console.error("Polling error:", err); + } + } + + if (running && filepath) { + // start polling + pollInterval.current = setInterval(poll, 500); // 2 Hz + } else if (!running && pollInterval.current) { + // poll once more then clear interval + poll().finally(() => { + clearInterval(pollInterval.current!); + pollInterval.current = null; + }); + } + + return () => { + if (pollInterval.current) { + clearInterval(pollInterval.current); + pollInterval.current = null; + } + }; + }, [running, filepath, fetchMap]); + + return { data, running }; +} diff --git a/apps/visr/src/context/instrumentSession/InstrumentSessionContext.ts b/apps/visr/src/context/instrumentSession/InstrumentSessionContext.ts new file mode 100644 index 0000000..74a9c93 --- /dev/null +++ b/apps/visr/src/context/instrumentSession/InstrumentSessionContext.ts @@ -0,0 +1,10 @@ +import { createContext } from "react"; + +export type InstrumentSessionContextType = { + instrumentSession: string; + setInstrumentSession: (session: string) => void; +}; + +export const InstrumentSessionContext = createContext< + InstrumentSessionContextType | undefined +>(undefined); diff --git a/apps/visr/src/context/instrumentSession/InstrumentSessionProvider.tsx b/apps/visr/src/context/instrumentSession/InstrumentSessionProvider.tsx new file mode 100644 index 0000000..056918d --- /dev/null +++ b/apps/visr/src/context/instrumentSession/InstrumentSessionProvider.tsx @@ -0,0 +1,28 @@ +import { useState, useEffect, type ReactNode } from "react"; +import { InstrumentSessionContext } from "./InstrumentSessionContext"; + +const STORAGE_KEY = "instrument-session-id"; + +export const InstrumentSessionProvider = ({ + children, + defaultSessionId = "cm40661-6", +}: { + children: ReactNode; + defaultSessionId?: string; +}) => { + const [instrumentSession, setInstrumentSession] = useState(() => { + return localStorage.getItem(STORAGE_KEY) ?? defaultSessionId; + }); + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, instrumentSession); + }, [instrumentSession]); + + return ( + + {children} + + ); +}; diff --git a/apps/visr/src/context/instrumentSession/useInstrumentSession.ts b/apps/visr/src/context/instrumentSession/useInstrumentSession.ts new file mode 100644 index 0000000..2b19f86 --- /dev/null +++ b/apps/visr/src/context/instrumentSession/useInstrumentSession.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import { InstrumentSessionContext } from "./InstrumentSessionContext"; + +export const useInstrumentSession = () => { + const context = useContext(InstrumentSessionContext); + if (!context) { + throw new Error( + "useInstrumentSession must be used within InstrumentSessionProvider", + ); + } + return context; +}; diff --git a/apps/visr/src/graphql/__generated__/workflowTemplateFragment.graphql.ts b/apps/visr/src/graphql/__generated__/workflowTemplateFragment.graphql.ts new file mode 100644 index 0000000..f519396 --- /dev/null +++ b/apps/visr/src/graphql/__generated__/workflowTemplateFragment.graphql.ts @@ -0,0 +1,90 @@ +/** + * @generated SignedSource<<7eb6c59915242a110bb98268ee6f12dc>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type workflowTemplateFragment$data = { + readonly arguments: any; + readonly description: string | null | undefined; + readonly maintainer: string; + readonly name: string; + readonly repository: string | null | undefined; + readonly title: string | null | undefined; + readonly uiSchema: any | null | undefined; + readonly " $fragmentType": "workflowTemplateFragment"; +}; +export type workflowTemplateFragment$key = { + readonly " $data"?: workflowTemplateFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"workflowTemplateFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "workflowTemplateFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "maintainer", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "arguments", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "uiSchema", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "repository", + "storageKey": null + } + ], + "type": "WorkflowTemplate", + "abstractKey": null +}; + +(node as any).hash = "959850317e316a423e70b0c89c011f27"; + +export default node; diff --git a/apps/visr/src/graphql/__generated__/workflowsQuery.graphql.ts b/apps/visr/src/graphql/__generated__/workflowsQuery.graphql.ts new file mode 100644 index 0000000..d55fe50 --- /dev/null +++ b/apps/visr/src/graphql/__generated__/workflowsQuery.graphql.ts @@ -0,0 +1,205 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type VisitInput = { + number: number; + proposalCode: string; + proposalNumber: number; +}; +export type WorkflowFilter = { + creator?: any | null | undefined; + template?: any | null | undefined; + workflowStatusFilter?: WorkflowStatusFilter | null | undefined; +}; +export type WorkflowStatusFilter = { + error?: boolean; + failed?: boolean; + pending?: boolean; + running?: boolean; + succeeded?: boolean; +}; +export type workflowsQuery$variables = { + cursor?: string | null | undefined; + filter?: WorkflowFilter | null | undefined; + limit: number; + visit: VisitInput; +}; +export type workflowsQuery$data = { + readonly workflows: { + readonly nodes: ReadonlyArray<{ + readonly name: string; + }>; + readonly pageInfo: { + readonly endCursor: string | null | undefined; + readonly hasNextPage: boolean; + readonly hasPreviousPage: boolean; + readonly startCursor: string | null | undefined; + }; + }; +}; +export type workflowsQuery = { + response: workflowsQuery$data; + variables: workflowsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "filter" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "limit" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "visit" +}, +v4 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "cursor", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "filter", + "variableName": "filter" + }, + { + "kind": "Variable", + "name": "limit", + "variableName": "limit" + }, + { + "kind": "Variable", + "name": "visit", + "variableName": "visit" + } + ], + "concreteType": "WorkflowConnection", + "kind": "LinkedField", + "name": "workflows", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Workflow", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "workflowsQuery", + "selections": (v4/*: any*/), + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v3/*: any*/), + (v0/*: any*/), + (v2/*: any*/), + (v1/*: any*/) + ], + "kind": "Operation", + "name": "workflowsQuery", + "selections": (v4/*: any*/) + }, + "params": { + "cacheID": "6e6da56e134de097d20cafd8abe7e0aa", + "id": null, + "metadata": {}, + "name": "workflowsQuery", + "operationKind": "query", + "text": "query workflowsQuery(\n $visit: VisitInput!\n $cursor: String\n $limit: Int!\n $filter: WorkflowFilter\n) {\n workflows(visit: $visit, cursor: $cursor, limit: $limit, filter: $filter) {\n nodes {\n name\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "7451d3fab389359463e7424e2fcaa8d0"; + +export default node; diff --git a/apps/visr/src/graphql/workflowTemplateFragment.ts b/apps/visr/src/graphql/workflowTemplateFragment.ts new file mode 100644 index 0000000..28db243 --- /dev/null +++ b/apps/visr/src/graphql/workflowTemplateFragment.ts @@ -0,0 +1,13 @@ +import { graphql } from "react-relay"; + +export const workflowTemplateFragment = graphql` + fragment workflowTemplateFragment on WorkflowTemplate { + name + maintainer + title + description + arguments + uiSchema + repository + } +`; diff --git a/apps/visr/src/graphql/workflowsQuery.ts b/apps/visr/src/graphql/workflowsQuery.ts new file mode 100644 index 0000000..e194444 --- /dev/null +++ b/apps/visr/src/graphql/workflowsQuery.ts @@ -0,0 +1,22 @@ +import { graphql } from "relay-runtime"; + +export const workflowsQuery = graphql` + query workflowsQuery( + $visit: VisitInput! + $cursor: String + $limit: Int! + $filter: WorkflowFilter + ) { + workflows(visit: $visit, cursor: $cursor, limit: $limit, filter: $filter) { + nodes { + name + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + } + } +`; diff --git a/apps/visr/src/main.tsx b/apps/visr/src/main.tsx new file mode 100644 index 0000000..e29eeb5 --- /dev/null +++ b/apps/visr/src/main.tsx @@ -0,0 +1,67 @@ +import { DiamondTheme, ThemeProvider } from "@diamondlightsource/sci-react-ui"; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; + +import Dashboard from "./routes/Dashboard.tsx"; +import { InstrumentSessionProvider } from "./context/instrumentSession/InstrumentSessionProvider.tsx"; +import JsonFormsPlans from "./routes/Plans.tsx"; +import { Layout } from "./routes/Layout.tsx"; +import Spectroscopy from "./routes/Spectroscopy.tsx"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +declare global { + interface Window { + global?: typeof globalThis; + } +} + +window.global ||= window; +import Workflows from "./routes/Workflows.tsx"; +import { RelayEnvironmentProvider } from "react-relay"; +import { RelayEnvironment } from "./RelayEnvironment.ts"; + +async function enableMocking() { + if (import.meta.env.DEV) { + const { worker } = await import("./mocks/browser"); + return worker.start(); + } +} + +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + index: true, + element: , + }, + { + path: "spectroscopy", + element: , + }, + { + path: "plans", + element: , + }, + { + path: "/workflows", + element: , + }, + ], + }, +]); + +enableMocking().then(() => { + createRoot(document.getElementById("root")!).render( + + + + + + + + + , + ); +}); diff --git a/apps/visr/src/mocks/browser.ts b/apps/visr/src/mocks/browser.ts new file mode 100644 index 0000000..bcd82e4 --- /dev/null +++ b/apps/visr/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from "msw/browser"; +import { handlers } from "./handlers"; + +export const worker = setupWorker(...handlers); diff --git a/apps/visr/src/mocks/handlers.ts b/apps/visr/src/mocks/handlers.ts new file mode 100644 index 0000000..b716c96 --- /dev/null +++ b/apps/visr/src/mocks/handlers.ts @@ -0,0 +1,122 @@ +import { http, HttpResponse } from "msw"; +import workflowsResponse from "./workflows-response.json"; +import plansResponse from "./plans-response.json"; +import instrumentSessionsResponse from "./instrumentSessions-response.json"; +import type { ScanEventMessage } from "../components/useSpectroscopyData"; + +const fakeTaskId = "7304e8e0-81c6-4978-9a9d-9046ab79ce3c"; + +function mapData(): (number | null)[][] { + return [ + [ + 5467227.0, 5467227.0, 5480663.0, 5478486.0, 5477020.0, 5474645.0, + 5472603.0, 5470330.0, 5468827.0, 5467346.0, + ], + [ + 5465947.0, 5464940.0, 5483401.0, 5480384.0, 5477378.0, 5474776.0, + 5471462.0, 5450634.0, 5454651.0, 5465208.0, + ], + [ + 5463907.0, 5463469.0, 5481975.0, 5479338.0, 5476649.0, 5474993.0, + 5473529.0, 5471431.0, 5470030.0, 5468721.0, + ], + [ + 5466907.0, 5466023.0, 5483679.0, 5480875.0, 5478044.0, 5475180.0, + 5473196.0, 5471168.0, 5469539.0, 5468190.0, + ], + [ + 5466626.0, 5465494.0, 5483503.0, 5480586.0, 5477659.0, 5475069.0, + 5473287.0, 5471380.0, 5469737.0, 5468496.0, + ], + [ + 5466713.0, 5465920.0, 5483587.0, 5480582.0, 5477829.0, 5475202.0, + 5473244.0, 5471103.0, 5468010.0, 5468222.0, + ], + [5467700.0, null, null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null, null, null], + ]; +} + +export const handlers = [ + http.post("/api/graphql", request => { + const referrer = request.request.referrer; + if (referrer.search("workflows") > 0) { + return HttpResponse.json(workflowsResponse); + } else { + return HttpResponse.json(instrumentSessionsResponse); + } + }), + + http.get("/api/plans", () => { + return HttpResponse.json(plansResponse); + }), + + http.put("/api/worker/task", () => { + return HttpResponse.json({ + task_id: fakeTaskId, + }); + }), + + http.post("/api/tasks", () => { + return HttpResponse.json({ + task_id: fakeTaskId, + }); + }), + + http.get("/api/data/map", ({ request }) => { + const url = new URL(request.url); + const filepath = url.searchParams.get("filepath"); + const datapath = url.searchParams.get("datapath"); + console.log("Mock /api/data/map called", { filepath, datapath }); + const data = mapData(); + return HttpResponse.json({ values: data }); + }), + + http.get("/api/data/events", async () => { + const encoder = new TextEncoder(); + + // Create a ReadableStream that emits fake scan events + const stream = new ReadableStream({ + start(controller) { + const send = (event: ScanEventMessage) => { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(event)}\n\n`), + ); + }; + + // scan starts + send({ + uuid: "fake-scan-uuid", + filepath: "/mock/path/fake.nxs", + status: "running", + }); + + // simulate data collection for ~5 seconds + let counter = 0; + const interval = setInterval(() => { + counter++; + console.log("Mock event tick", counter); + if (counter >= 25) { + clearInterval(interval); + // Scan stops + send({ + uuid: "fake-scan-uuid", + filepath: "/mock/path/fake.nxs", + status: "finished", + }); + } + }, 200); + }, + }); + + return new HttpResponse(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); + }), +]; diff --git a/apps/visr/src/mocks/instrumentSessions-response.json b/apps/visr/src/mocks/instrumentSessions-response.json new file mode 100644 index 0000000..c4629c7 --- /dev/null +++ b/apps/visr/src/mocks/instrumentSessions-response.json @@ -0,0 +1,71 @@ +{ + "data": { + "instrument": { + "instrumentSessions": [ + { + "instrumentSessionNumber": 1, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 39125 + } + }, + { + "instrumentSessionNumber": 2, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 39125 + } + }, + { + "instrumentSessionNumber": 3, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 39125 + } + }, + { + "instrumentSessionNumber": 1, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 40661 + } + }, + { + "instrumentSessionNumber": 2, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 40661 + } + }, + { + "instrumentSessionNumber": 3, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 40661 + } + }, + { + "instrumentSessionNumber": 4, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 40661 + } + }, + { + "instrumentSessionNumber": 5, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 40661 + } + }, + { + "instrumentSessionNumber": 6, + "proposal": { + "proposalCategory": "CM", + "proposalNumber": 40661 + } + } + ] + } + } +} diff --git a/apps/visr/src/mocks/plans-response.json b/apps/visr/src/mocks/plans-response.json new file mode 100644 index 0000000..ee809a5 --- /dev/null +++ b/apps/visr/src/mocks/plans-response.json @@ -0,0 +1,868 @@ +{ + "plans": [ + { + "name": "hi_plan", + "description": "Says hi to you", + "schema": { + "properties": { "name": { "title": "Name", "type": "string" } } + } + }, + { + "name": "count", + "description": "Reads from a number of devices.\n Wraps bluesky.plans.count(det, num, delay, md=metadata) exposing only serializable\n parameters and metadata.", + "schema": { + "additionalProperties": false, + "properties": { + "detectors": { + "items": { + "enum": [ + "sample_stage", + "sample_det", + "oav", + "synchrotron", + "panda" + ], + "type": "bluesky.protocols.Readable" + }, + "title": "Detectors", + "type": "array", + "uniqueItems": true + }, + "num": { + "title": "Num", + "type": "integer" + }, + "delay": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "title": "Delay" + }, + "metadata": { + "title": "Metadata", + "type": "object" + } + }, + "required": ["detectors"], + "title": "count", + "type": "object" + } + }, + { + "name": "spec_scan", + "description": "Generic plan for reading `detectors` at every point of a ScanSpec `Spec`.\n A `Spec` is an N-dimensional path.\n ", + "schema": { + "$defs": { + "Circle_Reference_": { + "additionalProperties": false, + "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)", + "properties": { + "x_axis": { + "description": "The name matching the x axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "X Axis", + "type": "bluesky.protocols.Movable" + }, + "y_axis": { + "description": "The name matching the y axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Y Axis", + "type": "bluesky.protocols.Movable" + }, + "x_middle": { + "description": "The central x point of the circle", + "title": "X Middle", + "type": "number" + }, + "y_middle": { + "description": "The central y point of the circle", + "title": "Y Middle", + "type": "number" + }, + "radius": { + "description": "Radius of the circle", + "exclusiveMinimum": 0, + "title": "Radius", + "type": "number" + }, + "type": { + "const": "Circle", + "default": "Circle", + "title": "Type", + "type": "string" + } + }, + "required": ["x_axis", "y_axis", "x_middle", "y_middle", "radius"], + "title": "Circle", + "type": "object" + }, + "CombinationOf_Reference_": { + "additionalProperties": false, + "description": "Abstract baseclass for a combination of two regions, left and right.", + "properties": { + "left": { + "$ref": "#/$defs/Region", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/$defs/Region", + "description": "The right-hand Region to combine" + }, + "type": { + "const": "CombinationOf", + "default": "CombinationOf", + "title": "Type", + "type": "string" + } + }, + "required": ["left", "right"], + "title": "CombinationOf", + "type": "object" + }, + "Concat_Reference_": { + "additionalProperties": false, + "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))", + "properties": { + "left": { + "$ref": "#/$defs/Spec", + "description": "The left-hand Spec to Concat, midpoints will appear earlier" + }, + "right": { + "$ref": "#/$defs/Spec", + "description": "The right-hand Spec to Concat, midpoints will appear later" + }, + "gap": { + "default": false, + "description": "If True, force a gap in the output at the join", + "title": "Gap", + "type": "boolean" + }, + "check_path_changes": { + "default": true, + "description": "If True path through scan will not be modified by squash", + "title": "Check Path Changes", + "type": "boolean" + }, + "type": { + "const": "Concat", + "default": "Concat", + "title": "Type", + "type": "string" + } + }, + "required": ["left", "right"], + "title": "Concat", + "type": "object" + }, + "DifferenceOf_Reference_": { + "additionalProperties": false, + "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])", + "properties": { + "left": { + "$ref": "#/$defs/Region", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/$defs/Region", + "description": "The right-hand Region to combine" + }, + "type": { + "const": "DifferenceOf", + "default": "DifferenceOf", + "title": "Type", + "type": "string" + } + }, + "required": ["left", "right"], + "title": "DifferenceOf", + "type": "object" + }, + "Ellipse_Reference_": { + "additionalProperties": false, + "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)", + "properties": { + "x_axis": { + "description": "The name matching the x axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "X Axis", + "type": "bluesky.protocols.Movable" + }, + "y_axis": { + "description": "The name matching the y axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Y Axis", + "type": "bluesky.protocols.Movable" + }, + "x_middle": { + "description": "The central x point of the ellipse", + "title": "X Middle", + "type": "number" + }, + "y_middle": { + "description": "The central y point of the ellipse", + "title": "Y Middle", + "type": "number" + }, + "x_radius": { + "description": "The radius along the x axis of the ellipse", + "exclusiveMinimum": 0, + "title": "X Radius", + "type": "number" + }, + "y_radius": { + "description": "The radius along the y axis of the ellipse", + "exclusiveMinimum": 0, + "title": "Y Radius", + "type": "number" + }, + "angle": { + "default": 0, + "description": "The angle of the ellipse (degrees)", + "title": "Angle", + "type": "number" + }, + "type": { + "const": "Ellipse", + "default": "Ellipse", + "title": "Type", + "type": "string" + } + }, + "required": [ + "x_axis", + "y_axis", + "x_middle", + "y_middle", + "x_radius", + "y_radius" + ], + "title": "Ellipse", + "type": "object" + }, + "IntersectionOf_Reference_": { + "additionalProperties": false, + "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])", + "properties": { + "left": { + "$ref": "#/$defs/Region", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/$defs/Region", + "description": "The right-hand Region to combine" + }, + "type": { + "const": "IntersectionOf", + "default": "IntersectionOf", + "title": "Type", + "type": "string" + } + }, + "required": ["left", "right"], + "title": "IntersectionOf", + "type": "object" + }, + "Line_Reference_": { + "additionalProperties": false, + "description": "Linearly spaced frames with start and stop as first and last midpoints.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 2, 5)", + "properties": { + "axis": { + "description": "An identifier for what to move", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Axis", + "type": "bluesky.protocols.Movable" + }, + "start": { + "description": "Midpoint of the first point of the line", + "title": "Start", + "type": "number" + }, + "stop": { + "description": "Midpoint of the last point of the line", + "title": "Stop", + "type": "number" + }, + "num": { + "description": "Number of frames to produce", + "minimum": 1, + "title": "Num", + "type": "integer" + }, + "type": { + "const": "Line", + "default": "Line", + "title": "Type", + "type": "string" + } + }, + "required": ["axis", "start", "stop", "num"], + "title": "Line", + "type": "object" + }, + "Mask_Reference_": { + "additionalProperties": false, + "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Frames objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`", + "properties": { + "spec": { + "$ref": "#/$defs/Spec", + "description": "The Spec containing the source midpoints" + }, + "region": { + "$ref": "#/$defs/Region", + "description": "The Region that midpoints will be inside" + }, + "check_path_changes": { + "default": true, + "description": "If True path through scan will not be modified by squash", + "title": "Check Path Changes", + "type": "boolean" + }, + "type": { + "const": "Mask", + "default": "Mask", + "title": "Type", + "type": "string" + } + }, + "required": ["spec", "region"], + "title": "Mask", + "type": "object" + }, + "Polygon_Reference_": { + "additionalProperties": false, + "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])", + "properties": { + "x_axis": { + "description": "The name matching the x axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "X Axis", + "type": "bluesky.protocols.Movable" + }, + "y_axis": { + "description": "The name matching the y axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Y Axis", + "type": "bluesky.protocols.Movable" + }, + "x_verts": { + "description": "The Nx1 x coordinates of the polygons vertices", + "items": { + "type": "number" + }, + "minItems": 3, + "title": "X Verts", + "type": "array" + }, + "y_verts": { + "description": "The Nx1 y coordinates of the polygons vertices", + "items": { + "type": "number" + }, + "minItems": 3, + "title": "Y Verts", + "type": "array" + }, + "type": { + "const": "Polygon", + "default": "Polygon", + "title": "Type", + "type": "string" + } + }, + "required": ["x_axis", "y_axis", "x_verts", "y_verts"], + "title": "Polygon", + "type": "object" + }, + "Product_Reference_": { + "additionalProperties": false, + "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)", + "properties": { + "outer": { + "$ref": "#/$defs/Spec", + "description": "Will be executed once" + }, + "inner": { + "$ref": "#/$defs/Spec", + "description": "Will be executed len(outer) times" + }, + "type": { + "const": "Product", + "default": "Product", + "title": "Type", + "type": "string" + } + }, + "required": ["outer", "inner"], + "title": "Product", + "type": "object" + }, + "Range_Reference_": { + "additionalProperties": false, + "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])", + "properties": { + "axis": { + "description": "The name matching the axis to mask in spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Axis", + "type": "bluesky.protocols.Movable" + }, + "min": { + "description": "The minimum inclusive value in the region", + "title": "Min", + "type": "number" + }, + "max": { + "description": "The minimum inclusive value in the region", + "title": "Max", + "type": "number" + }, + "type": { + "const": "Range", + "default": "Range", + "title": "Type", + "type": "string" + } + }, + "required": ["axis", "min", "max"], + "title": "Range", + "type": "object" + }, + "Rectangle_Reference_": { + "additionalProperties": false, + "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)", + "properties": { + "x_axis": { + "description": "The name matching the x axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "X Axis", + "type": "bluesky.protocols.Movable" + }, + "y_axis": { + "description": "The name matching the y axis of the spec", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Y Axis", + "type": "bluesky.protocols.Movable" + }, + "x_min": { + "description": "Minimum inclusive x value in the region", + "title": "X Min", + "type": "number" + }, + "y_min": { + "description": "Minimum inclusive y value in the region", + "title": "Y Min", + "type": "number" + }, + "x_max": { + "description": "Maximum inclusive x value in the region", + "title": "X Max", + "type": "number" + }, + "y_max": { + "description": "Maximum inclusive y value in the region", + "title": "Y Max", + "type": "number" + }, + "angle": { + "default": 0, + "description": "Clockwise rotation angle of the rectangle", + "title": "Angle", + "type": "number" + }, + "type": { + "const": "Rectangle", + "default": "Rectangle", + "title": "Type", + "type": "string" + } + }, + "required": [ + "x_axis", + "y_axis", + "x_min", + "y_min", + "x_max", + "y_max" + ], + "title": "Rectangle", + "type": "object" + }, + "Region": { + "discriminator": { + "mapping": { + "Circle": "#/$defs/Circle_Reference_", + "CombinationOf": "#/$defs/CombinationOf_Reference_", + "DifferenceOf": "#/$defs/DifferenceOf_Reference_", + "Ellipse": "#/$defs/Ellipse_Reference_", + "IntersectionOf": "#/$defs/IntersectionOf_Reference_", + "Polygon": "#/$defs/Polygon_Reference_", + "Range": "#/$defs/Range_Reference_", + "Rectangle": "#/$defs/Rectangle_Reference_", + "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf_Reference_", + "UnionOf": "#/$defs/UnionOf_Reference_" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/$defs/Range_Reference_" + }, + { + "$ref": "#/$defs/Rectangle_Reference_" + }, + { + "$ref": "#/$defs/Polygon_Reference_" + }, + { + "$ref": "#/$defs/Circle_Reference_" + }, + { + "$ref": "#/$defs/Ellipse_Reference_" + }, + { + "$ref": "#/$defs/CombinationOf_Reference_" + }, + { + "$ref": "#/$defs/UnionOf_Reference_" + }, + { + "$ref": "#/$defs/IntersectionOf_Reference_" + }, + { + "$ref": "#/$defs/DifferenceOf_Reference_" + }, + { + "$ref": "#/$defs/SymmetricDifferenceOf_Reference_" + } + ] + }, + "Repeat_Reference_": { + "additionalProperties": false, + "description": "Repeat an empty frame num times.\n\nCan be used on the outside of a scan to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 2 * ~Line.bounded(\"x\", 3, 4, 1)\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Line, Repeat\n\n spec = Repeat(2, gap=False) * ~Line.bounded(\"x\", 3, 4, 1)\n\n.. note:: There is no turnaround arrow at x=4", + "properties": { + "num": { + "description": "Number of frames to produce", + "minimum": 1, + "title": "Num", + "type": "integer" + }, + "gap": { + "default": true, + "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap", + "title": "Gap", + "type": "boolean" + }, + "type": { + "const": "Repeat", + "default": "Repeat", + "title": "Type", + "type": "string" + } + }, + "required": ["num"], + "title": "Repeat", + "type": "object" + }, + "Snake_Reference_": { + "additionalProperties": false, + "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)", + "properties": { + "spec": { + "$ref": "#/$defs/Spec", + "description": "The Spec to run in reverse every other iteration" + }, + "type": { + "const": "Snake", + "default": "Snake", + "title": "Type", + "type": "string" + } + }, + "required": ["spec"], + "title": "Snake", + "type": "object" + }, + "Spec": { + "discriminator": { + "mapping": { + "Concat": "#/$defs/Concat_Reference_", + "Line": "#/$defs/Line_Reference_", + "Mask": "#/$defs/Mask_Reference_", + "Product": "#/$defs/Product_Reference_", + "Repeat": "#/$defs/Repeat_Reference_", + "Snake": "#/$defs/Snake_Reference_", + "Spiral": "#/$defs/Spiral_Reference_", + "Squash": "#/$defs/Squash_Reference_", + "Static": "#/$defs/Static_Reference_", + "Zip": "#/$defs/Zip_Reference_" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/$defs/Line_Reference_" + }, + { + "$ref": "#/$defs/Static_Reference_" + }, + { + "$ref": "#/$defs/Squash_Reference_" + }, + { + "$ref": "#/$defs/Spiral_Reference_" + }, + { + "$ref": "#/$defs/Product_Reference_" + }, + { + "$ref": "#/$defs/Repeat_Reference_" + }, + { + "$ref": "#/$defs/Zip_Reference_" + }, + { + "$ref": "#/$defs/Snake_Reference_" + }, + { + "$ref": "#/$defs/Concat_Reference_" + }, + { + "$ref": "#/$defs/Mask_Reference_" + } + ] + }, + "Spiral_Reference_": { + "additionalProperties": false, + "description": "Archimedean spiral of \"x_axis\" and \"y_axis\".\n\nStarts at centre point (\"x_start\", \"y_start\") with angle \"rotate\". Produces\n\"num\" points in a spiral spanning width of \"x_range\" and height of \"y_range\"\n\n.. example_spec::\n\n from scanspec.specs import Spiral\n\n spec = Spiral(\"x\", \"y\", 1, 5, 10, 50, 30)", + "properties": { + "x_axis": { + "description": "An identifier for what to move for x", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "X Axis", + "type": "bluesky.protocols.Movable" + }, + "y_axis": { + "description": "An identifier for what to move for y", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Y Axis", + "type": "bluesky.protocols.Movable" + }, + "x_start": { + "description": "x centre of the spiral", + "title": "X Start", + "type": "number" + }, + "y_start": { + "description": "y centre of the spiral", + "title": "Y Start", + "type": "number" + }, + "x_range": { + "description": "x width of the spiral", + "title": "X Range", + "type": "number" + }, + "y_range": { + "description": "y width of the spiral", + "title": "Y Range", + "type": "number" + }, + "num": { + "description": "Number of frames to produce", + "minimum": 1, + "title": "Num", + "type": "integer" + }, + "rotate": { + "default": 0, + "description": "How much to rotate the angle of the spiral", + "title": "Rotate", + "type": "number" + }, + "type": { + "const": "Spiral", + "default": "Spiral", + "title": "Type", + "type": "string" + } + }, + "required": [ + "x_axis", + "y_axis", + "x_start", + "y_start", + "x_range", + "y_range", + "num" + ], + "title": "Spiral", + "type": "object" + }, + "Squash_Reference_": { + "additionalProperties": false, + "description": "Squash a stack of Frames together into a single expanded Frames object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))", + "properties": { + "spec": { + "$ref": "#/$defs/Spec", + "description": "The Spec to squash the dimensions of" + }, + "check_path_changes": { + "default": true, + "description": "If True path through scan will not be modified by squash", + "title": "Check Path Changes", + "type": "boolean" + }, + "type": { + "const": "Squash", + "default": "Squash", + "title": "Type", + "type": "string" + } + }, + "required": ["spec"], + "title": "Squash", + "type": "object" + }, + "Static_Reference_": { + "additionalProperties": false, + "description": "A static frame, repeated num times, with axis at value.\n\nCan be used to set axis=value at every point in a scan.\n\n.. example_spec::\n\n from scanspec.specs import Line, Static\n\n spec = Line(\"y\", 1, 2, 3).zip(Static(\"x\", 3))", + "properties": { + "axis": { + "description": "An identifier for what to move", + "enum": ["sample_stage.x", "sample_stage.y", "sample_stage.z"], + "title": "Axis", + "type": "bluesky.protocols.Movable" + }, + "value": { + "description": "The value at each point", + "title": "Value", + "type": "number" + }, + "num": { + "default": 1, + "description": "Number of frames to produce", + "minimum": 1, + "title": "Num", + "type": "integer" + }, + "type": { + "const": "Static", + "default": "Static", + "title": "Type", + "type": "string" + } + }, + "required": ["axis", "value"], + "title": "Static", + "type": "object" + }, + "SymmetricDifferenceOf_Reference_": { + "additionalProperties": false, + "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])", + "properties": { + "left": { + "$ref": "#/$defs/Region", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/$defs/Region", + "description": "The right-hand Region to combine" + }, + "type": { + "const": "SymmetricDifferenceOf", + "default": "SymmetricDifferenceOf", + "title": "Type", + "type": "string" + } + }, + "required": ["left", "right"], + "title": "SymmetricDifferenceOf", + "type": "object" + }, + "UnionOf_Reference_": { + "additionalProperties": false, + "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])", + "properties": { + "left": { + "$ref": "#/$defs/Region", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/$defs/Region", + "description": "The right-hand Region to combine" + }, + "type": { + "const": "UnionOf", + "default": "UnionOf", + "title": "Type", + "type": "string" + } + }, + "required": ["left", "right"], + "title": "UnionOf", + "type": "object" + }, + "Zip_Reference_": { + "additionalProperties": false, + "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))", + "properties": { + "left": { + "$ref": "#/$defs/Spec", + "description": "The left-hand Spec to Zip, will appear earlier in axes" + }, + "right": { + "$ref": "#/$defs/Spec", + "description": "The right-hand Spec to Zip, will appear later in axes" + }, + "type": { + "const": "Zip", + "default": "Zip", + "title": "Type", + "type": "string" + } + }, + "required": ["left", "right"], + "title": "Zip", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "detectors": { + "items": { + "enum": [ + "sample_stage", + "sample_det", + "oav", + "synchrotron", + "panda" + ], + "type": "bluesky.protocols.Readable" + }, + "title": "Detectors", + "type": "array", + "uniqueItems": true + }, + "spec": { + "$ref": "#/$defs/Spec" + }, + "metadata": { + "title": "Metadata", + "type": "object" + } + }, + "required": ["detectors", "spec"], + "title": "spec_scan", + "type": "object" + } + } + ] +} diff --git a/apps/visr/src/mocks/workflows-response.json b/apps/visr/src/mocks/workflows-response.json new file mode 100644 index 0000000..424c19d --- /dev/null +++ b/apps/visr/src/mocks/workflows-response.json @@ -0,0 +1,22 @@ +{ + "data": { + "workflowTemplate": { + "name": "visr", + "maintainer": "visr-group", + "title": "visr", + "description": "Data processing for the Visible Light Spectroscopy beamline (ViSR).\n", + "arguments": { + "properties": { + "inpath": { + "default": "", + "type": "string" + } + }, + "required": ["inpath"], + "type": "object" + }, + "uiSchema": null, + "repository": null + } + } +} diff --git a/apps/visr/src/routes/Dashboard.tsx b/apps/visr/src/routes/Dashboard.tsx new file mode 100644 index 0000000..fa1f7c4 --- /dev/null +++ b/apps/visr/src/routes/Dashboard.tsx @@ -0,0 +1,53 @@ +import { Container, Typography, Button, Stack } from "@mui/material"; +import ArticleIcon from "@mui/icons-material/Article"; +import FeedIcon from "@mui/icons-material/Feed"; +import AddchartIcon from "@mui/icons-material/Addchart"; +import { Link } from "react-router-dom"; +import InstrumentSessionView from "../components/InstrumentSessionSelection/InstrumentSessionView"; +import GetInstrumentSessions from "../components/InstrumentSessionSelection/InstrumentSession"; + +function Dashboard() { + return ( + <> + + + + Welcome to ViSR + + + + + + + + + + + ); +} + +export default Dashboard; diff --git a/apps/visr/src/routes/Layout.tsx b/apps/visr/src/routes/Layout.tsx new file mode 100644 index 0000000..09935f0 --- /dev/null +++ b/apps/visr/src/routes/Layout.tsx @@ -0,0 +1,15 @@ +import { Link, Outlet, useLocation } from "react-router-dom"; +import VisrNavbar from "../components/VisrNavbar"; +import { Breadcrumbs } from "@diamondlightsource/sci-react-ui"; + +/* A common layout for all routes, consisting of Navbar and breadcrumbs */ +export function Layout() { + const location = useLocation(); + return ( +
+ + + +
+ ); +} diff --git a/apps/visr/src/routes/Plans.tsx b/apps/visr/src/routes/Plans.tsx new file mode 100644 index 0000000..80281d5 --- /dev/null +++ b/apps/visr/src/routes/Plans.tsx @@ -0,0 +1,31 @@ +import { useEffect, useState } from "react"; +import { Box } from "@mui/material"; +import { getPlans, type PlansResponse } from "../utils/api"; +import PlanBrowser from "../components/PlanBrowser/PlanBrowser"; +import PlanParameters from "../components/PlanBrowser/PlanParameters"; + +function JsonFormsPlans() { + const [planData, setPlanData] = useState({ plans: [] }); + + useEffect(() => { + async function fetchPlans() { + const results = await getPlans(); + setPlanData(results); + } + + fetchPlans(); + }, []); + + return ( + <> + + } + /> + + + ); +} + +export default JsonFormsPlans; diff --git a/apps/visr/src/routes/Spectroscopy.tsx b/apps/visr/src/routes/Spectroscopy.tsx new file mode 100644 index 0000000..ffa28fa --- /dev/null +++ b/apps/visr/src/routes/Spectroscopy.tsx @@ -0,0 +1,14 @@ +import { Box } from "@mui/material"; +import SpectroscopyForm from "../components/SpectroscopyForm"; + +function Spectroscopy() { + return ( + <> + + + + + ); +} + +export default Spectroscopy; diff --git a/apps/visr/src/routes/Workflows.tsx b/apps/visr/src/routes/Workflows.tsx new file mode 100644 index 0000000..0501317 --- /dev/null +++ b/apps/visr/src/routes/Workflows.tsx @@ -0,0 +1,25 @@ +import { Suspense } from "react"; +import { Container, Box } from "@mui/material"; +import TemplateView from "../components/TemplateView"; + +const Workflows: React.FC = () => { + return ( + <> + + + + + + + + + ); +}; + +export default Workflows; diff --git a/apps/visr/src/test/global.d.ts b/apps/visr/src/test/global.d.ts new file mode 100644 index 0000000..edfa8b5 --- /dev/null +++ b/apps/visr/src/test/global.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/apps/visr/src/utils/api.ts b/apps/visr/src/utils/api.ts new file mode 100644 index 0000000..734c617 --- /dev/null +++ b/apps/visr/src/utils/api.ts @@ -0,0 +1,79 @@ +export interface Plan { + name: string; + description: string | undefined; + schema: object; +} + +export interface PlansResponse { + plans: Plan[]; +} + +export interface TaskResponse { + task_id: string; +} + +export interface TaskRequest { + name: string; + params: object; + instrument_session: string; +} + +export async function getPlans(): Promise { + const url = "/api/plans"; + + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + headers.append("X-Requested-By", "XMLHttpRequest"); + + const response = await fetch(url, { + method: "GET", + headers: headers, + }); + + return await response.json(); +} + +export async function createAndStartTask( + request: TaskRequest, +): Promise { + const task = await createTask(request); + return await startTask(task.task_id); +} + +export async function createTask(request: TaskRequest): Promise { + const url = "/api/tasks"; + + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + headers.append("X-Requested-By", "XMLHttpRequest"); + + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Error ${response.status}: ${response.statusText}\n${errorText}`, + ); + } + return await response.json(); +} + +export async function startTask(task_id: string): Promise { + const url = "/api/worker/task"; + + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + headers.append("X-Requested-By", "XMLHttpRequest"); + + const response = await fetch(url, { + method: "PUT", + headers: headers, + body: JSON.stringify({ task_id: task_id }), + }); + + return await response.json(); +} diff --git a/apps/visr/src/utils/common.ts b/apps/visr/src/utils/common.ts new file mode 100644 index 0000000..e27649f --- /dev/null +++ b/apps/visr/src/utils/common.ts @@ -0,0 +1,14 @@ +import { type Visit, regexToVisit } from "@diamondlightsource/sci-react-ui"; + +const visitRegex = /^([a-z]{2})([1-9]\d*)-([1-9]\d*)/; +export const visitWithTemplateRegex = new RegExp(`${visitRegex.source}-(.+)$`); + +export function visitTextToVisit(visitid?: string): Visit | null { + if (visitid) { + const parsedVisit = visitRegex.exec(visitid); + if (parsedVisit != null) { + return regexToVisit(parsedVisit); + } + } + return null; +} diff --git a/apps/visr/src/utils/schema.ts b/apps/visr/src/utils/schema.ts new file mode 100644 index 0000000..d23d0d7 --- /dev/null +++ b/apps/visr/src/utils/schema.ts @@ -0,0 +1,25 @@ +const shouldJustBeStrings = [ + "bluesky.protocols.Readable", + "bluesky.protocols.Movable", +]; + +const sanitizeSchema = (schema: object): object => { + const entries = Object.entries(schema); + + const processed = entries.map(([key, value]) => { + if (key == "type" && shouldJustBeStrings.includes(value)) { + return [key, "string"]; + } else if ( + typeof value === "object" && + !Array.isArray(value) && + value !== null + ) { + return [key, sanitizeSchema(value)]; + } else { + return [key, value]; + } + }); + return Object.fromEntries(processed); +}; + +export default sanitizeSchema; diff --git a/apps/visr/src/utils/types.ts b/apps/visr/src/utils/types.ts new file mode 100644 index 0000000..9599e83 --- /dev/null +++ b/apps/visr/src/utils/types.ts @@ -0,0 +1,94 @@ +import type { PayloadError } from "relay-runtime"; + +export type TaskStatus = + | "PENDING" + | "RUNNING" + | "SUCCEEDED" + | "SKIPPED" + | "FAILED" + | "ERROR" + | "OMITTED"; + +export interface Task { + id: string; + name: string; + status: TaskStatus; + depends?: string[]; + artifacts: Artifact[]; + workflow: string; + instrumentSession: Visit; + stepType: string; +} + +export interface Artifact { + name: string; + url: string; + mimeType: string; + parentTask: string; +} + +export interface TaskNode extends Task { + children?: TaskNode[]; +} + +export type WorkflowStatus = + | "Unknown" + | "WorkflowPendingStatus" + | "WorkflowRunningStatus" + | "WorkflowSucceededStatus" + | "WorkflowFailedStatus" + | "WorkflowErroredStatus"; + +export interface Workflow { + name: string; + instrumentSession: Visit; + status: WorkflowStatus; +} + +export interface Visit { + proposalCode: string; + proposalNumber: number; + number: number; +} + +export interface Template { + name: string; + description?: string | null; + title?: string | null; + maintainer: string; + repository?: string | null; +} + +export interface WorkflowStatusBool { + pending?: boolean; + running?: boolean; + succeeded?: boolean; + failed?: boolean; + error?: boolean; +} + +export interface SubmissionSuccessMessage { + type: "success"; + message: string; +} + +export interface SubmissionNetworkErrorMessage { + type: "networkError"; + error: Error; +} + +export interface SubmissionGraphQLErrorMessage { + type: "graphQLError"; + errors: PayloadError[]; +} + +export type JSONValue = + | string + | number + | boolean + | null + | JSONObject + | JSONValue[]; +export interface JSONObject { + [key: string]: JSONValue; +} diff --git a/apps/visr/src/vite-env.d.ts b/apps/visr/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/visr/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/visr/supergraph.graphql b/apps/visr/supergraph.graphql new file mode 100644 index 0000000..eb96ae8 --- /dev/null +++ b/apps/visr/supergraph.graphql @@ -0,0 +1,648 @@ +type Artifact { + """ + The file name of the artifact + """ + name: String! + """ + The download URL for the artifact + """ + url: Url! + """ + The MIME type of the artifact data + """ + mimeType: String! +} + +scalar Creator + +""" +Implement the DateTime scalar + +The input/output is a string in RFC3339 format. +""" +scalar DateTime + +""" +A scalar that can represent any JSON value. +""" +scalar JSON + +""" +A scalar that can represent any JSON Object value. +""" +scalar JSONObject + +""" +A single log line streamed from a pod +""" +type LogEntry { + """ + The log line content + """ + content: String! + """ + The name of the pod producing the log + """ + podName: String! +} + +""" +The root mutation of the service +""" +type Mutation { + submitWorkflowTemplate( + name: String! + visit: VisitInput! + parameters: JSON! + ): Workflow! +} + +""" +Information about pagination in a connection +""" +type PageInfo @shareable { + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String +} + +""" +Supported DLS science groups +""" +enum ScienceGroup { + """ + Macromolecular Crystallography + """ + MX + """ + Workflows Examples + """ + EXAMPLES + """ + Magnetic Materials + """ + MAGNETIC_MATERIALS + """ + Soft Condensed Matter + """ + CONDENSED_MATTER + """ + Imaging and Microscopy + """ + IMAGING + """ + Biological Cryo-Imaging + """ + BIO_CRYO_IMAGING + """ + Structures and Surfaces + """ + SURFACES + """ + Crystallography + """ + CRYSTALLOGRAPHY + """ + Spectroscopy + """ + SPECTROSCOPY +} + +""" +The root mutation of the service +""" +type Subscription { + """ + Processing to subscribe to logs for a single pod of a workflow + """ + logs(visit: VisitInput!, workflowName: String!, taskId: String!): LogEntry! + """ + Processing to subscribe to data for all workflows in a session + """ + workflow(visit: VisitInput!, name: String!): Workflow! +} + +type Task { + """ + Unique name of the task + """ + id: String! + """ + Display name of the task + """ + name: String! + """ + Current status of a task + """ + status: TaskStatus! + """ + Parent of a task + """ + depends: [String!]! + """ + Children of a task + """ + dependencies: [String!]! + """ + Artifacts produced by a task + """ + artifacts: [Artifact!]! + """ + Node type - Pod, DAG, etc + """ + stepType: String! + """ + Start time for a task on a workflow + """ + startTime: DateTime + """ + End time for a task on a workflow + """ + endTime: DateTime + """ + A human readable message indicating details about why this step is in this condition + """ + message: String +} + +enum TaskStatus { + PENDING + RUNNING + SUCCEEDED + SKIPPED + FAILED + ERROR + OMITTED +} + +scalar Template + +""" +URL is a String implementing the [URL Standard](http://url.spec.whatwg.org/) +""" +scalar Url + +""" +A visit to an instrument as part of a session +""" +type Visit { + """ + Project Proposal Code + """ + proposalCode: String! + """ + Project Proposal Number + """ + proposalNumber: Int! + """ + Session visit Number + """ + number: Int! +} + +""" +A visit to an instrument as part of a session +""" +input VisitInput { + """ + Project Proposal Code + """ + proposalCode: String! + """ + Project Proposal Number + """ + proposalNumber: Int! + """ + Session visit Number + """ + number: Int! +} + +type Workflow { + """ + The name given to the workflow, unique within a given visit + """ + name: String! + """ + The visit the Workflow was run against + """ + visit: Visit! + """ + The current status of the workflow + """ + status: WorkflowStatus + """ + The top-level workflow parameters + """ + parameters: JSONObject + """ + The name of the template used to run the workflow + """ + templateRef: String + """ + The workflow creator + """ + creator: WorkflowCreator! +} + +type WorkflowConnection @shareable { + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + """ + A list of edges. + """ + edges: [WorkflowEdge!]! + """ + A list of nodes. + """ + nodes: [Workflow!]! +} + +""" +Information about the creator of a workflow. +""" +type WorkflowCreator { + """ + An identifier unique to the creator of the workflow. + Typically this is the creator's Fed-ID. + """ + creatorId: String! +} + +""" +An edge in a connection. +""" +type WorkflowEdge @shareable { + """ + The item at the end of the edge + """ + node: Workflow! + """ + A cursor for use in pagination + """ + cursor: String! +} + +""" +All tasks in the workflow have errored +""" +type WorkflowErroredStatus { + """ + Time at which this workflow started + """ + startTime: DateTime! + """ + Time at which this workflow completed + """ + endTime: DateTime! + """ + A human readable message indicating details about why the workflow is in this condition + """ + message: String + """ + Tasks created by the workflow + """ + tasks: [Task!]! +} + +""" +All tasks in the workflow have failed +""" +type WorkflowFailedStatus { + """ + Time at which this workflow started + """ + startTime: DateTime! + """ + Time at which this workflow completed + """ + endTime: DateTime! + """ + A human readable message indicating details about why the workflow is in this condition + """ + message: String + """ + Tasks created by the workflow + """ + tasks: [Task!]! +} + +""" +All the supported Workflows filters +""" +input WorkflowFilter { + """ + The status field for a workflow + """ + workflowStatusFilter: WorkflowStatusFilter + """ + The fedid of the user who created the workflow + """ + creator: Creator + """ + The name of the workflow template + """ + template: Template +} + +type WorkflowPendingStatus { + """ + A human readable message indicating details about why the workflow is in this condition + """ + message: String +} + +type WorkflowRunningStatus { + """ + Time at which this workflow started + """ + startTime: DateTime! + """ + A human readable message indicating details about why the workflow is in this condition + """ + message: String + """ + Tasks created by the workflow + """ + tasks: [Task!]! +} + +""" +The status of a workflow +""" +union WorkflowStatus = + | WorkflowPendingStatus + | WorkflowRunningStatus + | WorkflowSucceededStatus + | WorkflowFailedStatus + | WorkflowErroredStatus + +""" +Represents workflow status filters +""" +input WorkflowStatusFilter { + pending: Boolean! = false + running: Boolean! = false + succeeded: Boolean! = false + failed: Boolean! = false + error: Boolean! = false +} + +""" +All tasks in the workflow have succeded +""" +type WorkflowSucceededStatus { + """ + Time at which this workflow started + """ + startTime: DateTime! + """ + Time at which this workflow completed + """ + endTime: DateTime! + """ + A human readable message indicating details about why the workflow is in this condition + """ + message: String + """ + Tasks created by the workflow + """ + tasks: [Task!]! +} + +type WorkflowTemplate { + """ + The name given to the workflow template, globally unique + """ + name: String! + """ + The group who maintains the workflow template + """ + maintainer: String! + """ + A human readable title for the workflow template + """ + title: String + """ + A human readable description of the workflow which is created + """ + description: String + """ + The repository storing the code associated with this template. + """ + repository: String + """ + A JSON Schema describing the arguments of a Workflow Template + """ + arguments: JSON! + """ + A JSON Forms UI Schema describing how to render the arguments of the Workflow Template + """ + uiSchema: JSON +} + +type WorkflowTemplateConnection @shareable { + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + """ + A list of edges. + """ + edges: [WorkflowTemplateEdge!]! + """ + A list of nodes. + """ + nodes: [WorkflowTemplate!]! +} + +""" +An edge in a connection. +""" +type WorkflowTemplateEdge @shareable { + """ + The item at the end of the edge + """ + node: WorkflowTemplate! + """ + A cursor for use in pagination + """ + cursor: String! +} + +""" +Supported label filters for ClusterWorkflowTemplates +""" +input WorkflowTemplatesFilter { + """ + The science group owning the template eg imaging + """ + scienceGroup: [ScienceGroup!] +} + +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +""" +Provides a scalar specification URL for specifying the behavior of custom scalar types. +""" +directive @specifiedBy(url: String!) on SCALAR + +type Account { + accountId: Int! + username: String! + emailAddress: String + title: String + givenName: String + familyName: String + type: AccountType! + state: AccountState! + proposalRoles: [ProposalAccount!]! + instrumentSessionRoles: [InstrumentSessionRole!]! +} + +enum AccountState { + enabled + disabled +} + +enum AccountType { + user + staff + functional +} + +""" +Date with time (isoformat) +""" +scalar DateTime + +type Instrument { + name: String! + scienceGroup: String + description: String + proposals: [Proposal!]! + instrumentSessions: [InstrumentSession!]! +} + +type InstrumentSession { + instrumentSessionId: Int! + instrumentSessionNumber: Int! + startTime: DateTime + endTime: DateTime + type: String + state: String + riskRating: String + proposal: Proposal + instrument: Instrument! + roles: [InstrumentSessionRole!]! +} + +type InstrumentSessionRole { + instrumentSession: InstrumentSession! + account: Account! + role: String! + onSite: Boolean! +} + +type Proposal { + proposalNumber: Int! + proposalCategory: String + title: String + summary: String + state: ProposalState! + instrumentSessions: [InstrumentSession!]! + instruments: [Instrument!]! + roles: [ProposalAccount!]! +} + +type ProposalAccount { + proposal: Proposal! + account: Account! + role: String! +} + +enum ProposalState { + Open + Closed + Cancelled +} + +type Query { + """ + Get a single [`Workflow`] by proposal, visit, and name + """ + workflow(visit: VisitInput!, name: String!): Workflow! + workflows( + visit: VisitInput! + cursor: String + limit: Int + filter: WorkflowFilter + ): WorkflowConnection! + workflowTemplate(name: String!): WorkflowTemplate! + workflowTemplates( + cursor: String + limit: Int + filter: WorkflowTemplatesFilter + ): WorkflowTemplateConnection! + """ + Get a proposal by its number + """ + proposal(proposalNumber: Int!): Proposal + + """ + Get a list of proposals + """ + proposals(proposalCategory: String = null): [Proposal!]! + + """ + Get a instrument session + """ + instrumentSession( + proposalNumber: Int! + instrumentSessionNumber: Int! + ): InstrumentSession + + """ + Get a instrument session + """ + instrumentSessions( + proposalNumber: Int = null + proposalCategory: String = null + ): [InstrumentSession!] + + """ + Get an instrument + """ + instrument(instrumentName: String!): Instrument + + """ + Get a list of instruments + """ + instruments(scienceGroup: String = null): [Instrument!]! + + """ + Get an account + """ + account(username: String!): Account +} diff --git a/apps/visr/tsconfig.json b/apps/visr/tsconfig.json new file mode 100644 index 0000000..8657541 --- /dev/null +++ b/apps/visr/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["vite/client", "@atlas/vitest-conf/global-types"] + }, + "include": ["src"] +} diff --git a/apps/visr/vite.config.ts b/apps/visr/vite.config.ts new file mode 100644 index 0000000..fab2919 --- /dev/null +++ b/apps/visr/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import relay from "vite-plugin-relay"; + +export default defineConfig({ + plugins: [react(), relay], + define: { + global: {}, + }, +}); diff --git a/apps/visr/vitest.config.ts b/apps/visr/vitest.config.ts new file mode 100644 index 0000000..6dbadc2 --- /dev/null +++ b/apps/visr/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vitest/config"; +import baseConfig from "@atlas/vitest-conf/vitest.config"; + +export default defineConfig({ + ...baseConfig, +}); diff --git a/docker/Dockerfile.web-app b/docker/Dockerfile.web-app new file mode 100644 index 0000000..5719602 --- /dev/null +++ b/docker/Dockerfile.web-app @@ -0,0 +1,44 @@ +FROM node:22-alpine AS base + +# Use a consistent working directory across all stages +WORKDIR /app + +# 1) Install dependencies +FROM base AS deps + +ARG APP_NAME + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc* ./ +COPY packages ./packages +COPY apps/${APP_NAME}/package.json ./apps/${APP_NAME}/ + +# Install dependencies +RUN corepack enable pnpm && pnpm i --frozen-lockfile --filter=@atlas/${APP_NAME}... + +# 2) Run the build +FROM base AS builder + +ARG APP_NAME + +# Copy all node_modules from deps stage +COPY --from=deps /app ./ + +# Copy source files +COPY apps/${APP_NAME} ./apps/${APP_NAME} +COPY packages ./packages +COPY turbo.json package.json pnpm-workspace.yaml tsconfig.json ./ +RUN corepack enable pnpm && pnpm --filter=@atlas/${APP_NAME}... build + +# 3) Create minimal image to serve the app +FROM nginxinc/nginx-unprivileged:1.25-alpine AS runner + +ARG APP_NAME + +# Copy built files to nginx web root +COPY --from=builder /app/apps/${APP_NAME}/dist /usr/share/nginx/html + +# Copy your custom nginx config +COPY docker/nginx.conf /etc/nginx/nginx.conf + +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..6eaa276 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,61 @@ +worker_processes auto; # Let Nginx decide based on CPU cores +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging to stdout/stderr is standard for Docker + access_log /dev/stdout; + error_log /dev/stderr warn; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip Compression - Critical for web app performance + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml image/svg+xml; + + # Temp paths for unprivileged user + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA routing: try file, then directory, then fall back to index.html + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store" always; + } + + # Cache hashed assets (JS/CSS/Images) for a long time + # This assumes your build tool uses hashes in filenames + location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 66c1e7e..c4863b6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "sort-package-json": "^3.4.0", "turbo": "^2.5.6", "typescript": "^5.9.2", - "typescript-eslint": "^8.44.1" + "typescript-eslint": "^8.44.1", + "vitest": "^4.0.13" }, "packageManager": "pnpm@10.17.1" } diff --git a/packages/vitest-conf/README.md b/packages/vitest-conf/README.md index a110631..faba088 100644 --- a/packages/vitest-conf/README.md +++ b/packages/vitest-conf/README.md @@ -8,41 +8,45 @@ This package centralises all testing setup — including Vitest configuration, j ## Features - - Shared **Vitest** configuration via `vitest.config.ts` - - Preconfigured **jsdom** environment for React component testing - - Includes **@testing-library/jest-dom** matchers globally - - TypeScript definitions for `expect`, `describe`, etc. - - Central place to update dependencies like `vitest` or `@testing-library/*` +- Shared **Vitest** configuration via `vitest.config.ts` +- Preconfigured **jsdom** environment for React component testing +- Includes **@testing-library/jest-dom** matchers globally +- TypeScript definitions for `expect`, `describe`, etc. +- Central place to update dependencies like `@testing-library/*` - --- +--- + +## Usage - ## Usage +1. add `vitest` and `@atlas/vitest-conf` as dev dependencies in `apps/my-app/package.json`: - 1. add `@atlas/vitest-conf` as a workspace dependency in `apps/my-app/package.json`: ```json "devDependencies": { + "vitest": "*", "@atlas/vitest-conf": "workspace:*" } ``` - 2. Create a `vitest.config.ts` that simply reuses the shared base config: +2. Create a `vitest.config.ts` that simply reuses the shared base config: + ```ts // apps/my-app/vitest.config.ts import { defineConfig } from "vitest/config"; -import baseConfig from "@atlas/vitest-conf"; +import baseConfig from "@atlas/vitest-conf/vitest.config"; export default defineConfig({ ...baseConfig, }); ``` - 3. Include global type definitions in `apps/my-app/tsconfig.json`: +3. Include global type definitions in `apps/my-app/tsconfig.json`: + ```json { - "compilerOptions": { - "types": ["@atlas/vitest-conf/global-types"] - } + "compilerOptions": { + "types": ["@atlas/vitest-conf/global-types"] + } } ``` -4. Write some tests! Vitest will find tests that match `**/*.test.{ts,tsx}`. \ No newline at end of file +4. Write some tests! Vitest will find tests that match `**/*.test.{ts,tsx}`. diff --git a/packages/vitest-conf/package.json b/packages/vitest-conf/package.json index 814ad29..a80d95b 100644 --- a/packages/vitest-conf/package.json +++ b/packages/vitest-conf/package.json @@ -5,6 +5,10 @@ "type": "module", "exports": { ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./vitest.config": { "import": "./dist/vitest.config.js", "types": "./dist/vitest.config.d.ts" }, @@ -16,8 +20,8 @@ "types": "./types/global-types/index.d.ts" } }, - "main": "./dist/vitest.config.js", - "types": "./dist/vitest.config.d.ts", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "files": [ "dist", "types" @@ -30,7 +34,9 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "jsdom": "^26.1.0", - "vitest": "^3.2.4" + "jsdom": "^26.1.0" + }, + "peerDependencies": { + "vitest": "*" } } diff --git a/packages/vitest-conf/src/index.ts b/packages/vitest-conf/src/index.ts new file mode 100644 index 0000000..33195b3 --- /dev/null +++ b/packages/vitest-conf/src/index.ts @@ -0,0 +1,3 @@ +export * from "@testing-library/react"; +export * from "@testing-library/user-event"; +export * from "@testing-library/dom"; diff --git a/packages/vitest-conf/tsconfig.json b/packages/vitest-conf/tsconfig.json index 34849b5..72d7c4c 100644 --- a/packages/vitest-conf/tsconfig.json +++ b/packages/vitest-conf/tsconfig.json @@ -1,12 +1,13 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "allowImportingTsExtensions": false, "declaration": true, - "declarationMap": false, - "emitDeclarationOnly": false, "esModuleInterop": true, - "resolveJsonModule": true, + "noEmit": false, + "outDir": "dist", "rootDir": "src" }, - "include": ["src", "src/globals.d.ts"] + "exclude": ["dist", "node_modules"], + "include": ["src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 94212c2..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,3102 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - react: - specifier: ^18.3.1 - version: 18.3.1 - react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) - devDependencies: - '@eslint/js': - specifier: ^9.36.0 - version: 9.36.0 - '@types/react': - specifier: ^18.3.1 - version: 18.3.24 - '@types/react-dom': - specifier: ^18.3.1 - version: 18.3.7(@types/react@18.3.24) - eslint: - specifier: ^9.36.0 - version: 9.36.0 - prettier: - specifier: ^3.6.2 - version: 3.6.2 - sort-package-json: - specifier: ^3.4.0 - version: 3.4.0 - turbo: - specifier: ^2.5.6 - version: 2.5.6 - typescript: - specifier: ^5.9.2 - version: 5.9.2 - typescript-eslint: - specifier: ^8.44.1 - version: 8.44.1(eslint@9.36.0)(typescript@5.9.2) - - packages/vitest-conf: - devDependencies: - '@testing-library/dom': - specifier: ^10.4.1 - version: 10.4.1 - '@testing-library/jest-dom': - specifier: ^6.6.4 - version: 6.8.0 - '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@testing-library/user-event': - specifier: ^14.6.1 - version: 14.6.1(@testing-library/dom@10.4.1) - jsdom: - specifier: ^26.1.0 - version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - vitest: - specifier: ^3.2.4 - version: 3.2.4(@types/node@24.5.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2)) - -packages: - - '@adobe/css-tools@4.4.4': - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} - engines: {node: '>=6.9.0'} - - '@bundled-es-modules/cookie@2.0.1': - resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} - - '@bundled-es-modules/statuses@1.0.1': - resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} - - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-color-parser@3.1.0': - resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 - - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} - - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.0': - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.3.1': - resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.15.2': - resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.36.0': - resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.3.5': - resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@inquirer/ansi@1.0.0': - resolution: {integrity: sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==} - engines: {node: '>=18'} - - '@inquirer/confirm@5.1.18': - resolution: {integrity: sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/core@10.2.2': - resolution: {integrity: sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/figures@1.0.13': - resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} - engines: {node: '>=18'} - - '@inquirer/type@3.0.8': - resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@mswjs/interceptors@0.39.6': - resolution: {integrity: sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==} - engines: {node: '>=18'} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@open-draft/deferred-promise@2.2.0': - resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} - - '@open-draft/logger@0.3.0': - resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} - - '@open-draft/until@2.1.0': - resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - - '@rollup/rollup-android-arm-eabi@4.52.2': - resolution: {integrity: sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.52.2': - resolution: {integrity: sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.52.2': - resolution: {integrity: sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.52.2': - resolution: {integrity: sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.52.2': - resolution: {integrity: sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.52.2': - resolution: {integrity: sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.52.2': - resolution: {integrity: sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.52.2': - resolution: {integrity: sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.52.2': - resolution: {integrity: sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.52.2': - resolution: {integrity: sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.52.2': - resolution: {integrity: sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.52.2': - resolution: {integrity: sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.52.2': - resolution: {integrity: sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.52.2': - resolution: {integrity: sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.52.2': - resolution: {integrity: sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.52.2': - resolution: {integrity: sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.52.2': - resolution: {integrity: sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openharmony-arm64@4.52.2': - resolution: {integrity: sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.52.2': - resolution: {integrity: sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.52.2': - resolution: {integrity: sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.52.2': - resolution: {integrity: sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.52.2': - resolution: {integrity: sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==} - cpu: [x64] - os: [win32] - - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - - '@testing-library/jest-dom@6.8.0': - resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - - '@types/chai@5.2.2': - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} - - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/node@24.5.2': - resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} - - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} - peerDependencies: - '@types/react': ^18.0.0 - - '@types/react@18.3.24': - resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==} - - '@types/statuses@2.0.6': - resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - - '@typescript-eslint/eslint-plugin@8.44.1': - resolution: {integrity: sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.44.1 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.44.1': - resolution: {integrity: sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.44.1': - resolution: {integrity: sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.44.1': - resolution: {integrity: sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.44.1': - resolution: {integrity: sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.44.1': - resolution: {integrity: sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.44.1': - resolution: {integrity: sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.44.1': - resolution: {integrity: sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.44.1': - resolution: {integrity: sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.44.1': - resolution: {integrity: sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - bufferutil@4.0.9: - resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} - engines: {node: '>=6.14.2'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - - cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-indent@7.0.2: - resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} - engines: {node: '>=12.20'} - - detect-newline@4.0.1: - resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - - dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.36.0: - resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} - engines: {node: '>=12.0.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - git-hooks-list@4.1.1: - resolution: {integrity: sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - graphql@16.11.0: - resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} - - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} - - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-node-process@1.2.0: - resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - - is-number@7.0.0: - 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==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - msw@2.11.3: - resolution: {integrity: sha512-878imp8jxIpfzuzxYfX0qqTq1IFQz/1/RBHs/PyirSjzi+xKM/RRfIpIqHSCWjH0GxidrjhgiiXC+DWXNDvT9w==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - typescript: '>= 4.8.x' - peerDependenciesMeta: - typescript: - optional: true - - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true - - nwsapi@2.2.22: - resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - outvariant@1.4.3: - resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true - - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} - peerDependencies: - react: ^18.3.1 - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} - engines: {node: '>=0.10.0'} - - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - rettime@0.7.0: - resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.52.2: - resolution: {integrity: sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - sort-object-keys@1.1.3: - resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} - - sort-package-json@3.4.0: - resolution: {integrity: sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA==} - engines: {node: '>=20'} - hasBin: true - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} - - strict-event-emitter@0.5.1: - resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - strip-literal@3.0.0: - resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} - - tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} - - tldts-core@7.0.16: - resolution: {integrity: sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==} - - tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} - hasBin: true - - tldts@7.0.16: - resolution: {integrity: sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==} - hasBin: true - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} - engines: {node: '>=16'} - - tough-cookie@6.0.0: - resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} - engines: {node: '>=16'} - - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - turbo-darwin-64@2.5.6: - resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==} - cpu: [x64] - os: [darwin] - - turbo-darwin-arm64@2.5.6: - resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==} - cpu: [arm64] - os: [darwin] - - turbo-linux-64@2.5.6: - resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.5.6: - resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.5.6: - resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.5.6: - resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==} - cpu: [arm64] - os: [win32] - - turbo@2.5.6: - resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==} - hasBin: true - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - typescript-eslint@8.44.1: - resolution: {integrity: sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.12.0: - resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} - - until-async@3.0.2: - resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - utf-8-validate@5.0.10: - resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} - engines: {node: '>=6.14.2'} - - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite@7.1.7: - resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} - engines: {node: '>=18'} - -snapshots: - - '@adobe/css-tools@4.4.4': {} - - '@asamuzakjp/css-color@3.2.0': - dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 10.4.3 - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.27.1 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/helper-validator-identifier@7.27.1': {} - - '@babel/runtime@7.28.4': {} - - '@bundled-es-modules/cookie@2.0.1': - dependencies: - cookie: 0.7.2 - optional: true - - '@bundled-es-modules/statuses@1.0.1': - dependencies: - statuses: 2.0.2 - optional: true - - '@csstools/color-helpers@5.1.0': {} - - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-tokenizer': 3.0.4 - - '@csstools/css-tokenizer@3.0.4': {} - - '@esbuild/aix-ppc64@0.25.10': - optional: true - - '@esbuild/android-arm64@0.25.10': - optional: true - - '@esbuild/android-arm@0.25.10': - optional: true - - '@esbuild/android-x64@0.25.10': - optional: true - - '@esbuild/darwin-arm64@0.25.10': - optional: true - - '@esbuild/darwin-x64@0.25.10': - optional: true - - '@esbuild/freebsd-arm64@0.25.10': - optional: true - - '@esbuild/freebsd-x64@0.25.10': - optional: true - - '@esbuild/linux-arm64@0.25.10': - optional: true - - '@esbuild/linux-arm@0.25.10': - optional: true - - '@esbuild/linux-ia32@0.25.10': - optional: true - - '@esbuild/linux-loong64@0.25.10': - optional: true - - '@esbuild/linux-mips64el@0.25.10': - optional: true - - '@esbuild/linux-ppc64@0.25.10': - optional: true - - '@esbuild/linux-riscv64@0.25.10': - optional: true - - '@esbuild/linux-s390x@0.25.10': - optional: true - - '@esbuild/linux-x64@0.25.10': - optional: true - - '@esbuild/netbsd-arm64@0.25.10': - optional: true - - '@esbuild/netbsd-x64@0.25.10': - optional: true - - '@esbuild/openbsd-arm64@0.25.10': - optional: true - - '@esbuild/openbsd-x64@0.25.10': - optional: true - - '@esbuild/openharmony-arm64@0.25.10': - optional: true - - '@esbuild/sunos-x64@0.25.10': - optional: true - - '@esbuild/win32-arm64@0.25.10': - optional: true - - '@esbuild/win32-ia32@0.25.10': - optional: true - - '@esbuild/win32-x64@0.25.10': - optional: true - - '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0)': - dependencies: - eslint: 9.36.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/config-array@0.21.0': - dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.3.1': {} - - '@eslint/core@0.15.2': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.36.0': {} - - '@eslint/object-schema@2.1.6': {} - - '@eslint/plugin-kit@0.3.5': - dependencies: - '@eslint/core': 0.15.2 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@inquirer/ansi@1.0.0': - optional: true - - '@inquirer/confirm@5.1.18(@types/node@24.5.2)': - dependencies: - '@inquirer/core': 10.2.2(@types/node@24.5.2) - '@inquirer/type': 3.0.8(@types/node@24.5.2) - optionalDependencies: - '@types/node': 24.5.2 - optional: true - - '@inquirer/core@10.2.2(@types/node@24.5.2)': - dependencies: - '@inquirer/ansi': 1.0.0 - '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@24.5.2) - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 24.5.2 - optional: true - - '@inquirer/figures@1.0.13': - optional: true - - '@inquirer/type@3.0.8(@types/node@24.5.2)': - optionalDependencies: - '@types/node': 24.5.2 - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@mswjs/interceptors@0.39.6': - dependencies: - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/logger': 0.3.0 - '@open-draft/until': 2.1.0 - is-node-process: 1.2.0 - outvariant: 1.4.3 - strict-event-emitter: 0.5.1 - optional: true - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - '@open-draft/deferred-promise@2.2.0': - optional: true - - '@open-draft/logger@0.3.0': - dependencies: - is-node-process: 1.2.0 - outvariant: 1.4.3 - optional: true - - '@open-draft/until@2.1.0': - optional: true - - '@rollup/rollup-android-arm-eabi@4.52.2': - optional: true - - '@rollup/rollup-android-arm64@4.52.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.52.2': - optional: true - - '@rollup/rollup-darwin-x64@4.52.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.52.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.52.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.52.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.52.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.52.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.52.2': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.52.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.52.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.52.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.52.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.52.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.52.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.52.2': - optional: true - - '@rollup/rollup-openharmony-arm64@4.52.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.52.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.52.2': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.52.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.52.2': - optional: true - - '@testing-library/dom@10.4.1': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.4 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - '@testing-library/jest-dom@6.8.0': - dependencies: - '@adobe/css-tools': 4.4.4 - aria-query: 5.3.2 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 - redent: 3.0.0 - - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.4 - '@testing-library/dom': 10.4.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.24 - '@types/react-dom': 18.3.7(@types/react@18.3.24) - - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': - dependencies: - '@testing-library/dom': 10.4.1 - - '@types/aria-query@5.0.4': {} - - '@types/chai@5.2.2': - dependencies: - '@types/deep-eql': 4.0.2 - - '@types/cookie@0.6.0': - optional: true - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.8': {} - - '@types/json-schema@7.0.15': {} - - '@types/node@24.5.2': - dependencies: - undici-types: 7.12.0 - optional: true - - '@types/prop-types@15.7.15': {} - - '@types/react-dom@18.3.7(@types/react@18.3.24)': - dependencies: - '@types/react': 18.3.24 - - '@types/react@18.3.24': - dependencies: - '@types/prop-types': 15.7.15 - csstype: 3.1.3 - - '@types/statuses@2.0.6': - optional: true - - '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.44.1(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/scope-manager': 8.44.1 - '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.44.1 - eslint: 9.36.0 - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.44.1(eslint@9.36.0)(typescript@5.9.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.44.1 - '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.44.1 - debug: 4.4.3 - eslint: 9.36.0 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.44.1(typescript@5.9.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2) - '@typescript-eslint/types': 8.44.1 - debug: 4.4.3 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.44.1': - dependencies: - '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/visitor-keys': 8.44.1 - - '@typescript-eslint/tsconfig-utils@8.44.1(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - - '@typescript-eslint/type-utils@8.44.1(eslint@9.36.0)(typescript@5.9.2)': - dependencies: - '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0)(typescript@5.9.2) - debug: 4.4.3 - eslint: 9.36.0 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.44.1': {} - - '@typescript-eslint/typescript-estree@8.44.1(typescript@5.9.2)': - dependencies: - '@typescript-eslint/project-service': 8.44.1(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2) - '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/visitor-keys': 8.44.1 - debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.44.1(eslint@9.36.0)(typescript@5.9.2)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) - '@typescript-eslint/scope-manager': 8.44.1 - '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) - eslint: 9.36.0 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.44.1': - dependencies: - '@typescript-eslint/types': 8.44.1 - eslint-visitor-keys: 4.2.1 - - '@vitest/expect@3.2.4': - dependencies: - '@types/chai': 5.2.2 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.19 - optionalDependencies: - msw: 2.11.3(@types/node@24.5.2)(typescript@5.9.2) - vite: 7.1.7(@types/node@24.5.2) - - '@vitest/pretty-format@3.2.4': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/runner@3.2.4': - dependencies: - '@vitest/utils': 3.2.4 - pathe: 2.0.3 - strip-literal: 3.0.0 - - '@vitest/snapshot@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.19 - pathe: 2.0.3 - - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 - - '@vitest/utils@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - agent-base@7.1.4: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - argparse@2.0.1: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - aria-query@5.3.2: {} - - assertion-error@2.0.1: {} - - balanced-match@1.0.2: {} - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - bufferutil@4.0.9: - dependencies: - node-gyp-build: 4.8.4 - optional: true - - cac@6.7.14: {} - - callsites@3.1.0: {} - - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - check-error@2.1.1: {} - - cli-width@4.1.0: - optional: true - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - optional: true - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - cookie@0.7.2: - optional: true - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css.escape@1.5.1: {} - - cssstyle@4.6.0: - dependencies: - '@asamuzakjp/css-color': 3.2.0 - rrweb-cssom: 0.8.0 - - csstype@3.1.3: {} - - data-urls@5.0.0: - dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decimal.js@10.6.0: {} - - deep-eql@5.0.2: {} - - deep-is@0.1.4: {} - - dequal@2.0.3: {} - - detect-indent@7.0.2: {} - - detect-newline@4.0.1: {} - - dom-accessibility-api@0.5.16: {} - - dom-accessibility-api@0.6.3: {} - - emoji-regex@8.0.0: - optional: true - - entities@6.0.1: {} - - es-module-lexer@1.7.0: {} - - esbuild@0.25.10: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 - - escalade@3.2.0: - optional: true - - escape-string-regexp@4.0.0: {} - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.36.0: - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.1 - '@eslint/core': 0.15.2 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.36.0 - '@eslint/plugin-kit': 0.3.5 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - esutils@2.0.3: {} - - expect-type@1.2.2: {} - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - - fsevents@2.3.3: - optional: true - - get-caller-file@2.0.5: - optional: true - - git-hooks-list@4.1.1: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@14.0.0: {} - - graphemer@1.4.0: {} - - graphql@16.11.0: - optional: true - - has-flag@4.0.0: {} - - headers-polyfill@4.0.3: - optional: true - - html-encoding-sniffer@4.0.0: - dependencies: - whatwg-encoding: 3.1.1 - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - indent-string@4.0.0: {} - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: - optional: true - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-node-process@1.2.0: - optional: true - - is-number@7.0.0: {} - - is-plain-obj@4.1.0: {} - - is-potential-custom-element-name@1.0.1: {} - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-tokens@9.0.1: {} - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): - dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.22 - parse5: 7.3.0 - rrweb-cssom: 0.8.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 5.1.2 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - loupe@3.2.1: {} - - lru-cache@10.4.3: {} - - lz-string@1.5.0: {} - - magic-string@0.30.19: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - min-indent@1.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - ms@2.1.3: {} - - msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2): - dependencies: - '@bundled-es-modules/cookie': 2.0.1 - '@bundled-es-modules/statuses': 1.0.1 - '@inquirer/confirm': 5.1.18(@types/node@24.5.2) - '@mswjs/interceptors': 0.39.6 - '@open-draft/deferred-promise': 2.2.0 - '@types/cookie': 0.6.0 - '@types/statuses': 2.0.6 - graphql: 16.11.0 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.7.0 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 4.41.0 - until-async: 3.0.2 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - '@types/node' - optional: true - - mute-stream@2.0.0: - optional: true - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - node-gyp-build@4.8.4: - optional: true - - nwsapi@2.2.22: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - outvariant@1.4.3: - optional: true - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-to-regexp@6.3.0: - optional: true - - pathe@2.0.3: {} - - pathval@2.0.1: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier@3.6.2: {} - - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - react-dom@18.3.1(react@18.3.1): - dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - - react-is@17.0.2: {} - - react@18.3.1: - dependencies: - loose-envify: 1.4.0 - - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - - require-directory@2.1.1: - optional: true - - resolve-from@4.0.0: {} - - rettime@0.7.0: - optional: true - - reusify@1.1.0: {} - - rollup@4.52.2: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.2 - '@rollup/rollup-android-arm64': 4.52.2 - '@rollup/rollup-darwin-arm64': 4.52.2 - '@rollup/rollup-darwin-x64': 4.52.2 - '@rollup/rollup-freebsd-arm64': 4.52.2 - '@rollup/rollup-freebsd-x64': 4.52.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.2 - '@rollup/rollup-linux-arm-musleabihf': 4.52.2 - '@rollup/rollup-linux-arm64-gnu': 4.52.2 - '@rollup/rollup-linux-arm64-musl': 4.52.2 - '@rollup/rollup-linux-loong64-gnu': 4.52.2 - '@rollup/rollup-linux-ppc64-gnu': 4.52.2 - '@rollup/rollup-linux-riscv64-gnu': 4.52.2 - '@rollup/rollup-linux-riscv64-musl': 4.52.2 - '@rollup/rollup-linux-s390x-gnu': 4.52.2 - '@rollup/rollup-linux-x64-gnu': 4.52.2 - '@rollup/rollup-linux-x64-musl': 4.52.2 - '@rollup/rollup-openharmony-arm64': 4.52.2 - '@rollup/rollup-win32-arm64-msvc': 4.52.2 - '@rollup/rollup-win32-ia32-msvc': 4.52.2 - '@rollup/rollup-win32-x64-gnu': 4.52.2 - '@rollup/rollup-win32-x64-msvc': 4.52.2 - fsevents: 2.3.3 - - rrweb-cssom@0.8.0: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safer-buffer@2.1.2: {} - - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 - - semver@7.7.2: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - siginfo@2.0.0: {} - - signal-exit@4.1.0: - optional: true - - sort-object-keys@1.1.3: {} - - sort-package-json@3.4.0: - dependencies: - detect-indent: 7.0.2 - detect-newline: 4.0.1 - git-hooks-list: 4.1.1 - is-plain-obj: 4.1.0 - semver: 7.7.2 - sort-object-keys: 1.1.3 - tinyglobby: 0.2.15 - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - statuses@2.0.2: - optional: true - - std-env@3.9.0: {} - - strict-event-emitter@0.5.1: - optional: true - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - optional: true - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - optional: true - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - - strip-json-comments@3.1.1: {} - - strip-literal@3.0.0: - dependencies: - js-tokens: 9.0.1 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - symbol-tree@3.2.4: {} - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} - - tldts-core@6.1.86: {} - - tldts-core@7.0.16: - optional: true - - tldts@6.1.86: - dependencies: - tldts-core: 6.1.86 - - tldts@7.0.16: - dependencies: - tldts-core: 7.0.16 - optional: true - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tough-cookie@5.1.2: - dependencies: - tldts: 6.1.86 - - tough-cookie@6.0.0: - dependencies: - tldts: 7.0.16 - optional: true - - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - - ts-api-utils@2.1.0(typescript@5.9.2): - dependencies: - typescript: 5.9.2 - - turbo-darwin-64@2.5.6: - optional: true - - turbo-darwin-arm64@2.5.6: - optional: true - - turbo-linux-64@2.5.6: - optional: true - - turbo-linux-arm64@2.5.6: - optional: true - - turbo-windows-64@2.5.6: - optional: true - - turbo-windows-arm64@2.5.6: - optional: true - - turbo@2.5.6: - optionalDependencies: - turbo-darwin-64: 2.5.6 - turbo-darwin-arm64: 2.5.6 - turbo-linux-64: 2.5.6 - turbo-linux-arm64: 2.5.6 - turbo-windows-64: 2.5.6 - turbo-windows-arm64: 2.5.6 - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@4.41.0: - optional: true - - typescript-eslint@8.44.1(eslint@9.36.0)(typescript@5.9.2): - dependencies: - '@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/parser': 8.44.1(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0)(typescript@5.9.2) - eslint: 9.36.0 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - typescript@5.9.2: {} - - undici-types@7.12.0: - optional: true - - until-async@3.0.2: - optional: true - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - utf-8-validate@5.0.10: - dependencies: - node-gyp-build: 4.8.4 - optional: true - - vite-node@3.2.4(@types/node@24.5.2): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.1.7(@types/node@24.5.2) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@7.1.7(@types/node@24.5.2): - dependencies: - esbuild: 0.25.10 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.52.2 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.5.2 - fsevents: 2.3.3 - - vitest@3.2.4(@types/node@24.5.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2)): - dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.2.2 - magic-string: 0.30.19 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.1.7(@types/node@24.5.2) - vite-node: 3.2.4(@types/node@24.5.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.5.2 - jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - - webidl-conversions@7.0.0: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@14.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - word-wrap@1.2.5: {} - - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - optional: true - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - optional: true - - ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.0.9 - utf-8-validate: 5.0.10 - - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - - y18n@5.0.8: - optional: true - - yargs-parser@21.1.1: - optional: true - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - optional: true - - yocto-queue@0.1.0: {} - - yoctocolors-cjs@2.1.3: - optional: true diff --git a/tsconfig.json b/tsconfig.json index 07bac83..966cae0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,15 @@ { "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "moduleResolution": "bundler", "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, "outDir": "dist", - "strict": true, "skipLibCheck": true, - "noEmit": true, - "baseUrl": "." + "strict": true, + "target": "ES2022", + "verbatimModuleSyntax": true } }