diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e42af27f..a1a7d415 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,6 +89,19 @@ jobs: npm run test:run -- src/testing/tests/TiledComponents.test.tsx npm run test:run -- src/testing/tests/TiledLinePlotMaker.test.tsx npm run test:run -- src/testing/tests/Widget.test.tsx + echo "Running ophyd sim test suites..." + npm run test:run -- src/testing/tests/BeamstopModel.test.tsx + npm run test:run -- src/testing/tests/createFallbackTransport.test.ts + npm run test:run -- src/testing/tests/simCameraSocket.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/dependencyGraph.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/detector.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/hexapod.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/motor.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/shutter.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/signal.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/state.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/transport.test.ts + npm run test:run -- src/testing/tests/ophyd-sim/useOphydPVSocket.integration.test.tsx env: CI: true @@ -206,6 +219,7 @@ jobs: `- ${testStepStatus} **Devices** - Camera, Shutter, DeviceControllerBox, DeviceControllerBoxSimple, TableDeviceController, ControllerAbsoluteMove, ControllerRelativeMove, Hexapod, BeamEnergy`, `- ${testStepStatus} **Services** - QueueServer, SignalMonitorPlots, Experiment`, `- ${testStepStatus} **Misc** - IFrame, GoogleDoc, SelectDropdown, ComponentViewer, ComponentViewerUtils`, + `- ${testStepStatus} **Ophyd Sim** - beamstopModel, createFallbackTransport, simCameraSocket, dependencyGraph, detector, hexapod, motor, shutter, signal, state, transport, useOphydPVSocket`, ``, `**Test Environment:** Node.js 20.x`, `**Linting:** ${lintStatus} ${lintText}${lintSection}`, diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 94ac7f4a..775969a7 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,8 +1,11 @@ import type { Preview } from "@storybook/react"; import "../tailwind.css"; +import { withOphydSim, defaultBeamline } from "../src/lib/ophyd-sim"; + const version = import.meta.env.STORYBOOK_FINCH_VERSION; console.log(`Finch UI version ${version}`); const preview: Preview = { + decorators: [withOphydSim(defaultBeamline)], parameters: { controls: { matchers: { diff --git a/Repo Instructions.txt b/Repo Instructions.txt new file mode 100644 index 00000000..81bc0534 --- /dev/null +++ b/Repo Instructions.txt @@ -0,0 +1,450 @@ +Finch Repository Instructions +============================= + +Project purpose +--------------- +Finch is a Vite + React + TypeScript component library for Bluesky beamline +applications. It provides reusable UI components, beamline-specific controls, +data visualization widgets, queue-server tools, Ophyd WebSocket hooks, Tiled +data-browser wrappers, Storybook documentation, and a local development app. + +The npm package name is `@blueskyproject/finch`. The library build is generated +from `src/index.ts`, while the local demo application starts from +`src/main.tsx` and `src/app/App.tsx`. + + +Top-level structure +------------------- +`.github/workflows/` + GitHub Actions pipelines for tests, Storybook publishing, and npm + publishing. + +`.storybook/` + Storybook configuration. Storybook uses the React/Vite framework, imports + Tailwind styles, serves files from `public/`, and injects the Finch package + version as `STORYBOOK_FINCH_VERSION`. + +`public/` + Static assets used by the demo app and Storybook, including Finch images, + icons, `finch.config.json`, and `mockServiceWorker.js` for MSW-backed + Storybook or test mocks. + +`src/` + Main source tree for the library, demo app, API hooks, components, features, + tests, stories, types, utilities, and assets. + +`index.html` + Vite HTML entry point for the local development app. + +`package.json` + Defines package metadata, npm exports, scripts, dependencies, peer + dependencies, and publishing information. + +`vite.config.ts` + Configures Vite for local development, library builds, declaration-file + generation, backend proxies, path aliases, and Vitest. + +`tailwind.config.js`, `tailwind.css`, `postcss.config.js` + Tailwind and PostCSS setup used by the library, demo app, and Storybook. + +`tsconfig*.json` + TypeScript configuration for app code, node-side config files, and the + overall project. + +`eslint.config.js`, `.prettierrc` + Linting and formatting rules. + +`README.md` + User-facing installation and app setup guide. + +`CONTRIBUTING.md` + Developer setup, environment variable notes, and publish guidance. + + +Source tree structure +--------------------- +`src/index.ts` + Public library entry point. This file imports Finch CSS and re-exports the + components, hooks, helper types, and API utilities that package consumers can + import from `@blueskyproject/finch`. + +`src/main.tsx` + Local Vite app entry point. It renders `src/app/App.tsx` inside + `BrowserRouter`. + +`src/app/` + Local development app and global app styles. + + `App.tsx` + Creates a demo `HubAppLayout` with routes for About, component review, + test content, and documentation. It wraps the app in + `FinchConfigProvider` and `QueryClientProvider`. + + `FinchConfigProvider.tsx` + Defines the shared runtime config context. It accepts optional backend + URLs/API keys, cleans URL values, supplies defaults through helper hooks, + and exposes `useFinchConfig()` / `useOptionalFinchConfig()`. + + `pages/` + Demo-app pages used by `App.tsx`. + +`src/api/` + Backend-facing API clients and hooks. + + `qServer/` + Queue Server REST client, request functions, React Query hooks, types, + and examples. `createQServerApiClient()` creates an Axios instance with + an `ApiKey` authorization header. Request functions call queue, history, + status, plan, device, environment, queue-item, and RunEngine endpoints. + Hooks wrap those calls with TanStack Query queries/mutations and + invalidate related cache keys after mutations. + + `ophyd/` + Ophyd WebSocket hooks and message types. Hooks connect to configured + WebSocket endpoints, subscribe to PVs or devices, process value/meta + updates, expose local device state, and provide setters for write + requests. `useOphydSocket()` is kept for backward compatibility and + delegates to `useOphydPVSocket()`. + +`src/components/` + Reusable UI and beamline components. Important groups include: + + General UI: + Button variants, inputs, sliders, dropdowns, tabs, table, popover, + layout primitives, plots, cards/paper, copy-to-clipboard, iframe, and + Google Doc wrappers. + + App layout: + `HubAppLayout`, `HubHeader`, `HubSidebar`, and `HubMainContent`. The + layout expects route definitions and renders a sidebar, header, and + routed content area. + + Beamline/device controls: + BeamEnergy, Hexapod, Histogram, Shutter, camera components, device + controller boxes, signal monitor plots, relative/absolute move controls, + BeamVis 3D synoptic tools, and ReactEDM. + + QServer: + Queue Server UI container, queue lists, plan submission form, console, + settings panels, popups, hooks, and CSS. `ContainerQServer` composes the + main UI: side panel, running worker, queue/history lists, plan-add panel, + console panel, and settings panel. + + Tiled: + Wrapper components around `@blueskyproject/tiled`, custom scatter and + heatmap components, and hooks such as `useTiledMostRecentDetImage()`. + The wrapper pulls Tiled URL/API key values from Finch config when props + are not passed directly. + + ReactEDM: + Runtime rendering for ADL/BOB-style UI definitions, parsers, mock + context, widgets, style rendering, scalable canvas, and included ADL/BOB + assets. + +`src/features/` + Higher-level feature components that compose lower-level components, such as + `ComponentViewer`, `QServerPlanMonitor`, `TiledHeatmapSelector`, Beamstop, + and Bolt control. + +`src/hooks/` + Shared React hooks, including device message helpers and resize observation. + +`src/utils/` + Shared utilities. `apiUtils.ts` centralizes default backend URLs and config + lookup for Ophyd, Queue Server, and Tiled. Other files handle errors, URL + conversion, and plot processing/generation. + +`src/types/` + Shared TypeScript types for device controllers, navigation route items, and + plots. + +`src/testing/` + Vitest setup and component test suites. Tests use Testing Library and + jsdom/happy-dom-related tooling depending on configuration and dependencies. + +`src/stories/` + Storybook stories and docs. Includes MD/MDX documentation, component + examples, Storybook-specific assets, and ReactEDM documentation. + +`src/assets/` + Shared icons and image assets used by components and the demo app. + +`src/lib/` + Small shared helpers, including `cn()` for class-name merging. + + +Runtime architecture +-------------------- +1. Local development starts with `npm run dev`. +2. Vite serves `index.html`. +3. `src/main.tsx` mounts React into the page and wraps the demo app in + `BrowserRouter`. +4. `src/app/App.tsx` creates route definitions and wraps `HubAppLayout` with: + - `FinchConfigProvider` + - `QueryClientProvider` +5. `FinchConfigProvider` receives environment-driven config values such as + Tiled, Ophyd, Queue Server, and Finch backend URLs. It normalizes the URLs and + makes them available through context. +6. Components and hooks use helpers from `src/utils/apiUtils.ts` to resolve + backend URLs. If no config is provided, they fall back to local defaults: + - Tiled: `http://:8000/api/v1` + - Ophyd: `http://:8001/api/v1` + - Queue Server: `http://:60610/api` + - Queue Server API key: `test` +7. Routed pages render Finch components. Data-oriented components use React + Query, fetch, Axios, or WebSocket hooks to communicate with services. + + +Backend/data flow +----------------- +Queue Server flow: +1. A component calls a hook from `src/api/qServer/hooks.ts`. +2. The hook reads Queue Server URL/API key values through `useQueueServerApiUrls()`. +3. The hook creates an Axios client with `createQServerApiClient()`. +4. Query hooks call request helpers such as `getQueue()`, `getStatus()`, + `getPlansAllowed()`, and `getRunsActive()`. +5. Mutation hooks call request helpers such as `addQueueItem()`, + `executeQueueItem()`, `removeQueueItem()`, `openEnvironment()`, `startRE()`, + `pauseRE()`, `resumeRE()`, and `abortRE()`. +6. Successful mutations invalidate relevant React Query cache keys so queue, + history, status, and run data refresh. + +Ophyd WebSocket flow: +1. A component calls an Ophyd hook with a list of device names or PV names. +2. The hook resolves a WebSocket URL from Finch config or local defaults. +3. On WebSocket open, the hook sends subscription messages for each device/PV. +4. Incoming messages are parsed as JSON and merged into React state. +5. Setter functions send JSON messages back through the socket when users change + device values. +6. Cleanup closes the socket when the component unmounts or subscriptions change. + +Tiled flow: +1. Tiled components read `tiledApiUrl` and `tiledApiKey` from Finch config. +2. `src/components/Tiled/Tiled.tsx` wraps `@blueskyproject/tiled` so Finch + consumers get the Tiled styles and default config automatically. +3. Custom hooks such as `useTiledMostRecentDetImage()` poll Tiled search and + metadata endpoints to find recent detector image runs. +4. Plot components render heatmaps/scatter plots from Tiled or local data. + +Camera/TIFF flow: +1. Camera components use Ophyd-derived WebSocket URLs or explicit props. +2. Hooks manage streaming state, control-panel state, canvas drawing, and TIFF + display. +3. Vite dev proxies can forward local `/api/camera` and `/api/tiff` WebSocket + paths to configured backend socket endpoints. + + +Development commands +-------------------- +Install dependencies: + npm install + +Run the local demo app: + npm run dev + +Run Storybook: + npm run storybook + +Build the library: + npm run build + +Preview a production build: + npm run preview + +Run lint: + npm run lint + +Auto-fix lint issues: + npm run lint:fix + +Check formatting: + npm run format + +Apply formatting: + npm run format:fix + +Run tests in watch mode: + npm test + +Run tests once: + npm run test:run + +Build Storybook: + npm run build-storybook + +Deploy Storybook manually: + npm run deploy-storybook + + +Build and package pipeline +-------------------------- +The library build is configured in `vite.config.ts`. + +1. `npm run build` runs `tsc && vite build`. +2. TypeScript checks the project. +3. Vite builds library output from `src/index.ts`. +4. `vite-plugin-dts` generates declaration files from `src/`. +5. Rollup outputs ES module and UMD bundles: + - `dist/finch.es.js` + - `dist/finch.umd.js` + - `dist/index.d.ts` + - `dist/finch.css` +6. Peer dependencies from `package.json` are externalized so consumers provide + React, React DOM, and React Router. +7. `package.json` exports the root package and CSS entry: + - `@blueskyproject/finch` + - `@blueskyproject/finch/style.css` + + +Vite dev-server proxies +----------------------- +`vite.config.ts` defines local proxy routes for development: + +`/api/qserver` + Proxies REST calls to `VITE_QSERVER_REST` or `http://localhost:60610`. + +`/api/qserver/console` + Proxies WebSocket traffic to `VITE_QSERVER_WS` or the default Queue Server + console socket URL. + +`/api/camera` + Proxies WebSocket traffic to `VITE_CAMERA_WS`. + +`/api/tiff` + Proxies WebSocket traffic to `VITE_TIFF_WS`. + +These proxies help the demo app talk to local services without hard-coding +backend addresses into component code. + + +Environment configuration +------------------------- +Finch commonly uses Vite environment variables in `.env` files. Examples: + + VITE_TILED_API_URL=http://localhost:8000/api/v1 + VITE_TILED_API_KEY=your-tiled-key + VITE_OPHYD_API_URL=http://localhost:8001/api/v1 + VITE_QSERVER_API_URL=http://localhost:60610/api + VITE_QSERVER_API_KEY=test + VITE_FINCH_API_URL=http://localhost:8003/api + +The Vite proxy layer also reads: + + VITE_QSERVER_REST=http://localhost:60610 + VITE_QSERVER_WS=ws://localhost:8001/api/v1/qs-console-socket + VITE_CAMERA_WS=ws://localhost:8001/api/v1/camera-socket + VITE_TIFF_WS=ws://localhost:8002/tiff-socket + +When adding new backend-aware components, prefer reading service addresses +through `FinchConfigProvider` and `src/utils/apiUtils.ts` instead of hard-coding +URLs. + + +Testing pipeline +---------------- +Tests live in `src/testing/tests/` and use Vitest with Testing Library. + +Local one-shot test command: + npm run test:run + +CI test workflow: +1. Runs on pull requests to `main` and `staging`, and pushes to `staging`. +2. Uses Node.js 20.x. +3. Installs dependencies with `npm ci`. +4. Runs lint and formatting checks, capturing output artifacts. +5. Runs the TypeScript/library build. +6. Runs listed component test suites individually for clearer CI output. +7. Uploads test artifacts. +8. On pull requests, posts a test summary comment with lint, format, build, and + test status. + + +Storybook pipeline +------------------ +Storybook source lives in `src/stories/` plus component-adjacent MDX/stories +matched by `.storybook/main.ts`. + +Local Storybook: + npm run storybook + +Build Storybook: + npm run build-storybook + +Publish Storybook: +1. The `Publish Storybook` GitHub Action runs on manual dispatch or push to + `main`. +2. It installs dependencies with `npm ci`. +3. It configures a GitHub Actions git identity and authenticated remote. +4. It runs `npm run deploy-storybook`. +5. The deploy script builds Storybook and publishes `storybook-static` to the + `gh-pages` branch. +6. The public docs site is served at `https://blueskyproject.io/finch/`. + + +npm publishing pipeline +----------------------- +Manual publishing guidance is in `CONTRIBUTING.md`, and automated release +publishing is configured in `.github/workflows/publish-npm.yml`. + +Automated workflow: +1. Runs on manual dispatch and published GitHub releases. +2. Installs dependencies with `npm ci`. +3. Runs lint, tests, and build when present. +4. Runs `npm pack --dry-run` to preview package contents. +5. Uploads `dist/` as an artifact. +6. On published releases, downloads `dist/`, installs dependencies, pins npm to + a known version, and publishes with provenance: + npm publish --access public --provenance + +Manual workflow: +1. Ensure the working tree is clean. +2. Increment the version, for example: + npm version patch +3. Build: + npm run build +4. Publish: + npm publish + + +How to add or change functionality +---------------------------------- +Adding a public component: +1. Add the component under `src/components/` or a domain-specific subfolder. +2. Add props/types near the component or in `src/types/` if shared. +3. Add or update tests in `src/testing/tests/`. +4. Add a Storybook story or docs page in `src/stories/` when useful. +5. Export the component and public types from `src/index.ts`. + +Adding backend functionality: +1. Put raw API calls in `src/api//requests.ts` or a similar service + module. +2. Put shared service types in `src/api//types.ts`. +3. Wrap calls in hooks when React state, caching, or mutations are needed. +4. Use `src/utils/apiUtils.ts` and `FinchConfigProvider` for URLs/API keys. +5. Invalidate React Query keys after mutations that change backend state. + +Adding a demo page: +1. Create a page under `src/app/pages/`. +2. Import it in `src/app/App.tsx`. +3. Add a `RouteItem` entry with path, label, element, and optional icon. + +Adding Storybook docs: +1. Add `.stories.tsx`, `.stories.ts`, `.md`, or `.mdx` files under `src/stories/` + or another `src/` location matched by Storybook. +2. Use `npm run storybook` to verify locally. +3. Use `npm run build-storybook` before publishing or opening a docs PR. + + +Important conventions +--------------------- +- Use TypeScript for source files. +- Keep public exports centralized in `src/index.ts`. +- Use the `@/` alias for imports rooted at `src/` when it matches existing + style. +- Prefer shared URL helpers in `src/utils/apiUtils.ts`. +- Keep backend-aware components configurable through props and + `FinchConfigProvider`. +- Import `@blueskyproject/finch/style.css` once in consumer apps. +- Keep tests focused on public behavior. +- Do not rely on generated `dist/` files during local development; source files + under `src/` are the source of truth. diff --git a/package-lock.json b/package-lock.json index 79647460..5f431fae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -199,6 +199,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -522,7 +523,6 @@ "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", "dev": true, - "peer": true, "dependencies": { "commander": "^2.15.1" }, @@ -534,8 +534,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@chromatic-com/storybook": { "version": "3.2.3", @@ -664,6 +663,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -702,6 +702,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1122,11 +1123,10 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dev": true, - "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -1162,52 +1162,24 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" + "minimatch": "^3.1.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1216,20 +1188,19 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, - "license": "MIT", "dependencies": { - "ajv": "^6.14.0", + "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1244,7 +1215,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -1252,50 +1222,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1406,11 +1356,10 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -1629,7 +1578,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1654,7 +1602,6 @@ "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", "dev": true, - "peer": true, "dependencies": { "get-stream": "^6.0.1", "minimist": "^1.2.6" @@ -1667,15 +1614,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", "dev": true, - "peer": true, "engines": { "node": ">= 0.6" } @@ -1685,7 +1630,6 @@ "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", "dev": true, - "peer": true, "peerDependencies": { "mapbox-gl": ">=0.32.1 <2.0.0" } @@ -1694,29 +1638,25 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@mapbox/tiny-sdf": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@mapbox/unitbezier": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@mapbox/vector-tile": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", "dev": true, - "peer": true, "dependencies": { "@mapbox/point-geometry": "~0.1.0" } @@ -1726,7 +1666,6 @@ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", "dev": true, - "peer": true, "engines": { "node": ">=6.0.0" } @@ -1736,7 +1675,6 @@ "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", "dev": true, - "peer": true, "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", @@ -1756,22 +1694,19 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", + "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/mdx": "^2.0.0" }, @@ -1785,48 +1720,72 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.58.8", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.58.8.tgz", - "integrity": "sha512-Y45rdEvZodD1WBAK9w8Wvqj7k/6z21YOEP8aVNWv1vemEzanjThvCowc3Eyt/bmJJyqI4gj0BQr9nLC51fsDiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/api-extractor-model": "7.33.8", - "@microsoft/tsdoc": "~0.16.0", - "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.23.1", - "@rushstack/rig-package": "0.7.3", - "@rushstack/terminal": "0.24.0", - "@rushstack/ts-command-line": "5.3.9", - "diff": "~8.0.2", - "minimatch": "10.2.3", + "version": "7.50.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.50.0.tgz", + "integrity": "sha512-Ds/PHTiVzuENQsmXrJKkSdfgNkr/SDG/2rDef0AWl3BchAnXdO7gXaYsAkNx4gWiC4OngNA3fQfd3+BcQxP1DQ==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.30.3", + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.11.0", + "@rushstack/rig-package": "0.5.3", + "@rushstack/terminal": "0.15.0", + "@rushstack/ts-command-line": "4.23.5", + "lodash": "~4.17.15", + "minimatch": "~3.0.3", "resolve": "~1.22.1", - "semver": "~7.7.4", + "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "5.9.3" + "typescript": "5.7.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.33.8", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.8.tgz", - "integrity": "sha512-aIcoQggPyer3B6Ze3usz0YWC/oBwUHfRH5ETUsr+oT2BRA6SfTJl7IKPcPZkX4UR+PohowzW4uMxsvjrn8vm+w==", + "version": "7.30.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.3.tgz", + "integrity": "sha512-yEAvq0F78MmStXdqz9TTT4PZ05Xu5R8nqgwI5xmUmQjWBQ9E6R2n8HB/iZMRciG4rf9iwI2mtuQwIzDXBvHn1w==", "dev": true, - "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "~0.16.0", - "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.23.1" + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.11.0" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/@microsoft/api-extractor/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" }, @@ -1835,11 +1794,10 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1848,37 +1806,40 @@ "node": ">=14.17" } }, + "node_modules/@microsoft/api-extractor/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@microsoft/tsdoc": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", - "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", - "dev": true, - "license": "MIT" + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "dev": true }, "node_modules/@microsoft/tsdoc-config": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.1.tgz", - "integrity": "sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz", + "integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==", "dev": true, - "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "0.16.0", - "ajv": "~8.18.0", + "@microsoft/tsdoc": "0.15.1", + "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, - "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", + "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" }, "funding": { "type": "github", @@ -1889,8 +1850,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@mswjs/interceptors": { "version": "0.37.6", @@ -1989,15 +1949,13 @@ "version": "3.8.2", "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.2.tgz", "integrity": "sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@plotly/d3-sankey": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", "dev": true, - "peer": true, "dependencies": { "d3-array": "1", "d3-collection": "1", @@ -2009,7 +1967,6 @@ "resolved": "https://registry.npmjs.org/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", "dev": true, - "peer": true, "dependencies": { "d3-array": "^1.2.1", "d3-collection": "^1.0.4", @@ -2022,8 +1979,7 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@plotly/d3-sankey-circular/node_modules/d3-shape": { "version": "1.3.7", @@ -2031,7 +1987,6 @@ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "d3-path": "1" } @@ -2041,8 +1996,7 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@plotly/d3-sankey/node_modules/d3-shape": { "version": "1.3.7", @@ -2050,7 +2004,6 @@ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "d3-path": "1" } @@ -2060,7 +2013,6 @@ "resolved": "https://registry.npmjs.org/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz", "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==", "dev": true, - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/geojson-types": "^1.0.2", @@ -2094,7 +2046,6 @@ "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", "dev": true, - "peer": true, "dependencies": { "array-bounds": "^1.0.1", "binary-search-bounds": "^2.0.4", @@ -2946,7 +2897,6 @@ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", "license": "MIT", - "peer": true, "dependencies": { "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" @@ -2960,7 +2910,6 @@ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", "license": "MIT", - "peer": true, "dependencies": { "@react-spring/animated": "~9.7.5", "@react-spring/shared": "~9.7.5", @@ -2978,15 +2927,13 @@ "version": "9.7.5", "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@react-spring/shared": { "version": "9.7.5", "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", "license": "MIT", - "peer": true, "dependencies": { "@react-spring/rafz": "~9.7.5", "@react-spring/types": "~9.7.5" @@ -2999,8 +2946,7 @@ "version": "9.7.5", "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@react-spring/web": { "version": "9.7.5", @@ -3020,10 +2966,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", - "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", - "license": "MIT", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "engines": { "node": ">=14.0.0" } @@ -3051,11 +2996,10 @@ } }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -3064,9 +3008,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", - "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", + "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", "cpu": [ "arm" ], @@ -3078,9 +3022,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", - "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", + "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", "cpu": [ "arm64" ], @@ -3092,9 +3036,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", - "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", + "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", "cpu": [ "arm64" ], @@ -3106,9 +3050,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", - "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", + "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", "cpu": [ "x64" ], @@ -3120,9 +3064,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", - "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", + "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", "cpu": [ "arm64" ], @@ -3134,9 +3078,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", - "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", + "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", "cpu": [ "x64" ], @@ -3148,9 +3092,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", - "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", + "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", "cpu": [ "arm" ], @@ -3162,9 +3106,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", - "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", + "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", "cpu": [ "arm" ], @@ -3176,9 +3120,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", - "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", + "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", "cpu": [ "arm64" ], @@ -3190,9 +3134,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", - "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", + "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", "cpu": [ "arm64" ], @@ -3203,24 +3147,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", - "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", - "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", + "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", "cpu": [ "loong64" ], @@ -3231,24 +3161,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", - "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", - "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", + "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", "cpu": [ "ppc64" ], @@ -3260,9 +3176,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", - "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", + "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", "cpu": [ "riscv64" ], @@ -3274,9 +3190,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", - "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", + "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", "cpu": [ "riscv64" ], @@ -3288,9 +3204,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", - "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", + "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", "cpu": [ "s390x" ], @@ -3302,9 +3218,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", - "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", + "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", "cpu": [ "x64" ], @@ -3316,9 +3232,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", - "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", + "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", "cpu": [ "x64" ], @@ -3329,38 +3245,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", - "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", - "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", - "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", + "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", "cpu": [ "arm64" ], @@ -3372,9 +3260,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", - "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", + "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", "cpu": [ "ia32" ], @@ -3385,24 +3273,10 @@ "win32" ] }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", - "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", - "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", + "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", "cpu": [ "x64" ], @@ -3414,20 +3288,19 @@ ] }, "node_modules/@rushstack/node-core-library": { - "version": "5.23.1", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.23.1.tgz", - "integrity": "sha512-wlKmIKIYCKuCASbITvOxLZXepPbwXvrv7S6ig6XNWFchSyhL/E2txmVXspHY49Wu2dzf7nI27a2k/yV5BA3EiA==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.11.0.tgz", + "integrity": "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ==", "dev": true, - "license": "MIT", "dependencies": { - "ajv": "~8.18.0", + "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", - "semver": "~7.7.4" + "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" @@ -3439,16 +3312,16 @@ } }, "node_modules/@rushstack/node-core-library/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -3460,7 +3333,6 @@ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, - "license": "MIT", "peerDependencies": { "ajv": "^8.5.0" }, @@ -3470,19 +3342,49 @@ } } }, + "node_modules/@rushstack/node-core-library/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@rushstack/node-core-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, - "license": "MIT" + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/@rushstack/node-core-library/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" }, @@ -3490,41 +3392,29 @@ "node": ">=10" } }, - "node_modules/@rushstack/problem-matcher": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.2.1.tgz", - "integrity": "sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } + "node_modules/@rushstack/node-core-library/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/@rushstack/rig-package": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.3.tgz", - "integrity": "sha512-aAA518n6wxxjCfnTAOjQnm7ngNE0FVHxHAw2pxKlIhxrMn0XQjGcXKF0oKWpjBgJOmsaJpVob/v+zr3zxgPWuA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz", + "integrity": "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==", "dev": true, - "license": "MIT", "dependencies": { - "jju": "~1.4.0", - "resolve": "~1.22.1" + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" } }, "node_modules/@rushstack/terminal": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.24.0.tgz", - "integrity": "sha512-8ZQS4MMaGsv27EXCBiH7WMPkRZrffeDoIevs6z9TM5dzqiY6+Hn4evfK/G+gvgBTjfvfkHIZPQQmalmI2sM4TQ==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.0.tgz", + "integrity": "sha512-vXQPRQ+vJJn4GVqxkwRe+UGgzNxdV8xuJZY2zem46Y0p3tlahucH9/hPmLGj2i9dQnUBFiRnoM9/KW7PYw8F4Q==", "dev": true, - "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.23.1", - "@rushstack/problem-matcher": "0.2.1", + "@rushstack/node-core-library": "5.11.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -3541,7 +3431,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3553,13 +3442,12 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.9.tgz", - "integrity": "sha512-GIHqU+sRGQ3LGWAZu1O+9Yh++qwtyNIIGuNbcWHJjBTm2qRez0cwINUHZ+pQLR8UuzZDcMajrDaNbUYoaL/XtQ==", + "version": "4.23.5", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.5.tgz", + "integrity": "sha512-jg70HfoK44KfSP3MTiL5rxsZH7X1ktX3cZs9Sl8eDu1/LxJSbPsh0MOFRC710lIuYYSgxWjI5AjbCBAl7u3RxA==", "dev": true, - "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.24.0", + "@rushstack/terminal": "0.15.0", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -3570,7 +3458,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -3579,15 +3466,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@storybook/addon-actions": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.6.18.tgz", - "integrity": "sha512-GcYhtE91GjIQTuZlwpTJ8jfMp6NC79nkpe1DGe0eetTpyQqLq1WUt+ACkk0Z5lqq2u8HBc09zCCGw+D8iCLpYQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.7.tgz", + "integrity": "sha512-mjtD5JxcPuW74T6h7nqMxWTvDneFtokg88p6kQ5OnC1M259iAXb//yiSZgu/quunMHPCXSiqn4FNOSgASTSbsA==", "dev": true, - "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", @@ -3600,15 +3485,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-backgrounds": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.6.18.tgz", - "integrity": "sha512-froND3WwvSCYzjEBO8QODStaWNL+aGXqxBEbrMnGYejDFST4qEFkvM2IYWMnLBkRgrgJ0yIqTeDQoyH9b9/8uQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.4.7.tgz", + "integrity": "sha512-I4/aErqtFiazcoWyKafOAm3bLpxTj6eQuH/woSbk1Yx+EzN+Dbrgx1Updy8//bsNtKkcrXETITreqHC+a57DHQ==", "dev": true, - "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3", @@ -3619,15 +3503,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-controls": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.6.18.tgz", - "integrity": "sha512-K09dHDCfGW3cudsfuyfu0Yi49aZ2h7VYK4IXDGo1sfmtzVh4xd3HrZQQMVUeKLcfDP/NnJowT+fLVwg04CLrxQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.4.7.tgz", + "integrity": "sha512-377uo5IsJgXLnQLJixa47+11V+7Wn9KcDEw+96aGCBCfLbWNH8S08tJHHnSu+jXg9zoqCAC23MetntVp6LetHA==", "dev": true, - "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "dequal": "^2.0.2", @@ -3638,22 +3521,21 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-docs": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.6.18.tgz", - "integrity": "sha512-55ADer0yNmmeR928Y3UAv3r4i7bJSd9LwywsQ+lRol/FNe0ZcwLEz31xL+jVsqQFNnDh/imsDIp8aYapGMtfEQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.4.7.tgz", + "integrity": "sha512-NwWaiTDT5puCBSUOVuf6ME7Zsbwz7Y79WF5tMZBx/sLQ60vpmJVQsap6NSjvK1Ravhc21EsIXqemAcBjAWu80w==", "dev": true, - "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/blocks": "8.6.18", - "@storybook/csf-plugin": "8.6.18", - "@storybook/react-dom-shim": "8.6.18", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "@storybook/blocks": "8.4.7", + "@storybook/csf-plugin": "8.4.7", + "@storybook/react-dom-shim": "8.4.7", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "ts-dedent": "^2.0.0" }, "funding": { @@ -3661,58 +3543,24 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" - } - }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.6.18.tgz", - "integrity": "sha512-x1ioz/L0CwaelCkHci3P31YtvwayN3FBftvwQOPbvRh9qeb4Cpz5IdVDmyvSxxYwXN66uAORNoqgjTi7B4/y5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unplugin": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.6.18" - } - }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.6.18.tgz", - "integrity": "sha512-N4xULcAWZQTUv4jy1/d346Tyb4gufuC3UaLCuU/iVSZ1brYF4OW3ANr+096btbMxY8pR/65lmtoqr5CTGwnBvA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-essentials": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.6.18.tgz", - "integrity": "sha512-MmH7gFb8pyfRoAth0w2RW8j7mBaEJbEWGP3juIoH03ZqTGmbMUbJXElCuRgxQhve7pyz39zLsgtE78D7G+76ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/addon-actions": "8.6.18", - "@storybook/addon-backgrounds": "8.6.18", - "@storybook/addon-controls": "8.6.18", - "@storybook/addon-docs": "8.6.18", - "@storybook/addon-highlight": "8.6.18", - "@storybook/addon-measure": "8.6.18", - "@storybook/addon-outline": "8.6.18", - "@storybook/addon-toolbars": "8.6.18", - "@storybook/addon-viewport": "8.6.18", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.4.7.tgz", + "integrity": "sha512-+BtZHCBrYtQKILtejKxh0CDRGIgTl9PumfBOKRaihYb4FX1IjSAxoV/oo/IfEjlkF5f87vouShWsRa8EUauFDw==", + "dev": true, + "dependencies": { + "@storybook/addon-actions": "8.4.7", + "@storybook/addon-backgrounds": "8.4.7", + "@storybook/addon-controls": "8.4.7", + "@storybook/addon-docs": "8.4.7", + "@storybook/addon-highlight": "8.4.7", + "@storybook/addon-measure": "8.4.7", + "@storybook/addon-outline": "8.4.7", + "@storybook/addon-toolbars": "8.4.7", + "@storybook/addon-viewport": "8.4.7", "ts-dedent": "^2.0.0" }, "funding": { @@ -3720,15 +3568,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-highlight": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.6.18.tgz", - "integrity": "sha512-wTFJ1DPM0C8gK6nGTJxH75byayQj7BPAz02fME4AOmT6clrBpVl1zSTFTkXaSr+k4xOfeMR/xNUfVskaXz6T9w==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.4.7.tgz", + "integrity": "sha512-whQIDBd3PfVwcUCrRXvCUHWClXe9mQ7XkTPCdPo4B/tZ6Z9c6zD8JUHT76ddyHivixFLowMnA8PxMU6kCMAiNw==", "dev": true, - "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0" }, @@ -3737,7 +3584,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-interactions": { @@ -3761,11 +3608,10 @@ } }, "node_modules/@storybook/addon-measure": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.6.18.tgz", - "integrity": "sha512-fMEOJXgPrTm6qHlWoRM+WTLE7Mr1QBIf2ei+pujBQFcWkD6Gjc2pV8zKzvh93d+EA13wD8AmwOq1DEw9J+XH+g==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.4.7.tgz", + "integrity": "sha512-QfvqYWDSI5F68mKvafEmZic3SMiK7zZM8VA0kTXx55hF/+vx61Mm0HccApUT96xCXIgmwQwDvn9gS4TkX81Dmw==", "dev": true, - "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "tiny-invariant": "^1.3.1" @@ -3775,15 +3621,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-outline": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.6.18.tgz", - "integrity": "sha512-TErFqfCtlV2xt9B6/kskROt69TPjr6AXdHpMselaRrN1X4WEjcMk9GT9PcNP7FXqL88/VYqUb3uNMiAmpDmS/g==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.4.7.tgz", + "integrity": "sha512-6LYRqUZxSodmAIl8icr585Oi8pmzbZ90aloZJIpve+dBAzo7ydYrSQxxoQEVltXbKf3VeVcrs64ouAYqjisMYA==", "dev": true, - "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" @@ -3793,29 +3638,27 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-toolbars": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.6.18.tgz", - "integrity": "sha512-x037KXCEcNfPISGX485DtiP+8Bw/cOT45plcQa8eiAQVrVcUwYaDoLubE9YV5b5CsSAjX8sDviGTme6ALfq7+w==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.4.7.tgz", + "integrity": "sha512-OSfdv5UZs+NdGB+nZmbafGUWimiweJ/56gShlw8Neo/4jOJl1R3rnRqqY7MYx8E4GwoX+i3GF5C3iWFNQqlDcw==", "dev": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-viewport": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.6.18.tgz", - "integrity": "sha512-z9sDJSkuWQb4BP+Z1+H+y/Q0rFbPSDcw+OBBEhMfRcJPPXavdC2pNQ0GdQNVw+tDwhAXj+U7jehKnMDKaP7TyA==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.4.7.tgz", + "integrity": "sha512-hvczh/jjuXXcOogih09a663sRDDSATXwbE866al1DXgbDFraYD/LxX/QDb38W9hdjU9+Qhx8VFIcNWoMQns5HQ==", "dev": true, - "license": "MIT", "dependencies": { "memoizerific": "^1.11.3" }, @@ -3824,7 +3667,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.6.18" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addons": { @@ -3911,12 +3754,12 @@ } }, "node_modules/@storybook/blocks": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.6.18.tgz", - "integrity": "sha512-esZv4msPQ9LxgTb8YUIZhhxVMuI6BPi5bkXtk8c7w7sWuAsqsCe/RnVInn7ooUry2gjnD4hd9+8Eqj0b8oTVoA==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.7.tgz", + "integrity": "sha512-+QH7+JwXXXIyP3fRCxz/7E2VZepAanXJM7G8nbR3wWsqWgrRp4Wra6MvybxAYCxU7aNfJX5c+RW84SNikFpcIA==", "dev": true, - "license": "MIT", "dependencies": { + "@storybook/csf": "^0.1.11", "@storybook/icons": "^1.2.12", "ts-dedent": "^2.0.0" }, @@ -3925,9 +3768,9 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^8.6.18" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7" }, "peerDependenciesMeta": { "react": { @@ -4000,12 +3843,12 @@ } }, "node_modules/@storybook/core": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.6.18.tgz", - "integrity": "sha512-dRBP2TnX6fGdS0T2mXBHjkS/3Nlu1ra1huovZVFuM67CYMzrhM/3hX/zru1vWSC5rqY93ZaAhjMciPW4pK5mMQ==", + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.6.14.tgz", + "integrity": "sha512-1P/w4FSNRqP8j3JQBOi3yGt8PVOgSRbP66Ok520T78eJBeqx9ukCfl912PQZ7SPbW3TIunBwLXMZOjZwBB/JmA==", "license": "MIT", "dependencies": { - "@storybook/theming": "8.6.18", + "@storybook/theming": "8.6.14", "better-opn": "^3.0.2", "browser-assert": "^1.2.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", @@ -4043,9 +3886,9 @@ } }, "node_modules/@storybook/core/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4323,10 +4166,9 @@ "dev": true }, "node_modules/@storybook/theming": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.18.tgz", - "integrity": "sha512-n6OEjEtHupa2PdTwWzRepr7cO8NkDd4rgF6BKLitRbujOspLxzMBEqdphs+QLcuiCIgf33SqmEA64QWnbSMhPw==", - "license": "MIT", + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.14.tgz", + "integrity": "sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" @@ -4379,6 +4221,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4463,7 +4306,6 @@ "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.2.0.tgz", "integrity": "sha512-zuTTdQ4eoTI9nSSjerIy4QwgvxqwJVciQJ8tOPuMHbXJ9N/dNjI7bU8tasjhxas/Cx3NE9NxVHtNpYHL0FSzoA==", "dev": true, - "peer": true, "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", @@ -4479,7 +4321,6 @@ "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.2.0.tgz", "integrity": "sha512-wzHEjCXlYZiDludDbXkpBSmv8Zu6tPGLmJ1sXQ6qDwpLE1Ew3mcWqt8AaxfTP5QwDNQa3sf2vvgTEzNbPQkCiA==", "dev": true, - "peer": true, "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", @@ -4495,7 +4336,6 @@ "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-7.2.0.tgz", "integrity": "sha512-yJqDSw25T7P48au5KjvYqbDVZ7qVnipziVfZ9aSo7P2/jTE7d4BP21w0/XLi3T/9bry/t9PR1GDDDQljN4KfDw==", "dev": true, - "peer": true, "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", @@ -4511,7 +4351,6 @@ "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.2.0.tgz", "integrity": "sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==", "dev": true, - "peer": true, "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" @@ -4525,7 +4364,6 @@ "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.2.0.tgz", "integrity": "sha512-igzTdHsQc8TV1RhPuOLVo74Px/hyPrVgVOTgjWQZzt3J9BVseCdpfY/0cJBdlSRI4S/yTmmHl7gAqjhpYH5Yaw==", "dev": true, - "peer": true, "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10" @@ -4545,8 +4383,7 @@ "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/aria-query": { "version": "5.0.4", @@ -4605,7 +4442,6 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, - "license": "MIT", "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -4721,8 +4557,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/doctrine": { "version": "0.0.9", @@ -4730,10 +4565,30 @@ "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", "dev": true }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -4769,7 +4624,6 @@ "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", "dev": true, - "peer": true, "dependencies": { "@types/geojson": "*" } @@ -4783,8 +4637,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/lodash": { "version": "4.17.23", @@ -4796,15 +4649,13 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/mapbox__vector-tile": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", "dev": true, - "peer": true, "dependencies": { "@types/geojson": "*", "@types/mapbox__point-geometry": "*", @@ -4812,11 +4663,10 @@ } }, "node_modules/@types/mdx": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", - "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", - "dev": true, - "license": "MIT" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true }, "node_modules/@types/mime": { "version": "1.3.5", @@ -4835,8 +4685,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/plotly.js": { "version": "2.35.1", @@ -4929,7 +4778,6 @@ "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", "dev": true, - "peer": true, "dependencies": { "@types/geojson": "*" } @@ -4960,8 +4808,7 @@ "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/webxr": { "version": "0.5.22", @@ -5019,6 +4866,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz", "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.18.1", "@typescript-eslint/types": "8.18.1", @@ -5118,9 +4966,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5128,13 +4976,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5771,13 +5618,12 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.8", + "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -5786,7 +5632,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -5798,11 +5644,10 @@ } }, "node_modules/@vitest/mocker/node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, - "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } @@ -5812,7 +5657,6 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -5830,13 +5674,12 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.8", + "@vitest/utils": "4.0.18", "pathe": "^2.0.3" }, "funding": { @@ -5844,52 +5687,46 @@ } }, "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, - "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner/node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -5898,39 +5735,22 @@ } }, "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -6045,9 +5865,9 @@ } }, "node_modules/@vue/language-core/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6055,13 +5875,12 @@ } }, "node_modules/@vue/language-core/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6081,7 +5900,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -6091,29 +5909,25 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6124,15 +5938,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6145,7 +5957,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -6155,7 +5966,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -6164,15 +5974,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6189,7 +5997,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -6203,7 +6010,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6216,7 +6022,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6231,7 +6036,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -6248,29 +6052,26 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/abs-svg-path": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, - "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6278,26 +6079,11 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -6312,11 +6098,11 @@ } }, "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6329,11 +6115,10 @@ } }, "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -6347,11 +6132,10 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6367,8 +6151,16 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT" + "peerDependencies": { + "ajv": "^6.9.1" + } }, "node_modules/alien-signals": { "version": "0.4.14", @@ -6380,8 +6172,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/almost-equal/-/almost-equal-1.1.0.tgz", "integrity": "sha512-0V/PkoculFl5+0Lp47JoxUcO0xSxhIBvm+BxHdD/OgXNmdRpRHCFnKVuUoWyS9EzQP+otSGv0m9Lb4yVkQBn2A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/ansi-escapes": { "version": "4.3.2", @@ -6461,8 +6252,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/aria-hidden": { "version": "1.2.6", @@ -6489,15 +6279,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-bounds/-/array-bounds-1.0.1.tgz", "integrity": "sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6507,7 +6295,6 @@ "resolved": "https://registry.npmjs.org/array-normalize/-/array-normalize-1.1.4.tgz", "integrity": "sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==", "dev": true, - "peer": true, "dependencies": { "array-bounds": "^1.0.0" } @@ -6516,15 +6303,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-range/-/array-range-1.0.1.tgz", "integrity": "sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/array-rearrange": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/array-rearrange/-/array-rearrange-2.2.2.tgz", "integrity": "sha512-UfobP5N12Qm4Qu4fwLDIi2v6+wZsSf6snYSxAMeKhrh37YGnNWZPRmVEKc/2wfms53TLQnzfpG8wCx2Y/6NG1w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/array-union": { "version": "2.1.0", @@ -6622,9 +6407,9 @@ } }, "node_modules/axios": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", - "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "dev": true, "license": "MIT", "dependencies": { @@ -6671,24 +6456,10 @@ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "dev": true, - "peer": true, "engines": { "node": ">= 0.6.0" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.35", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", - "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -6725,38 +6496,34 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/bit-twiddle": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz", "integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/bitmap-sdf": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/bl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", "dev": true, - "peer": true, "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6781,9 +6548,9 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==" }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -6799,13 +6566,12 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", + "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -6818,31 +6584,17 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "peer": true - }, - "node_modules/buffer-image-size": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", - "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - }, - "engines": { - "node": ">=4.0" - } + "dev": true }, "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" }, "engines": { @@ -6886,7 +6638,6 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -6900,9 +6651,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001797", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", - "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -6917,15 +6668,13 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/canvas-fit": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/canvas-fit/-/canvas-fit-1.5.0.tgz", "integrity": "sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==", "dev": true, - "peer": true, "dependencies": { "element-size": "^1.1.1" } @@ -7010,7 +6759,6 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, - "peer": true, "engines": { "node": ">=6.0" } @@ -7019,8 +6767,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/class-variance-authority": { "version": "0.7.1", @@ -7134,7 +6881,6 @@ "resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.4.tgz", "integrity": "sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==", "dev": true, - "peer": true, "dependencies": { "color-parse": "^1.3.8" } @@ -7144,7 +6890,6 @@ "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", "dev": true, - "peer": true, "dependencies": { "color-name": "^1.0.0" } @@ -7165,7 +6910,6 @@ "resolved": "https://registry.npmjs.org/color-id/-/color-id-1.1.0.tgz", "integrity": "sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==", "dev": true, - "peer": true, "dependencies": { "clamp": "^1.0.1" } @@ -7180,7 +6924,6 @@ "resolved": "https://registry.npmjs.org/color-normalize/-/color-normalize-1.5.0.tgz", "integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==", "dev": true, - "peer": true, "dependencies": { "clamp": "^1.0.1", "color-rgba": "^2.1.1", @@ -7192,7 +6935,6 @@ "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-2.0.0.tgz", "integrity": "sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==", "dev": true, - "peer": true, "dependencies": { "color-name": "^1.0.0" } @@ -7202,7 +6944,6 @@ "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-2.1.1.tgz", "integrity": "sha512-VaX97wsqrMwLSOR6H7rU1Doa2zyVdmShabKrPEIFywLlHoibgD3QW9Dw6fSqM4+H/LfjprDNAUUW31qEQcGzNw==", "dev": true, - "peer": true, "dependencies": { "clamp": "^1.0.1", "color-parse": "^1.3.8", @@ -7214,7 +6955,6 @@ "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", "dev": true, - "peer": true, "dependencies": { "color-name": "^1.0.0" } @@ -7224,7 +6964,6 @@ "resolved": "https://registry.npmjs.org/color-space/-/color-space-1.16.0.tgz", "integrity": "sha512-A6WMiFzunQ8KEPFmj02OnnoUnqhmSaHaZ/0LVFcPTdlvm8+3aMJ5x1HRHy3bDHPkovkf4sS0f4wsVvwk71fKkg==", "dev": true, - "peer": true, "dependencies": { "hsluv": "^0.0.3", "mumath": "^3.3.4" @@ -7277,7 +7016,6 @@ "engines": [ "node >= 0.8" ], - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -7310,15 +7048,13 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/country-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.1.0.tgz", "integrity": "sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -7338,7 +7074,6 @@ "resolved": "https://registry.npmjs.org/css-font/-/css-font-1.2.0.tgz", "integrity": "sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==", "dev": true, - "peer": true, "dependencies": { "css-font-size-keywords": "^1.0.0", "css-font-stretch-keywords": "^1.0.1", @@ -7355,43 +7090,37 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/css-font-stretch-keywords": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz", "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/css-font-style-keywords": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz", "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/css-font-weight-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz", "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/css-global-keywords": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/css-global-keywords/-/css-global-keywords-1.0.1.tgz", "integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/css-loader": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", "dev": true, - "peer": true, "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", @@ -7427,7 +7156,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -7439,8 +7167,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz", "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/css-tree": { "version": "3.2.1", @@ -7465,8 +7192,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/cssesc": { "version": "3.0.0", @@ -7513,7 +7239,6 @@ "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dev": true, - "peer": true, "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" @@ -7526,15 +7251,13 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/d3-collection": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/d3-color": { "version": "3.1.0", @@ -7560,15 +7283,13 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/d3-force": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", "dev": true, - "peer": true, "dependencies": { "d3-collection": "1", "d3-dispatch": "1", @@ -7586,7 +7307,6 @@ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", "dev": true, - "peer": true, "dependencies": { "d3-array": "1" } @@ -7596,7 +7316,6 @@ "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", "dev": true, - "peer": true, "dependencies": { "commander": "2", "d3-array": "1", @@ -7615,15 +7334,13 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/d3-hierarchy": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/d3-interpolate": { "version": "3.0.1", @@ -7655,8 +7372,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/d3-random": { "version": "2.2.2", @@ -7733,8 +7449,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/d3-voronoi": { "version": "1.1.4", @@ -7834,7 +7549,6 @@ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", "dev": true, - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7870,8 +7584,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-kerning/-/detect-kerning-2.1.2.tgz", "integrity": "sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/detect-node-es": { "version": "1.1.0", @@ -7884,16 +7597,6 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, - "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7934,7 +7637,6 @@ "resolved": "https://registry.npmjs.org/draw-svg-path/-/draw-svg-path-1.0.0.tgz", "integrity": "sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==", "dev": true, - "peer": true, "dependencies": { "abs-svg-path": "~0.1.1", "normalize-svg-path": "~0.1.0" @@ -7945,7 +7647,6 @@ "resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz", "integrity": "sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==", "dev": true, - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -7967,15 +7668,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/dup/-/dup-1.0.0.tgz", "integrity": "sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dev": true, - "peer": true, "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -7987,8 +7686,7 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -7996,25 +7694,22 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { - "version": "1.5.371", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", - "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", - "dev": true, - "license": "ISC" + "version": "1.5.75", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", + "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", + "dev": true }, "node_modules/element-size": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/element-size/-/element-size-1.1.1.tgz", "integrity": "sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/elementary-circuits-directed-graph": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/elementary-circuits-directed-graph/-/elementary-circuits-directed-graph-1.3.1.tgz", "integrity": "sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==", "dev": true, - "peer": true, "dependencies": { "strongly-connected-components": "^1.0.1" } @@ -8035,21 +7730,18 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, - "peer": true, "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", - "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" + "tapable": "^2.2.0" }, "engines": { "node": ">=10.13.0" @@ -8084,11 +7776,10 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -8124,7 +7815,6 @@ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", @@ -8140,7 +7830,6 @@ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "dev": true, - "peer": true, "dependencies": { "d": "1", "es5-ext": "^0.10.35", @@ -8152,7 +7841,6 @@ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dev": true, - "peer": true, "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" @@ -8166,7 +7854,6 @@ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "dev": true, - "peer": true, "dependencies": { "d": "1", "es5-ext": "^0.10.46", @@ -8180,6 +7867,7 @@ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8252,7 +7940,6 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, - "peer": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -8270,32 +7957,32 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.17.0", + "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", - "ajv": "^6.14.0", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -8307,7 +7994,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -8384,11 +8071,10 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -8401,11 +8087,10 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -8413,25 +8098,11 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "dev": true, - "peer": true, "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", @@ -8443,15 +8114,14 @@ } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8525,7 +8195,6 @@ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "dev": true, - "peer": true, "dependencies": { "d": "1", "es5-ext": "~0.10.14" @@ -8536,7 +8205,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, - "peer": true, "engines": { "node": ">=0.8.x" } @@ -8555,7 +8223,6 @@ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, - "peer": true, "dependencies": { "type": "^2.7.2" } @@ -8565,7 +8232,6 @@ "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.5.tgz", "integrity": "sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==", "dev": true, - "peer": true, "dependencies": { "acorn": "^7.1.1", "isarray": "^2.0.1" @@ -8579,7 +8245,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8624,7 +8289,6 @@ "resolved": "https://registry.npmjs.org/fast-isnumeric/-/fast-isnumeric-1.1.4.tgz", "integrity": "sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==", "dev": true, - "peer": true, "dependencies": { "is-string-blank": "^1.0.1" } @@ -8642,9 +8306,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", "dev": true, "funding": [ { @@ -8655,8 +8319,7 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "license": "BSD-3-Clause" + ] }, "node_modules/fastq": { "version": "1.18.0", @@ -8800,18 +8463,16 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true }, "node_modules/flatten-vertex-data": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flatten-vertex-data/-/flatten-vertex-data-1.0.2.tgz", "integrity": "sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==", "dev": true, - "peer": true, "dependencies": { "dtype": "^2.0.0" } @@ -8842,7 +8503,6 @@ "resolved": "https://registry.npmjs.org/font-atlas/-/font-atlas-2.1.0.tgz", "integrity": "sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==", "dev": true, - "peer": true, "dependencies": { "css-font": "^1.0.0" } @@ -8852,7 +8512,6 @@ "resolved": "https://registry.npmjs.org/font-measure/-/font-measure-1.2.2.tgz", "integrity": "sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==", "dev": true, - "peer": true, "dependencies": { "css-font": "^1.2.0" } @@ -8888,17 +8547,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -8922,7 +8581,6 @@ "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, - "peer": true, "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -8963,15 +8621,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8985,8 +8634,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -9001,8 +8649,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-canvas-context/-/get-canvas-context-1.0.2.tgz", "integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/get-intrinsic": { "version": "1.3.0", @@ -9054,7 +8701,6 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -9097,22 +8743,19 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.2.0.tgz", "integrity": "sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/gl-matrix": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/gl-text": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.4.0.tgz", "integrity": "sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==", "dev": true, - "peer": true, "dependencies": { "bit-twiddle": "^1.0.2", "color-normalize": "^1.5.0", @@ -9138,7 +8781,6 @@ "resolved": "https://registry.npmjs.org/gl-util/-/gl-util-3.1.3.tgz", "integrity": "sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==", "dev": true, - "peer": true, "dependencies": { "is-browser": "^2.0.1", "is-firefox": "^1.0.3", @@ -9150,11 +8792,9 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -9185,26 +8825,23 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true + "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -9218,7 +8855,6 @@ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", "dev": true, - "peer": true, "dependencies": { "ini": "^4.1.3", "kind-of": "^6.0.3", @@ -9233,7 +8869,6 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "peer": true, "engines": { "node": ">=16" } @@ -9243,7 +8878,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, - "peer": true, "dependencies": { "isexe": "^3.1.1" }, @@ -9297,7 +8931,6 @@ "resolved": "https://registry.npmjs.org/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz", "integrity": "sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==", "dev": true, - "peer": true, "dependencies": { "glsl-token-inject-block": "^1.0.0", "glsl-token-string": "^1.0.1", @@ -9309,7 +8942,6 @@ "resolved": "https://registry.npmjs.org/glsl-resolve/-/glsl-resolve-0.0.1.tgz", "integrity": "sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==", "dev": true, - "peer": true, "dependencies": { "resolve": "^0.6.1", "xtend": "^2.1.2" @@ -9319,15 +8951,13 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-resolve/node_modules/xtend": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz", "integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==", "dev": true, - "peer": true, "engines": { "node": ">=0.4" } @@ -9336,15 +8966,13 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz", "integrity": "sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-token-defines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz", "integrity": "sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==", "dev": true, - "peer": true, "dependencies": { "glsl-tokenizer": "^2.0.0" } @@ -9353,15 +8981,13 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz", "integrity": "sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-token-descope": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz", "integrity": "sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==", "dev": true, - "peer": true, "dependencies": { "glsl-token-assignments": "^2.0.0", "glsl-token-depth": "^1.1.0", @@ -9373,43 +8999,37 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz", "integrity": "sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-token-properties": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz", "integrity": "sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-token-scope": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz", "integrity": "sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-token-string": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/glsl-token-string/-/glsl-token-string-1.0.1.tgz", "integrity": "sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-token-whitespace-trim": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz", "integrity": "sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-tokenizer": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz", "integrity": "sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==", "dev": true, - "peer": true, "dependencies": { "through2": "^0.6.3" } @@ -9418,15 +9038,13 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-tokenizer/node_modules/readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", "dev": true, - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -9438,15 +9056,13 @@ "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/glsl-tokenizer/node_modules/through2": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", "dev": true, - "peer": true, "dependencies": { "readable-stream": ">=1.0.33-1 <1.1.0-0", "xtend": ">=4.0.0 <4.1.0-0" @@ -9457,7 +9073,6 @@ "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", "dev": true, - "peer": true, "dependencies": { "bl": "^2.2.1", "concat-stream": "^1.5.2", @@ -9484,7 +9099,6 @@ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-5.1.1.tgz", "integrity": "sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==", "dev": true, - "peer": true, "dependencies": { "glsl-inject-defines": "^1.0.1", "glsl-token-defines": "^1.0.0", @@ -9503,7 +9117,6 @@ "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", "dev": true, - "peer": true, "dependencies": { "@choojs/findup": "^0.2.0", "events": "^3.2.0", @@ -9550,23 +9163,20 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/happy-dom": { - "version": "20.10.2", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.10.2.tgz", - "integrity": "sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==", + "version": "20.8.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.3.tgz", + "integrity": "sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", - "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", - "ws": "^8.21.0" + "ws": "^8.18.3" }, "engines": { "node": ">=20.0.0" @@ -9607,7 +9217,6 @@ "resolved": "https://registry.npmjs.org/has-hover/-/has-hover-1.0.1.tgz", "integrity": "sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==", "dev": true, - "peer": true, "dependencies": { "is-browser": "^2.0.1" } @@ -9617,7 +9226,6 @@ "resolved": "https://registry.npmjs.org/has-passive-events/-/has-passive-events-1.0.0.tgz", "integrity": "sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==", "dev": true, - "peer": true, "dependencies": { "is-browser": "^2.0.1" } @@ -9661,9 +9269,10 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -9690,8 +9299,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/hsluv/-/hsluv-0.0.3.tgz", "integrity": "sha512-08iL2VyCRbkQKBySkSh6m8zMUa3sADAxGVWs3Z1aPcUkTJeK0ETG4Fc27tEmQBGUAXZjIsXOZqBvacuVNSC/fQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", @@ -9752,7 +9360,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -9765,7 +9372,6 @@ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, - "peer": true, "engines": { "node": "^10 || ^12 || >= 14" }, @@ -9791,8 +9397,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "peer": true + ] }, "node_modules/ignore": { "version": "5.3.2", @@ -9804,11 +9409,10 @@ } }, "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -9825,7 +9429,6 @@ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -9858,7 +9461,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", "dev": true, - "peer": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -9903,8 +9505,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-browser/-/is-browser-2.1.0.tgz", "integrity": "sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/is-callable": { "version": "1.2.7", @@ -9960,7 +9561,6 @@ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" }, @@ -9973,7 +9573,6 @@ "resolved": "https://registry.npmjs.org/is-firefox/-/is-firefox-1.0.3.tgz", "integrity": "sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9987,14 +9586,13 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -10021,7 +9619,6 @@ "resolved": "https://registry.npmjs.org/is-iexplorer/-/is-iexplorer-1.0.0.tgz", "integrity": "sha512-YeLzceuwg3K6O0MLM3UyUUjKAlyULetwryFp1mHy1I5PfArK0AEqlfa+MR4gkJjcbuJXoDJCvXbyqZVf5CR2Sg==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10030,8 +9627,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==", - "dev": true, - "peer": true + "dev": true }, "node_modules/is-node-process": { "version": "1.2.0", @@ -10052,7 +9648,6 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10062,7 +9657,6 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10095,15 +9689,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-string-blank/-/is-string-blank-1.0.1.tgz", "integrity": "sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/is-svg-path": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-svg-path/-/is-svg-path-1.0.2.tgz", "integrity": "sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/is-typed-array": { "version": "1.1.15", @@ -10136,8 +9728,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/isexe": { "version": "2.0.0", @@ -10163,8 +9754,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10179,8 +9768,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10203,8 +9790,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -10212,21 +9798,10 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -10235,9 +9810,9 @@ } }, "node_modules/jsdoc-type-pratt-parser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.8.0.tgz", - "integrity": "sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -10313,6 +9888,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -10329,8 +9910,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json5": { "version": "2.2.3", @@ -10359,8 +9939,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/keyv": { "version": "4.5.4", @@ -10376,7 +9955,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10417,18 +9995,12 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/loader-runner": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", - "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, - "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" } }, "node_modules/local-pkg": { @@ -10463,10 +10035,9 @@ } }, "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -10547,7 +10118,6 @@ "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", "dev": true, - "peer": true, "dependencies": { "once": "~1.3.0" } @@ -10557,7 +10127,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", "dev": true, - "peer": true, "dependencies": { "wrappy": "1" } @@ -10606,7 +10175,6 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", "dev": true, - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -10647,50 +10215,43 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/maplibre-gl/node_modules/earcut": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/maplibre-gl/node_modules/geojson-vt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/maplibre-gl/node_modules/potpack": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/maplibre-gl/node_modules/quickselect": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/maplibre-gl/node_modules/supercluster": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", "dev": true, - "peer": true, "dependencies": { "kdbush": "^4.0.2" } @@ -10699,8 +10260,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/math-expression-evaluator": { "version": "1.4.0", @@ -10721,7 +10281,6 @@ "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10744,9 +10303,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT", - "peer": true + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -10806,42 +10363,15 @@ } }, "node_modules/minimatch": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", - "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimatch/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" + "node": "*" } }, "node_modules/minimist": { @@ -10884,7 +10414,6 @@ "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz", "integrity": "sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==", "dev": true, - "peer": true, "dependencies": { "mouse-event": "^1.0.0" } @@ -10893,22 +10422,19 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/mouse-event/-/mouse-event-1.0.5.tgz", "integrity": "sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/mouse-event-offset": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", "integrity": "sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/mouse-wheel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mouse-wheel/-/mouse-wheel-1.2.0.tgz", "integrity": "sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==", "dev": true, - "peer": true, "dependencies": { "right-now": "^1.0.0", "signum": "^1.0.0", @@ -10926,6 +10452,7 @@ "integrity": "sha512-GLU8gx0o7RBG/3x/eTnnLd5S5ZInxXRRRMN8GJwaPZ4jpJTxzQfWGvwr90e8L5dkKJnz+gT4gQYCprLy/c4kVw==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", @@ -11000,7 +10527,6 @@ "integrity": "sha512-VAFIOG6rsxoc7q/IaY3jdjmrsuX9f15KlRLYTHmixASBZkZEKC1IFqE2BC5CdhXmK6WLM1Re33z//AGmeRI6FA==", "deprecated": "Redundant dependency in your project.", "dev": true, - "peer": true, "dependencies": { "almost-equal": "^1.1.0" } @@ -11009,8 +10535,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/mute-stream": { "version": "2.0.0", @@ -11032,9 +10557,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -11053,8 +10578,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -11067,7 +10591,6 @@ "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", "dev": true, - "peer": true, "dependencies": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -11085,7 +10608,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "peer": true, "dependencies": { "ms": "^2.1.1" } @@ -11094,25 +10616,19 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true }, "node_modules/normalize-path": { "version": "3.0.0", @@ -11135,15 +10651,13 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/number-is-integer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-integer/-/number-is-integer-1.0.1.tgz", "integrity": "sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==", "dev": true, - "peer": true, "dependencies": { "is-finite": "^1.0.1" }, @@ -11193,7 +10707,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "peer": true, "dependencies": { "wrappy": "1" } @@ -11287,7 +10800,6 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -11299,15 +10811,13 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/parse-rect": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parse-rect/-/parse-rect-1.2.0.tgz", "integrity": "sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==", "dev": true, - "peer": true, "dependencies": { "pick-by-alias": "^1.2.0" } @@ -11316,15 +10826,13 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/parse-unit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-unit/-/parse-unit-1.0.1.tgz", "integrity": "sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/parse5": { "version": "8.0.0", @@ -11433,7 +10941,6 @@ "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", "dev": true, - "peer": true, "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" @@ -11446,15 +10953,13 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true, - "peer": true + "dev": true }, "node_modules/pick-by-alias": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pick-by-alias/-/pick-by-alias-1.2.0.tgz", "integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/picocolors": { "version": "1.1.1", @@ -11462,10 +10967,9 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { "node": ">=8.6" }, @@ -11629,8 +11133,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/polished": { "version": "4.3.1", @@ -11648,8 +11151,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/polybooljs/-/polybooljs-1.2.2.tgz", "integrity": "sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/possible-typed-array-names": { "version": "1.1.0", @@ -11661,9 +11163,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -11679,8 +11181,9 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11761,7 +11264,6 @@ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, - "peer": true, "engines": { "node": "^10 || ^12 || >= 14" }, @@ -11774,7 +11276,6 @@ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, - "peer": true, "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", @@ -11792,7 +11293,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11806,7 +11306,6 @@ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, - "peer": true, "dependencies": { "postcss-selector-parser": "^7.0.0" }, @@ -11822,7 +11321,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11836,7 +11334,6 @@ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, - "peer": true, "dependencies": { "icss-utils": "^5.0.0" }, @@ -11892,8 +11389,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -11910,6 +11406,7 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11960,7 +11457,6 @@ "resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz", "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", "dev": true, - "peer": true, "dependencies": { "lodash.merge": "^4.6.2", "needle": "^2.5.2", @@ -11980,8 +11476,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "peer": true + "dev": true }, "node_modules/prop-types": { "version": "15.8.1", @@ -11999,12 +11494,10 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/protocol-buffers-schema": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", - "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", - "dev": true, - "license": "MIT", - "peer": true + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "dev": true }, "node_modules/proxy-from-env": { "version": "2.1.0", @@ -12038,10 +11531,9 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dependencies": { "side-channel": "^1.1.0" }, @@ -12081,15 +11573,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "dev": true, - "peer": true, "dependencies": { "performance-now": "^2.1.0" } @@ -12103,10 +11593,20 @@ "url": "https://opencollective.com/ramda" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12163,6 +11663,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12247,11 +11748,10 @@ } }, "node_modules/react-router": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", - "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -12270,13 +11770,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", - "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", - "license": "MIT", + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", "dependencies": { - "@remix-run/router": "1.23.3", - "react-router": "6.30.4" + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" }, "engines": { "node": ">=14.0.0" @@ -12287,12 +11786,12 @@ } }, "node_modules/react-router-dom/node_modules/react-router": { - "version": "6.30.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", - "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.3" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -12306,7 +11805,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -12379,7 +11877,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -12394,15 +11891,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/readdirp": { "version": "3.6.0", @@ -12487,15 +11982,13 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@plotly/regl/-/regl-2.1.2.tgz", "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/regl-error2d": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/regl-error2d/-/regl-error2d-2.0.12.tgz", "integrity": "sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==", "dev": true, - "peer": true, "dependencies": { "array-bounds": "^1.0.1", "color-normalize": "^1.5.0", @@ -12511,7 +12004,6 @@ "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.1.3.tgz", "integrity": "sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==", "dev": true, - "peer": true, "dependencies": { "array-bounds": "^1.0.1", "array-find-index": "^1.0.2", @@ -12531,7 +12023,6 @@ "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.3.1.tgz", "integrity": "sha512-seOmMIVwaCwemSYz/y4WE0dbSO9svNFSqtTh5RE57I7PjGo3tcUYKtH0MTSoshcAsreoqN8HoCtnn8wfHXXfKQ==", "dev": true, - "peer": true, "dependencies": { "@plotly/point-cluster": "^3.1.9", "array-range": "^1.0.1", @@ -12555,7 +12046,6 @@ "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.14.tgz", "integrity": "sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==", "dev": true, - "peer": true, "dependencies": { "array-bounds": "^1.0.1", "array-range": "^1.0.1", @@ -12615,7 +12105,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -12625,7 +12114,6 @@ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", "dev": true, - "peer": true, "dependencies": { "protocol-buffers-schema": "^3.3.1" } @@ -12643,8 +12131,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz", "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/robust-predicates": { "version": "3.0.2", @@ -12653,13 +12140,14 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", - "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", + "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/estree": "1.0.9" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -12669,31 +12157,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.61.1", - "@rollup/rollup-android-arm64": "4.61.1", - "@rollup/rollup-darwin-arm64": "4.61.1", - "@rollup/rollup-darwin-x64": "4.61.1", - "@rollup/rollup-freebsd-arm64": "4.61.1", - "@rollup/rollup-freebsd-x64": "4.61.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", - "@rollup/rollup-linux-arm-musleabihf": "4.61.1", - "@rollup/rollup-linux-arm64-gnu": "4.61.1", - "@rollup/rollup-linux-arm64-musl": "4.61.1", - "@rollup/rollup-linux-loong64-gnu": "4.61.1", - "@rollup/rollup-linux-loong64-musl": "4.61.1", - "@rollup/rollup-linux-ppc64-gnu": "4.61.1", - "@rollup/rollup-linux-ppc64-musl": "4.61.1", - "@rollup/rollup-linux-riscv64-gnu": "4.61.1", - "@rollup/rollup-linux-riscv64-musl": "4.61.1", - "@rollup/rollup-linux-s390x-gnu": "4.61.1", - "@rollup/rollup-linux-x64-gnu": "4.61.1", - "@rollup/rollup-linux-x64-musl": "4.61.1", - "@rollup/rollup-openbsd-x64": "4.61.1", - "@rollup/rollup-openharmony-arm64": "4.61.1", - "@rollup/rollup-win32-arm64-msvc": "4.61.1", - "@rollup/rollup-win32-ia32-msvc": "4.61.1", - "@rollup/rollup-win32-x64-gnu": "4.61.1", - "@rollup/rollup-win32-x64-msvc": "4.61.1", + "@rollup/rollup-android-arm-eabi": "4.44.1", + "@rollup/rollup-android-arm64": "4.44.1", + "@rollup/rollup-darwin-arm64": "4.44.1", + "@rollup/rollup-darwin-x64": "4.44.1", + "@rollup/rollup-freebsd-arm64": "4.44.1", + "@rollup/rollup-freebsd-x64": "4.44.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", + "@rollup/rollup-linux-arm-musleabihf": "4.44.1", + "@rollup/rollup-linux-arm64-gnu": "4.44.1", + "@rollup/rollup-linux-arm64-musl": "4.44.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-musl": "4.44.1", + "@rollup/rollup-linux-s390x-gnu": "4.44.1", + "@rollup/rollup-linux-x64-gnu": "4.44.1", + "@rollup/rollup-linux-x64-musl": "4.44.1", + "@rollup/rollup-win32-arm64-msvc": "4.44.1", + "@rollup/rollup-win32-ia32-msvc": "4.44.1", + "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" } }, @@ -12723,8 +12206,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/safe-buffer": { "version": "5.2.1", @@ -12744,8 +12226,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "peer": true + ] }, "node_modules/safe-regex-test": { "version": "1.1.0", @@ -12768,15 +12249,13 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/saxes": { "version": "6.0.0", @@ -12799,17 +12278,14 @@ } }, "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { "node": ">= 10.13.0" @@ -12819,65 +12295,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -12887,12 +12304,20 @@ "semver": "bin/semver.js" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -12915,8 +12340,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", "integrity": "sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/shebang-command": { "version": "2.0.0", @@ -13026,8 +12450,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/signum/-/signum-1.0.0.tgz", "integrity": "sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/slash": { "version": "3.0.0", @@ -13059,7 +12482,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -13069,15 +12491,13 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/stack-trace": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", "dev": true, - "peer": true, "engines": { "node": "*" } @@ -13093,7 +12513,6 @@ "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", "dev": true, - "peer": true, "dependencies": { "escodegen": "^2.1.0" } @@ -13108,11 +12527,10 @@ } }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true }, "node_modules/store2": { "version": "2.14.4", @@ -13120,12 +12538,13 @@ "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==" }, "node_modules/storybook": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.6.18.tgz", - "integrity": "sha512-p8seiSI6FiVY6P3V0pG+5v7c8pDMehMAFRWEhG5XqIBSQszzOjDnW2rNvm3odoLKfo3V3P6Cs6Hv9ILzymULyQ==", + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.6.14.tgz", + "integrity": "sha512-sVKbCj/OTx67jhmauhxc2dcr1P+yOgz/x3h0krwjyMgdc5Oubvxyg4NYDZmzAw+ym36g/lzH8N0Ccp4dwtdfxw==", "license": "MIT", + "peer": true, "dependencies": { - "@storybook/core": "8.6.18" + "@storybook/core": "8.6.14" }, "bin": { "getstorybook": "bin/index.cjs", @@ -13150,7 +12569,6 @@ "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", "dev": true, - "peer": true, "dependencies": { "debug": "2" } @@ -13160,7 +12578,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -13169,15 +12586,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/strict-event-emitter": { "version": "0.5.1", @@ -13190,7 +12605,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -13199,15 +12613,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6.19" } @@ -13217,7 +12629,6 @@ "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", "dev": true, - "peer": true, "dependencies": { "parenthesis": "^3.1.5" } @@ -13371,15 +12782,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/strongly-connected-components/-/strongly-connected-components-1.0.1.tgz", "integrity": "sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", "dev": true, - "peer": true, "engines": { "node": ">= 18.12.0" }, @@ -13417,7 +12826,6 @@ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", "dev": true, - "peer": true, "dependencies": { "kdbush": "^3.0.0" } @@ -13426,15 +12834,13 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", - "dev": true, - "peer": true + "dev": true }, "node_modules/superscript-text": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/superscript-text/-/superscript-text-1.0.0.tgz", "integrity": "sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/supports-color": { "version": "7.2.0", @@ -13463,15 +12869,13 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", - "dev": true, - "peer": true + "dev": true }, "node_modules/svg-path-bounds": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", "integrity": "sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==", "dev": true, - "peer": true, "dependencies": { "abs-svg-path": "^0.1.1", "is-svg-path": "^1.0.1", @@ -13484,7 +12888,6 @@ "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", "dev": true, - "peer": true, "dependencies": { "svg-arc-to-cubic-bezier": "^3.0.0" } @@ -13494,7 +12897,6 @@ "resolved": "https://registry.npmjs.org/svg-path-sdf/-/svg-path-sdf-1.1.3.tgz", "integrity": "sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==", "dev": true, - "peer": true, "dependencies": { "bitmap-sdf": "^1.0.0", "draw-svg-path": "^1.0.0", @@ -13528,6 +12930,7 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13570,18 +12973,12 @@ } }, "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, - "license": "MIT", - "peer": true, "engines": { "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" } }, "node_modules/telejson": { @@ -13597,7 +12994,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -13612,16 +13008,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", - "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -13635,50 +13030,76 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { - "@minify-html/node": { - "optional": true - }, "@swc/core": { "optional": true }, - "@swc/css": { - "optional": true - }, - "@swc/html": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "cssnano": { - "optional": true - }, - "csso": { - "optional": true - }, "esbuild": { "optional": true }, - "html-minifier-terser": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "postcss": { - "optional": true - }, "uglify-js": { "optional": true } } }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/thenify": { "version": "3.3.1", @@ -13710,7 +13131,6 @@ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, - "peer": true, "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" @@ -13731,8 +13151,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/tinyexec": { "version": "1.0.2", @@ -13777,11 +13196,11 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13793,8 +13212,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/tinyrainbow": { "version": "1.2.0", @@ -13836,15 +13254,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/to-float32/-/to-float32-1.1.0.tgz", "integrity": "sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/to-px": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz", "integrity": "sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==", "dev": true, - "peer": true, "dependencies": { "parse-unit": "^1.0.1" } @@ -13865,7 +13281,6 @@ "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", "dev": true, - "peer": true, "dependencies": { "commander": "2" }, @@ -13879,8 +13294,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/tough-cookie": { "version": "4.1.4", @@ -14013,8 +13427,7 @@ "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/type-check": { "version": "0.4.0", @@ -14043,15 +13456,13 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/typedarray-pool": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/typedarray-pool/-/typedarray-pool-1.2.0.tgz", "integrity": "sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==", "dev": true, - "peer": true, "dependencies": { "bit-twiddle": "^1.0.0", "dup": "^1.0.0" @@ -14062,6 +13473,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14099,11 +13511,10 @@ "dev": true }, "node_modules/undici": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", - "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", "dev": true, - "license": "MIT", "engines": { "node": ">=20.18.1" } @@ -14138,13 +13549,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -14160,10 +13570,9 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.1" + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -14176,8 +13585,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-diff/-/update-diff-1.1.0.tgz", "integrity": "sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/uri-js": { "version": "4.4.1", @@ -14263,23 +13671,22 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -14410,11 +13817,12 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14423,31 +13831,30 @@ } }, "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", + "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14463,15 +13870,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -14492,12 +13896,6 @@ "@vitest/browser-webdriverio": { "optional": true }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, "@vitest/ui": { "optional": true }, @@ -14506,63 +13904,55 @@ }, "jsdom": { "optional": true - }, - "vite": { - "optional": false } } }, "node_modules/vitest/node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, - "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vitest/node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, - "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vitest/node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, - "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vitest/node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -14573,17 +13963,15 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -14592,11 +13980,10 @@ } }, "node_modules/vitest/node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -14612,7 +13999,6 @@ "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", "dev": true, - "peer": true, "dependencies": { "@mapbox/point-geometry": "0.1.0", "@mapbox/vector-tile": "^1.3.1", @@ -14632,12 +14018,10 @@ } }, "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -14650,15 +14034,13 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/webgl-context": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/webgl-context/-/webgl-context-2.2.0.tgz", "integrity": "sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==", "dev": true, - "peer": true, "dependencies": { "get-canvas-context": "^1.0.1" } @@ -14673,36 +14055,35 @@ } }, "node_modules/webpack": { - "version": "5.107.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", - "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.22.0", - "es-module-lexer": "^2.1.0", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.2", - "mime-db": "^1.54.0", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.5.0", - "watchpack": "^2.5.1", - "webpack-sources": "^3.5.0" + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" @@ -14721,12 +14102,10 @@ } }, "node_modules/webpack-sources": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", - "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, - "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -14742,7 +14121,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -14756,22 +14134,10 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", @@ -14810,13 +14176,13 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", + "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -14860,7 +14226,6 @@ "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.3.tgz", "integrity": "sha512-sAjLZkBnsbHkHWVhrsCU5Sa/EVuf9QqgvrN8zyJ2L/F9FR9Oc6CvVK0674+PGAtmmmYQMH98tCUSO4QLQv3/TQ==", "dev": true, - "peer": true, "dependencies": { "object-assign": "^4.1.0" } @@ -14950,13 +14315,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -14994,7 +14358,6 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true, - "peer": true, "engines": { "node": ">=0.4" } @@ -15015,18 +14378,14 @@ "dev": true }, "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" + "node": ">= 14" } }, "node_modules/yargs": { diff --git a/public/images/det_1m_1.png b/public/images/det_1m_1.png new file mode 100644 index 00000000..ed9ac165 Binary files /dev/null and b/public/images/det_1m_1.png differ diff --git a/public/images/det_1m_2.png b/public/images/det_1m_2.png new file mode 100644 index 00000000..34a153c7 Binary files /dev/null and b/public/images/det_1m_2.png differ diff --git a/public/images/det_1m_3.png b/public/images/det_1m_3.png new file mode 100644 index 00000000..724d1a84 Binary files /dev/null and b/public/images/det_1m_3.png differ diff --git a/public/images/det_1m_beamstop.png b/public/images/det_1m_beamstop.png new file mode 100644 index 00000000..351aecf5 Binary files /dev/null and b/public/images/det_1m_beamstop.png differ diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 0bf12188..0c8e0df6 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -5,23 +5,24 @@ * Mock Service Worker. * @see https://github.com/mswjs/msw * - Please do NOT modify this file. + * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = '2.10.3' -const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' +const PACKAGE_VERSION = '2.8.4' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() -addEventListener('install', function () { +self.addEventListener('install', function () { self.skipWaiting() }) -addEventListener('activate', function (event) { +self.addEventListener('activate', function (event) { event.waitUntil(self.clients.claim()) }) -addEventListener('message', async function (event) { - const clientId = Reflect.get(event.source || {}, 'id') +self.addEventListener('message', async function (event) { + const clientId = event.source.id if (!clientId || !self.clients) { return @@ -93,18 +94,17 @@ addEventListener('message', async function (event) { } }) -addEventListener('fetch', function (event) { +self.addEventListener('fetch', function (event) { + const { request } = event + // Bypass navigation requests. - if (event.request.mode === 'navigate') { + if (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' - ) { + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { return } @@ -115,62 +115,48 @@ addEventListener('fetch', function (event) { return } + // Generate unique request ID. 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: { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, type: responseClone.type, status: responseClone.status, statusText: responseClone.statusText, - headers: Object.fromEntries(responseClone.headers.entries()), body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), }, }, - }, - responseClone.body ? [serializedRequest.body, responseClone.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} - */ +// 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. async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) @@ -198,16 +184,12 @@ async function resolveMainClient(event) { }) } -/** - * @param {FetchEvent} event - * @param {Client | undefined} client - * @param {string} requestId - * @returns {Promise} - */ async function getResponse(event, client, requestId) { + const { request } = event + // 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() + const requestClone = request.clone() function passthrough() { // Cast the request headers to a new Headers instance @@ -248,17 +230,29 @@ async function getResponse(event, client, requestId) { } // Notify the client that a request has been intercepted. - const serializedRequest = await serializeRequest(event.request) + const requestBuffer = await request.arrayBuffer() const clientMessage = await sendToClient( client, { type: 'REQUEST', payload: { id: requestId, - ...serializedRequest, + 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: requestBuffer, + keepalive: request.keepalive, }, }, - [serializedRequest.body], + [requestBuffer], ) switch (clientMessage.type) { @@ -274,12 +268,6 @@ async function getResponse(event, client, requestId) { 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() @@ -292,18 +280,14 @@ function sendToClient(client, message, transferrables = []) { resolve(event.data) } - client.postMessage(message, [ - channel.port2, - ...transferrables.filter(Boolean), - ]) + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) }) } -/** - * @param {Response} response - * @returns {Response} - */ -function respondWithMock(response) { +async 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 @@ -321,24 +305,3 @@ function respondWithMock(response) { 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/src/api/ophyd/OphydTransportContext.ts b/src/api/ophyd/OphydTransportContext.ts new file mode 100644 index 00000000..a72d22c0 --- /dev/null +++ b/src/api/ophyd/OphydTransportContext.ts @@ -0,0 +1,74 @@ +import { createContext, useContext, useMemo } from 'react'; +import { useOphydApiUrls } from '@/utils/apiUtils'; +import { createWebSocketTransport } from './transport/createWebSocketTransport'; +import { createWebSocketDeviceTransport } from './transport/createWebSocketDeviceTransport'; +import type { OphydPVTransport } from './transport/types'; +import type { OphydDeviceTransport } from './transport/deviceTypes'; +import type { CameraSocketFactory, CameraSocketLike } from '@/lib/ophyd-sim'; + +export const PVTransportContext = createContext(null); +export const DeviceTransportContext = createContext(null); +export const CameraSocketFactoryContext = createContext(null); + +/** + * Default camera socket: a thin wrapper over the real browser `WebSocket`. The + * real socket's surface is wider than {@link CameraSocketLike} (its `onmessage` + * carries a full `MessageEvent`), so we cast — consumers only touch the subset + * the interface declares. + */ +const defaultCameraSocketFactory: CameraSocketFactory = (url) => + new WebSocket(url) as unknown as CameraSocketLike; + +/** + * Read the PV-socket transport from context, or construct a default + * WebSocket transport pointed at the configured ophyd backend. + * + * The fallback transport is memoized per URL so re-renders of consumers + * don't churn connections. + */ +export function useOphydPVTransport(overrideUrl?: string): OphydPVTransport { + const fromContext = useContext(PVTransportContext); + const configWsUrl = useOphydApiUrls().getWsUrl('pv-socket'); + const url = overrideUrl ?? configWsUrl; + + // Construct fallback transports on demand. useMemo keyed on url means we + // get one transport per (url) per consumer that lacks a provider. This + // matches the prior per-hook WebSocket behavior — see plan for the + // shared-state caveat that motivated the refactor. + const fallback = useMemo(() => { + if (fromContext) return null; + return createWebSocketTransport({ url }); + }, [fromContext, url]); + + if (fromContext) return fromContext; + if (!fallback) { + throw new Error('useOphydPVTransport: no transport available'); + } + return fallback; +} + +export function useOphydDeviceTransport(overrideUrl?: string): OphydDeviceTransport { + const fromContext = useContext(DeviceTransportContext); + const configWsUrl = useOphydApiUrls().getWsUrl('device-socket'); + const url = overrideUrl ?? configWsUrl; + + const fallback = useMemo(() => { + if (fromContext) return null; + return createWebSocketDeviceTransport({ url }); + }, [fromContext, url]); + + if (fromContext) return fromContext; + if (!fallback) { + throw new Error('useOphydDeviceTransport: no transport available'); + } + return fallback; +} + +/** + * Read the camera socket factory from context, or fall back to one that wraps + * the real browser `WebSocket`. Unlike the transport hooks this never throws — + * a default always exists, so cameras work with no provider present. + */ +export function useCameraSocketFactory(): CameraSocketFactory { + return useContext(CameraSocketFactoryContext) ?? defaultCameraSocketFactory; +} diff --git a/src/api/ophyd/OphydTransportProvider.tsx b/src/api/ophyd/OphydTransportProvider.tsx new file mode 100644 index 00000000..1c249833 --- /dev/null +++ b/src/api/ophyd/OphydTransportProvider.tsx @@ -0,0 +1,83 @@ +import { useMemo, type ReactNode } from 'react'; +import { useOphydApiUrls } from '@/utils/apiUtils'; +import { createWebSocketTransport } from './transport/createWebSocketTransport'; +import { createWebSocketDeviceTransport } from './transport/createWebSocketDeviceTransport'; +import { createFallbackTransport } from './transport/createFallbackTransport'; +import type { OphydPVTransport } from './transport/types'; +import type { OphydDeviceTransport } from './transport/deviceTypes'; +import type { CameraSocketFactory } from '@/lib/ophyd-sim'; +import { + PVTransportContext, + DeviceTransportContext, + CameraSocketFactoryContext, +} from './OphydTransportContext'; + +export interface OphydTransportProviderProps { + /** Override the PV-socket transport (e.g. a simulator). */ + transport?: OphydPVTransport; + /** Override the device-socket transport. */ + deviceTransport?: OphydDeviceTransport; + /** + * When true, the provided override transports are wrapped so that, if they + * report `'error'` or `'closed'`, descendants seamlessly switch to the real + * WebSocket backend (URLs from config). No-op when no override is supplied. + * Defaults to false, preserving the prior pass-through behavior. + */ + fallbackToReal?: boolean; + /** + * Override how the camera image socket is created (e.g. a simulator from + * `createSimCameraSocketFactory()`). Descendant `CameraCanvas`es read this + * via {@link useCameraSocketFactory}; when omitted they default to the real + * `WebSocket`. This is the seam that lets a subtree run a mock camera + * without prop-drilling a factory through every intermediate component. + */ + cameraSocketFactory?: CameraSocketFactory; + children: ReactNode; +} + +/** + * Provides PV- and device-socket transports to descendant hooks. When no + * override is supplied, hooks fall back to lazily-constructed real + * WebSocket transports via `useOphydPVTransport` / `useOphydDeviceTransport`, + * preserving the pre-refactor behavior for callers that never wrap in a + * provider. + * + * With `fallbackToReal`, an override transport (e.g. a simulator) becomes the + * primary and the real WebSocket backend becomes a live fallback — see + * {@link createFallbackTransport}. + */ +export function OphydTransportProvider({ + transport, + deviceTransport, + fallbackToReal = false, + cameraSocketFactory, + children, +}: OphydTransportProviderProps) { + const apiUrls = useOphydApiUrls(); + const pvUrl = apiUrls.getWsUrl('pv-socket'); + const deviceUrl = apiUrls.getWsUrl('device-socket'); + + const pvValue = useMemo(() => { + if (!transport) return null; + if (!fallbackToReal) return transport; + return createFallbackTransport(transport, () => createWebSocketTransport({ url: pvUrl })); + }, [transport, fallbackToReal, pvUrl]); + + const deviceValue = useMemo(() => { + if (!deviceTransport) return null; + if (!fallbackToReal) return deviceTransport; + return createFallbackTransport(deviceTransport, () => + createWebSocketDeviceTransport({ url: deviceUrl }), + ); + }, [deviceTransport, fallbackToReal, deviceUrl]); + + return ( + + + + {children} + + + + ); +} diff --git a/src/api/ophyd/transport/createFallbackTransport.ts b/src/api/ophyd/transport/createFallbackTransport.ts new file mode 100644 index 00000000..f97951a3 --- /dev/null +++ b/src/api/ophyd/transport/createFallbackTransport.ts @@ -0,0 +1,161 @@ +import type { OphydTransportStatus, Unsubscribe } from './types'; + +/** + * Structural shape shared by {@link OphydPVTransport} and + * {@link OphydDeviceTransport}. Both differ only in their message types, so the + * fallback wrapper is written once against this generic interface. + */ +interface GenericTransport { + send(message: TOut): void; + onMessage(listener: (message: TIn) => void): Unsubscribe; + onStatus(listener: (status: OphydTransportStatus) => void): Unsubscribe; + close(): void; +} + +/** Outgoing messages carry an `action`; subscriptions also carry a key field. */ +type SubscribeAction = 'subscribe' | 'subscribeSafely' | 'subscribeReadOnly'; +interface ActionMessage { + action: string; + pv?: string; + device?: string; +} + +const SUBSCRIBE_ACTIONS: ReadonlySet = new Set([ + 'subscribe', + 'subscribeSafely', + 'subscribeReadOnly', +]); + +/** Default key extraction for the PV/device protocols: prefer `pv`, then `device`. */ +function defaultSubKey(message: ActionMessage): string { + return message.pv ?? message.device ?? ''; +} + +export interface CreateFallbackTransportOptions { + /** + * Extract the subscription key from an outgoing message so it can be + * replayed onto the fallback after a switch. Defaults to `pv ?? device`. + */ + getSubKey?: (message: TOut) => string; +} + +/** + * Wrap a `primary` transport so that, if it reports `'error'` or `'closed'`, + * the wrapper seamlessly switches to a freshly-constructed fallback transport + * (typically the real WebSocket backend) without changing its own object + * identity. + * + * Identity stability is the whole point: hooks key their subscribe effect on + * the transport object, so swapping the underlying transport here — rather than + * swapping the context value — means consumers never tear down and re-subscribe. + * The wrapper replays active subscriptions onto the fallback and forwards its + * messages to the listeners the hooks already registered. + * + * Trigger is status-only: a `'connecting'` -> `'open'` primary stays primary. + * The primary's terminal `'error'`/`'closed'` is suppressed from consumers; the + * fallback's status supersedes it from the switch onward. + */ +export function createFallbackTransport( + primary: GenericTransport, + makeFallback: () => GenericTransport, + options: CreateFallbackTransportOptions = {}, +): GenericTransport { + const getSubKey = + options.getSubKey ?? ((m: TOut) => defaultSubKey(m as unknown as ActionMessage)); + + const messageListeners = new Set<(message: TIn) => void>(); + const statusListeners = new Set<(status: OphydTransportStatus) => void>(); + // Last subscribe message per key, so we can replay them onto the fallback. + const activeSubs = new Map(); + + let active: GenericTransport = primary; + let status: OphydTransportStatus = 'connecting'; + let switched = false; + let closedByUser = false; + + const forwardMessage = (message: TIn): void => { + for (const listener of messageListeners) { + try { + listener(message); + } catch (err) { + console.error('[ophyd-fallback-transport] message listener threw:', err); + } + } + }; + + const setStatus = (next: OphydTransportStatus): void => { + status = next; + for (const listener of statusListeners) { + try { + listener(next); + } catch (err) { + console.error('[ophyd-fallback-transport] status listener threw:', err); + } + } + }; + + const switchToFallback = (): void => { + switched = true; + + // Detach from the primary and tear it down so it stops emitting. + detachPrimaryMessage?.(); + detachPrimaryStatus?.(); + try { + primary.close(); + } catch (err) { + console.error('[ophyd-fallback-transport] primary close threw:', err); + } + + active = makeFallback(); + active.onMessage(forwardMessage); + active.onStatus(setStatus); + + // Replay active subscriptions so the fallback backend re-emits meta/value + // to the listeners the hooks already registered. + for (const message of activeSubs.values()) { + active.send(message); + } + }; + + const onPrimaryStatus = (next: OphydTransportStatus): void => { + if (!switched && !closedByUser && (next === 'error' || next === 'closed')) { + switchToFallback(); + return; + } + // Pass through non-terminal primary status (e.g. connecting -> open). + if (!switched) setStatus(next); + }; + + const detachPrimaryMessage: Unsubscribe | undefined = primary.onMessage(forwardMessage); + const detachPrimaryStatus: Unsubscribe | undefined = primary.onStatus(onPrimaryStatus); + + return { + send(message) { + if (closedByUser) return; + const m = message as unknown as ActionMessage; + if (SUBSCRIBE_ACTIONS.has(m.action)) { + activeSubs.set(getSubKey(message), message); + } else if (m.action === 'unsubscribe') { + activeSubs.delete(getSubKey(message)); + } + active.send(message); + }, + onMessage(listener) { + messageListeners.add(listener); + return () => messageListeners.delete(listener); + }, + onStatus(listener) { + statusListeners.add(listener); + listener(status); + return () => statusListeners.delete(listener); + }, + close() { + closedByUser = true; + try { + active.close(); + } catch (err) { + console.error('[ophyd-fallback-transport] active close threw:', err); + } + }, + }; +} diff --git a/src/api/ophyd/transport/createWebSocketDeviceTransport.ts b/src/api/ophyd/transport/createWebSocketDeviceTransport.ts new file mode 100644 index 00000000..fff9674b --- /dev/null +++ b/src/api/ophyd/transport/createWebSocketDeviceTransport.ts @@ -0,0 +1,98 @@ +import type { + OphydDeviceIncomingMessage, + OphydDeviceMessageListener, + OphydDeviceOutgoingMessage, + OphydDeviceTransport, +} from './deviceTypes'; +import type { OphydTransportStatus, OphydTransportStatusListener } from './types'; + +export interface CreateWebSocketDeviceTransportOptions { + url: string; +} + +/** + * WebSocket-backed implementation of OphydDeviceTransport. Mirrors + * createWebSocketTransport but speaks the device-socket protocol (messages + * use `device` instead of `pv`). + */ +export function createWebSocketDeviceTransport( + options: CreateWebSocketDeviceTransportOptions, +): OphydDeviceTransport { + const messageListeners = new Set(); + const statusListeners = new Set(); + let status: OphydTransportStatus = 'connecting'; + const buffer: OphydDeviceOutgoingMessage[] = []; + let closedByUser = false; + + const ws = new WebSocket(options.url); + + const setStatus = (next: OphydTransportStatus): void => { + status = next; + for (const l of statusListeners) { + try { + l(next); + } catch (err) { + console.error('[ophyd-ws-device-transport] status listener threw:', err); + } + } + }; + + ws.onopen = () => { + setStatus('open'); + while (buffer.length > 0) { + const msg = buffer.shift()!; + ws.send(JSON.stringify(msg)); + } + }; + + ws.onmessage = (event) => { + let parsed: OphydDeviceIncomingMessage; + try { + parsed = JSON.parse(event.data); + } catch (err) { + console.error('[ophyd-ws-device-transport] failed to parse message:', err); + return; + } + for (const listener of messageListeners) { + try { + listener(parsed); + } catch (err) { + console.error('[ophyd-ws-device-transport] message listener threw:', err); + } + } + }; + + ws.onerror = () => { + setStatus('error'); + }; + + ws.onclose = () => { + setStatus('closed'); + }; + + return { + send(message) { + if (closedByUser) return; + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(message)); + } else { + buffer.push(message); + } + }, + onMessage(listener) { + messageListeners.add(listener); + return () => messageListeners.delete(listener); + }, + onStatus(listener) { + statusListeners.add(listener); + listener(status); + return () => statusListeners.delete(listener); + }, + close() { + closedByUser = true; + if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) { + ws.close(); + } + }, + }; +} diff --git a/src/api/ophyd/transport/createWebSocketTransport.ts b/src/api/ophyd/transport/createWebSocketTransport.ts new file mode 100644 index 00000000..aeebe5df --- /dev/null +++ b/src/api/ophyd/transport/createWebSocketTransport.ts @@ -0,0 +1,101 @@ +import type { + OphydPVIncomingMessage, + OphydPVMessageListener, + OphydPVOutgoingMessage, + OphydPVTransport, + OphydTransportStatus, + OphydTransportStatusListener, +} from './types'; + +export interface CreateWebSocketTransportOptions { + url: string; +} + +/** + * WebSocket-backed implementation of OphydPVTransport. One transport owns + * one WebSocket. Outgoing messages are buffered until the connection opens. + * + * Errors and closure are surfaced through the status channel. Reconnect + * logic is not yet implemented — consumers can recreate the transport. + */ +export function createWebSocketTransport( + options: CreateWebSocketTransportOptions, +): OphydPVTransport { + const messageListeners = new Set(); + const statusListeners = new Set(); + let status: OphydTransportStatus = 'connecting'; + const buffer: OphydPVOutgoingMessage[] = []; + let closedByUser = false; + + const ws = new WebSocket(options.url); + + const setStatus = (next: OphydTransportStatus): void => { + status = next; + for (const l of statusListeners) { + try { + l(next); + } catch (err) { + console.error('[ophyd-ws-transport] status listener threw:', err); + } + } + }; + + ws.onopen = () => { + setStatus('open'); + while (buffer.length > 0) { + const msg = buffer.shift()!; + ws.send(JSON.stringify(msg)); + } + }; + + ws.onmessage = (event) => { + let parsed: OphydPVIncomingMessage; + try { + parsed = JSON.parse(event.data); + } catch (err) { + console.error('[ophyd-ws-transport] failed to parse message:', err); + return; + } + for (const listener of messageListeners) { + try { + listener(parsed); + } catch (err) { + console.error('[ophyd-ws-transport] message listener threw:', err); + } + } + }; + + ws.onerror = () => { + setStatus('error'); + }; + + ws.onclose = () => { + setStatus('closed'); + }; + + return { + send(message) { + if (closedByUser) return; + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(message)); + } else { + buffer.push(message); + } + }, + onMessage(listener) { + messageListeners.add(listener); + return () => messageListeners.delete(listener); + }, + onStatus(listener) { + statusListeners.add(listener); + listener(status); + return () => statusListeners.delete(listener); + }, + close() { + closedByUser = true; + if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) { + ws.close(); + } + }, + }; +} diff --git a/src/api/ophyd/transport/deviceTypes.ts b/src/api/ophyd/transport/deviceTypes.ts new file mode 100644 index 00000000..8aa2b445 --- /dev/null +++ b/src/api/ophyd/transport/deviceTypes.ts @@ -0,0 +1,32 @@ +import type { + ActionRequestSubscribe, + ActionRequestUnsubscribe, + ActionRequestSet, + ActionRequestRefresh, + MessageResponse, + ErrorResponse, + ValueUpdateResponse, + MetaUpdateResponse, +} from '@/api/ophyd/ophydDeviceSocketTypes'; +import type { OphydTransportStatus, Unsubscribe } from './types'; + +export type OphydDeviceOutgoingMessage = + | ActionRequestSubscribe + | ActionRequestUnsubscribe + | ActionRequestSet + | ActionRequestRefresh; + +export type OphydDeviceIncomingMessage = + | MessageResponse + | ErrorResponse + | ValueUpdateResponse + | MetaUpdateResponse; + +export type OphydDeviceMessageListener = (message: OphydDeviceIncomingMessage) => void; + +export interface OphydDeviceTransport { + send(message: OphydDeviceOutgoingMessage): void; + onMessage(listener: OphydDeviceMessageListener): Unsubscribe; + onStatus(listener: (status: OphydTransportStatus) => void): Unsubscribe; + close(): void; +} diff --git a/src/api/ophyd/transport/types.ts b/src/api/ophyd/transport/types.ts new file mode 100644 index 00000000..0e2fd225 --- /dev/null +++ b/src/api/ophyd/transport/types.ts @@ -0,0 +1,43 @@ +import type { + ActionRequestSubscribe, + ActionRequestUnsubscribe, + ActionRequestSet, + ActionRequestRefresh, + MessageResponse, + ErrorResponse, + ValueUpdateResponse, + MetaUpdateResponse, +} from '@/api/ophyd/ophydPVSocketTypes'; + +export type OphydPVOutgoingMessage = + | ActionRequestSubscribe + | ActionRequestUnsubscribe + | ActionRequestSet + | ActionRequestRefresh; + +export type OphydPVIncomingMessage = + | MessageResponse + | ErrorResponse + | ValueUpdateResponse + | MetaUpdateResponse; + +export type OphydTransportStatus = 'connecting' | 'open' | 'closed' | 'error'; + +export type OphydPVMessageListener = (message: OphydPVIncomingMessage) => void; +export type OphydTransportStatusListener = (status: OphydTransportStatus) => void; +export type Unsubscribe = () => void; + +/** + * Abstraction over the wire used by useOphydPVSocket. + * + * Implementations route the existing PV-socket message protocol to either a + * real WebSocket backend or an in-process simulator. Hooks consume a transport + * from context; per-call lifecycle (open/close) is owned by the transport, not + * the hook. + */ +export interface OphydPVTransport { + send(message: OphydPVOutgoingMessage): void; + onMessage(listener: OphydPVMessageListener): Unsubscribe; + onStatus(listener: OphydTransportStatusListener): Unsubscribe; + close(): void; +} diff --git a/src/api/ophyd/useOphydDeviceSocket.ts b/src/api/ophyd/useOphydDeviceSocket.ts index 9015383c..58ee5b34 100644 --- a/src/api/ophyd/useOphydDeviceSocket.ts +++ b/src/api/ophyd/useOphydDeviceSocket.ts @@ -1,51 +1,23 @@ -import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; -import { OphydDevices } from 'src/types/deviceControllerTypes'; -import { useOphydApiUrls } from '@/utils/apiUtils'; -import { - MessageResponse, - ErrorResponse, - ValueUpdateResponse, - MetaUpdateResponse, -} from '@/api/ophyd/ophydDeviceSocketTypes'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import type { OphydDevices } from '@/types/deviceControllerTypes'; +import { useOphydDeviceTransport } from './OphydTransportContext'; +import type { OphydDeviceTransport } from './transport/deviceTypes'; /** - * Custom hook for managing WebSocket connections to Ophyd devices. - * Provides real-time device state management and control functions. + * Device-socket counterpart of {@link useOphydPVSocket}. Uses the + * device-channel protocol where messages key on `device` instead of `pv`. * - * @param deviceNameList - Array of Ophyd device names to subscribe to - * @param wsUrl - Optional WebSocket URL. If not provided, will use environment variables or default to localhost:8001 - * @returns Object containing device states and control functions + * See OphydTransportProvider for the transport context. When no provider is + * mounted, a fallback WebSocket transport is constructed lazily. */ export default function useOphydDeviceSocket(deviceNameList: string[], wsUrl?: string) { - const configWsUrl = useOphydApiUrls().getWsUrl('device-socket'); - const apiUrl: string = wsUrl ?? configWsUrl; - const [devices, setDevices] = useState(() => { - const initialDevices: OphydDevices = {}; - deviceNameList.forEach((deviceName) => { - initialDevices[deviceName] = { - name: deviceName, - value: '', - connected: false, - locked: false, - timestamp: 0, - expanded: false, - device: deviceName, - read_access: false, - write_access: false, - }; - }); - return initialDevices; - }); // eslint-disable-next-line react-hooks/exhaustive-deps - const memoizedDeviceNames = useMemo(() => deviceNameList, [JSON.stringify(deviceNameList)]); //device updates can retrigger the hook if inputs aren't memoized - const wsRef = useRef(null); + const memoizedDeviceNames = useMemo(() => deviceNameList, [JSON.stringify(deviceNameList)]); + const transport: OphydDeviceTransport = useOphydDeviceTransport(wsUrl); + + const [devices, setDevices] = useState(() => initialDevices(memoizedDeviceNames)); const hasRenderedOnlyOnce = useRef(false); - /** - * Toggles the lock state of a device between locked and unlocked. - * When locked, the device UI will typically be disabled or read-only. - * @param deviceName - The name/identifier of the device to toggle - */ const toggleDeviceLock = useCallback((deviceName: string) => { setDevices((prevDevices) => ({ ...prevDevices, @@ -56,30 +28,13 @@ export default function useOphydDeviceSocket(deviceNameList: string[], wsUrl?: s })); }, []); - /** - * Sends a device value update request through the WebSocket connection. - * @param deviceName - The name/identifier of the device to update - * @param value - The new value to set for the device (string, number, or boolean) - */ const handleSetValueRequest = useCallback( (deviceName: string, value: string | number | boolean) => { - if (wsRef.current) { - const setValueMessage = { - action: 'set', - device: deviceName, - value: value, - }; - wsRef.current.send(JSON.stringify(setValueMessage)); - } + transport.send({ action: 'set', device: deviceName, value }); }, - [], + [transport], ); - /** - * Toggles the expanded state of a device in the UI. - * Used to show/hide additional device details or controls. - * @param deviceName - The name/identifier of the device to expand/collapse - */ const toggleExpand = useCallback((deviceName: string) => { setDevices((prevDevices) => ({ ...prevDevices, @@ -92,114 +47,62 @@ export default function useOphydDeviceSocket(deviceNameList: string[], wsUrl?: s useEffect(() => { if (hasRenderedOnlyOnce.current) { - //after the initial render, if the deviceNameList changes, reset the entire devices state - const initialDevices: OphydDevices = {}; - memoizedDeviceNames.forEach((deviceName) => { - initialDevices[deviceName] = { - name: deviceName, - value: '', - connected: false, - locked: false, - timestamp: 0, - expanded: false, - device: deviceName, - read_access: false, - write_access: false, - }; - }); - setDevices(initialDevices); + setDevices(initialDevices(memoizedDeviceNames)); } else { hasRenderedOnlyOnce.current = true; } }, [memoizedDeviceNames]); - // Initialize WebSocket connection useEffect(() => { - // Don't connect if no devices to subscribe to - if (memoizedDeviceNames.length === 0) { - // Close existing connection if devices list becomes empty - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - return; - } - - console.log('initializing WebSocket connection to', apiUrl); - const ws = new WebSocket(apiUrl); - wsRef.current = ws; - - // Open WebSocket connection and subscribe to devices - ws.onopen = () => { - console.log('WebSocket connection opened'); - memoizedDeviceNames.forEach((deviceName) => { - const subscribeMessage = { - action: 'subscribe', - device: deviceName, - }; - ws.send(JSON.stringify(subscribeMessage)); - }); - }; - - // Handle incoming WebSocket messages - ws.onmessage = (event) => { - try { - const message: - | MessageResponse - | ErrorResponse - | ValueUpdateResponse - | MetaUpdateResponse = JSON.parse(event.data); + if (memoizedDeviceNames.length === 0) return; - if ('sub_type' in message && message.sub_type === 'meta') { - //meta updates occur when we first subscribe to a device, or if the connection changes (lost or regained EPICS connection) - setDevices((prevDevices) => ({ - ...prevDevices, - [message.device]: { - ...prevDevices[message.device], - ...message, - //connected: message.connected, - //units: message.units, - min: message.lower_ctrl_limit, - max: message.upper_ctrl_limit, - }, - })); - } else if ('device' in message) { - //device is in the message on regular updates to the device value - const deviceName = message.device; - setDevices((prevDevices) => ({ - ...prevDevices, - [deviceName]: { - ...prevDevices[deviceName], - ...message, - }, - })); - } - if ('error' in message) { - console.error('WebSocket error message:', message.error); - } - } catch (error) { - console.error('Error parsing WebSocket message:', error); + // Register the listener before sending subscribe — the sim transport + // emits the initial meta+value synchronously, so a listener attached + // afterward would miss it. + const unsubscribeMessages = transport.onMessage((message) => { + if ('sub_type' in message && message.sub_type === 'meta') { + setDevices((prevDevices) => + prevDevices[message.device] + ? { + ...prevDevices, + [message.device]: { + ...prevDevices[message.device], + ...message, + min: message.lower_ctrl_limit, + max: message.upper_ctrl_limit, + }, + } + : prevDevices, + ); + } else if ('device' in message) { + const deviceName = message.device; + setDevices((prevDevices) => + prevDevices[deviceName] + ? { + ...prevDevices, + [deviceName]: { + ...prevDevices[deviceName], + ...message, + }, + } + : prevDevices, + ); + } else if ('error' in message) { + console.error('Ophyd device socket error:', message.error); } - }; - - // Handle WebSocket errors - ws.onerror = (error) => { - console.error('WebSocket error:', error); - }; + }); - // Handle WebSocket closure - ws.onclose = () => { - console.log('WebSocket connection closed'); - }; + for (const deviceName of memoizedDeviceNames) { + transport.send({ action: 'subscribe', device: deviceName }); + } - // Cleanup WebSocket connection on unmount return () => { - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; + unsubscribeMessages(); + for (const deviceName of memoizedDeviceNames) { + transport.send({ action: 'unsubscribe', device: deviceName }); } }; - }, [wsUrl, memoizedDeviceNames, apiUrl]); + }, [transport, memoizedDeviceNames]); return { devices, @@ -208,3 +111,21 @@ export default function useOphydDeviceSocket(deviceNameList: string[], wsUrl?: s toggleExpand, }; } + +function initialDevices(names: string[]): OphydDevices { + const map: OphydDevices = {}; + for (const deviceName of names) { + map[deviceName] = { + name: deviceName, + value: '', + connected: false, + locked: false, + timestamp: 0, + expanded: false, + device: deviceName, + read_access: false, + write_access: false, + }; + } + return map; +} diff --git a/src/api/ophyd/useOphydPVSocket.tsx b/src/api/ophyd/useOphydPVSocket.tsx index ba62e5bd..0adaa937 100644 --- a/src/api/ophyd/useOphydPVSocket.tsx +++ b/src/api/ophyd/useOphydPVSocket.tsx @@ -1,51 +1,32 @@ -import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; -import { Devices } from 'src/types/deviceControllerTypes'; -import { useOphydApiUrls } from '@/utils/apiUtils'; -import { - MessageResponse, - ErrorResponse, - ValueUpdateResponse, - MetaUpdateResponse, -} from '@/api/ophyd/ophydPVSocketTypes'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import type { Devices } from '@/types/deviceControllerTypes'; +import { useOphydPVTransport } from './OphydTransportContext'; +import type { OphydPVTransport } from './transport/types'; /** - * Custom hook for managing WebSocket connections to Ophyd devices. - * Provides real-time device state management and control functions. + * Manage subscriptions to a set of EPICS PVs and surface their values as a + * Devices map. * - * @param deviceNameList - Array of EPICS PVs to subscribe to - * @param wsUrl - Optional WebSocket URL. If not provided, will use environment variables or default to localhost:8001 - * @returns Object containing device states and control functions + * Connection lifecycle is delegated to a transport read from + * OphydTransportProvider — see [src/api/ophyd/OphydTransportProvider.tsx]. + * When no provider is mounted, a fallback transport pointed at the + * configured ophyd-websocket backend is created lazily (one per + * consumer-URL pair). To make two hook calls observe the same state, wrap + * the tree in an OphydTransportProvider with a shared transport. + * + * @param deviceNameList - EPICS PVs to subscribe to. + * @param wsUrl - Optional URL override that builds an ad-hoc WebSocket + * transport for this consumer only. Ignored when an OphydTransportProvider + * supplies a transport. */ export default function useOphydPVSocket(deviceNameList: string[], wsUrl?: string) { // eslint-disable-next-line react-hooks/exhaustive-deps - const memoizedDeviceNames = useMemo(() => deviceNameList, [JSON.stringify(deviceNameList)]); //device updates can retrigger the hook if inputs aren't memoized - const configWsUrl = useOphydApiUrls().getWsUrl('pv-socket'); - const apiUrl: string = wsUrl ?? configWsUrl; - const [devices, setDevices] = useState(() => { - const initialDevices: Devices = {}; - memoizedDeviceNames.forEach((deviceName) => { - initialDevices[deviceName] = { - name: deviceName, - value: '', - connected: false, - locked: false, - timestamp: 0, - expanded: false, - pv: deviceName, - read_access: false, - write_access: false, - }; - }); - return initialDevices; - }); - const wsRef = useRef(null); + const memoizedDeviceNames = useMemo(() => deviceNameList, [JSON.stringify(deviceNameList)]); + const transport: OphydPVTransport = useOphydPVTransport(wsUrl); + + const [devices, setDevices] = useState(() => initialDevices(memoizedDeviceNames)); const hasRenderedOnlyOnce = useRef(false); - /** - * Toggles the lock state of a device between locked and unlocked. - * When locked, the device UI will typically be disabled or read-only. - * @param deviceName - The name/identifier of the device to toggle - */ const toggleDeviceLock = useCallback((deviceName: string) => { setDevices((prevDevices) => ({ ...prevDevices, @@ -56,30 +37,13 @@ export default function useOphydPVSocket(deviceNameList: string[], wsUrl?: strin })); }, []); - /** - * Sends a device value update request through the WebSocket connection. - * @param deviceName - The name/identifier of the device to update - * @param value - The new value to set for the device (string, number, or boolean) - */ const handleSetValueRequest = useCallback( (deviceName: string, value: string | number | boolean) => { - if (wsRef.current) { - const setValueMessage = { - action: 'set', - pv: deviceName, - value: value, - }; - wsRef.current.send(JSON.stringify(setValueMessage)); - } + transport.send({ action: 'set', pv: deviceName, value }); }, - [], + [transport], ); - /** - * Toggles the expanded state of a device in the UI. - * Used to show/hide additional device details or controls. - * @param deviceName - The name/identifier of the device to expand/collapse - */ const toggleExpand = useCallback((deviceName: string) => { setDevices((prevDevices) => ({ ...prevDevices, @@ -92,113 +56,62 @@ export default function useOphydPVSocket(deviceNameList: string[], wsUrl?: strin useEffect(() => { if (hasRenderedOnlyOnce.current) { - //after the initial render, if the deviceNameList changes, reset the entire devices state - const initialDevices: Devices = {}; - memoizedDeviceNames.forEach((deviceName) => { - initialDevices[deviceName] = { - name: deviceName, - value: '', - connected: false, - locked: false, - timestamp: 0, - expanded: false, - pv: deviceName, - read_access: false, - write_access: false, - }; - }); - setDevices(initialDevices); + setDevices(initialDevices(memoizedDeviceNames)); } else { hasRenderedOnlyOnce.current = true; } }, [memoizedDeviceNames]); - // Initialize WebSocket connection useEffect(() => { - // Don't connect if no devices to subscribe to - if (memoizedDeviceNames.length === 0) { - // Close existing connection if devices list becomes empty - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - return; - } - - //console.log('initializing WebSocket connection to', apiUrl); - const ws = new WebSocket(apiUrl); - wsRef.current = ws; - - // Open WebSocket connection and subscribe to devices - ws.onopen = () => { - //console.log('WebSocket connection opened'); - memoizedDeviceNames.forEach((deviceName) => { - const subscribeMessage = { - action: 'subscribe', - pv: deviceName, - }; - ws.send(JSON.stringify(subscribeMessage)); - }); - }; + if (memoizedDeviceNames.length === 0) return; - // Handle incoming WebSocket messages - ws.onmessage = (event) => { - try { - const message: - | MessageResponse - | ErrorResponse - | ValueUpdateResponse - | MetaUpdateResponse = JSON.parse(event.data); - if ('sub_type' in message && message.sub_type === 'meta') { - //meta updates occur when we first subscribe to a device, or if the connection changes (lost or regained EPICS connection) - setDevices((prevDevices) => ({ - ...prevDevices, - [message.pv]: { - ...prevDevices[message.pv], - ...message, - //connected: message.connected, - //units: message.units, - min: message.lower_ctrl_limit, - max: message.upper_ctrl_limit, - }, - })); - } else if ('pv' in message) { - //pv is in the message on regular updates to the device value - const deviceName = message.pv; - setDevices((prevDevices) => ({ - ...prevDevices, - [deviceName]: { - ...prevDevices[deviceName], - ...message, - }, - })); - } - if ('error' in message) { - console.error('WebSocket error message:', message.error); - } - } catch (error) { - console.error('Error parsing WebSocket message:', error); + // Register the message listener before sending subscribe — the sim + // transport replays the initial meta+value synchronously inside + // send(subscribe), so a listener attached afterward would miss it. + const unsubscribeMessages = transport.onMessage((message) => { + if ('sub_type' in message && message.sub_type === 'meta') { + setDevices((prevDevices) => + prevDevices[message.pv] + ? { + ...prevDevices, + [message.pv]: { + ...prevDevices[message.pv], + ...message, + min: message.lower_ctrl_limit, + max: message.upper_ctrl_limit, + }, + } + : prevDevices, + ); + } else if ('pv' in message) { + const deviceName = message.pv; + setDevices((prevDevices) => + prevDevices[deviceName] + ? { + ...prevDevices, + [deviceName]: { + ...prevDevices[deviceName], + ...message, + }, + } + : prevDevices, + ); + } else if ('error' in message) { + console.error('Ophyd PV socket error:', message.error); } - }; - - // Handle WebSocket errors - ws.onerror = (error) => { - console.error('WebSocket error:', error); - }; + }); - // Handle WebSocket closure - ws.onclose = () => { - //console.log('WebSocket connection closed'); - }; + for (const deviceName of memoizedDeviceNames) { + transport.send({ action: 'subscribe', pv: deviceName }); + } - // Cleanup WebSocket connection on unmount return () => { - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; + unsubscribeMessages(); + for (const deviceName of memoizedDeviceNames) { + transport.send({ action: 'unsubscribe', pv: deviceName }); } }; - }, [wsUrl, memoizedDeviceNames, apiUrl]); + }, [transport, memoizedDeviceNames]); return { devices, @@ -207,3 +120,21 @@ export default function useOphydPVSocket(deviceNameList: string[], wsUrl?: strin toggleExpand, }; } + +function initialDevices(names: string[]): Devices { + const map: Devices = {}; + for (const deviceName of names) { + map[deviceName] = { + name: deviceName, + value: '', + connected: false, + locked: false, + timestamp: 0, + expanded: false, + pv: deviceName, + read_access: false, + write_access: false, + }; + } + return map; +} diff --git a/src/app/pages/AllComponentsPage.tsx b/src/app/pages/AllComponentsPage.tsx index bfb886cf..ebaaa90d 100644 --- a/src/app/pages/AllComponentsPage.tsx +++ b/src/app/pages/AllComponentsPage.tsx @@ -1,10 +1,16 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import ComponentViewerExampleReal from '@/features/ComponentViewer/ComponentViewerExampleReal'; import ComponentViewerExampleSim from '@/features/ComponentViewer/ComponentViewerExampleSim'; +import { OphydTransportProvider } from '@/api/ophyd/OphydTransportProvider'; +import { OphydSimProvider, createOphydSimTransport, hexapodBeamline } from '@/lib/ophyd-sim'; import { Cube, CubeTransparent } from '@phosphor-icons/react'; export default function AllComponentsPage() { const [mode, setMode] = useState<'real' | 'sim'>('sim'); + // The sim connects through useOphydSocket, so it needs a sim + // transport in context — otherwise it opens a real WebSocket. Scope the + // providers to the sim view only, so real mode keeps its live backend. + const hexapodTransport = useMemo(() => createOphydSimTransport(hexapodBeamline), []); return (
@@ -38,7 +44,15 @@ export default function AllComponentsPage() { - {mode === 'real' ? : } + {mode === 'real' ? ( + + ) : ( + + + + + + )}
); } diff --git a/src/app/pages/TestPage.tsx b/src/app/pages/TestPage.tsx index 14e07d61..d7bcb499 100644 --- a/src/app/pages/TestPage.tsx +++ b/src/app/pages/TestPage.tsx @@ -1,20 +1,3 @@ -import DeviceControllerBox from '@/components/DeviceControllerBox'; -import SignalMonitorPlotDevice from '@/components/SignalMonitorPlotDevice'; -import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; export default function TestPage() { - const { devices, handleSetValueRequest, toggleDeviceLock } = useOphydPVSocket([ - 'IOC:m1', - 'IOC:m1.RBV', - ]); - return ( -
- - -
- ); + return

Add your test components here in src/app/pages/TestPage.tsx

; } diff --git a/src/components/Camera/CameraCanvas.tsx b/src/components/Camera/CameraCanvas.tsx index 3f69b04e..491fbf07 100644 --- a/src/components/Camera/CameraCanvas.tsx +++ b/src/components/Camera/CameraCanvas.tsx @@ -14,6 +14,13 @@ export type CameraCanvasProps = { prefix?: string; /** WebSocket URL for the image stream. Falls back to the application default when omitted. */ wsUrl?: string; + /** Begin acquisition automatically on mount instead of waiting for the Acquire button. */ + autoStart?: boolean; + /** + * Externally control acquisition: pauses the stream when `true`, resumes when + * `false`, on each change. Leave undefined for fully manual control. + */ + paused?: boolean; }; export default function CameraCanvas(props: CameraCanvasProps) { @@ -31,64 +38,91 @@ export default function CameraCanvas(props: CameraCanvasProps) { const { canvasSize = 'medium' } = props; + // `socketStatus` is `'Open'` while streaming and `'closed'` otherwise. + const isAcquiring = socketStatus !== 'closed'; + return ( -
- {/* Canvas Element - background*/} - +
+
+ {/* Canvas Element - background*/} + - {/* FPS counter - top left */} -

{fps} fps

+ {/* FPS counter - top left */} +

{fps} fps

- + - {/* Overlay when disconnected */} -
-
-
-
- {socketError === null && ( -

- Websocket Disconnected -

- )} - {socketError && ( -

- {socketError} -

- )} -
- {phosphorIcons.plugs} + {/* Overlay on connection error (clean paused/idle state uses the Acquire button instead) */} +
+
+
+
+ {socketError === null && ( +

+ Websocket Disconnected +

+ )} + {socketError && ( +

+ {socketError} +

+ )} +
+ {phosphorIcons.plugs} +
-
-
-

- Connect? -

-
- {phosphorIcons.plugsConnected} +
+

+ Connect? +

+
+ {phosphorIcons.plugsConnected} +
+ + {/* Acquire / Pause - start and stop the image stream, below the image */} +
+ + +
); } diff --git a/src/components/Camera/hooks/useCameraCanvas.ts b/src/components/Camera/hooks/useCameraCanvas.ts index 683b5a93..934c02c0 100644 --- a/src/components/Camera/hooks/useCameraCanvas.ts +++ b/src/components/Camera/hooks/useCameraCanvas.ts @@ -1,8 +1,10 @@ import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { useOphydApiUrls } from '@/utils/apiUtils'; import { ophydSocketCameraPath } from '@/api/ophyd/socketPaths'; +import { useCameraSocketFactory } from '@/api/ophyd/OphydTransportContext'; import { CanvasSizes } from '../CameraCanvas'; import { getErrorMessage } from '@/utils/errorHandling'; +import type { CameraSocketLike } from '@/lib/ophyd-sim'; export type UseCameraCanvasProps = { imageArrayPV?: string; @@ -10,6 +12,14 @@ export type UseCameraCanvasProps = { canvasSize?: CanvasSizes; prefix?: string; wsUrl?: string; + /** Begin acquisition automatically on mount instead of waiting for the Acquire button. */ + autoStart?: boolean; + /** + * Externally control acquisition. When provided, the stream starts on `false` + * and pauses on `true` — each time the value changes. Leave undefined to keep + * acquisition fully manual (Acquire / Pause buttons). + */ + paused?: boolean; }; export function useCameraCanvas({ @@ -18,16 +28,18 @@ export function useCameraCanvas({ canvasSize = 'medium', prefix = '', wsUrl, + autoStart = false, + paused, }: UseCameraCanvasProps) { + const socketFactory = useCameraSocketFactory(); const canvasRef = useRef(null); const [fps, setFps] = useState('0'); const [socketStatus, setSocketStatus] = useState('closed'); const [isImageLogScale, setIsImageLogScale] = useState(true); const [socketError, setSocketError] = useState(null); - const ws = useRef(null); + const ws = useRef(null); const frameCount = useRef(null); const startTime = useRef(null); - const isInitialized = useRef(false); const configWsUrl = useOphydApiUrls().getWsUrl(ophydSocketCameraPath); const resolvedWsUrl = wsUrl ?? configWsUrl; @@ -167,7 +179,7 @@ export function useCameraCanvas({ try { const url = resolvedWsUrl; - ws.current = new WebSocket(url); + ws.current = socketFactory(url); } catch (error) { //This catch block only handles synchronous errors thrown during the WebSocket constructor call (invalid url, protocol), most other errors will be in the ws.onerror callback console.log({ error }); @@ -258,20 +270,42 @@ export function useCameraCanvas({ setSocketStatus('closed'); frameCount.current = 0; }; - }, [resolvedWsUrl, getImageArrayPV, getSizePVs, canvasSize]); + }, [resolvedWsUrl, getImageArrayPV, getSizePVs, canvasSize, socketFactory]); useEffect(() => { - if (!isInitialized.current) { - isInitialized.current = true; - startWebSocket(); - } + // By default the stream starts paused — the user begins acquisition via + // the Acquire button (startWebSocket). We only register the unmount + // cleanup so a socket opened during this mount is torn down on unmount. return () => { if (ws.current) { ws.current.close(); } }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // Empty dependency array for initialization only + }, []); // Empty dependency array — cleanup only; acquisition is user-driven + + // Optionally begin acquisition on mount. Guarded so it fires once and does + // not reconnect if startWebSocket's identity changes. + const hasAutoStarted = useRef(false); + useEffect(() => { + if (autoStart && !hasAutoStarted.current) { + hasAutoStarted.current = true; + startWebSocket(); + } + }, [autoStart, startWebSocket]); + + // Externally-controlled acquisition. Act only on an actual change of `paused` + // so unrelated re-renders (or startWebSocket identity changes) don't churn the + // socket, and manual Acquire/Pause between changes still works. + const prevPaused = useRef(undefined); + useEffect(() => { + if (paused === undefined || prevPaused.current === paused) return; + prevPaused.current = paused; + if (paused) { + closeWebSocket(); + } else { + startWebSocket(); + } + }, [paused, startWebSocket, closeWebSocket]); return { canvasRef, diff --git a/src/components/EndstationDisplay/EndstationDisplay.tsx b/src/components/EndstationDisplay/EndstationDisplay.tsx new file mode 100644 index 00000000..af0e9d7b --- /dev/null +++ b/src/components/EndstationDisplay/EndstationDisplay.tsx @@ -0,0 +1,62 @@ +import { cn } from '@/lib/utils'; +import useEndstationDisplay, { type UseEndstationDisplayOptions } from './useEndstationDisplay'; +import baseSvg from './assets/0_base.svg'; +import lightBlockedSvg from './assets/1_light_blocked.svg'; +import lightUnblockedSvg from './assets/1_light_unblocked.svg'; +import shutterSvg from './assets/2_shutter_moveable.svg'; +import beamstopSvg from './assets/3_beamstop_moveable.svg'; +import monoLightSvg from './assets/4_light_mono_moveable.svg'; +import SampleHolder from './SampleHolder'; + +type EndstationDisplayProps = UseEndstationDisplayOptions & { + /** Classes applied to the root container — use this to set the background + * color and/or sizing (e.g. `bg-white w-[485px]`). */ + className?: string; +}; + +/** Shared positioning for every full-canvas stacked layer. */ +const LAYER_CLASS = 'pointer-events-none absolute inset-0 h-full w-full select-none'; + +/** + * Layered SVG graphic of the endstation. Stacks the beamline assets and drives + * the light, shutter, and beamstop layers from live ophyd device state via + * [useEndstationDisplay]. Purely presentational — all logic lives in the hook. + */ +export default function EndstationDisplay({ className, ...hookOptions }: EndstationDisplayProps) { + const { lightLayer, monoVisible, shutterTransform, beamstopTransform } = + useEndstationDisplay(hookOptions); + + return ( +
+ {/* z0 — static base diagram */} + Endstation diagram + {/* z1 — beam, blocked or reaching through */} + + {/* z2 — shutter, slides out of the beam when open */} + + {/* z3 — beamstop, tracks its X/Y motor readbacks */} + + {/* z4 — mono light, only while the beam is unblocked */} + {monoVisible && } + {/* z5 — sample holder, tracks its X/Y motor readbacks */} + +
+ ); +} diff --git a/src/components/EndstationDisplay/SampleHolder.tsx b/src/components/EndstationDisplay/SampleHolder.tsx new file mode 100644 index 00000000..90293ebd --- /dev/null +++ b/src/components/EndstationDisplay/SampleHolder.tsx @@ -0,0 +1,75 @@ +import { useMemo } from 'react'; +import { cn } from '@/lib/utils'; +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; +import sampleHolderSvg from './assets/5_sample_holder.svg'; + +/** Intrinsic viewBox of the endstation canvas. X/Y offsets in SVG units convert + * to percentage transforms against these so motion scales with the rendered + * container width rather than the small graphic's own size. Matches the + * constants in [useEndstationDisplay]. */ +const VIEW_W = 485; +const VIEW_H = 215; + +/** Placement of the sample-holder graphic (a small standalone SVG, not a + * full-canvas layer) as percentages of the container. Its authored 33×29 + * viewBox maps to ~6.8% width; left/top seat it on the beam between the mono + * and the beamstop. Nudge these to reposition the resting spot. */ +const SAMPLE_HOLDER_STYLE = { left: '53.2%', top: '50%', width: '6.8%' } as const; + +export type SampleHolderProps = { + /** Sample X readback PV (mm). Defaults to the `beamstopBeamline` motor. */ + sampleXPV?: string; + /** Sample Y readback PV (mm). Defaults to the `beamstopBeamline` motor. */ + sampleYPV?: string; + /** SVG units the holder travels per mm of motor motion. Calibrate visually; + * defaults to 2. */ + pxPerMm?: number; +}; + +/** Convert an (x, y) offset in SVG units to a percentage `translate()` relative + * to the full canvas. Applied to a full-size wrapper so the percentages track + * the container, not the small graphic inside it. */ +function svgTranslate(xSvg: number, ySvg: number): string { + const xPct = (xSvg / VIEW_W) * 100; + const yPct = (ySvg / VIEW_H) * 100; + return `translate(${xPct}%, ${yPct}%)`; +} + +/** + * Sample-holder graphic that tracks its X/Y motor readbacks. Subscribes to the + * sample stage PVs and slides the holder proportionally across the endstation + * canvas. Mirrors the beamstop tracking in [useEndstationDisplay]; render it as + * a stacked layer inside [EndstationDisplay]. + */ +export default function SampleHolder({ + sampleXPV = 'bl531:sample_x_mm.RBV', + sampleYPV = 'bl531:sample_y_mm.RBV', + pxPerMm = 2, +}: SampleHolderProps = {}) { + const deviceList = useMemo(() => [sampleXPV, sampleYPV], [sampleXPV, sampleYPV]); + const { devices } = useOphydPVSocket(deviceList); + + const transform = useMemo(() => { + const xMm = parseFloat(devices[sampleXPV]?.value as string) || 0; + const yMm = parseFloat(devices[sampleYPV]?.value as string) || 0; + // Positive X mm slides right; positive Y mm slides up (negative SVG y). + return svgTranslate(xMm * pxPerMm, -yMm * pxPerMm); + }, [devices, sampleXPV, sampleYPV, pxPerMm]); + + return ( +
+ Sample holder +
+ ); +} diff --git a/src/components/EndstationDisplay/assets/0_base.svg b/src/components/EndstationDisplay/assets/0_base.svg new file mode 100644 index 00000000..ff3b4e86 --- /dev/null +++ b/src/components/EndstationDisplay/assets/0_base.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/components/EndstationDisplay/assets/1_light_blocked.svg b/src/components/EndstationDisplay/assets/1_light_blocked.svg new file mode 100644 index 00000000..ad620bea --- /dev/null +++ b/src/components/EndstationDisplay/assets/1_light_blocked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/EndstationDisplay/assets/1_light_unblocked.svg b/src/components/EndstationDisplay/assets/1_light_unblocked.svg new file mode 100644 index 00000000..9959bea1 --- /dev/null +++ b/src/components/EndstationDisplay/assets/1_light_unblocked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/EndstationDisplay/assets/2_shutter_moveable.svg b/src/components/EndstationDisplay/assets/2_shutter_moveable.svg new file mode 100644 index 00000000..b2243de4 --- /dev/null +++ b/src/components/EndstationDisplay/assets/2_shutter_moveable.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/EndstationDisplay/assets/3_beamstop_moveable.svg b/src/components/EndstationDisplay/assets/3_beamstop_moveable.svg new file mode 100644 index 00000000..2b14c7f5 --- /dev/null +++ b/src/components/EndstationDisplay/assets/3_beamstop_moveable.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/EndstationDisplay/assets/4_light_mono_moveable.svg b/src/components/EndstationDisplay/assets/4_light_mono_moveable.svg new file mode 100644 index 00000000..c4dd3c50 --- /dev/null +++ b/src/components/EndstationDisplay/assets/4_light_mono_moveable.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/EndstationDisplay/assets/5_sample_holder.svg b/src/components/EndstationDisplay/assets/5_sample_holder.svg new file mode 100644 index 00000000..2e2e7610 --- /dev/null +++ b/src/components/EndstationDisplay/assets/5_sample_holder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/EndstationDisplay/useEndstationDisplay.ts b/src/components/EndstationDisplay/useEndstationDisplay.ts new file mode 100644 index 00000000..346a41a9 --- /dev/null +++ b/src/components/EndstationDisplay/useEndstationDisplay.ts @@ -0,0 +1,107 @@ +import { useMemo } from 'react'; +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; + +/** Intrinsic viewBox of every layer SVG. Offsets in SVG units convert to + * percentage transforms against these so motion scales with the rendered + * container width. */ +const VIEW_W = 485; +const VIEW_H = 215; + +/** Constant upward nudge (SVG units) applied to the shutter's resting position. */ +const SHUTTER_BASE_Y = -16; + +/** Constant nudge (SVG units) that seats the beamstop marker on the beam at the + * scenario's resting motor position. Without it the beamstop's authored spot is + * pulled up-left by the default motor offset and the beam overshoots it. */ +const BEAMSTOP_BASE_X = -5; +const BEAMSTOP_BASE_Y = 3; + +/** Which beam graphic to show: blocked stops short of the sample, unblocked + * reaches through. */ +export type LightLayer = 'blocked' | 'unblocked'; + +export type UseEndstationDisplayOptions = { + /** Shutter analog-output PV. Defaults to the `beamstopBeamline` shutter. */ + shutterPV?: string; + /** PV value that means the shutter is open (beam passes). Defaults to 0. */ + shutterOpenValue?: number; + /** Beamstop X readback PV (mm). Defaults to the `beamstopBeamline` motor. */ + beamstopXPV?: string; + /** Beamstop Y readback PV (mm). Defaults to the `beamstopBeamline` motor. */ + beamstopYPV?: string; + /** SVG units the beamstop layer travels per mm of motor motion. Calibrate + * visually; defaults to 2. */ + pxPerMm?: number; + /** SVG units the shutter layer slides out of the beam when open. Calibrate + * visually; defaults to 24. */ + shutterOpenOffset?: number; +}; + +export type EndstationDisplayState = { + /** Beam graphic to render for layer 1. */ + lightLayer: LightLayer; + /** Whether the mono-light layer (layer 4) is visible — only when unblocked. */ + monoVisible: boolean; + /** CSS `transform` for the shutter layer. */ + shutterTransform: string; + /** CSS `transform` for the beamstop layer. */ + beamstopTransform: string; +}; + +/** Convert an (x, y) offset in SVG units to a percentage `translate()` so the + * layer moves proportionally regardless of the rendered size. */ +function svgTranslate(xSvg: number, ySvg: number): string { + const xPct = (xSvg / VIEW_W) * 100; + const yPct = (ySvg / VIEW_H) * 100; + return `translate(${xPct}%, ${yPct}%)`; +} + +/** + * Derive the visual state of the endstation from live ophyd device values. + * + * Subscribes to the shutter and beamstop readback PVs and maps them to the + * layer state consumed by EndstationDisplay. All logic lives here; the + * component only renders. Mirrors the value-parsing pattern in + * [src/components/Shutter.tsx]. + */ +export default function useEndstationDisplay({ + shutterPV = 'bl531:LJT4:1:AO0', + shutterOpenValue = 0, + beamstopXPV = 'bl531_xps2:beamstop_x_mm.RBV', + beamstopYPV = 'bl531_xps2:beamstop_y_mm.RBV', + pxPerMm = 2, + shutterOpenOffset = 24, +}: UseEndstationDisplayOptions = {}): EndstationDisplayState { + const deviceList = useMemo( + () => [shutterPV, beamstopXPV, beamstopYPV], + [shutterPV, beamstopXPV, beamstopYPV], + ); + const { devices } = useOphydPVSocket(deviceList); + + const isOpen = useMemo(() => { + const value = parseFloat(devices[shutterPV]?.value as string); + return value === shutterOpenValue; + }, [devices, shutterPV, shutterOpenValue]); + + const beamstopTransform = useMemo(() => { + const xMm = parseFloat(devices[beamstopXPV]?.value as string) || 0; + const yMm = parseFloat(devices[beamstopYPV]?.value as string) || 0; + // Positive X mm slides right; positive Y mm slides up (negative SVG y). + // The base nudge seats the resting position on the beam. + return svgTranslate(BEAMSTOP_BASE_X + xMm * pxPerMm, BEAMSTOP_BASE_Y - yMm * pxPerMm); + }, [devices, beamstopXPV, beamstopYPV, pxPerMm]); + + // Sit the shutter a little above its authored position, then slide it down + // and out of the beam when open. + const shutterTransform = useMemo( + () => svgTranslate(0, SHUTTER_BASE_Y + (isOpen ? shutterOpenOffset : 0)), + [isOpen, shutterOpenOffset], + ); + + return { + lightLayer: isOpen ? 'unblocked' : 'blocked', + monoVisible: isOpen, + shutterTransform, + beamstopTransform, + }; +} diff --git a/src/components/FinchHeader.tsx b/src/components/FinchHeader.tsx index 6dc5500d..a058d5c8 100644 --- a/src/components/FinchHeader.tsx +++ b/src/components/FinchHeader.tsx @@ -26,7 +26,7 @@ export type FinchHeaderProps = { * Pass `logoIcon` to render a custom React element as the logo. * If only `logoUrl` is provided, an `` is rendered instead. */ -export default function HubHeader({ +export default function FinchHeader({ title = 'BEAMLINE APP', logoUrl = 'https://img.icons8.com/?size=100&id=11743&format=png&color=000000', logoIcon, diff --git a/src/components/OphydSocketViewer/OphydSocketViewer.tsx b/src/components/OphydSocketViewer/OphydSocketViewer.tsx new file mode 100644 index 00000000..787b3881 --- /dev/null +++ b/src/components/OphydSocketViewer/OphydSocketViewer.tsx @@ -0,0 +1,8 @@ +export default function OphydSocketViewer() { + return ( +
+

Ophyd Socket View

+

Print out messages here.

+
+ ); +} diff --git a/src/components/OphydSocketViewer/useOphydSocketViewer.ts b/src/components/OphydSocketViewer/useOphydSocketViewer.ts new file mode 100644 index 00000000..18b44399 --- /dev/null +++ b/src/components/OphydSocketViewer/useOphydSocketViewer.ts @@ -0,0 +1,219 @@ +//Instructions +// This here is a copy-paste from useOphydPVSocket.tsx +// This contains alll the code we need to talk to Ophyd Websocket for EPICS pvs. +// Modify this source code so that we return an additional 'messages' state variable +// that can then be dispalyed in 'OphydSocketViewer' + +// To do: +// Modify this code so that first, it prints out every message going +// out to the websocket, and every message that comes back from the websocket. + +// Once you have that done, the next step is to save each message into a React +// state variable, which is returned from this hook. +//After that's done, you can wire this hook up in TestPage.tsx to replace useOphydPVSocket + +import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; +import { Devices } from 'src/types/deviceControllerTypes'; +import { useOphydApiUrls } from '@/utils/apiUtils'; +import { + MessageResponse, + ErrorResponse, + ValueUpdateResponse, + MetaUpdateResponse, +} from '@/api/ophyd/ophydPVSocketTypes'; + +/** + * Custom hook for managing WebSocket connections to Ophyd devices. + * Provides real-time device state management and control functions. + * + * @param deviceNameList - Array of EPICS PVs to subscribe to + * @param wsUrl - Optional WebSocket URL. If not provided, will use environment variables or default to localhost:8001 + * @returns Object containing device states, control functions, and message history + */ +export default function useOphydPVSocket(deviceNameList: string[], wsUrl?: string) { + // eslint-disable-next-line react-hooks/exhaustive-deps + const memoizedDeviceNames = useMemo(() => deviceNameList, [JSON.stringify(deviceNameList)]); + const configWsUrl = useOphydApiUrls().getWsUrl('pv-socket'); + const apiUrl: string = wsUrl ?? configWsUrl; + + const [devices, setDevices] = useState(() => { + const initialDevices: Devices = {}; + memoizedDeviceNames.forEach((deviceName) => { + initialDevices[deviceName] = { + name: deviceName, + value: '', + connected: false, + locked: false, + timestamp: 0, + expanded: false, + pv: deviceName, + read_access: false, + write_access: false, + }; + }); + return initialDevices; + }); + + // --- NEW STATE FOR MESSAGES --- + const [messages, setMessages] = useState<{ direction: 'SENT' | 'RCVD'; data: string }[]>([]); + + const wsRef = useRef(null); + const hasRenderedOnlyOnce = useRef(false); + + /** + * Helper to log and store websocket messages + */ + const logMessage = useCallback((direction: 'SENT' | 'RCVD', data: string) => { + console.log(`[WebSocket ${direction}]:`, data); + setMessages((prev) => [...prev, { direction, data }]); + }, []); + + const toggleDeviceLock = useCallback((deviceName: string) => { + setDevices((prevDevices) => ({ + ...prevDevices, + [deviceName]: { + ...prevDevices[deviceName], + locked: !prevDevices[deviceName].locked, + }, + })); + }, []); + + const handleSetValueRequest = useCallback( + (deviceName: string, value: string | number | boolean) => { + if (wsRef.current) { + const setValueMessage = { + action: 'set', + pv: deviceName, + value: value, + }; + const payload = JSON.stringify(setValueMessage); + + // Log outgoing message + logMessage('SENT', payload); + wsRef.current.send(payload); + } + }, + [logMessage], + ); + + const toggleExpand = useCallback((deviceName: string) => { + setDevices((prevDevices) => ({ + ...prevDevices, + [deviceName]: { + ...prevDevices[deviceName], + expanded: !prevDevices[deviceName].expanded, + }, + })); + }, []); + + useEffect(() => { + if (hasRenderedOnlyOnce.current) { + const initialDevices: Devices = {}; + memoizedDeviceNames.forEach((deviceName) => { + initialDevices[deviceName] = { + name: deviceName, + value: '', + connected: false, + locked: false, + timestamp: 0, + expanded: false, + pv: deviceName, + read_access: false, + write_access: false, + }; + }); + setDevices(initialDevices); + } else { + hasRenderedOnlyOnce.current = true; + } + }, [memoizedDeviceNames]); + + useEffect(() => { + if (memoizedDeviceNames.length === 0) { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + return; + } + + const ws = new WebSocket(apiUrl); + wsRef.current = ws; + + ws.onopen = () => { + memoizedDeviceNames.forEach((deviceName) => { + const subscribeMessage = { + action: 'subscribe', + pv: deviceName, + }; + const payload = JSON.stringify(subscribeMessage); + + // Log outgoing subscription messages + logMessage('SENT', payload); + ws.send(payload); + }); + }; + + ws.onmessage = (event) => { + // Log incoming message + logMessage('RCVD', event.data); + + try { + const message: + | MessageResponse + | ErrorResponse + | ValueUpdateResponse + | MetaUpdateResponse = JSON.parse(event.data); + if ('sub_type' in message && message.sub_type === 'meta') { + setDevices((prevDevices) => ({ + ...prevDevices, + [message.pv]: { + ...prevDevices[message.pv], + ...message, + min: message.lower_ctrl_limit, + max: message.upper_ctrl_limit, + }, + })); + } else if ('pv' in message) { + const deviceName = message.pv; + setDevices((prevDevices) => ({ + ...prevDevices, + [deviceName]: { + ...prevDevices[deviceName], + ...message, + }, + })); + } + if ('error' in message) { + console.error('WebSocket error message:', message.error); + } + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + ws.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + ws.onclose = () => { + // Optional: log closure to messages state + // logMessage('RCVD', 'Connection Closed'); + }; + + return () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + }; + }, [wsUrl, memoizedDeviceNames, apiUrl, logMessage]); + + return { + devices, + messages, // Now returning the history of messages + toggleDeviceLock, + handleSetValueRequest, + toggleExpand, + }; +} diff --git a/src/components/Shutter.tsx b/src/components/Shutter.tsx index a123a2e1..07690e25 100644 --- a/src/components/Shutter.tsx +++ b/src/components/Shutter.tsx @@ -1,8 +1,11 @@ -import { useEffect, useMemo, useState, useRef } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useState, useRef } from 'react'; +import { createPortal } from 'react-dom'; import { CaretDown, Check } from '@phosphor-icons/react'; import useOphydPVSocket from '../api/ophyd/useOphydPVSocket'; import { cn } from '@/lib/utils'; +type ShutterState = 'open' | 'closed' | 'unknown' | 'disconnected'; + type ShutterProps = { /** EPICS process variable name for the shutter device. Defaults to 'bl531:LJT4:1:AO0'. */ pv?: string; @@ -12,6 +15,8 @@ type ShutterProps = { valueWhenClosed?: number; /** Additional CSS classes applied to the root container div. */ className?: string; + /** Additional CSS classes applied to the inner content row (e.g. to adjust padding/height). */ + classNameContent?: string; /** Additional CSS classes applied to the dropdown control panel. */ classNameDropdown?: string; /** Additional CSS classes applied to the status circle when the shutter is open. */ @@ -22,11 +27,43 @@ type ShutterProps = { classNameStatusCircleDisconnected?: string; }; +/** Per-state presentation: the status circle, the state pill, and the readable label. */ +const STATE_STYLES: Record< + ShutterState, + { circle: string; pill: string; label: string; status: string } +> = { + open: { + circle: 'bg-green-500 animate-pulse', + pill: 'bg-green-100 text-green-700', + label: 'Open', + status: 'HUTCH SHUTTER OPEN', + }, + closed: { + circle: 'bg-yellow-300', + pill: 'bg-yellow-100 text-yellow-800', + label: 'Closed', + status: 'HUTCH SHUTTER CLOSED', + }, + unknown: { + circle: 'bg-gray-400', + pill: 'bg-gray-100 text-gray-600', + label: 'Unknown', + status: 'HUTCH SHUTTER UNKNOWN', + }, + disconnected: { + circle: 'bg-gray-400', + pill: 'bg-gray-100 text-gray-600', + label: 'Disconnected', + status: 'HUTCH SHUTTER DISCONNECTED', + }, +}; + export default function Shutter({ pv = 'bl531:LJT4:1:AO0', valueWhenOpen = 0, valueWhenClosed = 5, className = '', + classNameContent = '', classNameDropdown = '', classNameStatusCircleOpen = '', classNameStatusCircleClosed = '', @@ -35,13 +72,47 @@ export default function Shutter({ }: ShutterProps) { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); + const menuRef = useRef(null); + // Screen-space position for the portaled menu (fixed-positioned). + const [menuPos, setMenuPos] = useState<{ top: number; left: number; width: number }>({ + top: 0, + left: 0, + width: 0, + }); const deviceList = useMemo(() => [pv], [pv]); const { devices, handleSetValueRequest } = useOphydPVSocket(deviceList); - // Close dropdown when clicking outside + // The menu is portaled to so it overlays sibling components instead of + // being clipped by an ancestor's overflow/stacking context. That means we must + // position it manually from the trigger's on-screen rect. + useLayoutEffect(() => { + if (!isDropdownOpen) return; + + const updatePosition = () => { + const rect = dropdownRef.current?.getBoundingClientRect(); + if (!rect) return; + setMenuPos({ top: rect.bottom + 4, left: rect.left, width: rect.width }); + }; + + updatePosition(); + window.addEventListener('resize', updatePosition); + window.addEventListener('scroll', updatePosition, true); + return () => { + window.removeEventListener('resize', updatePosition); + window.removeEventListener('scroll', updatePosition, true); + }; + }, [isDropdownOpen]); + + // Close dropdown when clicking outside the trigger or the portaled menu. useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + const target = event.target as Node; + if ( + dropdownRef.current && + !dropdownRef.current.contains(target) && + menuRef.current && + !menuRef.current.contains(target) + ) { setIsDropdownOpen(false); } }; @@ -63,55 +134,36 @@ export default function Shutter({ [currentValue, valueWhenClosed], ); - const getStatusCircle = () => { - if (shutter?.connected === false) { - // Disconnected state - gray circle, non-flashing - return ( -
- ); - } - if (isOpen) { - return ( -
- ); - } else if (isClosed) { - return ( -
- ); - } else { - // Unknown state - gray circle, non-flashing - return ( -
- ); - } - }; + const state: ShutterState = useMemo(() => { + if (shutter?.connected === false) return 'disconnected'; + if (isOpen) return 'open'; + if (isClosed) return 'closed'; + return 'unknown'; + }, [shutter?.connected, isOpen, isClosed]); - const getStatusText = () => { - if (shutter?.connected === false) return 'HUTCH SHUTTER DISCONNECTED'; - if (isOpen) return 'HUTCH SHUTTER OPEN'; - if (isClosed) return 'HUTCH SHUTTER CLOSED'; - return 'HUTCH SHUTTER UNKNOWN'; - }; + const styles = STATE_STYLES[state]; + + // Circle color is state-driven; the per-state className overrides keep the + // existing customization points working. + const circleOverride = + state === 'open' + ? classNameStatusCircleOpen + : state === 'closed' + ? classNameStatusCircleClosed + : classNameStatusCircleDisconnected; + + // Formatted PV readout. Handles a 0 value (which is falsy) instead of + // dropping to '--', trims pointless trailing decimals, and appends units. + const valueLabel = useMemo(() => { + const raw = shutter?.value; + if (shutter?.connected === false || raw === undefined || raw === null || raw === '') { + return '--'; + } + const num = Number(raw); + if (!Number.isFinite(num)) return String(raw); + const formatted = Number.isInteger(num) ? num.toString() : num.toFixed(2); + return shutter?.units ? `${formatted} ${shutter.units}` : formatted; + }, [shutter?.value, shutter?.connected, shutter?.units]); const handleDropdownToggle = () => { setIsDropdownOpen(!isDropdownOpen); @@ -128,20 +180,41 @@ export default function Shutter({ }; return ( -
-
- {getStatusCircle()} -
- {getStatusText()} +
+
+
+
+
+ {styles.status} + + {styles.label} + +
- {pv}: {shutter.value || '--'} - {shutter?.connected === false && ' (Disconnected)'} + {pv} + · + {valueLabel}
- {/* Dropdown Menu */} - {isDropdownOpen && ( -
- - -
- )} + + +
, + document.body, + )}
); } diff --git a/src/components/TableDeviceController.tsx b/src/components/TableDeviceController.tsx index b16cba55..c1fa383a 100644 --- a/src/components/TableDeviceController.tsx +++ b/src/components/TableDeviceController.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import { Lock, LockOpen } from '@phosphor-icons/react'; +import { Lock, LockOpen, CaretDown } from '@phosphor-icons/react'; import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from './ui/table'; import ControllerAbsoluteMove from './ControllerAbsoluteMove'; import ControllerRelativeMove from './ControllerRelativeMove'; @@ -16,6 +16,12 @@ export type TableDeviceControllerProps = { toggleDeviceLock: (deviceName: string, locked: boolean) => void; /** Called to toggle the expanded state for a device row, showing or hiding its raw JSON data. */ toggleExpand: (deviceName: string) => void; + /** + * When true, the Relative Move column starts collapsed (hidden on render) and + * the column header becomes a toggle that reveals it. Defaults to false, so + * the relative-move controls are always shown. + */ + collapsibleRelativeMove?: boolean; /** Additional CSS classes applied to the root container. */ className?: string; }; @@ -25,11 +31,15 @@ export default function TableDeviceController({ handleSetValueRequest, toggleDeviceLock, toggleExpand, + collapsibleRelativeMove = false, className, ...props }: TableDeviceControllerProps) { // State to track flashing rows const [flashingRows, setFlashingRows] = useState>({}); + // Relative Move column visibility. When collapsible, it starts hidden; when + // not, it is permanently open (no toggle rendered). + const [isRelativeMoveOpen, setIsRelativeMoveOpen] = useState(!collapsibleRelativeMove); useEffect(() => { const updatedFlashingRows: Record = {}; @@ -68,7 +78,25 @@ export default function TableDeviceController({ Absolute Move - Relative Move + {collapsibleRelativeMove ? ( + + ) : ( + 'Relative Move' + )} @@ -123,19 +151,23 @@ export default function TableDeviceController({ /> - - input !== null && - handleSetValueRequest(deviceName, input) - } - inputLabel={device.units && device.units.slice(0, 3)} - currentValue={ - typeof device.value === 'number' ? device.value : null - } - classNameInput="bg-sky-200 shadow-inner rounded-md" - locked={device.locked} - /> + {isRelativeMoveOpen && ( + + input !== null && + handleSetValueRequest(deviceName, input) + } + inputLabel={device.units && device.units.slice(0, 3)} + currentValue={ + typeof device.value === 'number' + ? device.value + : null + } + classNameInput="bg-sky-200 shadow-inner rounded-md" + locked={device.locked} + /> + )} ); diff --git a/src/components/TableDeviceControllerWithRBV.tsx b/src/components/TableDeviceControllerWithRBV.tsx new file mode 100644 index 00000000..b2a6132d --- /dev/null +++ b/src/components/TableDeviceControllerWithRBV.tsx @@ -0,0 +1,205 @@ +import { useEffect, useState } from 'react'; + +import { Lock, LockOpen } from '@phosphor-icons/react'; +import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from './ui/table'; +import ControllerAbsoluteMove from './ControllerAbsoluteMove'; +import ControllerRelativeMove from './ControllerRelativeMove'; +import SelectDropdown from './SelectDropdown'; +import { Devices, Device } from '@/types/deviceControllerTypes'; +import { cn } from '@/lib/utils'; + +const ABSOLUTE_MOVE = 'Absolute Move'; +const RELATIVE_MOVE = 'Relative Move'; +type MoveMode = typeof ABSOLUTE_MOVE | typeof RELATIVE_MOVE; + +export type TableDeviceControllerWithRBVProps = { + /** Map of device names to their current state objects. Each entry renders as one table row. */ + devices: Devices; + devicesRBV: Devices; + /** Called when the user submits an absolute or relative move value for a device. */ + handleSetValueRequest: (deviceName: string, value: number) => void; + /** Called to toggle the locked state for a device, enabling or disabling its move controls. */ + toggleDeviceLock: (deviceName: string, locked: boolean) => void; + /** Called to toggle the expanded state for a device row, showing or hiding its raw JSON data. */ + toggleExpand: (deviceName: string) => void; + /** + * When true, the absolute- and relative-move controls share a single "Move" + * column whose header is a dropdown for switching between the two, so the + * relative-move UI never reserves empty space. Defaults to false, which + * renders Absolute Move and Relative Move as two always-visible columns. + */ + collapsibleRelativeMove?: boolean; + /** Additional CSS classes applied to the root container. */ + className?: string; +}; + +export default function TableDeviceControllerWithRBV({ + devices, + devicesRBV, + handleSetValueRequest, + toggleDeviceLock, + toggleExpand, + collapsibleRelativeMove = false, + className, + ...props +}: TableDeviceControllerWithRBVProps) { + // State to track flashing rows + const [flashingRows, setFlashingRows] = useState>({}); + // In collapsible mode, which move control the single "Move" column shows. + const [moveMode, setMoveMode] = useState(ABSOLUTE_MOVE); + + const getFormattedValue = (device: Device) => { + //Returns the formatted valiues of devices RBV, If it exists in device RBV + // Otherwise, just returns that devices formatted value. + if (device.pv + '.RBV' in devicesRBV) { + const readbackDevice = devicesRBV[device.pv + '.RBV']; + return `${typeof readbackDevice.value === 'number' ? readbackDevice.value.toPrecision(4) : readbackDevice.value} ${readbackDevice.units ? readbackDevice.units.slice(0, 3) : 'n/a'}`; + } else { + return `${typeof device.value === 'number' ? device.value.toPrecision(4) : device.value} ${device.units ? device.units.slice(0, 3) : 'n/a'}`; + } + }; + useEffect(() => { + const updatedFlashingRows: Record = {}; + const currentTime = Date.now() / 1000; // Current time in seconds + + Object.keys(devices).forEach((deviceName) => { + const device = devices[deviceName]; + if (device.timestamp && currentTime - device.timestamp <= 0.03) { + updatedFlashingRows[deviceName] = true; + + // Remove the flash effect after 1 second timeout, this timeout needs to match the animation duration of the tailwind class to avoid flutter effect + setTimeout(() => { + setFlashingRows((prev) => ({ + ...prev, + [deviceName]: false, + })); + }, 500); + } + }); + + setFlashingRows(updatedFlashingRows); + }, [devices]); + + const renderAbsoluteMove = (deviceName: string, device: Device) => ( + input !== null && handleSetValueRequest(deviceName, input)} + inputLabel={device.units && device.units.slice(0, 3)} + classNameInput="bg-sky-200 shadow-inner rounded-md" + locked={device.locked} + /> + ); + + const renderRelativeMove = (deviceName: string, device: Device) => ( + input !== null && handleSetValueRequest(deviceName, input)} + inputLabel={device.units && device.units.slice(0, 3)} + currentValue={typeof device.value === 'number' ? device.value : null} + classNameInput="bg-sky-200 shadow-inner rounded-md" + resultantTextClassName="hidden" + locked={device.locked} + /> + ); + + return ( +
+ + + + Device Name + + Current Value + + {collapsibleRelativeMove ? ( + // Single move column: the header dropdown swaps the cell + // controls between absolute and relative, so the unused + // mode never takes up space. + + setMoveMode(value as MoveMode)} + triggerClassName="ml-0 mr-auto" + /> + + ) : ( + <> + + {ABSOLUTE_MOVE} + + + {RELATIVE_MOVE} + + + )} + + + + {Object.keys(devices).map((deviceName) => { + const device = devices[deviceName]; + return ( + + toggleExpand(deviceName)} + > + <> +
+ +

{deviceName}

+
+ {device.expanded && ( +
+                                                {JSON.stringify(device, null, 2)}
+                                            
+ )} + +
+ + {getFormattedValue(device)} + + {collapsibleRelativeMove ? ( + + {moveMode === ABSOLUTE_MOVE + ? renderAbsoluteMove(deviceName, device) + : renderRelativeMove(deviceName, device)} + + ) : ( + <> + + {renderAbsoluteMove(deviceName, device)} + + + {renderRelativeMove(deviceName, device)} + + + )} +
+ ); + })} +
+
+
+ ); +} diff --git a/src/features/Beamstop.tsx b/src/features/Beamstop.tsx index 4baa5927..b702f37b 100644 --- a/src/features/Beamstop.tsx +++ b/src/features/Beamstop.tsx @@ -2,6 +2,7 @@ import { useState, useMemo, useEffect } from 'react'; import SignalMonitorPlotPV from '@/components/SignalMonitorPlotPV'; import DeviceControllerBox from '@/components/DeviceControllerBox'; +import EnergyVsCurrentPlotPV from '@/features/EnergyVsCurrentPlotPV'; import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; import Button from '@/components/Button'; @@ -11,10 +12,17 @@ export type BeamstopProps = { beamstopXName: string; beamstopYName: string; beamstopCurrentName: string; + /** + * Optional writable beam-energy PV. When provided, an energy control and a + * live Energy-vs-Current plot are rendered. Selecting an energy shifts the + * beam (via the DCM Bragg angle), changing the diode current. + */ + beamstopEnergyName?: string; beamstopXIcon?: JSX.Element; beamstopYIcon?: JSX.Element; beamstopXTitle?: string; beamstopYTitle?: string; + beamstopEnergyTitle?: string; enableBestOption?: boolean; stackVertical?: boolean; }; @@ -23,15 +31,21 @@ export default function Beamstop({ beamstopXName, beamstopYName, beamstopCurrentName, + beamstopEnergyName, beamstopXIcon = deviceIcons.beamstopX, beamstopYIcon = deviceIcons.beamstopY, beamstopXTitle, beamstopYTitle, + beamstopEnergyTitle = 'Beam Energy', enableBestOption, stackVertical = true, }: BeamstopProps) { const beamstopXNameRBV = useMemo(() => beamstopXName + '.RBV', [beamstopXName]); const beamstopYNameRBV = useMemo(() => beamstopYName + '.RBV', [beamstopYName]); + const beamstopEnergyRBV = useMemo( + () => (beamstopEnergyName ? beamstopEnergyName + '.RBV' : undefined), + [beamstopEnergyName], + ); const deviceNameList = useMemo( () => [ beamstopXName, @@ -39,8 +53,19 @@ export default function Beamstop({ beamstopXNameRBV, beamstopYNameRBV, beamstopCurrentName, + ...(beamstopEnergyName && beamstopEnergyRBV + ? [beamstopEnergyName, beamstopEnergyRBV] + : []), + ], + [ + beamstopXName, + beamstopYName, + beamstopXNameRBV, + beamstopYNameRBV, + beamstopCurrentName, + beamstopEnergyName, + beamstopEnergyRBV, ], - [beamstopXName, beamstopYName, beamstopXNameRBV, beamstopYNameRBV, beamstopCurrentName], ); const { devices, handleSetValueRequest, toggleDeviceLock } = useOphydPVSocket(deviceNameList); const [bestCurrent, setBestCurrent] = useState(null); @@ -84,15 +109,19 @@ export default function Beamstop({
-

- Beamstop Current:{' '} - {devices[beamstopCurrentName] && devices[beamstopCurrentName].value}{' '} - {devices[beamstopCurrentName] && - devices[beamstopCurrentName].units?.slice(0, 3)} -

+ +

Beamstop Current:

+ +

+ {devices[beamstopCurrentName] && + Number(devices[beamstopCurrentName].value).toPrecision(4)}{' '} + {devices[beamstopCurrentName] && + devices[beamstopCurrentName].units?.slice(0, 3)} +

+
@@ -118,7 +147,7 @@ export default function Beamstop({ )}
+ {beamstopEnergyName && ( + + )}
+ {beamstopEnergyName && ( +
+ +
+ )} ); } diff --git a/src/features/ComponentViewer/ComponentViewerExampleSim.tsx b/src/features/ComponentViewer/ComponentViewerExampleSim.tsx index bf1db3bc..511c8f71 100644 --- a/src/features/ComponentViewer/ComponentViewerExampleSim.tsx +++ b/src/features/ComponentViewer/ComponentViewerExampleSim.tsx @@ -10,6 +10,7 @@ import Histogram from '@/components/Histogram/Histogram'; import { CubeTransparent } from '@phosphor-icons/react'; import useSimOphydPVSocket from '@/api/ophyd/useSimOphydPVSocket'; +import SimulatedBeamline from '../SimulatedBeamline/SimulatedBeamline'; const SIM_DEVICES = ['sineSignal', 'noisySignal', 'motor1', 'motor2']; export default function ComponentViewerExampleSim() { @@ -47,8 +48,8 @@ export default function ComponentViewerExampleSim() { }, SimControl4: { name: 'Hexapod Controller', - element: , - info: 'This component accepts a prefix intended for an EPICS hexapod device, and connects via useOphydSocketPV. This display utilizes a demo mode with a simulated device.', + element: , + info: 'This component accepts a prefix intended for an EPICS hexapod device, and connects via useOphydSocketPV. This display is wired to the ophyd-sim hexapod (prefix SYM:HEX01) through the OphydSimProvider on this page.', }, SimMonitor1: { name: 'Signal Monitor Plot', @@ -67,6 +68,11 @@ export default function ComponentViewerExampleSim() { ), info: 'This component accepts an array PV and an acquire control PV for EPICS systems to display a histogram plot. This display uses a demo mode with simulated histogram data.', }, + OphydSim1: { + name: 'Simulated Beamline', + element: , + info: 'This component accepts an array PV and an acquire control PV for EPICS systems to display a histogram plot. This display uses a demo mode with simulated histogram data.', + }, }; return ; } diff --git a/src/features/EnergyVsCurrentPlotPV.tsx b/src/features/EnergyVsCurrentPlotPV.tsx new file mode 100644 index 00000000..c00bae13 --- /dev/null +++ b/src/features/EnergyVsCurrentPlotPV.tsx @@ -0,0 +1,125 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import PlotlyScatter from '@/components/PlotlyScatter'; +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; +import { beamstopCurrentModel, ENERGY_MIN_EV, ENERGY_MAX_EV } from '@/lib/ophyd-sim'; +import { recencyOpacities } from '@/utils/plotGenerators'; +import { cn } from '@/lib/utils'; + +export type EnergyVsCurrentPlotPVProps = { + /** Writable beam-energy PV (x-axis of the plot). */ + energyPv: string; + /** Beamstop diode current PV (y-axis of the plot). */ + currentPv: string; + /** Beamstop X motor readback PV — positions the expected curve. */ + beamstopXRbvPv: string; + /** Beamstop Y motor readback PV — positions the expected curve. */ + beamstopYRbvPv: string; + /** Max measured points retained before the oldest is dropped. Defaults to 60. */ + numVisiblePoints?: number; + /** Interval in ms between measured samples. Defaults to 500. */ + pollingIntervalMs?: number; + /** Number of points in the theoretical expected curve. Defaults to 100. */ + expectedCurvePoints?: number; + className?: string; +}; + +type Pair = { energy: number; current: number }; + +/** + * Live "Beam Energy vs Beamstop Current" plot. + * + * Overlays two traces, mirroring the reference diagram: + * - Measured: live (energy, current) samples accumulating as the user sweeps + * energy, kept in a rolling window (markers). + * - Expected: the noise-free `beamstopCurrentModel` swept across the energy + * range at the *current* beamstop position (dashed line). It re-shapes when + * the beamstop moves. + */ +export default function EnergyVsCurrentPlotPV({ + energyPv, + currentPv, + beamstopXRbvPv, + beamstopYRbvPv, + numVisiblePoints = 60, + pollingIntervalMs = 500, + expectedCurvePoints = 100, + className, +}: EnergyVsCurrentPlotPVProps) { + const { devices } = useOphydPVSocket([energyPv, currentPv, beamstopXRbvPv, beamstopYRbvPv]); + + const [measured, setMeasured] = useState([]); + + // Refs so the sampling interval reads the latest values without resetting. + const energyRef = useRef(null); + const currentRef = useRef(null); + const energyValue = devices[energyPv]?.value; + const currentValue = devices[currentPv]?.value; + energyRef.current = typeof energyValue === 'number' ? energyValue : null; + currentRef.current = typeof currentValue === 'number' ? currentValue : null; + + useEffect(() => { + const interval = setInterval(() => { + const energy = energyRef.current; + const current = currentRef.current; + if (energy === null || current === null) return; + setMeasured((prev) => { + const next = [...prev, { energy, current }]; + if (next.length > numVisiblePoints) next.shift(); + return next; + }); + }, pollingIntervalMs); + return () => clearInterval(interval); + }, [numVisiblePoints, pollingIntervalMs]); + + const beamstopX = devices[beamstopXRbvPv]?.value; + const beamstopY = devices[beamstopYRbvPv]?.value; + + // Expected curve at the current beamstop position, swept across energy. + const expected = useMemo(() => { + const x = typeof beamstopX === 'number' ? beamstopX : 0; + const y = typeof beamstopY === 'number' ? beamstopY : 0; + const xs: number[] = []; + const ys: number[] = []; + const steps = Math.max(2, expectedCurvePoints); + for (let i = 0; i < steps; i++) { + const energyEV = ENERGY_MIN_EV + ((ENERGY_MAX_EV - ENERGY_MIN_EV) * i) / (steps - 1); + xs.push(energyEV); + ys.push(beamstopCurrentModel({ x, y, energyEV })); + } + return { xs, ys }; + }, [beamstopX, beamstopY, expectedCurvePoints]); + + const data = useMemo( + () => [ + { + x: expected.xs, + y: expected.ys, + type: 'scatter' as const, + mode: 'lines' as const, + name: 'Expected', + line: { color: '#082f49', dash: 'dash' as const }, + }, + { + x: measured.map((p) => p.energy), + y: measured.map((p) => p.current), + type: 'scatter' as const, + mode: 'markers' as const, + name: 'Measured', + // Fade older samples so the newest reading stands out. + marker: { color: '#0e7490', size: 7, opacity: recencyOpacities(measured.length) }, + }, + ], + [expected, measured], + ); + + return ( + + ); +} diff --git a/src/features/SampleStage.tsx b/src/features/SampleStage.tsx new file mode 100644 index 00000000..60cdb231 --- /dev/null +++ b/src/features/SampleStage.tsx @@ -0,0 +1,65 @@ +import { useMemo } from 'react'; + +import DeviceControllerBox from '@/components/DeviceControllerBox'; +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; +import { deviceIcons } from '@/assets/icons'; + +export type SampleStageProps = { + /** Sample X motor setpoint PV (mm). */ + sampleXName: string; + /** Sample Y motor setpoint PV (mm). */ + sampleYName: string; + sampleXTitle?: string; + sampleYTitle?: string; + /** Lay the two axis controls out side by side instead of stacked. */ + stackVertical?: boolean; +}; + +/** + * X/Y control for the sample-holder stage. Wires two DeviceControllerBox cards + * to the sample motors and drives the holder graphic in , + * which tracks the same PVs' readbacks. Mirrors the motor wiring in + * [src/features/Beamstop.tsx]. + */ +export default function SampleStage({ + sampleXName, + sampleYName, + sampleXTitle = 'Sample - X', + sampleYTitle = 'Sample - Y', + stackVertical = false, +}: SampleStageProps) { + const sampleXNameRBV = useMemo(() => sampleXName + '.RBV', [sampleXName]); + const sampleYNameRBV = useMemo(() => sampleYName + '.RBV', [sampleYName]); + const deviceNameList = useMemo( + () => [sampleXName, sampleYName, sampleXNameRBV, sampleYNameRBV], + [sampleXName, sampleYName, sampleXNameRBV, sampleYNameRBV], + ); + const { devices, handleSetValueRequest, toggleDeviceLock } = useOphydPVSocket(deviceNameList); + + const xDevice = devices[sampleXName]; + const yDevice = devices[sampleYName]; + if (!xDevice || !yDevice) return null; + + return ( +
+ + +
+ ); +} diff --git a/src/features/SimulatedBeamline/SimulatedBeamline.tsx b/src/features/SimulatedBeamline/SimulatedBeamline.tsx new file mode 100644 index 00000000..683b6b98 --- /dev/null +++ b/src/features/SimulatedBeamline/SimulatedBeamline.tsx @@ -0,0 +1,78 @@ +import { OphydTransportProvider } from '@/api/ophyd/OphydTransportProvider'; +import { + OphydSimProvider, + createOphydSimTransport, + beamstopBeamline, + simDetectorConfig, + createSimDetectorCameraSocketFactory, +} from '@/lib/ophyd-sim'; +import { useMemo } from 'react'; +import SignalMonitorPlotPV from '@/components/SignalMonitorPlotPV'; +import EndstationDisplay from '@/components/EndstationDisplay/EndstationDisplay'; +import SimulatedBeamlineDeviceTable from './SimulatedBeamlineDeviceTable'; +import SimulatedBeamlineDetector from './SimulatedBeamlineDetector'; +import SimulatedBeamlineShutter from './SimulatedBeamlineShutter'; +import { cn } from '@/lib/utils'; + +/** Shared height for the device table and the beamstop-current plot beside it, + * so the two panels line up. Sized to fit the table's fixed rows. */ +const PANEL_HEIGHT = 'h-[280px]'; +/** Shared height for the endstation graphic and the detector canvas in the top row. */ +const IMAGE_HEIGHT = 'h-[340px]'; + +/** Beamstop-current PV shown in the trend plot. */ +const BEAMSTOP_CURRENT = 'bl201-beamstop:current'; + +type SimulatedBeamlineProps = { + /** Additional CSS classes applied to the widget's root container. */ + className?: string; +}; + +/** + * Self-contained simulated-beamline widget: bundles the Ophyd sim providers with + * the endstation graphic, simulated detector, beamline device table, and the + * beamstop-current trend plot — plus the beam-shutter control. Because it carries + * its own sim transport and camera socket factory it can be dropped in anywhere + * without external wiring; every panel reads and writes the same simulated + * beamline, so moves in the table update the graphic, detector, and plot live. + */ +export default function SimulatedBeamline({ className }: SimulatedBeamlineProps) { + const transport = useMemo(() => createOphydSimTransport(beamstopBeamline), []); + // Camera frames are driven by the '13SIM1' detector in the same sim, so the + // energy control in the device table modulates the image live. + const cameraSocketFactory = useMemo( + () => createSimDetectorCameraSocketFactory(beamstopBeamline, simDetectorConfig), + [], + ); + + return ( + + +
+ {/* Top row: endstation graphic beside the simulated detector, both + * pinned to the same height. Wraps on narrow widths. Closing the + * shutter (5 V) pauses the detector and zeroes the diode current. */} +
+
+ + +
+ +
+ {/* Bottom row: device table beside the live beamstop-current trend. */} +
+ + +
+
+
+
+ ); +} diff --git a/src/features/SimulatedBeamline/SimulatedBeamlineDetector.tsx b/src/features/SimulatedBeamline/SimulatedBeamlineDetector.tsx new file mode 100644 index 00000000..feda0c7d --- /dev/null +++ b/src/features/SimulatedBeamline/SimulatedBeamlineDetector.tsx @@ -0,0 +1,23 @@ +import { useMemo } from 'react'; +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; +import CameraCanvas from '@/components/Camera/CameraCanvas'; +import { SHUTTER_OPEN_VALUE } from '@/lib/ophyd-sim'; + +/** Shutter analog-output PV (0 V open / 5 V closed) — matches the default. */ +const SHUTTER_PV = 'bl531:LJT4:1:AO0'; + +/** + * Simulated detector, paused whenever the beam shutter is not open. Subscribes + * to the shutter PV and hands `CameraCanvas` a controlled `paused` flag so the + * stream stops when the shutter closes and resumes when it reopens. + */ +export default function SimulatedBeamlineDetector() { + const deviceList = useMemo(() => [SHUTTER_PV], []); + const { devices } = useOphydPVSocket(deviceList); + const shutterOpen = Number(devices[SHUTTER_PV]?.value) === SHUTTER_OPEN_VALUE; + return ( +
+ +
+ ); +} diff --git a/src/features/SimulatedBeamline/SimulatedBeamlineDeviceTable.tsx b/src/features/SimulatedBeamline/SimulatedBeamlineDeviceTable.tsx new file mode 100644 index 00000000..10222220 --- /dev/null +++ b/src/features/SimulatedBeamline/SimulatedBeamlineDeviceTable.tsx @@ -0,0 +1,47 @@ +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; +import TableDeviceControllerWithRBV from '@/components/TableDeviceControllerWithRBV'; + +/** + * Beam energy, sample X/Y, and beamstop X/Y — the writable beamline devices, + * driven through the same sim transport that feeds EndstationDisplay and the + * camera, so moves made here update those views live. + */ +const BEAMLINE_DEVICES = [ + 'bl531_xps2:beamstop_x_mm', + 'bl531_xps2:beamstop_y_mm', + 'bl531:sample_x_mm', + 'bl531:sample_y_mm', + 'bl531:mono_energy_eV', +]; +const BEAMLINE_DEVICES_RBV = [ + 'bl531_xps2:beamstop_x_mm.RBV', + 'bl531_xps2:beamstop_y_mm.RBV', + 'bl531:sample_x_mm.RBV', + 'bl531:sample_y_mm.RBV', + 'bl531:mono_energy_eV.RBV', +]; + +type SimulatedBeamlineDeviceTableProps = { + /** Additional CSS classes applied to the device table (e.g. a shared panel height). */ + className?: string; +}; + +export default function SimulatedBeamlineDeviceTable({ + className, +}: SimulatedBeamlineDeviceTableProps) { + const { devices, handleSetValueRequest, toggleDeviceLock, toggleExpand } = + useOphydPVSocket(BEAMLINE_DEVICES); + + const { devices: devicesRBV } = useOphydPVSocket(BEAMLINE_DEVICES_RBV); + return ( + + ); +} diff --git a/src/features/SimulatedBeamline/SimulatedBeamlineShutter.tsx b/src/features/SimulatedBeamline/SimulatedBeamlineShutter.tsx new file mode 100644 index 00000000..bbf5c325 --- /dev/null +++ b/src/features/SimulatedBeamline/SimulatedBeamlineShutter.tsx @@ -0,0 +1,14 @@ +import Shutter from '@/components/Shutter'; + +/** + * Beam-shutter control anchored to the top-left of the beamline cartoon. Because it + * lives inside the widget's Ophyd sim providers it drives the simulated beamline + * directly; `absolute` positioning pins it to the top-left of the endstation graphic's + * `relative` container. Drives bl531:LJT4:1:AO0; closing it (5 V) pauses the detector + * and zeroes the diode current. + */ +export default function SimulatedBeamlineShutter() { + return ( + + ); +} diff --git a/src/lib/ophyd-sim/README.md b/src/lib/ophyd-sim/README.md new file mode 100644 index 00000000..143ee4a7 --- /dev/null +++ b/src/lib/ophyd-sim/README.md @@ -0,0 +1,530 @@ +# ophyd-sim + +A self-contained, browser-side simulator for [Ophyd](https://blueskyproject.io/ophyd/) +beamline devices. It models EPICS-style PVs — motors, shutters, detectors, and +derived signals — entirely in-process, so finch's normal Ophyd socket hooks can +be driven without a real IOC or WebSocket backend. It powers Storybook stories, +tests, and the [`SimulatedBeamline`](../../features/SimulatedBeamline/SimulatedBeamline.tsx) +widget. + +Everything is re-exported from the package root ([index.ts](index.ts)); import +from `@/lib/ophyd-sim`, never from subfolders. + +> This README is the usage / getting-started guide. For the architecture and the +> internals of the store, scheduler, and dependency graph, see +> [skills.md](skills.md). + +--- + +## Table of contents + +1. [Getting started](#getting-started) — minimal sim + providers + one `DeviceControllerBox` +2. [Device catalog](#device-catalog) — instantiating every device type +3. [Creating connected devices](#creating-connected-devices) — one device driven by another +4. [How ophyd-sim works](#how-ophyd-sim-works) — architecture diagram +5. [When & how ophyd-sim kicks in](#when--how-ophyd-sim-kicks-in) — the transport seam and the hooks that use it + +--- + +## Getting started + +The bare minimum: build a sim, wrap the tree in the two providers, and render a +single [`DeviceControllerBox`](../../components/DeviceControllerBox.tsx) wired to +a simulated motor. Because `DeviceControllerBox` is a plain presentational +component, you connect it to the sim through the ordinary +[`useOphydPVSocket`](../../api/ophyd/useOphydPVSocket.tsx) hook — the exact same +hook used against a real IOC. + +```tsx +import { useMemo } from 'react'; +import { + OphydSimProvider, + createOphydSimTransport, + createOphydSim, + motor, +} from '@/lib/ophyd-sim'; +import { OphydTransportProvider } from '@/api/ophyd/OphydTransportProvider'; +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; +import DeviceControllerBox from '@/components/DeviceControllerBox'; + +// 1. Build a sim. Keep it stable across renders (module scope or useMemo). +const sim = createOphydSim({ + devices: [motor({ name: 'IOC:m1', limits: [-10, 10], velocity: 2, units: 'mm' })], +}); + +// 2. A leaf that talks to the sim through the normal Ophyd PV hook. +function MotorControl() { + const { devices, handleSetValueRequest, toggleDeviceLock } = useOphydPVSocket([ + 'IOC:m1', + 'IOC:m1.RBV', + ]); + + return ( + + ); +} + +// 3. Wrap the tree: OphydSimProvider starts/stops the tick loop; +// OphydTransportProvider routes the PV hooks at the sim instead of a WebSocket. +export function Example() { + const transport = useMemo(() => createOphydSimTransport(sim), []); + return ( + + + + + + ); +} +``` + +Moving the motor from the box animates `IOC:m1.RBV` toward the setpoint and the +readback ticks live — no backend involved. + +**In Storybook**, skip the boilerplate with the `withOphydSim` decorator, which +mounts both providers for you: + +```tsx +import { withOphydSim, defaultBeamline } from '@/lib/ophyd-sim'; + +export const decorators = [withOphydSim(defaultBeamline)]; +``` + +--- + +## Device catalog + +Every device is a **factory** you drop into the `devices` array of +`createOphydSim`. Below is a comprehensive example of each supported type. + +### Signal + +A scalar PV. It can be a static literal, a periodically-recomputed function, a +value derived from other PVs, or any combination. Signals are **read-only by +default** — pass `writeAccess: true` to make them writable. + +```ts +import { createOphydSim, signal, randomNoise } from '@/lib/ophyd-sim'; + +createOphydSim({ + devices: [ + // Static, read-only scalar. + signal({ name: 'IOC:temperature', initialValue: 21.5, units: 'C' }), + + // Writable enum (In / Out), used as a control input for other devices. + signal({ + name: 'IOC:bs', + initialValue: 0, + writeAccess: true, + enumStrs: ['Out', 'In'], + }), + + // Periodic signal: recomputes every 100 ms with fresh noise. + signal({ + name: 'IOC:pressure', + units: 'mbar', + periodMs: 100, + value: ({ random }) => 1000 + randomNoise({ random, sigma: 3 }), + }), + ], +}); +``` + +Key `SignalOptions`: `initialValue`, `value` (literal or `(ctx) => value`), +`dependsOn`, `periodMs`, `units`, `limits`, `writeAccess`, `precision`, +`enumStrs`. (See [signal.ts](devices/signal.ts).) + +### Motor + +A linear-velocity motor. One factory seeds **three PVs** that share a single +motion state: + +| PV | Meaning | +| --- | --- | +| `name` | setpoint (writable; clamps into `limits`) | +| `name.RBV` | readback — animates toward the setpoint at `velocity` units/s | +| `name.MOVN` | moving flag — `1` while moving, `0` once settled | + +```ts +import { createOphydSim, motor } from '@/lib/ophyd-sim'; + +createOphydSim({ + devices: [ + motor({ + name: 'IOC:m1', + initialPosition: 0, + velocity: 2, // units per second + limits: [-10, 10], // soft limits clamp setpoint writes + units: 'mm', + // epsilon: 1e-4, // settle tolerance (optional) + // readbackName: 'IOC:m1.RBV', // override defaults (optional) + // movingName: 'IOC:m1.MOVN', + }), + ], +}); +``` + +Writing the setpoint (`sim.set('IOC:m1', 5)`) sets `MOVN=1` and advances `.RBV` +by `velocity * dt` each tick until it lands within `epsilon`, then snaps and +clears `MOVN`. (See [motor.ts](devices/motor.ts).) + +### Detector + +A simulated area detector. Unlike single-value devices, a detector seeds a +**cluster** of standard Area-Detector PVs from one `prefix` — `cam1:SizeX`, +`cam1:SizeY`, `cam1:MinX`, `cam1:MinY`, `cam1:ColorMode`, `cam1:DataType`, +`cam1:Acquire`, and the image `image1:Mode`. The image pixels themselves are not +a PV; they stream over the [camera socket](#the-camera-socket) — but their scalar +parameters (opacity, overlay positions) are PVs that other devices can drive. + +```ts +import { createOphydSim, detector } from '@/lib/ophyd-sim'; + +createOphydSim({ + devices: [ + detector({ + prefix: '13SIM1', + image: { + mode: 'image_file', // or 'noisy' for random-noise frames + sizeX: 1024, + sizeY: 1024, + file: '/images/diffraction.png', + // files: ['/f0.png', '/f1.png'], // round-robin per frame (optional) + overlays: [ + { + file: '/images/beamstop-dot.png', + width: 32, + height: 32, + x: 512, // center in pixels (fallback if unbound) + y: 512, + }, + ], + }, + // Derived image parameters other devices drive — see next section. + modulations: [], + }), + ], +}); +``` + +Detectors are usually loaded from JSON — see +[simDetector.json](scenarios/simDetector.json) and `simDetectorConfig`. To render +frames to a canvas, pair the detector with +`createSimDetectorCameraSocketFactory` (see [The camera socket](#the-camera-socket)). +(See [detector.ts](devices/detector.ts).) + +### Bonus: shutter & hexapod + +Two higher-level factories cover common beamline hardware: + +```ts +import { createOphydSim, shutter, hexapod } from '@/lib/ophyd-sim'; + +createOphydSim({ + devices: [ + // Beam shutter: one analog-output PV, 0 V open / 5 V closed. Defaults open. + shutter({ name: 'bl531:LJT4:1:AO0' /*, initial: 'closed' */ }), + + // Symetrie six-axis hexapod: seeds *_RBV readbacks, MOVE_PTP setpoints, + // and the MOVE_PTP execute/STOP command PVs under the given prefix. + hexapod({ prefix: 'SYM:HEX01', translationVelocity: 4, rotationVelocity: 2 }), + ], +}); +``` + +See [shutter.ts](devices/shutter.ts) and [hexapod.ts](devices/hexapod.ts) for +the full PV maps and options. + +--- + +## Creating connected devices + +The whole point of the simulator is that one device can react to the **current +value of another**. Devices never reference each other directly — they read and +write the same shared state. A device becomes "connected" by listing the PVs it +cares about in `dependsOn`; its `compute` function then re-runs, in dependency +order, whenever any of those inputs change. + +Inside `compute(ctx)` you get a `SimValueContext`: + +- `ctx.get.number(pv)` / `.boolean(pv)` / `.string(pv)` / `.value(pv)` — read inputs +- `ctx.time`, `ctx.dt` — current time (ms) and elapsed seconds since last tick +- `ctx.random()` — the sim's random source (override `random` in `createOphydSim` + for deterministic tests) + +> `compute` also runs **once at registration** to seed an initial value, so guard +> against inputs that may not be seeded yet. Order matters: a dependency must be +> registered *before* the device that depends on it. A `compute` that throws is +> logged and skipped, not fatal. + +### Example 1 — a signal derived from a motor and a control input + +A diode reading that peaks when a motor's readback is near 2, is attenuated when +a beamstop is inserted, and carries measurement noise: + +```ts +import { createOphydSim, motor, signal, gaussian, randomNoise } from '@/lib/ophyd-sim'; + +createOphydSim({ + devices: [ + motor({ name: 'IOC:m1', limits: [-10, 10], velocity: 1, units: 'mm' }), + signal({ name: 'IOC:bs', initialValue: 0, writeAccess: true, enumStrs: ['Out', 'In'] }), + + // I0 recomputes whenever the motor readback OR the beamstop changes, + // and also every 100 ms so the noise keeps moving. + signal({ + name: 'I0', + units: 'arb.', + periodMs: 100, + dependsOn: ['IOC:m1.RBV', 'IOC:bs'], // <-- the connection + value: ({ get, random }) => { + const beam = 1000 * gaussian({ x: get.number('IOC:m1.RBV'), center: 2, sigma: 0.5 }); + const attenuated = get.boolean('IOC:bs') ? beam * 0.01 : beam; + return attenuated + randomNoise({ random, sigma: 5 }); + }, + }), + ], +}); +``` + +Now `sim.set('IOC:m1', 2)` makes `I0` climb as the readback approaches the peak, +and `sim.set('IOC:bs', 1)` drops it ~99% — all automatically. This is exactly the +[`defaultBeamline`](scenarios/defaultBeamline.ts) scenario. + +### Example 2 — chaining multiple derived signals + +`dependsOn` targets can themselves be derived PVs, so you can build a coupling +chain. Here energy drives a Bragg angle, which drives a beam position: + +```ts +import { createOphydSim, motor, signal, braggAngle } from '@/lib/ophyd-sim'; + +createOphydSim({ + devices: [ + motor({ name: 'bl:mono_energy_eV', limits: [2000, 7000], velocity: 100, initialPosition: 4500 }), + + // θ derived from energy. + signal({ + name: 'bl:bragg_deg', + units: 'deg', + dependsOn: ['bl:mono_energy_eV.RBV'], + value: ({ get }) => braggAngle({ energyEV: get.number('bl:mono_energy_eV.RBV') }), + }), + + // beam Y derived from θ — a second-order dependency. + signal({ + name: 'bl:beam_y_mm', + units: 'mm', + dependsOn: ['bl:bragg_deg'], + value: ({ get }) => 4 * Math.cos((get.number('bl:bragg_deg') * Math.PI) / 180), + }), + ], +}); +``` + +### Example 3 — a detector image driven by another device + +A detector `modulation` is just a derived PV under the hood: it binds an image +parameter (e.g. `image1:Opacity`) to a source PV via a linear-clamped map. Here +beam energy dims the rendered image, and a beamstop motor's readback drives an +overlay marker's position: + +```ts +detector({ + prefix: '13SIM1', + image: { + mode: 'image_file', + sizeX: 1024, + sizeY: 1024, + file: '/images/diffraction.png', + overlays: [ + { + file: '/images/beamstop-dot.png', + width: 32, + height: 32, + x: 512, + y: 512, + // Bind the marker's X to a motor readback: -10..10 mm -> 0..1024 px. + positionX: { + source: 'bl531_xps2:beamstop_x_mm.RBV', + from: { in: -10, out: 0 }, + to: { in: 10, out: 1024 }, + }, + }, + ], + }, + // Energy 2000..7000 eV maps to image opacity 1.0..0.2. + modulations: [ + { + source: 'bl:mono_energy_eV.RBV', + effect: 'opacity', + from: { in: 2000, out: 1.0 }, + to: { in: 7000, out: 0.2 }, + }, + ], +}); +``` + +The flagship [`beamstopBeamline`](scenarios/beamstopBeamline.ts) scenario wires +all of these patterns together (energy → Bragg → beam Y → 2D-Gaussian diode +current, gated by a shutter). It's worth reading as a full worked example — the +physics is documented in [skills.md](skills.md#beamstopbeamline). + +--- + +## How ophyd-sim works + +A sim is a bag of named PVs over shared state, plus two update mechanisms: + +- **Ticks** — a periodic loop (`tickMs`, ~33 ms ≈ 30 Hz) that advances time-based + devices: motor motion, periodic signals. +- **Derived signals** — values with a `dependsOn` list that recompute, in + dependency order, whenever an input PV changes. + +The React app never talks to the sim directly. It uses its ordinary Ophyd PV +hooks, which read a **transport** from context. In production that transport is a +WebSocket to a real IOC; `createOphydSimTransport` swaps in an implementation of +the same interface that reads and writes the in-memory sim instead. + +```mermaid +flowchart TB + subgraph react["React tree"] + comp["Component (e.g. DeviceControllerBox)"] + hook["useOphydPVSocket(['IOC:m1', ...])"] + comp -->|"handleSetValueRequest / devices"| hook + end + + subgraph provider["OphydTransportProvider (context seam)"] + ctx["PVTransportContext"] + end + + hook -->|"reads transport via useOphydPVTransport()"| ctx + + ctx -.->|"production"| ws["WebSocket → real IOC"] + ctx -->|"simulation"| transport["createOphydSimTransport(sim)"] + + subgraph sim["OphydSim instance"] + store["SimState store
(PV values + metadata)"] + depGraph["DependencyGraph
(derived recompute)"] + sched["Scheduler
(tick loop, tickMs)"] + devs["Device factories
motor / signal / detector / …"] + end + + transport -->|"send: subscribe / set / unsubscribe / refresh"| store + store -->|"value change events"| transport + transport -->|"meta + value messages"| hook + + devs -->|"seed / registerDerived / onTick / onSet"| store + store <--> depGraph + sched -->|"advance(dt) each tick"| store + provs["OphydSimProvider"] -->|"start() on mount, stop() on unmount"| sched +``` + +The two providers have distinct jobs: + +- **`OphydSimProvider`** owns the sim's lifecycle — it calls `sim.start()` on + mount and `sim.stop()` on unmount, and exposes the sim to the sim-specific + hooks (`useSimSignal`, `useSimSet`). +- **`OphydTransportProvider`** installs the transport into context so the + *generic* Ophyd hooks resolve to the sim instead of a WebSocket. (It can also + wrap the sim with `fallbackToReal` so descendants fail over to a real backend + if the override reports an error.) + +--- + +## When & how ophyd-sim kicks in + +ophyd-sim engages purely through the **transport seam**. finch's Ophyd hooks +never open a socket themselves — they pull a transport out of React context via +`useOphydPVTransport()`. Whichever transport is in context wins: + +- **No `OphydTransportProvider`, or no override** → the hook lazily constructs a + real WebSocket transport pointed at the configured backend. (Production.) +- **An `OphydTransportProvider` supplies `transport={createOphydSimTransport(sim)}`** + → every descendant PV hook is transparently driven by the sim. Nothing else in + the component changes; the same code runs against sim or IOC. (Storybook, tests, + `SimulatedBeamline`.) + +### What the sim transport speaks + +`createOphydSimTransport` implements the `OphydPVTransport` wire protocol. The +hooks exercise it through these actions ([createOphydSimTransport.ts](transport/createOphydSimTransport.ts)): + +| Action sent by a hook | What the sim transport does | +| --- | --- | +| `subscribe` / `subscribeSafely` / `subscribeReadOnly` | Emits a synthesized `meta` message (so the device shows connected) then streams every value change for that PV | +| `set` | Writes to the sim (`sim.set`), triggering derived recompute + motor motion | +| `unsubscribe` | Drops the most recent listener for that PV | +| `refresh` | Replays the latest value of every active subscription | +| `onStatus` | Reports `'open'` synchronously — there's no real handshake | + +### The Ophyd hooks that use the transport + +These are the specific hooks that resolve their transport from context and are +therefore driven by ophyd-sim when a sim transport is provided: + +- **[`useOphydPVSocket(pvs, wsUrl?)`](../../api/ophyd/useOphydPVSocket.tsx)** — the + primary PV-socket hook. Reads its transport via `useOphydPVTransport()`, sends + `subscribe`/`set`/`unsubscribe`/`refresh`, and returns `{ devices, + handleSetValueRequest, toggleDeviceLock, toggleExpand }`. **This is the hook the + sim is built to serve.** +- **[`useOphydSocket(pvs, wsUrl?)`](../../api/ophyd/useOphydSocket.ts)** — legacy + alias kept for backwards compatibility; delegates to `useOphydPVSocket`, so it + is sim-driven too. + +The React-native sim hooks in [`react/`](react/) talk to the sim **directly** +(not through the transport), and are handy when you're already inside an +`OphydSimProvider` and don't need the full PV-socket abstraction: + +- **`useSimSignal(pv)`** — subscribe to one PV's value; re-renders on change. +- **`useSimSet()`** — returns a stable `(pv, value) => void` writer. +- **`useOphydSim()` / `useOphydSimOptional()`** — grab the sim instance itself. + +### The camera socket + +Detector **image frames** don't travel over the PV transport — they stream over a +separate camera-socket seam. `CameraCanvas` reads its factory from +`useCameraSocketFactory()`, which `OphydTransportProvider` supplies via its +`cameraSocketFactory` prop. Pass +`createSimDetectorCameraSocketFactory(sim, detectorConfig)` there to render a +detector's live derived state (opacity, overlay positions) to canvas frames — see +[`SimulatedBeamline`](../../features/SimulatedBeamline/SimulatedBeamline.tsx) for +the full wiring. + +### Not backed by ophyd-sim + +- **[`useOphydDeviceSocket`](../../api/ophyd/useOphydDeviceSocket.ts)** uses the + separate *device*-socket transport. `createOphydSimTransport` is a *PV* + transport only, so this hook is not driven by ophyd-sim (it falls back to the + real device backend unless you supply a `deviceTransport` override). +- **[`useSimOphydPVSocket`](../../api/ophyd/useSimOphydPVSocket.tsx)** is an older, + standalone mock (`sineSignal` / `noisySignal` keywords) that is **unrelated** to + this package — it doesn't use a transport at all. Prefer `useOphydPVSocket` + + a sim transport for new work. + +### Driving the sim without React + +The transport and providers are optional. You can drive a sim directly — useful +in tests, where you skip the real loop and step time by hand: + +```ts +const sim = createOphydSim({ devices: [motor({ name: 'IOC:m1', velocity: 2 })] }); + +sim.set('IOC:m1', 5); // command a move +sim.advance(1000); // step 1 s of simulated time (no real timers) +sim.get('IOC:m1.RBV'); // => 2 (moved velocity * dt) + +// or run the real loop: +sim.start(); +// ... later ... +sim.stop(); +``` + +`advance(deltaMs)` runs a single synthetic tick, so motor motion and periodic +recompute are fully deterministic when you also override `random` and `now` in +`createOphydSim`. diff --git a/src/lib/ophyd-sim/camera/SimCameraSocket.ts b/src/lib/ophyd-sim/camera/SimCameraSocket.ts new file mode 100644 index 00000000..18816686 --- /dev/null +++ b/src/lib/ophyd-sim/camera/SimCameraSocket.ts @@ -0,0 +1,165 @@ +import type { CameraSocketFactory, CameraSocketLike, CameraSocketMessageEvent } from './types'; + +/** Payload a frame generator may return. `null` skips the frame. */ +export type CameraFramePayload = Blob | ArrayBuffer | null; + +export interface SimCameraSocketOptions { + /** + * Frames per second to emit. Defaults to 10 — enough for a simulated + * stream to read as live while keeping the browser's per-frame work low + * (e.g. on the documentation site). + */ + fps?: number; + /** Generated frame width in pixels. Defaults to 512. */ + width?: number; + /** Generated frame height in pixels. Defaults to 512. */ + height?: number; + /** + * Produce one frame. Defaults to a grayscale random-noise JPEG. Override in + * tests (or to swap in structured imagery) to avoid touching canvas APIs. + */ + generateFrame?: (size: { + width: number; + height: number; + }) => Promise | CameraFramePayload; +} + +const OPEN = 1; +const CLOSED = 3; + +/** + * Draw uncorrelated grayscale noise and JPEG-encode it, mirroring what the real + * Python camera server puts on the wire (the frontend only ever sees JPEG + * frames). Returns `null` when `OffscreenCanvas` is unavailable so callers can + * simply skip the frame rather than crash. + */ +async function generateNoiseJpeg({ + width, + height, +}: { + width: number; + height: number; +}): Promise { + if (typeof OffscreenCanvas === 'undefined') return null; + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext('2d'); + if (!context) return null; + + const image = context.createImageData(width, height); + const data = image.data; + for (let i = 0; i < data.length; i += 4) { + const v = Math.floor(Math.random() * 256); + data[i] = v; // R + data[i + 1] = v; // G + data[i + 2] = v; // B + data[i + 3] = 255; // A + } + context.putImageData(image, 0, 0); + return canvas.convertToBlob({ type: 'image/jpeg', quality: 0.6 }); +} + +/** + * A `WebSocket`-shaped fake that stands in for the real camera stream. After an + * async "connection", it emits binary JPEG frames at `fps`, replicating the + * server protocol {@link useCameraCanvas} expects: an initial `{ x, y }` + * dimensions message, JPEG frame blobs, and a `{ logNormalization }` echo in + * response to `toggleLogNormalization`. The image content (random noise by + * default) carries no real data — it exists to drive the UI without a backend. + */ +export class SimCameraSocket implements CameraSocketLike { + readyState = 0; // CONNECTING + + onopen: ((event: Event) => void) | null = null; + onmessage: ((event: CameraSocketMessageEvent) => void) | null = null; + onerror: ((event: Event) => void) | null = null; + onclose: ((event: Event) => void) | null = null; + + private readonly fps: number; + private readonly width: number; + private readonly height: number; + private readonly generateFrame: NonNullable; + private timer: ReturnType | null = null; + private logNormalization = true; + private emittingFrame = false; + + constructor(options: SimCameraSocketOptions = {}) { + this.fps = options.fps ?? 10; + this.width = options.width ?? 512; + this.height = options.height ?? 512; + this.generateFrame = options.generateFrame ?? generateNoiseJpeg; + // Defer "open" like a real socket so listeners assigned after + // construction still fire. + setTimeout(() => this.open(), 0); + } + + private open(): void { + if (this.readyState === CLOSED) return; + this.readyState = OPEN; + this.onopen?.(new Event('open')); + // Seed canvas dimensions for consumers using `canvasSize: 'automatic'`. + this.onmessage?.({ data: JSON.stringify({ x: this.width, y: this.height }) }); + this.startFrames(); + } + + private startFrames(): void { + const intervalMs = Math.max(1, Math.round(1000 / this.fps)); + this.timer = setInterval(() => { + void this.emitFrame(); + }, intervalMs); + } + + private async emitFrame(): Promise { + // Skip if a prior frame is still encoding so slow envs don't pile up work. + if (this.emittingFrame || this.readyState !== OPEN) return; + this.emittingFrame = true; + try { + const frame = await this.generateFrame({ width: this.width, height: this.height }); + if (frame && this.readyState === OPEN) { + this.onmessage?.({ data: frame }); + } + } finally { + this.emittingFrame = false; + } + } + + send(data: string): void { + if (this.readyState !== OPEN) return; + let message: unknown; + try { + message = JSON.parse(data); + } catch { + return; // Ignore non-JSON control messages. + } + if (message && typeof message === 'object' && 'toggleLogNormalization' in message) { + this.logNormalization = Boolean( + (message as { toggleLogNormalization: unknown }).toggleLogNormalization, + ); + this.onmessage?.({ + data: JSON.stringify({ logNormalization: this.logNormalization }), + }); + } + // The initial config message (image/size PV names) needs no response — + // the sim renders fixed-size noise regardless. + } + + close(): void { + if (this.readyState === CLOSED) return; + this.readyState = CLOSED; + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + this.onclose?.(new Event('close')); + } +} + +/** + * Factory that produces {@link SimCameraSocket}s, suitable for the hook's + * `socketFactory` seam. The `url` is accepted for signature compatibility and + * ignored — there is no backend to connect to. + */ +export function createSimCameraSocketFactory( + options: SimCameraSocketOptions = {}, +): CameraSocketFactory { + return (_url: string) => new SimCameraSocket(options); +} diff --git a/src/lib/ophyd-sim/camera/createSimDetectorCameraSocketFactory.ts b/src/lib/ophyd-sim/camera/createSimDetectorCameraSocketFactory.ts new file mode 100644 index 00000000..d341f2fb --- /dev/null +++ b/src/lib/ophyd-sim/camera/createSimDetectorCameraSocketFactory.ts @@ -0,0 +1,170 @@ +import type { OphydSim } from '../core/types'; +import type { DetectorConfig } from '../devices/detector'; +import { + MODE_SUFFIX, + OPACITY_SUFFIX, + overlayCenterXSuffix, + overlayCenterYSuffix, +} from '../devices/detector'; +import type { CameraFramePayload } from './SimCameraSocket'; +import { SimCameraSocket } from './SimCameraSocket'; +import type { CameraSocketFactory } from './types'; + +export interface SimDetectorCameraOptions { + /** Frames per second to emit. Defaults to 10 (see `SimCameraSocket`). */ + fps?: number; +} + +/** Lazily load and cache an image as an ImageBitmap (keyed per URL). */ +const bitmapCache = new Map>(); + +function loadImageBitmap(url: string): Promise { + let pending = bitmapCache.get(url); + if (!pending) { + pending = (async () => { + try { + const response = await fetch(url); + const blob = await response.blob(); + return await createImageBitmap(blob); + } catch (err) { + console.error('[ophyd-sim] failed to load detector image', url, err); + return null; + } + })(); + bitmapCache.set(url, pending); + } + return pending; +} + +/** One image layer to composite, in destination (top-left) pixel coordinates. */ +export interface RenderLayer { + file: string; + /** `globalAlpha` to draw at (0–1). */ + opacity: number; + x: number; + y: number; + width: number; + height: number; +} + +/** + * Render one detector frame to a JPEG blob over a black background. + * + * `noisy` fills grayscale noise scaled by `opacity` (lower energy → darker + * noise). `image_file` composites `layers` bottom-first, each drawn at its own + * `globalAlpha` and destination rect (JPEG has no alpha, so compositing over + * black is what reads as "darker"). Returns `null` when canvas APIs are + * unavailable (e.g. jsdom). + */ +export async function renderDetectorFrame(params: { + width: number; + height: number; + mode: 'noisy' | 'image_file'; + /** Noise dimming for the `noisy` path. Defaults to 1. */ + opacity?: number; + /** Layers to composite for the `image_file` path, bottom-first. */ + layers?: RenderLayer[]; +}): Promise { + const { width, height, mode, opacity = 1, layers = [] } = params; + if (typeof OffscreenCanvas === 'undefined') return null; + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext('2d'); + if (!context) return null; + + // Black background so dimming reads as darker. + context.fillStyle = 'black'; + context.fillRect(0, 0, width, height); + + if (mode === 'image_file') { + for (const layer of layers) { + const bitmap = await loadImageBitmap(layer.file); + if (!bitmap) continue; + context.globalAlpha = layer.opacity; + context.drawImage(bitmap, layer.x, layer.y, layer.width, layer.height); + } + context.globalAlpha = 1; + } else { + const frame = context.createImageData(width, height); + const data = frame.data; + for (let i = 0; i < data.length; i += 4) { + const v = Math.floor(Math.random() * 256 * opacity); + data[i] = v; // R + data[i + 1] = v; // G + data[i + 2] = v; // B + data[i + 3] = 255; // A + } + context.putImageData(frame, 0, 0); + } + + return canvas.convertToBlob({ type: 'image/jpeg', quality: 0.6 }); +} + +/** + * Build a camera socket factory whose frames are driven by a detector in a live + * `OphydSim`. Each frame reads the detector's current `image1:Opacity` (set by + * modulating devices such as beam energy) and `image1:Mode`, so changing those + * PVs visibly changes the stream. + */ +export function createSimDetectorCameraSocketFactory( + sim: OphydSim, + config: DetectorConfig, + options: SimDetectorCameraOptions = {}, +): CameraSocketFactory { + const { prefix, image } = config; + const opacityPV = `${prefix}:${OPACITY_SUFFIX}`; + const modePV = `${prefix}:${MODE_SUFFIX}`; + const overlayPVs = (image.overlays ?? []).map((overlay, i) => ({ + overlay, + xPV: `${prefix}:${overlayCenterXSuffix(i)}`, + yPV: `${prefix}:${overlayCenterYSuffix(i)}`, + })); + + const num = (value: unknown, fallback: number): number => + typeof value === 'number' ? value : fallback; + + // Base images to cycle through, one per emitted frame. `files` wins over the + // single `file`; an empty list means no base layer (overlays only). + const baseFiles = + image.files && image.files.length > 0 ? image.files : image.file ? [image.file] : []; + + return (_url: string) => { + // Per-socket round-robin cursor so each stream advances independently. + let frameIndex = 0; + return new SimCameraSocket({ + fps: options.fps, + width: image.sizeX, + height: image.sizeY, + generateFrame: ({ width, height }) => { + const opacity = num(sim.get(opacityPV), 1); + const mode = sim.get(modePV) === 'image_file' ? 'image_file' : 'noisy'; + if (mode !== 'image_file') { + return renderDetectorFrame({ width, height, mode, opacity }); + } + + // Base image fills the canvas, dimmed by the energy-driven opacity. + // Cycle to the next base image each frame so the stream animates. + const layers: RenderLayer[] = []; + if (baseFiles.length > 0) { + const file = baseFiles[frameIndex % baseFiles.length]; + frameIndex += 1; + layers.push({ file, opacity, x: 0, y: 0, width, height }); + } + // Overlays composite on top at full opacity, centered on their + // (possibly PV-driven) center coordinate. + for (const { overlay, xPV, yPV } of overlayPVs) { + const centerX = num(sim.get(xPV), overlay.x); + const centerY = num(sim.get(yPV), overlay.y); + layers.push({ + file: overlay.file, + opacity: 1, + x: centerX - overlay.width / 2, + y: centerY - overlay.height / 2, + width: overlay.width, + height: overlay.height, + }); + } + return renderDetectorFrame({ width, height, mode, layers }); + }, + }); + }; +} diff --git a/src/lib/ophyd-sim/camera/types.ts b/src/lib/ophyd-sim/camera/types.ts new file mode 100644 index 00000000..389ca050 --- /dev/null +++ b/src/lib/ophyd-sim/camera/types.ts @@ -0,0 +1,31 @@ +/** + * The minimal slice of the browser `WebSocket` API that {@link useCameraCanvas} + * actually consumes. A simulator only has to implement this surface — not the + * full `WebSocket` interface — to stand in for the real camera stream. + * + * The real `WebSocket` is structurally wider than this (its `onmessage` carries + * a full `MessageEvent`), so the default factory casts it to this shape; see + * `defaultCameraSocketFactory` in the hook. + */ +export interface CameraSocketMessageEvent { + /** JSON string for control messages, or a binary JPEG frame. */ + data: string | ArrayBuffer | Blob; +} + +export interface CameraSocketLike { + /** Mirrors `WebSocket.readyState` (`1` === OPEN). */ + readyState: number; + onopen: ((event: Event) => void) | null; + onmessage: ((event: CameraSocketMessageEvent) => void) | null; + onerror: ((event: Event) => void) | null; + onclose: ((event: Event) => void) | null; + /** Outgoing control messages are always JSON strings. */ + send(data: string): void; + close(): void; +} + +/** + * Constructs a camera socket for a given URL. The hook defaults to one that + * wraps the real `WebSocket`; pass a sim factory to render mock frames instead. + */ +export type CameraSocketFactory = (url: string) => CameraSocketLike; diff --git a/src/lib/ophyd-sim/core/createOphydSim.ts b/src/lib/ophyd-sim/core/createOphydSim.ts new file mode 100644 index 00000000..baa3de56 --- /dev/null +++ b/src/lib/ophyd-sim/core/createOphydSim.ts @@ -0,0 +1,170 @@ +import { DependencyGraph } from './dependencyGraph'; +import { Scheduler } from './scheduler'; +import { SimState } from './state'; +import type { + CreateOphydSimOptions, + OphydSim, + PVMetadata, + SimListener, + SimRegistration, + SimValue, + SimValueContext, + SimValueFn, + TickHandler, + Unsubscribe, +} from './types'; + +const DEFAULT_TICK_MS = 100; + +/** + * Build a simulator instance. Wires up shared state, a dependency graph for + * derived signals, and a tick scheduler for periodic/motor updates. Devices + * register themselves via the factory functions passed in `options.devices`. + * + * The simulator does not start ticking until `start()` is called — typically + * by OphydSimProvider on mount. + */ +export function createOphydSim(options: CreateOphydSimOptions): OphydSim { + const now = options.now ?? Date.now; + const random = options.random ?? Math.random; + const state = new SimState(now); + const graph = new DependencyGraph(); + const scheduler = new Scheduler(options.tickMs ?? DEFAULT_TICK_MS, now); + + const setHandlers = new Map void>>(); + const recomputeFns = new Map void>(); + + const buildContext = (dt: number, time: number): SimValueContext => ({ + time, + dt, + random, + get: { + value: (n) => state.get(n), + number: (n) => { + const v = state.get(n); + return typeof v === 'number' ? v : Number(v ?? 0); + }, + boolean: (n) => { + const v = state.get(n); + return typeof v === 'boolean' ? v : Boolean(v); + }, + string: (n) => { + const v = state.get(n); + return v === undefined ? '' : String(v); + }, + }, + }); + + // `set` cascades: write value → notify subscribers → call onSet handlers → + // recompute downstream derived signals in topological order. Recomputation + // is best-effort: a derived signal that throws logs and is skipped. + const setValue = (name: string, value: SimValue): void => { + const changed = state.set(name, value); + if (!changed) return; + + const handlers = setHandlers.get(name); + if (handlers) { + for (const h of handlers) { + try { + h(value); + } catch (err) { + console.error(`[ophyd-sim] onSet handler for "${name}" threw:`, err); + } + } + } + + for (const dependent of graph.dependentsOf(name)) { + const fn = recomputeFns.get(dependent); + if (fn) { + try { + fn(); + } catch (err) { + console.error(`[ophyd-sim] recompute for "${dependent}" threw:`, err); + } + } + } + }; + + const registration: SimRegistration = { + seed(name, value, metadata) { + state.seed(name, value, metadata); + }, + setMetadata(name, metadata) { + state.setMetadata(name, metadata); + }, + registerDerived(name: string, dependsOn: string[], compute: SimValueFn) { + graph.register(name, dependsOn); + const recompute = (): void => { + const ctx = buildContext(0, now()); + const next = compute(ctx); + setValue(name, next); + }; + recomputeFns.set(name, recompute); + // Seed initial value from compute() so dependents have something + // to read on first subscribe. Compute may legitimately read + // dependencies that aren't seeded yet — guard inside compute. + try { + const ctx = buildContext(0, now()); + state.seed(name, compute(ctx)); + } catch (err) { + console.error( + `[ophyd-sim] initial compute for "${name}" failed (will retry on dep change):`, + err, + ); + } + }, + onTick(handler: TickHandler): Unsubscribe { + return scheduler.onTick(handler); + }, + onSet(name, handler) { + let set = setHandlers.get(name); + if (!set) { + set = new Set(); + setHandlers.set(name, set); + } + set.add(handler); + return () => set!.delete(handler); + }, + get(name) { + return state.get(name); + }, + set: setValue, + metadata(name) { + return state.metadata(name); + }, + random, + now, + context: buildContext, + }; + + for (const factory of options.devices) { + factory(registration); + } + + const sim: OphydSim = { + subscribe(name, listener: SimListener) { + return state.subscribe(name, listener); + }, + set: setValue, + get(name) { + return state.get(name); + }, + metadata(name): PVMetadata | undefined { + return state.metadata(name); + }, + names() { + return state.names(); + }, + start() { + scheduler.start(); + }, + stop() { + scheduler.stop(); + }, + advance(deltaMs) { + scheduler.advance(deltaMs); + }, + }; + + return sim; +} diff --git a/src/lib/ophyd-sim/core/dependencyGraph.ts b/src/lib/ophyd-sim/core/dependencyGraph.ts new file mode 100644 index 00000000..f41a6be7 --- /dev/null +++ b/src/lib/ophyd-sim/core/dependencyGraph.ts @@ -0,0 +1,84 @@ +/** + * Forward-edge dependency map: when a source PV changes, walk its dependents + * and recompute them in topological order. + * + * Cycles throw at registration time — derived signals must form a DAG. + */ +export class DependencyGraph { + private readonly dependents = new Map>(); + private readonly dependencies = new Map>(); + + /** Declare that `name` depends on every PV in `dependsOn`. */ + register(name: string, dependsOn: string[]): void { + const deps = this.dependencies.get(name) ?? new Set(); + for (const source of dependsOn) { + deps.add(source); + const fwd = this.dependents.get(source) ?? new Set(); + fwd.add(name); + this.dependents.set(source, fwd); + } + this.dependencies.set(name, deps); + + if (this.hasCycle()) { + throw new Error( + `[ophyd-sim] dependency cycle detected involving "${name}" (depends on: ${dependsOn.join( + ', ', + )})`, + ); + } + } + + /** + * Return dependents of `name` in topological order — every dependent + * appears after each of its own (transitive) dependencies in this list. + * Useful for cascading recomputation after a source value changes. + */ + dependentsOf(name: string): string[] { + const visited = new Set(); + const order: string[] = []; + + const visit = (node: string): void => { + if (visited.has(node)) return; + visited.add(node); + const fwd = this.dependents.get(node); + if (fwd) { + for (const next of fwd) visit(next); + } + order.push(node); + }; + + const direct = this.dependents.get(name); + if (!direct) return []; + for (const d of direct) visit(d); + // visit() pushes in post-order; reverse so dependencies precede dependents. + return order.reverse(); + } + + private hasCycle(): boolean { + const WHITE = 0; + const GRAY = 1; + const BLACK = 2; + const color = new Map(); + + const dfs = (node: string): boolean => { + color.set(node, GRAY); + const fwd = this.dependents.get(node); + if (fwd) { + for (const next of fwd) { + const c = color.get(next) ?? WHITE; + if (c === GRAY) return true; + if (c === WHITE && dfs(next)) return true; + } + } + color.set(node, BLACK); + return false; + }; + + for (const node of this.dependencies.keys()) { + if ((color.get(node) ?? WHITE) === WHITE) { + if (dfs(node)) return true; + } + } + return false; + } +} diff --git a/src/lib/ophyd-sim/core/scheduler.ts b/src/lib/ophyd-sim/core/scheduler.ts new file mode 100644 index 00000000..5a26048f --- /dev/null +++ b/src/lib/ophyd-sim/core/scheduler.ts @@ -0,0 +1,104 @@ +import type { TickHandler, TickHandlerContext, Unsubscribe } from './types'; + +/** + * Drives per-tick work for motors and periodic signals. + * + * Prefers requestAnimationFrame in the browser for smoothness; falls back to + * setInterval (e.g. for tests in jsdom, where rAF may run synchronously). + * + * Time is monotonic across pause/resume — devices see dt measured against the + * previous tick, not wall-clock. + */ +export class Scheduler { + private handlers = new Set(); + private running = false; + private rafHandle: number | null = null; + private intervalHandle: ReturnType | null = null; + private lastTickMs: number | null = null; + private readonly tickMs: number; + private readonly now: () => number; + + constructor(tickMs: number, now: () => number) { + this.tickMs = tickMs; + this.now = now; + } + + onTick(handler: TickHandler): Unsubscribe { + this.handlers.add(handler); + return () => { + this.handlers.delete(handler); + }; + } + + start(): void { + if (this.running) return; + this.running = true; + this.lastTickMs = this.now(); + + const useRaf = + typeof globalThis !== 'undefined' && + typeof (globalThis as { requestAnimationFrame?: unknown }).requestAnimationFrame === + 'function'; + + if (useRaf) { + const loop = (): void => { + if (!this.running) return; + this.tick(); + this.rafHandle = ( + globalThis as { requestAnimationFrame: (cb: FrameRequestCallback) => number } + ).requestAnimationFrame(loop); + }; + this.rafHandle = ( + globalThis as { requestAnimationFrame: (cb: FrameRequestCallback) => number } + ).requestAnimationFrame(loop); + } else { + this.intervalHandle = setInterval(() => this.tick(), this.tickMs); + } + } + + stop(): void { + if (!this.running) return; + this.running = false; + if (this.rafHandle !== null) { + const cancel = (globalThis as { cancelAnimationFrame?: (h: number) => void }) + .cancelAnimationFrame; + if (cancel) cancel(this.rafHandle); + this.rafHandle = null; + } + if (this.intervalHandle !== null) { + clearInterval(this.intervalHandle); + this.intervalHandle = null; + } + this.lastTickMs = null; + } + + /** + * Run one synthetic tick. Useful for tests that want to advance time + * without running a real animation loop. + */ + advance(deltaMs: number): void { + const time = this.lastTickMs ?? this.now(); + const dt = deltaMs / 1000; + const ctx: TickHandlerContext = { time, dt }; + this.runHandlers(ctx); + this.lastTickMs = time + deltaMs; + } + + private tick(): void { + const time = this.now(); + const last = this.lastTickMs ?? time; + const dt = Math.max(0, (time - last) / 1000); + this.lastTickMs = time; + this.runHandlers({ time, dt }); + } + + private runHandlers(ctx: TickHandlerContext): void { + for (const handler of this.handlers) { + try { + handler(ctx); + } catch (err) { + console.error('[ophyd-sim] tick handler threw:', err); + } + } + } +} diff --git a/src/lib/ophyd-sim/core/state.ts b/src/lib/ophyd-sim/core/state.ts new file mode 100644 index 00000000..a69401d3 --- /dev/null +++ b/src/lib/ophyd-sim/core/state.ts @@ -0,0 +1,92 @@ +import type { PVMetadata, SimEvent, SimListener, SimValue, Unsubscribe } from './types'; + +interface PVEntry { + value: SimValue | undefined; + metadata: PVMetadata; + listeners: Set; +} + +/** + * Shared state map: PV name → value + metadata + subscribers. + * + * Subscribers receive the latest known value immediately on subscribe if one + * exists. `set` only emits when the value actually changed (reference or + * primitive equality), so static signals don't generate update storms. + */ +export class SimState { + private readonly entries = new Map(); + private readonly now: () => number; + + constructor(now: () => number) { + this.now = now; + } + + private entry(name: string): PVEntry { + let e = this.entries.get(name); + if (!e) { + e = { value: undefined, metadata: {}, listeners: new Set() }; + this.entries.set(name, e); + } + return e; + } + + seed(name: string, value: SimValue, metadata: PVMetadata = {}): void { + const e = this.entry(name); + e.value = value; + e.metadata = { ...e.metadata, ...metadata }; + } + + setMetadata(name: string, metadata: PVMetadata): void { + const e = this.entry(name); + e.metadata = { ...e.metadata, ...metadata }; + } + + metadata(name: string): PVMetadata | undefined { + return this.entries.get(name)?.metadata; + } + + get(name: string): SimValue | undefined { + return this.entries.get(name)?.value; + } + + has(name: string): boolean { + return this.entries.has(name); + } + + names(): string[] { + return Array.from(this.entries.keys()); + } + + /** + * Set a value. Returns true if it changed (and listeners were notified). + */ + set(name: string, value: SimValue): boolean { + const e = this.entry(name); + if (e.value === value) return false; + e.value = value; + const event: SimEvent = { name, value, timestamp: this.now() }; + for (const listener of e.listeners) { + try { + listener(event); + } catch (err) { + console.error('[ophyd-sim] subscriber threw:', err); + } + } + return true; + } + + subscribe(name: string, listener: SimListener): Unsubscribe { + const e = this.entry(name); + e.listeners.add(listener); + if (e.value !== undefined) { + try { + listener({ name, value: e.value, timestamp: this.now() }); + } catch (err) { + console.error('[ophyd-sim] subscriber threw on replay:', err); + } + } + return () => { + e.listeners.delete(listener); + }; + } +} diff --git a/src/lib/ophyd-sim/core/types.ts b/src/lib/ophyd-sim/core/types.ts new file mode 100644 index 00000000..b7621713 --- /dev/null +++ b/src/lib/ophyd-sim/core/types.ts @@ -0,0 +1,103 @@ +export type SimValue = string | number | boolean; + +export interface PVMetadata { + units?: string; + lower_ctrl_limit?: number | null; + upper_ctrl_limit?: number | null; + precision?: number; + enum_strs?: string[] | null; + write_access?: boolean; + read_access?: boolean; +} + +export interface SimEvent { + name: string; + value: SimValue; + timestamp: number; +} + +export type SimListener = (event: SimEvent) => void; +export type Unsubscribe = () => void; + +export interface SimValueContext { + get: { + value(name: string): SimValue | undefined; + number(name: string): number; + boolean(name: string): boolean; + string(name: string): string; + }; + time: number; + dt: number; + random: () => number; +} + +export type SimValueFn = (ctx: SimValueContext) => SimValue; + +export interface TickHandlerContext { + time: number; + dt: number; +} + +export type TickHandler = (ctx: TickHandlerContext) => void; + +/** + * Internal handle a device factory uses to wire itself into the simulator. + * + * The factory receives this from `createOphydSim` at registration time and is + * free to seed initial state, declare derived signals, and register tick or + * set handlers. Devices do not see other devices directly — they read shared + * state via the same `get`/`set` API user code uses. + */ +export interface SimRegistration { + seed(name: string, value: SimValue, metadata?: PVMetadata): void; + setMetadata(name: string, metadata: PVMetadata): void; + /** + * Declare a derived signal. `compute` is invoked with a value context any + * time one of `dependsOn` changes (and once at registration time to seed + * the initial value). + */ + registerDerived(name: string, dependsOn: string[], compute: SimValueFn): void; + onTick(handler: TickHandler): Unsubscribe; + onSet(name: string, handler: (value: SimValue) => void): Unsubscribe; + get(name: string): SimValue | undefined; + set(name: string, value: SimValue): void; + metadata(name: string): PVMetadata | undefined; + random(): number; + now(): number; + /** Build a SimValueContext for use in tick callbacks. */ + context(dt: number, time: number): SimValueContext; +} + +export type DeviceFactory = (registration: SimRegistration) => void; + +export interface OphydSim { + /** Subscribe to value updates for a PV. Replays the latest value synchronously if known. */ + subscribe(name: string, listener: SimListener): Unsubscribe; + /** Write to a PV. Triggers dependent recomputation and notifies subscribers if value changed. */ + set(name: string, value: SimValue): void; + /** Read the latest value for a PV. */ + get(name: string): SimValue | undefined; + /** Read metadata (units, limits, access) for a PV if known. */ + metadata(name: string): PVMetadata | undefined; + /** Names of every PV currently registered. */ + names(): string[]; + /** Begin the tick loop. Idempotent. */ + start(): void; + /** Stop the tick loop. Idempotent. */ + stop(): void; + /** + * Run a single synthetic tick of `deltaMs`. Useful in tests to drive + * motor motion and periodic recompute without spinning the real loop. + */ + advance(deltaMs: number): void; +} + +export interface CreateOphydSimOptions { + devices: DeviceFactory[]; + /** Global tick interval in ms. Defaults to ~33ms (≈30 Hz). */ + tickMs?: number; + /** Random source. Defaults to Math.random. Override for deterministic tests. */ + random?: () => number; + /** Override the clock. Defaults to Date.now. */ + now?: () => number; +} diff --git a/src/lib/ophyd-sim/devices/detector.ts b/src/lib/ophyd-sim/devices/detector.ts new file mode 100644 index 00000000..256c092e --- /dev/null +++ b/src/lib/ophyd-sim/devices/detector.ts @@ -0,0 +1,201 @@ +import type { DeviceFactory, PVMetadata } from '../core/types'; + +/** A point mapping an input value (e.g. an energy in eV) to an output (e.g. opacity). */ +export interface ModulationPoint { + in: number; + out: number; +} + +/** + * Declarative rule by which another device's PV adjusts the detector image. + * For now the only `effect` is `'opacity'`. The output is a linear interpolation + * from `from` to `to`, clamped to that output range. + */ +export interface DetectorModulation { + /** PV name whose value drives this effect (e.g. the beam-energy PV). */ + source: string; + effect: 'opacity'; + from: ModulationPoint; + to: ModulationPoint; +} + +/** + * Live binding of one overlay coordinate (center X or Y, in pixels) to a source + * PV. The source value is linearly mapped from `from.in→to.in` onto the pixel + * range `from.out→to.out`, clamped to that range — identical math to an opacity + * modulation, but the output is a pixel coordinate rather than 0–1. + */ +export interface AxisBinding { + /** PV name whose value drives this coordinate (e.g. a beamstop motor RBV). */ + source: string; + from: ModulationPoint; + to: ModulationPoint; +} + +/** + * An image layer drawn on top of the base image. Positioned by its center so a + * marker (e.g. a beamstop dot) sits where you expect. The center defaults to the + * static `x`/`y`; pass `positionX`/`positionY` to drive it from a live PV. + */ +export interface DetectorOverlay { + /** URL of the overlay image. */ + file: string; + /** Render size in pixels. */ + width: number; + height: number; + /** Center position in pixels (initial value; also the fallback if unbound). */ + x: number; + y: number; + /** Bind the center X to a source PV. */ + positionX?: AxisBinding; + /** Bind the center Y to a source PV. */ + positionY?: AxisBinding; +} + +export interface DetectorImageConfig { + /** Image source: random noise, or a static image file. */ + mode: 'noisy' | 'image_file'; + sizeX: number; + sizeY: number; + /** URL of the base (bottom) image to render when `mode === 'image_file'`. */ + file?: string; + /** + * URLs of base images to cycle through, one per emitted frame (round-robin), + * when `mode === 'image_file'`. Takes precedence over `file`; use a single + * static base via `file` or a one-element `files`. Lets the stream animate + * through a set of frames (e.g. a captured diffraction sequence). + */ + files?: string[]; + /** Image layers drawn over the base, in order (`image_file` mode only). */ + overlays?: DetectorOverlay[]; +} + +/** + * A detector defined declaratively (typically loaded from a JSON file). The + * `prefix` becomes the PV prefix (e.g. `'13SIM1'`) under which the standard + * Area-Detector PVs are seeded. + */ +export interface DetectorConfig { + prefix: string; + image: DetectorImageConfig; + modulations?: DetectorModulation[]; +} + +/** PV suffix carrying the derived image opacity (0–1), under `:`. */ +export const OPACITY_SUFFIX = 'image1:Opacity'; +/** PV suffix carrying the image value mode (`'noisy' | 'image_file'`). */ +export const MODE_SUFFIX = 'image1:Mode'; + +/** PV suffix for an overlay's center-X (pixels). `index` is zero-based. */ +export function overlayCenterXSuffix(index: number): string { + return `image1:Overlay${index + 1}:CenterX`; +} +/** PV suffix for an overlay's center-Y (pixels). `index` is zero-based. */ +export function overlayCenterYSuffix(index: number): string { + return `image1:Overlay${index + 1}:CenterY`; +} + +/** + * Linearly map `value` from `from.in→to.in` onto `from.out→to.out`, clamping + * the normalized position to [0, 1] so the result never leaves the output range. + */ +export function mapLinearClamped( + value: number, + from: ModulationPoint, + to: ModulationPoint, +): number { + const span = to.in - from.in; + const t = span === 0 ? 0 : (value - from.in) / span; + const clamped = Math.max(0, Math.min(1, t)); + return from.out + clamped * (to.out - from.out); +} + +/** + * Simulated area detector. + * + * Unlike `motor`/`signal` (single-value devices), a detector seeds a *cluster* + * of PVs from one `prefix` so a caller passing `'13SIM1'` can immediately read + * the standard camera PVs (`cam1:SizeX`, `cam1:SizeY`, `cam1:MinX`, `cam1:MinY`, + * `cam1:ColorMode`, `cam1:DataType`, `cam1:Acquire`) plus the image `value` mode + * (`image1:Mode`). + * + * The image itself is not a PV (it streams over the camera socket), but its + * scalar parameters are. Each `modulation` registers a derived PV that other + * devices drive — e.g. beam energy → `image1:Opacity` — which the camera socket + * reads per frame to dim the rendered image. + */ +export function detector(config: DetectorConfig): DeviceFactory { + return (reg) => { + const { prefix, image } = config; + const pv = (suffix: string): string => `${prefix}:${suffix}`; + + const pixelMeta: PVMetadata = { + units: 'px', + read_access: true, + write_access: true, + precision: 0, + }; + + reg.seed(pv('cam1:SizeX'), image.sizeX, pixelMeta); + reg.seed(pv('cam1:SizeY'), image.sizeY, pixelMeta); + reg.seed(pv('cam1:MinX'), 0, pixelMeta); + reg.seed(pv('cam1:MinY'), 0, pixelMeta); + reg.seed(pv('cam1:ColorMode'), 0, { + read_access: true, + write_access: true, + enum_strs: ['Mono', 'RGB1'], + }); + reg.seed(pv('cam1:DataType'), 1, { + read_access: true, + write_access: true, + enum_strs: [ + 'Int8', + 'UInt8', + 'Int16', + 'UInt16', + 'Int32', + 'UInt32', + 'Float32', + 'Float64', + ], + }); + reg.seed(pv('cam1:Acquire'), 0, { + read_access: true, + write_access: true, + enum_strs: ['Done', 'Acquire'], + }); + reg.seed(pv(MODE_SUFFIX), image.mode, { + read_access: true, + write_access: true, + enum_strs: ['noisy', 'image_file'], + }); + + for (const mod of config.modulations ?? []) { + if (mod.effect === 'opacity') { + const { from, to } = mod; + reg.registerDerived(pv(OPACITY_SUFFIX), [mod.source], ({ get }) => + mapLinearClamped(get.number(mod.source), from, to), + ); + } + } + + // Seed a pixel coordinate PV at `pixelPV`, then — if `binding` is given — + // make it derived so it tracks the source PV (e.g. a beamstop motor). The + // seed runs first so the PV always exists with pixel metadata even when + // unbound; registerDerived overwrites the value but preserves metadata. + const bindAxis = (pixelPV: string, fallback: number, binding?: AxisBinding): void => { + reg.seed(pixelPV, fallback, pixelMeta); + if (binding) { + const { source, from, to } = binding; + reg.registerDerived(pixelPV, [source], ({ get }) => + mapLinearClamped(get.number(source), from, to), + ); + } + }; + + (image.overlays ?? []).forEach((overlay, i) => { + bindAxis(pv(overlayCenterXSuffix(i)), overlay.x, overlay.positionX); + bindAxis(pv(overlayCenterYSuffix(i)), overlay.y, overlay.positionY); + }); + }; +} diff --git a/src/lib/ophyd-sim/devices/hexapod.ts b/src/lib/ophyd-sim/devices/hexapod.ts new file mode 100644 index 00000000..1a7d9009 --- /dev/null +++ b/src/lib/ophyd-sim/devices/hexapod.ts @@ -0,0 +1,167 @@ +import type { DeviceFactory, SimValue } from '../core/types'; + +export interface HexapodOptions { + /** Device PV prefix (e.g. 'SYM:HEX01'). Defaults to 'SYM:HEX01'. */ + prefix?: string; + /** Translation-axis velocity in mm/s. Defaults to 4. */ + translationVelocity?: number; + /** Rotation-axis velocity in deg/s. Defaults to 2. */ + rotationVelocity?: number; + /** Translation soft limits (mm). Defaults to [-10, 10]. */ + translationLimits?: [number, number]; + /** Rotation soft limits (deg). Defaults to [-5, 5]. */ + rotationLimits?: [number, number]; + /** Settle tolerance in axis units. Defaults to 1e-4. */ + epsilon?: number; +} + +type Axis = 'tx' | 'ty' | 'tz' | 'rx' | 'ry' | 'rz'; +const TRANSLATION_AXES: Axis[] = ['tx', 'ty', 'tz']; +const ROTATION_AXES: Axis[] = ['rx', 'ry', 'rz']; +const AXES: Axis[] = [...TRANSLATION_AXES, ...ROTATION_AXES]; + +const DEFAULT_PREFIX = 'SYM:HEX01'; + +const clamp = (value: number, [lower, upper]: [number, number]) => + Math.min(upper, Math.max(lower, value)); + +/** + * Symetrie six-axis hexapod, modelled entirely as signals — the device has + * explicit `_RBV` readbacks separate from its setpoints, so it does not map + * cleanly onto the linear `motor()` (one setpoint ⇒ one readback). Instead each + * axis owns a readback that animates toward a captured target, and the move is + * gated by the shared `MOVE_PTP` command PV, mirroring the real controller. + * + * PVs, relative to `prefix` (matching hexapodUtils.ts): + * - `s_uto_{tx,ty,tz}_RBV` — translation readbacks (mm), read-only + * - `s_uto_{rx,ry,rz}_RBV` — rotation readbacks (deg), read-only + * - `MOVE_PTP:{Tx,Ty,Tz,Rx,Ry,Rz}` — per-axis setpoints (writable "goals") + * - `MOVE_PTP` — execute command: write 1 to move all axes to + * their setpoints; auto-returns to 0 + * - `MOVE_PTP:MoveType` — 0 = absolute, 1 = relative + * - `s_hexa:InPosition_RBV` — 1 when settled/stopped, 0 while moving + * - `STOP` — write 1 to halt immediately; auto-returns to 0 + * + * Writing a setpoint alone does nothing; motion only begins on the 0→1 edge of + * `MOVE_PTP`, at which point targets are captured (absolute, or added to the + * current readback when relative) and clamped to the axis limits. + */ +export function hexapod(opts: HexapodOptions = {}): DeviceFactory { + return (reg) => { + const prefix = opts.prefix ?? DEFAULT_PREFIX; + const translationVelocity = opts.translationVelocity ?? 4; + const rotationVelocity = opts.rotationVelocity ?? 2; + const translationLimits = opts.translationLimits ?? [-10, 10]; + const rotationLimits = opts.rotationLimits ?? [-5, 5]; + const epsilon = opts.epsilon ?? 1e-4; + + const pv = (suffix: string) => `${prefix}:${suffix}`; + const rbvPV = (axis: Axis) => pv(`s_uto_${axis}_RBV`); + const setpointPV = (axis: Axis) => pv(`MOVE_PTP:${axis[0].toUpperCase()}${axis[1]}`); + const executeAllPV = pv('MOVE_PTP'); + const inPositionPV = pv('s_hexa:InPosition_RBV'); + const stopPV = pv('STOP'); + const moveTypePV = pv('MOVE_PTP:MoveType'); + + const axisConfig = (axis: Axis) => { + const isRotation = ROTATION_AXES.includes(axis); + return { + units: isRotation ? 'deg' : 'mm', + limits: isRotation ? rotationLimits : translationLimits, + velocity: isRotation ? rotationVelocity : translationVelocity, + }; + }; + + // Seed readbacks + setpoints for every axis. Readbacks are read-only; + // setpoints are writable goals the controller stages before executing. + AXES.forEach((axis) => { + const { units, limits } = axisConfig(axis); + const [lower, upper] = limits; + reg.seed(rbvPV(axis), 0, { + units, + lower_ctrl_limit: lower, + upper_ctrl_limit: upper, + read_access: true, + write_access: false, + precision: 4, + }); + reg.seed(setpointPV(axis), 0, { + units, + lower_ctrl_limit: lower, + upper_ctrl_limit: upper, + read_access: true, + write_access: true, + precision: 4, + }); + }); + + reg.seed(executeAllPV, 0, { read_access: true, write_access: true }); + reg.seed(moveTypePV, 0, { + read_access: true, + write_access: true, + enum_strs: ['Absolute', 'Relative'], + }); + reg.seed(stopPV, 0, { read_access: true, write_access: true }); + // Starts settled (in position) at the all-zero home pose. + reg.seed(inPositionPV, 1, { + read_access: true, + write_access: false, + enum_strs: ['Moving', 'InPosition'], + }); + + // The pose each readback is currently animating toward. Equal to the + // readbacks at rest, so no motion happens until MOVE_PTP captures targets. + const targets: Record = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0 }; + + const asNumber = (value: SimValue | undefined) => Number(value ?? 0); + + // MOVE_PTP command: on the 0→1 edge, capture per-axis targets from the + // staged setpoints (absolute) or offset from the current readback + // (relative), clamp to limits, and begin moving. Return to 0 so the next + // write re-triggers. + reg.onSet(executeAllPV, (value) => { + if (asNumber(value) !== 1) return; + const relative = asNumber(reg.get(moveTypePV)) === 1; + AXES.forEach((axis) => { + const setpoint = asNumber(reg.get(setpointPV(axis))); + const current = asNumber(reg.get(rbvPV(axis))); + const raw = relative ? current + setpoint : setpoint; + targets[axis] = clamp(raw, axisConfig(axis).limits); + }); + reg.set(inPositionPV, 0); + reg.set(executeAllPV, 0); + }); + + // STOP command: freeze every axis at its current readback and settle. + reg.onSet(stopPV, (value) => { + if (asNumber(value) !== 1) return; + AXES.forEach((axis) => { + targets[axis] = asNumber(reg.get(rbvPV(axis))); + }); + reg.set(inPositionPV, 1); + reg.set(stopPV, 0); + }); + + // Advance each readback toward its target at the axis velocity. When all + // axes are within epsilon, snap and mark the hexapod in position. + reg.onTick(({ dt }) => { + let allSettled = true; + AXES.forEach((axis) => { + const target = targets[axis]; + const position = asNumber(reg.get(rbvPV(axis))); + const distance = target - position; + const absDistance = Math.abs(distance); + if (absDistance < epsilon) { + if (position !== target) reg.set(rbvPV(axis), target); + return; + } + allSettled = false; + const step = Math.min(absDistance, axisConfig(axis).velocity * dt); + reg.set(rbvPV(axis), position + Math.sign(distance) * step); + }); + if (allSettled && asNumber(reg.get(inPositionPV)) !== 1) { + reg.set(inPositionPV, 1); + } + }); + }; +} diff --git a/src/lib/ophyd-sim/devices/motor.ts b/src/lib/ophyd-sim/devices/motor.ts new file mode 100644 index 00000000..ea95c60c --- /dev/null +++ b/src/lib/ophyd-sim/devices/motor.ts @@ -0,0 +1,110 @@ +import type { DeviceFactory, PVMetadata } from '../core/types'; + +export interface MotorOptions { + /** Setpoint PV name (e.g. "IOC:m1"). */ + name: string; + /** Readback PV name. Defaults to `${name}.RBV`. */ + readbackName?: string; + /** Moving-flag PV name (1 while in motion, 0 otherwise). Defaults to `${name}.MOVN`. */ + movingName?: string; + initialPosition?: number; + /** Velocity in units/second. */ + velocity?: number; + /** Soft control limits applied to setpoint writes. */ + limits?: [number, number]; + units?: string; + /** Within how close (units) the readback must be to the setpoint to settle. */ + epsilon?: number; +} + +/** + * Linear-velocity motor. + * + * Exposes three PVs sharing one motion state: + * - `name` — setpoint (writable; clamps to limits) + * - `name.RBV` (default) — readback, animates toward setpoint + * - `name.MOVN` (default) — 1 while moving, 0 otherwise + * + * Writes to the setpoint clamp into `[low, high]`, set MOVN=1, and start the + * readback advancing by `velocity * dt` per tick toward the target. When + * within `epsilon`, readback snaps to setpoint and MOVN clears. + */ +export function motor(opts: MotorOptions): DeviceFactory { + return (reg) => { + const setpointName = opts.name; + const readbackName = opts.readbackName ?? `${opts.name}.RBV`; + const movingName = opts.movingName ?? `${opts.name}.MOVN`; + + const velocity = opts.velocity ?? 1; + const epsilon = opts.epsilon ?? 1e-4; + const lower = opts.limits ? opts.limits[0] : null; + const upper = opts.limits ? opts.limits[1] : null; + const initial = opts.initialPosition ?? 0; + + const setpointMetadata: PVMetadata = { + units: opts.units, + lower_ctrl_limit: lower, + upper_ctrl_limit: upper, + read_access: true, + write_access: true, + precision: 4, + }; + const readbackMetadata: PVMetadata = { + units: opts.units, + lower_ctrl_limit: lower, + upper_ctrl_limit: upper, + read_access: true, + write_access: false, + precision: 4, + }; + const movingMetadata: PVMetadata = { + read_access: true, + write_access: false, + enum_strs: ['Done', 'Moving'], + }; + + reg.seed(setpointName, initial, setpointMetadata); + reg.seed(readbackName, initial, readbackMetadata); + reg.seed(movingName, 0, movingMetadata); + + // Clamp setpoint writes to limits. Motor motion handler reads target + // back via reg.get, so clamping has to update the stored value. + reg.onSet(setpointName, (value) => { + if (typeof value !== 'number') return; + const clamped = + lower !== null && upper !== null + ? Math.min(upper, Math.max(lower, value)) + : lower !== null + ? Math.max(lower, value) + : upper !== null + ? Math.min(upper, value) + : value; + if (clamped !== value) { + // Re-issue the clamped value; onSet won't recurse because + // state.set early-returns on equality. + reg.set(setpointName, clamped); + } + }); + + reg.onTick(({ dt }) => { + const targetRaw = reg.get(setpointName); + const positionRaw = reg.get(readbackName); + if (typeof targetRaw !== 'number' || typeof positionRaw !== 'number') return; + + const distance = targetRaw - positionRaw; + const absDistance = Math.abs(distance); + + if (absDistance < epsilon) { + if (positionRaw !== targetRaw) reg.set(readbackName, targetRaw); + if (reg.get(movingName) !== 0) reg.set(movingName, 0); + return; + } + + if (reg.get(movingName) !== 1) reg.set(movingName, 1); + + const step = Math.min(absDistance, velocity * dt); + const nextPosition = positionRaw + Math.sign(distance) * step; + reg.set(readbackName, nextPosition); + }); + }; +} diff --git a/src/lib/ophyd-sim/devices/shutter.ts b/src/lib/ophyd-sim/devices/shutter.ts new file mode 100644 index 00000000..54b1aed2 --- /dev/null +++ b/src/lib/ophyd-sim/devices/shutter.ts @@ -0,0 +1,45 @@ +import type { DeviceFactory, PVMetadata } from '../core/types'; + +/** Default analog-output value (volts) that means the shutter is open. */ +export const SHUTTER_OPEN_VALUE = 0; +/** Default analog-output value (volts) that means the shutter is closed. */ +export const SHUTTER_CLOSED_VALUE = 5; + +export interface ShutterOptions { + /** PV name of the shutter analog output (e.g. "bl531:LJT4:1:AO0"). */ + name: string; + /** Value that means open. Defaults to `SHUTTER_OPEN_VALUE` (0). */ + openValue?: number; + /** Value that means closed. Defaults to `SHUTTER_CLOSED_VALUE` (5). */ + closedValue?: number; + /** Initial state. Defaults to 'open'. */ + initial?: 'open' | 'closed'; +} + +/** + * Beam shutter driven through a single analog-output PV, matching the real + * `bl531:LJT4:1:AO0` LabJack channel the `` component controls. + * + * The PV is both command and readback: writing `closedValue` (5 V) blocks the + * beam, `openValue` (0 V) lets it through. Any other value is neither, which + * the UI surfaces as an unknown state. + * + * The shutter holds only its own value — downstream signals gate themselves by + * listing this PV in their `dependsOn` and comparing against `openValue`, + * mirroring how `IOC:bs` gates `I0` in the default beamline. + */ +export function shutter(opts: ShutterOptions): DeviceFactory { + return (reg) => { + const openValue = opts.openValue ?? SHUTTER_OPEN_VALUE; + const closedValue = opts.closedValue ?? SHUTTER_CLOSED_VALUE; + + const metadata: PVMetadata = { + units: 'V', + read_access: true, + write_access: true, + precision: 2, + }; + + reg.seed(opts.name, opts.initial === 'closed' ? closedValue : openValue, metadata); + }; +} diff --git a/src/lib/ophyd-sim/devices/signal.ts b/src/lib/ophyd-sim/devices/signal.ts new file mode 100644 index 00000000..59c35a47 --- /dev/null +++ b/src/lib/ophyd-sim/devices/signal.ts @@ -0,0 +1,94 @@ +import type { + DeviceFactory, + PVMetadata, + SimValue, + SimValueContext, + SimValueFn, +} from '../core/types'; + +export interface SignalOptions { + name: string; + /** Initial value seeded immediately. Required unless `value` is a function. */ + initialValue?: SimValue; + /** Static literal, or a function evaluated periodically and/or on dep change. */ + value?: SimValue | SimValueFn; + /** Other PVs this signal depends on. Triggers recompute on their changes. */ + dependsOn?: string[]; + /** Recompute every `periodMs` regardless of dependency changes. */ + periodMs?: number; + units?: string; + /** EPICS-style control limits. */ + limits?: [number, number]; + /** Defaults to false — signals are read-only by default. */ + writeAccess?: boolean; + precision?: number; + enumStrs?: string[]; +} + +/** + * Scalar signal: static literal, dynamic function, derived from other PVs, + * or any combination. + * + * Resolution order at registration: + * 1. If `value` is a function and `dependsOn` is non-empty → derived signal + * (recomputes on any dep change AND optionally on `periodMs`). + * 2. If `value` is a function and no deps → periodic-only signal. + * 3. Otherwise → static signal seeded from `initialValue` or `value`. + */ +export function signal(opts: SignalOptions): DeviceFactory { + return (reg) => { + const metadata: PVMetadata = { + units: opts.units, + lower_ctrl_limit: opts.limits ? opts.limits[0] : null, + upper_ctrl_limit: opts.limits ? opts.limits[1] : null, + precision: opts.precision, + enum_strs: opts.enumStrs ?? null, + read_access: true, + write_access: opts.writeAccess ?? false, + }; + + const isFn = typeof opts.value === 'function'; + const valueFn = isFn ? (opts.value as SimValueFn) : null; + const deps = opts.dependsOn ?? []; + + if (valueFn && deps.length > 0) { + reg.setMetadata(opts.name, metadata); + reg.registerDerived(opts.name, deps, valueFn); + } else { + const seed: SimValue = + opts.initialValue !== undefined + ? opts.initialValue + : valueFn + ? safeInitial(valueFn, reg.context(0, reg.now())) + : ((opts.value as SimValue) ?? 0); + reg.seed(opts.name, seed, metadata); + } + + if (valueFn && opts.periodMs && opts.periodMs > 0) { + let accumMs = 0; + const periodMs = opts.periodMs; + reg.onTick(({ dt, time }) => { + accumMs += dt * 1000; + if (accumMs < periodMs) return; + accumMs = 0; + const ctx = reg.context(dt, time); + try { + reg.set(opts.name, valueFn(ctx)); + } catch (err) { + console.error( + `[ophyd-sim] periodic recompute for signal "${opts.name}" threw:`, + err, + ); + } + }); + } + }; +} + +function safeInitial(fn: SimValueFn, ctx: SimValueContext): SimValue { + try { + return fn(ctx); + } catch { + return 0; + } +} diff --git a/src/lib/ophyd-sim/generators/bragg.ts b/src/lib/ophyd-sim/generators/bragg.ts new file mode 100644 index 00000000..da24c6bb --- /dev/null +++ b/src/lib/ophyd-sim/generators/bragg.ts @@ -0,0 +1,29 @@ +export interface BraggAngleOptions { + /** Photon energy in electron-volts. */ + energyEV: number; + /** + * Crystal 2d-spacing in Ångström. Defaults to Si(111) ≈ 6.271 Å, the most + * common monochromator crystal. + */ + twoD?: number; +} + +/** Photon energy × wavelength product, hc, in eV·Å. */ +const HC_EV_ANGSTROM = 12398.42; + +/** + * Bragg angle for a double-crystal monochromator selecting `energyEV`. + * + * From Bragg's law (nλ = 2d·sinθ, n = 1) and E = hc/λ: + * sinθ = hc / (2d · E) + * Higher energy → smaller angle. The sin argument is clamped to [-1, 1] so + * energies outside the physically reachable range saturate at θ = π/2 instead + * of returning NaN. + * + * @returns the Bragg angle θ in radians, within (0, π/2]. + */ +export function braggAngle({ energyEV, twoD = 6.271 }: BraggAngleOptions): number { + const sinTheta = HC_EV_ANGSTROM / (twoD * energyEV); + const clamped = Math.min(1, Math.max(-1, sinTheta)); + return Math.asin(clamped); +} diff --git a/src/lib/ophyd-sim/generators/gaussian.ts b/src/lib/ophyd-sim/generators/gaussian.ts new file mode 100644 index 00000000..28f355c9 --- /dev/null +++ b/src/lib/ophyd-sim/generators/gaussian.ts @@ -0,0 +1,45 @@ +export interface GaussianOptions { + x: number; + center: number; + sigma: number; +} + +/** 1D Gaussian, peak value 1 at x = center. */ +export function gaussian({ x, center, sigma }: GaussianOptions): number { + if (sigma === 0) return x === center ? 1 : 0; + const z = (x - center) / sigma; + return Math.exp(-0.5 * z * z); +} + +/** Inputs for evaluating a 2D Gaussian at a single point. */ +export interface Gaussian2dOptions { + /** X coordinate at which to evaluate the Gaussian. */ + x: number; + /** Y coordinate at which to evaluate the Gaussian. */ + y: number; + /** X coordinate of the Gaussian's peak. */ + centerX: number; + /** Y coordinate of the Gaussian's peak. */ + centerY: number; + /** Standard deviation along the X axis (controls horizontal spread). */ + sigmaX: number; + /** Standard deviation along the Y axis (controls vertical spread). */ + sigmaY: number; +} + +/** + * Evaluates a separable 2D Gaussian at a single point, with a peak value of 1 + * at the center `(centerX, centerY)`. + * + * The X and Y axes are independent (no covariance term), so the result is the + * product of two 1D Gaussians. A sigma of 0 collapses that axis to a spike: + * the contribution is 1 exactly on the center coordinate and 0 elsewhere. + * + * @param options - Point to evaluate, peak center, and per-axis spreads. + * @returns The Gaussian amplitude in the range [0, 1]. + */ +export function gaussian2d({ x, y, centerX, centerY, sigmaX, sigmaY }: Gaussian2dOptions): number { + const zx = sigmaX === 0 ? (x === centerX ? 0 : Infinity) : (x - centerX) / sigmaX; + const zy = sigmaY === 0 ? (y === centerY ? 0 : Infinity) : (y - centerY) / sigmaY; + return Math.exp(-0.5 * (zx * zx + zy * zy)); +} diff --git a/src/lib/ophyd-sim/generators/index.ts b/src/lib/ophyd-sim/generators/index.ts new file mode 100644 index 00000000..c826c1c8 --- /dev/null +++ b/src/lib/ophyd-sim/generators/index.ts @@ -0,0 +1,6 @@ +export { gaussian, gaussian2d } from './gaussian'; +export type { GaussianOptions, Gaussian2dOptions } from './gaussian'; +export { randomNoise, randomWalk } from './noise'; +export type { RandomNoiseOptions, RandomWalkOptions } from './noise'; +export { braggAngle } from './bragg'; +export type { BraggAngleOptions } from './bragg'; diff --git a/src/lib/ophyd-sim/generators/noise.ts b/src/lib/ophyd-sim/generators/noise.ts new file mode 100644 index 00000000..afb1033e --- /dev/null +++ b/src/lib/ophyd-sim/generators/noise.ts @@ -0,0 +1,44 @@ +export interface RandomNoiseOptions { + random: () => number; + sigma: number; + mean?: number; +} + +/** + * Sample a single value from N(mean, sigma) using the Box-Muller transform. + * `random` must return [0, 1). Pass a seeded PRNG for deterministic tests. + */ +export function randomNoise({ random, sigma, mean = 0 }: RandomNoiseOptions): number { + // Box-Muller: avoid u1 = 0 because log(0) = -Infinity. + const u1 = Math.max(random(), Number.EPSILON); + const u2 = random(); + const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); + return mean + sigma * z; +} + +export interface RandomWalkOptions { + initial: number; + sigma: number; + min?: number; + max?: number; +} + +/** + * Build a stateful random-walk step function. Each call advances by a + * gaussian step of stddev `sigma` and clips into `[min, max]` if set. + * + * Use as the `value` of a periodic signal: + * const walk = randomWalk({ initial: 400, sigma: 0.25, min: 390, max: 410 }); + * signal({ name: 'ring_current', periodMs: 500, value: ({random}) => walk(random) }) + */ +export function randomWalk(opts: RandomWalkOptions): (random: () => number) => number { + let current = opts.initial; + return (random) => { + const step = randomNoise({ random, sigma: opts.sigma }); + let next = current + step; + if (opts.min !== undefined) next = Math.max(opts.min, next); + if (opts.max !== undefined) next = Math.min(opts.max, next); + current = next; + return next; + }; +} diff --git a/src/lib/ophyd-sim/index.ts b/src/lib/ophyd-sim/index.ts new file mode 100644 index 00000000..5a329e78 --- /dev/null +++ b/src/lib/ophyd-sim/index.ts @@ -0,0 +1,105 @@ +// Core +export { createOphydSim } from './core/createOphydSim'; +export type { + OphydSim, + CreateOphydSimOptions, + DeviceFactory, + SimRegistration, + SimValue, + SimValueContext, + SimValueFn, + SimEvent, + SimListener, + PVMetadata, + TickHandler, + Unsubscribe, +} from './core/types'; + +// Devices +export { signal } from './devices/signal'; +export type { SignalOptions } from './devices/signal'; +export { motor } from './devices/motor'; +export type { MotorOptions } from './devices/motor'; +export { shutter, SHUTTER_OPEN_VALUE, SHUTTER_CLOSED_VALUE } from './devices/shutter'; +export type { ShutterOptions } from './devices/shutter'; +export { hexapod } from './devices/hexapod'; +export type { HexapodOptions } from './devices/hexapod'; +export { + detector, + mapLinearClamped, + OPACITY_SUFFIX, + MODE_SUFFIX, + overlayCenterXSuffix, + overlayCenterYSuffix, +} from './devices/detector'; +export type { + DetectorConfig, + DetectorImageConfig, + DetectorModulation, + DetectorOverlay, + AxisBinding, + ModulationPoint, +} from './devices/detector'; + +// Generators +export { gaussian, gaussian2d, randomNoise, randomWalk, braggAngle } from './generators'; +export type { + GaussianOptions, + Gaussian2dOptions, + RandomNoiseOptions, + RandomWalkOptions, + BraggAngleOptions, +} from './generators'; + +// Transport +export { createOphydSimTransport } from './transport/createOphydSimTransport'; + +// Camera +export { SimCameraSocket, createSimCameraSocketFactory } from './camera/SimCameraSocket'; +export type { SimCameraSocketOptions, CameraFramePayload } from './camera/SimCameraSocket'; +export { + createSimDetectorCameraSocketFactory, + renderDetectorFrame, +} from './camera/createSimDetectorCameraSocketFactory'; +export type { + SimDetectorCameraOptions, + RenderLayer, +} from './camera/createSimDetectorCameraSocketFactory'; +export type { + CameraSocketLike, + CameraSocketFactory, + CameraSocketMessageEvent, +} from './camera/types'; + +// React +export { + OphydSimProvider, + useOphydSim, + useOphydSimOptional, + useSimSignal, + useSimSet, +} from './react'; +export type { OphydSimProviderProps } from './react'; + +// Storybook +export { withOphydSim } from './storybook/decorator'; + +// Scenarios +export { defaultBeamline } from './scenarios/defaultBeamline'; +export { hexapodBeamline } from './scenarios/hexapodBeamline'; +export { beamstopBeamline, simDetectorConfig } from './scenarios/beamstopBeamline'; +export { + beamstopCurrentModel, + beamYAtEnergy, + ENERGY_PV, + ENERGY_MIN_EV, + ENERGY_MAX_EV, + ENERGY_REF_EV, + CENTER_X, + CENTER_Y, + SIGMA_X, + SIGMA_Y, + PEAK_CURRENT, + BEAM_SHIFT_AMPLITUDE_MM, +} from './scenarios/beamstopModel'; +export type { BeamstopCurrentModelOptions } from './scenarios/beamstopModel'; diff --git a/src/lib/ophyd-sim/react/OphydSimContext.ts b/src/lib/ophyd-sim/react/OphydSimContext.ts new file mode 100644 index 00000000..b1ed9925 --- /dev/null +++ b/src/lib/ophyd-sim/react/OphydSimContext.ts @@ -0,0 +1,16 @@ +import { createContext, useContext } from 'react'; +import type { OphydSim } from '../core/types'; + +export const OphydSimContext = createContext(null); + +export function useOphydSim(): OphydSim { + const sim = useContext(OphydSimContext); + if (!sim) { + throw new Error('useOphydSim called outside '); + } + return sim; +} + +export function useOphydSimOptional(): OphydSim | null { + return useContext(OphydSimContext); +} diff --git a/src/lib/ophyd-sim/react/OphydSimProvider.tsx b/src/lib/ophyd-sim/react/OphydSimProvider.tsx new file mode 100644 index 00000000..3dae97c1 --- /dev/null +++ b/src/lib/ophyd-sim/react/OphydSimProvider.tsx @@ -0,0 +1,21 @@ +import { useEffect, type ReactNode } from 'react'; +import type { OphydSim } from '../core/types'; +import { OphydSimContext } from './OphydSimContext'; + +export interface OphydSimProviderProps { + /** + * The simulator instance. Must be stable across renders — wrap creation + * in useMemo or define at module scope to avoid resetting on each render. + */ + sim: OphydSim; + children: ReactNode; +} + +export function OphydSimProvider({ sim, children }: OphydSimProviderProps) { + useEffect(() => { + sim.start(); + return () => sim.stop(); + }, [sim]); + + return {children}; +} diff --git a/src/lib/ophyd-sim/react/hooks.ts b/src/lib/ophyd-sim/react/hooks.ts new file mode 100644 index 00000000..bd9fb675 --- /dev/null +++ b/src/lib/ophyd-sim/react/hooks.ts @@ -0,0 +1,27 @@ +import { useEffect, useState, useCallback } from 'react'; +import type { SimValue } from '../core/types'; +import { useOphydSim } from './OphydSimContext'; + +/** + * Subscribe to a single PV's value. Re-renders when it changes; returns the + * latest known value (or undefined if never set). + */ +export function useSimSignal(name: string): T | undefined { + const sim = useOphydSim(); + const [value, setValue] = useState(() => sim.get(name) as T | undefined); + + useEffect(() => { + const unsub = sim.subscribe(name, (event) => setValue(event.value as T)); + return unsub; + }, [sim, name]); + + return value; +} + +/** + * Return a stable `set(name, value)` writer bound to the current simulator. + */ +export function useSimSet(): (name: string, value: SimValue) => void { + const sim = useOphydSim(); + return useCallback((name: string, value: SimValue) => sim.set(name, value), [sim]); +} diff --git a/src/lib/ophyd-sim/react/index.ts b/src/lib/ophyd-sim/react/index.ts new file mode 100644 index 00000000..10373b93 --- /dev/null +++ b/src/lib/ophyd-sim/react/index.ts @@ -0,0 +1,4 @@ +export { OphydSimProvider } from './OphydSimProvider'; +export type { OphydSimProviderProps } from './OphydSimProvider'; +export { useOphydSim, useOphydSimOptional } from './OphydSimContext'; +export { useSimSignal, useSimSet } from './hooks'; diff --git a/src/lib/ophyd-sim/scenarios/beamstopBeamline.ts b/src/lib/ophyd-sim/scenarios/beamstopBeamline.ts new file mode 100644 index 00000000..496922b8 --- /dev/null +++ b/src/lib/ophyd-sim/scenarios/beamstopBeamline.ts @@ -0,0 +1,119 @@ +import { createOphydSim } from '../core/createOphydSim'; +import { motor } from '../devices/motor'; +import { signal } from '../devices/signal'; +import { shutter, SHUTTER_OPEN_VALUE } from '../devices/shutter'; +import { detector, type DetectorConfig } from '../devices/detector'; +import { randomNoise } from '../generators/noise'; +import { + beamstopCurrentModel, + ENERGY_PV, + ENERGY_REF_EV, + ENERGY_MIN_EV, + ENERGY_MAX_EV, +} from './beamstopModel'; +import rawSimDetectorConfig from './simDetector.json'; + +/** The detector defined in `simDetector.json` (prefix '13SIM1'). */ +export const simDetectorConfig = rawSimDetectorConfig as DetectorConfig; + +/** + * Beamstop scenario backing the feature. Provides the exact PV + * names that component references on TestPage: + * + * Devices: + * - bl531_xps2:beamstop_x_mm — X motor setpoint (+ .RBV / .MOVN) + * - bl531_xps2:beamstop_y_mm — Y motor setpoint (+ .RBV / .MOVN) + * - bl531:mono_energy_eV — writable beam energy. Selecting an energy + * sets the DCM Bragg angle, which shifts the + * beam vertically and therefore changes the + * current at a fixed beamstop position. + * - bl531:LJT4:1:AO0 — beam shutter analog output (writable; the + * PV the component controls). + * 0 V = Open, 5 V = Closed. Defaults to Open. + * - bl201-beamstop:current — beamstop diode current, derived from the + * two readbacks AND the energy via the shared + * beamstopCurrentModel. Peaks when the stop is + * centered on the (energy-dependent) beam, + * which is what the "best option" logic hunts. + * Reads 0 (plus noise) unless the shutter is + * open, since no beam reaches the diode while + * it is closed. + * + * Motors start off the optimum so "Go To Best" has somewhere to move to. + */ +export const SHUTTER_PV = 'bl531:LJT4:1:AO0'; +export const beamstopBeamline = createOphydSim({ + devices: [ + motor({ + name: 'bl531_xps2:beamstop_x_mm', + initialPosition: -5, + velocity: 0.8, + limits: [-10, 10], + units: 'mm', + }), + motor({ + name: 'bl531_xps2:beamstop_y_mm', + initialPosition: 5, + velocity: 0.8, + limits: [-10, 10], + units: 'mm', + }), + // Sample-holder stage: two independent linear axes that translate the + // sample-holder graphic in . Start centered so the + // holder sits on the beam by default. + motor({ + name: 'bl531:sample_x_mm', + initialPosition: 0, + velocity: 2, + limits: [-10, 10], + units: 'mm', + }), + motor({ + name: 'bl531:sample_y_mm', + initialPosition: 0, + velocity: 2, + limits: [-10, 10], + units: 'mm', + }), + motor({ + name: ENERGY_PV, + initialPosition: ENERGY_REF_EV, + velocity: 100, + units: 'eV', + limits: [ENERGY_MIN_EV, ENERGY_MAX_EV], + }), + // Beam shutter: writable analog output (0 V Open / 5 V Closed). Defaults + // to Open so the diode reads the modelled current unless something + // deliberately blocks the beam. Seeded before the diode so its dependsOn + // resolves at registration. + shutter({ name: SHUTTER_PV, initial: 'open' }), + signal({ + name: 'bl201-beamstop:current', + units: 'A', + periodMs: 100, + dependsOn: [ + 'bl531_xps2:beamstop_x_mm.RBV', + 'bl531_xps2:beamstop_y_mm.RBV', + ENERGY_PV, + SHUTTER_PV, + ], + value: ({ get, random }) => { + // Beam reaches the diode only when the shutter is open; any other + // value (closed at 5 V, or mid-transit) reads a clean zero — no + // beam means no signal and no measurement noise. + if (get.number(SHUTTER_PV) !== SHUTTER_OPEN_VALUE) return 0; + const current = beamstopCurrentModel({ + x: get.number('bl531_xps2:beamstop_x_mm.RBV'), + y: get.number('bl531_xps2:beamstop_y_mm.RBV'), + energyEV: get.number(ENERGY_PV), + }); + return current + randomNoise({ random, sigma: 2e-8 }); + }, + }), + + // Detector defined from a JSON file (prefix '13SIM1'). Placed after the + // energy signal so its derived image1:Opacity computes from a seeded + // energy at registration; opacity then tracks energy on every change. + detector(simDetectorConfig), + ], +}); diff --git a/src/lib/ophyd-sim/scenarios/beamstopModel.ts b/src/lib/ophyd-sim/scenarios/beamstopModel.ts new file mode 100644 index 00000000..b7b2ec55 --- /dev/null +++ b/src/lib/ophyd-sim/scenarios/beamstopModel.ts @@ -0,0 +1,77 @@ +import { gaussian2d } from '../generators/gaussian'; +import { braggAngle } from '../generators/bragg'; + +/** + * Shared, noise-free physics model for the beamstop diode current. + * + * Pulled out of `beamstopBeamline.ts` so two consumers stay in sync: + * - the simulator scenario, which adds measurement noise on top, and + * - the Energy-vs-Current plot, which draws this as the "expected" curve. + * + * Coupling chain (see the "Physical Beam Shift vs DCM Angle" reference plot): + * energy → DCM Bragg angle θ → beam vertical position shifts as cos(θ) + * → the 2D-Gaussian intercept of beam and beamstop → diode current. + */ + +/** Writable PV the user sets to choose photon energy. */ +export const ENERGY_PV = 'bl531:mono_energy_eV'; + +// Energy sweep range (eV). Chosen so the Si(111) Bragg angle lands in a +// realistic spread and the resulting beam shift stays within motor travel. +export const ENERGY_MIN_EV = 2000; +export const ENERGY_MAX_EV = 7000; +/** Reference energy at which the beam sits exactly at CENTER_Y. */ +export const ENERGY_REF_EV = 4500; + +// Beam position (the magnitude of the current peaks when the stop sits here). +export const CENTER_X = 2.5; +export const CENTER_Y = 1.5; +export const SIGMA_X = 2; +export const SIGMA_Y = 4; +/** Full-strength beamstop current. */ +export const PEAK_CURRENT = 100; + +/** + * How far (mm) the beam center travels vertically across the energy range. + * Kept small enough that the swept beam center stays comfortably inside the + * motors' [-10, 10] mm travel, so "Go To Best" can always reach the peak. + */ +export const BEAM_SHIFT_AMPLITUDE_MM = 4; + +const COS_THETA_REF = Math.cos(braggAngle({ energyEV: ENERGY_REF_EV })); + +/** + * Vertical beam position (mm) for a given energy. Equals CENTER_Y at the + * reference energy and shifts by cos(θ) — the cosine relationship from the + * reference diagram — as energy moves away from it. + */ +export function beamYAtEnergy(energyEV: number): number { + const cosTheta = Math.cos(braggAngle({ energyEV })); + return CENTER_Y + BEAM_SHIFT_AMPLITUDE_MM * (cosTheta - COS_THETA_REF); +} + +export interface BeamstopCurrentModelOptions { + /** Beamstop X position (mm). */ + x: number; + /** Beamstop Y position (mm). */ + y: number; + /** Photon energy (eV). */ + energyEV: number; +} + +/** + * Deterministic beamstop diode current for a given stop position and energy. + * Peaks at PEAK_CURRENT when the stop sits at (CENTER_X, beamYAtEnergy) and + * falls off as a 2D Gaussian away from it. + */ +export function beamstopCurrentModel({ x, y, energyEV }: BeamstopCurrentModelOptions): number { + const intercept = gaussian2d({ + x, + y, + centerX: CENTER_X, + centerY: beamYAtEnergy(energyEV), + sigmaX: SIGMA_X, + sigmaY: SIGMA_Y, + }); + return PEAK_CURRENT * intercept; +} diff --git a/src/lib/ophyd-sim/scenarios/defaultBeamline.ts b/src/lib/ophyd-sim/scenarios/defaultBeamline.ts new file mode 100644 index 00000000..a17f6013 --- /dev/null +++ b/src/lib/ophyd-sim/scenarios/defaultBeamline.ts @@ -0,0 +1,57 @@ +import { createOphydSim } from '../core/createOphydSim'; +import { motor } from '../devices/motor'; +import { signal } from '../devices/signal'; +import { gaussian } from '../generators/gaussian'; +import { randomNoise } from '../generators/noise'; + +/** + * A default beamline scenario used by Storybook and TestPage when no + * specific scenario is provided. Mirrors the PV names finch components + * already reference (IOC:m1 / IOC:m1.RBV) so existing stories work without + * code changes. + * + * Devices: + * - IOC:m1 — motor setpoint (limits -10..10, velocity 1) + * - IOC:m1.RBV — readback, animates toward setpoint + * - IOC:m1.MOVN — moving flag + * - IOC:bs — beamstop, writable In/Out (defaults to Out) + * - I0 — derived scalar peaked near readback=2 with noise, + * attenuated ~99% while the beamstop is In + */ +export const defaultBeamline = createOphydSim({ + devices: [ + motor({ + name: 'IOC:m1', + initialPosition: 0, + velocity: 1, + limits: [-10, 10], + units: 'mm', + }), + // Beamstop: binary In/Out, writable. 0 = Out, 1 = In. Defaults to Out + // so I0 is unchanged unless something deliberately inserts the stop. + signal({ + name: 'IOC:bs', + initialValue: 0, + writeAccess: true, + enumStrs: ['Out', 'In'], + }), + signal({ + name: 'I0', + units: 'arb.', + periodMs: 100, + dependsOn: ['IOC:m1.RBV', 'IOC:bs'], + value: ({ get, random }) => { + const beam = + 1000 * + gaussian({ + x: get.number('IOC:m1.RBV'), + center: 2, + sigma: 0.5, + }); + // Beamstop attenuates the direct beam ~99% when In. + const attenuated = get.boolean('IOC:bs') ? beam * 0.01 : beam; + return attenuated + randomNoise({ random, sigma: 5 }); + }, + }), + ], +}); diff --git a/src/lib/ophyd-sim/scenarios/hexapodBeamline.ts b/src/lib/ophyd-sim/scenarios/hexapodBeamline.ts new file mode 100644 index 00000000..e0785054 --- /dev/null +++ b/src/lib/ophyd-sim/scenarios/hexapodBeamline.ts @@ -0,0 +1,23 @@ +import { createOphydSim } from '../core/createOphydSim'; +import { hexapod } from '../devices/hexapod'; + +/** + * Standalone scenario backing the feature. Provides the single + * Symetrie hexapod device at the default prefix 'SYM:HEX01', matching the PV + * names hexapodUtils.ts generates when no prefix is supplied. + * + * - Translation axes (tx/ty/tz): [-10, 10] mm + * - Rotation axes (rx/ry/rz): [-5, 5] deg + * + * Starts at the all-zero home pose. See {@link hexapod} for the full PV list + * and move semantics. + */ +export const hexapodBeamline = createOphydSim({ + devices: [ + hexapod({ + prefix: 'SYM:HEX01', + translationLimits: [-10, 10], + rotationLimits: [-5, 5], + }), + ], +}); diff --git a/src/lib/ophyd-sim/scenarios/simDetector.json b/src/lib/ophyd-sim/scenarios/simDetector.json new file mode 100644 index 00000000..dbece4e9 --- /dev/null +++ b/src/lib/ophyd-sim/scenarios/simDetector.json @@ -0,0 +1,40 @@ +{ + "prefix": "13SIM1", + "image": { + "mode": "image_file", + "sizeX": 512, + "sizeY": 512, + "files": [ + "/images/det_1m_1.png", + "/images/det_1m_2.png", + "/images/det_1m_3.png" + ], + "overlays": [ + { + "file": "/images/det_1m_beamstop.png", + "width": 512, + "height": 512, + "x": 256, + "y": 256, + "positionX": { + "source": "bl531_xps2:beamstop_x_mm.RBV", + "from": { "in": -10, "out": 218.5 }, + "to": { "in": 10, "out": 278.5 } + }, + "positionY": { + "source": "bl531_xps2:beamstop_y_mm.RBV", + "from": { "in": -10, "out": 290.5 }, + "to": { "in": 10, "out": 230.5 } + } + } + ] + }, + "modulations": [ + { + "source": "bl531:mono_energy_eV", + "effect": "opacity", + "from": { "in": 2000, "out": 0.1 }, + "to": { "in": 7000, "out": 1.0 } + } + ] +} diff --git a/src/lib/ophyd-sim/skills.md b/src/lib/ophyd-sim/skills.md new file mode 100644 index 00000000..e8f6a351 --- /dev/null +++ b/src/lib/ophyd-sim/skills.md @@ -0,0 +1,153 @@ +# ophyd-sim + +A self-contained, browser-side simulator for [Ophyd](https://blueskyproject.io/ophyd/) +beamline devices. It models EPICS-style PVs — motors, shutters, detectors, and +derived signals — entirely in-process, so the app's normal Ophyd socket hooks +can be driven without a real IOC. It powers Storybook stories, tests, and the +[SimulatedBeamline](../../features/SimulatedBeamline/SimulatedBeamline.tsx) widget. + +Everything is re-exported from the package root ([index.ts](index.ts)); import from +`@/lib/ophyd-sim` rather than reaching into subfolders. + +## Mental model + +A sim is a bag of named PVs over shared state, plus two update mechanisms: + +- **Ticks** — a periodic loop (`tickMs`, default 100 ms) that advances + time-based devices (motor motion, periodic signals). +- **Derived signals** — values declared with a `dependsOn` list that recompute, + in dependency order, whenever an input PV changes. + +Devices are just **factories** that wire themselves into a `SimRegistration` at +build time — they seed PVs, register derived signals, and attach tick/set +handlers. Devices never reference each other directly; they read and write the +same shared state through the same `get`/`set` API user code uses. + +```ts +import { + createOphydSim, + motor, + shutter, + signal, + createOphydSimTransport, +} from '@/lib/ophyd-sim'; + +const sim = createOphydSim({ + devices: [ + motor({ name: 'IOC:m1', limits: [-10, 10], velocity: 2 }), + shutter({ name: 'IOC:shutter' }), + signal({ name: 'IOC:reading', dependsOn: ['IOC:m1.RBV'], compute: (ctx) => ... }), + ], +}); + +const transport = createOphydSimTransport(sim); +// or drive it directly: +sim.start(); +sim.set('IOC:m1', 5); // motor animates its .RBV toward 5, sets .MOVN +sim.get('IOC:m1.RBV'); +``` + +The sim does **not** tick until `start()` is called (usually by `OphydSimProvider` +on mount). In tests, skip the real loop and drive time by hand with +`sim.advance(deltaMs)`. + +## Building blocks + +Each subsystem lives in its own folder; the highlights: + +| Folder | What's there | +| --- | --- | +| [core/](core/) | `createOphydSim`, the `SimState` store, `DependencyGraph` for derived values, the `Scheduler` tick loop, and all shared [types](core/types.ts) (`OphydSim`, `SimRegistration`, `SimValueContext`, …). | +| [devices/](devices/) | Device factories: [`motor`](devices/motor.ts) (setpoint + `.RBV` + `.MOVN`), [`shutter`](devices/shutter.ts) (`SHUTTER_OPEN_VALUE` / `SHUTTER_CLOSED_VALUE`), [`signal`](devices/signal.ts), [`hexapod`](devices/hexapod.ts), and [`detector`](devices/detector.ts) (image opacity/mode/overlay modulation). | +| [generators/](generators/) | Pure value functions for `compute`: `gaussian`, `gaussian2d`, `randomNoise`, `randomWalk`, `braggAngle`. | +| [transport/](transport/) | [`createOphydSimTransport`](transport/createOphydSimTransport.ts) — adapts a sim to the app's Ophyd transport interface so the normal PV socket hooks work against it. | +| [camera/](camera/) | `SimCameraSocket` and `createSimDetectorCameraSocketFactory` — render a detector's derived state to canvas frames over a camera-socket-shaped API. | +| [react/](react/) | `OphydSimProvider` plus hooks (`useOphydSim`, `useOphydSimOptional`, `useSimSignal`, `useSimSet`). | +| [storybook/](storybook/) | `withOphydSim` decorator for stories. | +| [scenarios/](scenarios/) | Prebuilt, ready-to-use sims (see below). | + +## Writing a device or derived signal + +Inside a `compute(ctx)` you get a `SimValueContext`: + +- `ctx.get.number(pv)` / `.boolean(pv)` / `.string(pv)` / `.value(pv)` — read inputs. +- `ctx.time`, `ctx.dt` — current time and elapsed ms. +- `ctx.random()` — the sim's random source (override `random` in + `createOphydSim` for deterministic tests). + +`compute` runs once at registration to seed an initial value, so guard against +inputs that may not be seeded yet. Recomputation is best-effort: a `compute` (or +`onSet` handler) that throws is logged and skipped, not fatal. + +## Scenarios + +Ready-made sims exported from the root, defined in [scenarios/](scenarios/): + +- [`defaultBeamline`](scenarios/defaultBeamline.ts) — a basic motor/signal beamline. +- [`hexapodBeamline`](scenarios/hexapodBeamline.ts) — a hexapod-focused sim. +- [`beamstopBeamline`](scenarios/beamstopBeamline.ts) — the flagship scenario (below). + +### beamstopBeamline + +Models a beamstop-alignment beamline: motors, a beam shutter, an +energy-dependent diode-current signal, and a simulated area detector. Defined in +[beamstopBeamline.ts](scenarios/beamstopBeamline.ts), with the noise-free physics +shared through [beamstopModel.ts](scenarios/beamstopModel.ts). It backs the +`` feature and the +[SimulatedBeamline](../../features/SimulatedBeamline/SimulatedBeamline.tsx) widget — +every panel there reads and writes this same sim, so a move in the device table +updates the endstation graphic, detector, and current plot live. + +#### Devices & PVs + +| PV | Kind | Range / units | Notes | +| --- | --- | --- | --- | +| `bl531_xps2:beamstop_x_mm` | motor | `[-10, 10]` mm, v=0.8 | Beamstop X. Also `.RBV` / `.MOVN`. Starts at −5. | +| `bl531_xps2:beamstop_y_mm` | motor | `[-10, 10]` mm, v=0.8 | Beamstop Y. Also `.RBV` / `.MOVN`. Starts at +5. | +| `bl531:sample_x_mm` | motor | `[-10, 10]` mm, v=2 | Sample-holder X (translates the graphic). Starts centered. | +| `bl531:sample_y_mm` | motor | `[-10, 10]` mm, v=2 | Sample-holder Y (translates the graphic). Starts centered. | +| `bl531:mono_energy_eV` | motor | `[2000, 7000]` eV, v=100 | Writable photon energy. Starts at 4500 eV (`ENERGY_REF_EV`). | +| `bl531:LJT4:1:AO0` | shutter | 0 V open / 5 V closed | Beam shutter. Driven by ``. Defaults to **open**. | +| `bl201-beamstop:current` | signal | A, 100 ms period | Diode current, derived from the two beamstop RBVs + energy + shutter. | + +Also exports `simDetectorConfig` (the `13SIM1` detector from +[simDetector.json](scenarios/simDetector.json)). + +#### Physics model + +The coupling chain (deterministic, noise-free) lives in `beamstopCurrentModel`: + +``` +energy → DCM Bragg angle θ → beam Y shifts as cos(θ) + → 2D-Gaussian intercept of beam and beamstop → diode current +``` + +- The current peaks at `PEAK_CURRENT` (100) when the stop sits at + `(CENTER_X = 2.5, beamYAtEnergy(energy))` and falls off as a 2D Gaussian + (`SIGMA_X = 2`, `SIGMA_Y = 4`) away from it. +- Energy changes shift the beam **vertically**, so the optimal beamstop Y depends + on energy. At `ENERGY_REF_EV` (4500) the beam sits exactly at `CENTER_Y = 1.5`. +- The scenario adds Gaussian measurement noise (`sigma: 2e-8`) on top of the model; + the Energy-vs-Current plot draws the bare model as the "expected" curve. + +Motors start **off** the optimum so the feature's "Go To Best" logic has somewhere +to move to, and `BEAM_SHIFT_AMPLITUDE_MM` (4) is kept inside motor travel so the +peak is always reachable. + +#### Shutter coupling + +Beam reaches the diode only when the shutter is open (`SHUTTER_OPEN_VALUE`). Any +other value — closed at 5 V, or mid-transit — makes `bl201-beamstop:current` read a +clean **zero** (no beam ⇒ no signal, no noise). In `SimulatedBeamline`, closing the +shutter also pauses the simulated detector stream. + +#### Registration-order gotchas + +Device order in the `devices` array matters because derived values resolve their +`dependsOn` at registration: + +1. The **shutter** is seeded before the diode signal so the signal's `dependsOn` + on the shutter PV resolves. +2. The **detector** is placed after the energy motor so its derived + `image1:Opacity` computes from a seeded energy at registration, then tracks + energy on every change. diff --git a/src/lib/ophyd-sim/storybook/decorator.tsx b/src/lib/ophyd-sim/storybook/decorator.tsx new file mode 100644 index 00000000..fa402028 --- /dev/null +++ b/src/lib/ophyd-sim/storybook/decorator.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from 'react'; +import type { OphydSim } from '../core/types'; +import { OphydSimProvider } from '../react/OphydSimProvider'; +import { OphydTransportProvider } from '@/api/ophyd/OphydTransportProvider'; +import { createOphydSimTransport } from '../transport/createOphydSimTransport'; + +/** + * Build a Storybook decorator that wraps stories in a shared simulator + * provider + a transport provider. Apply globally in .storybook/preview.ts + * or per-story for scenario-specific simulators. + * + * Usage: + * export const decorators = [withOphydSim(defaultBeamline)]; + */ +export function withOphydSim(sim: OphydSim) { + const transport = createOphydSimTransport(sim); + return function OphydSimDecorator(Story: () => ReactNode): ReactNode { + return ( + + + + + + ); + }; +} diff --git a/src/lib/ophyd-sim/transport/createOphydSimTransport.ts b/src/lib/ophyd-sim/transport/createOphydSimTransport.ts new file mode 100644 index 00000000..764fcfa4 --- /dev/null +++ b/src/lib/ophyd-sim/transport/createOphydSimTransport.ts @@ -0,0 +1,175 @@ +import type { + OphydPVIncomingMessage, + OphydPVMessageListener, + OphydPVOutgoingMessage, + OphydPVTransport, + OphydTransportStatusListener, + Unsubscribe, +} from '@/api/ophyd/transport/types'; +import type { MetaUpdateResponse } from '@/api/ophyd/ophydPVSocketTypes'; +import type { OphydSim, SimValue, PVMetadata } from '../core/types'; + +/** + * Adapt an OphydSim instance to the OphydPVTransport interface, speaking the + * existing PV-socket wire protocol. + * + * On `subscribe`, immediately emits a synthesized `meta` message so finch + * shows the device as connected, then forwards every value change as a + * value-update message. `set` writes to the sim. `unsubscribe` drops the + * subscription. The transport reports `'open'` synchronously on first status + * listener attach — there's no real handshake to wait for. + * + * Multiple subscribe messages for the same PV install independent listeners + * (each gets its own value stream); unsubscribe drops the most recent + * listener for that PV, matching how the existing hook expects to operate. + */ +export function createOphydSimTransport(sim: OphydSim): OphydPVTransport { + const messageListeners = new Set(); + const statusListeners = new Set(); + const pvSubscriptions = new Map(); + let closed = false; + + const emit = (message: OphydPVIncomingMessage): void => { + for (const listener of messageListeners) { + try { + listener(message); + } catch (err) { + console.error('[ophyd-sim] transport message listener threw:', err); + } + } + }; + + const send = (message: OphydPVOutgoingMessage): void => { + if (closed) return; + switch (message.action) { + case 'subscribe': + case 'subscribeSafely': + case 'subscribeReadOnly': { + const pv = message.pv; + emit(buildMetaMessage(pv, sim)); + const unsub = sim.subscribe(pv, (event) => { + emit(buildValueMessage(pv, event.value, event.timestamp, sim)); + }); + const list = pvSubscriptions.get(pv) ?? []; + list.push(unsub); + pvSubscriptions.set(pv, list); + break; + } + case 'unsubscribe': { + const pv = message.pv; + const list = pvSubscriptions.get(pv); + const popped = list?.pop(); + if (popped) popped(); + if (list && list.length === 0) pvSubscriptions.delete(pv); + break; + } + case 'set': { + sim.set(message.pv, message.value); + break; + } + case 'refresh': { + // Replay every active subscription's latest value. + for (const pv of pvSubscriptions.keys()) { + const v = sim.get(pv); + if (v !== undefined) { + emit(buildValueMessage(pv, v, Date.now(), sim)); + } + } + break; + } + } + }; + + return { + send, + onMessage(listener) { + messageListeners.add(listener); + return () => messageListeners.delete(listener); + }, + onStatus(listener) { + statusListeners.add(listener); + // Sim transport is always open from the consumer's perspective — + // emit synchronously so hook subscribers see the connection. + listener(closed ? 'closed' : 'open'); + return () => statusListeners.delete(listener); + }, + close() { + if (closed) return; + closed = true; + for (const list of pvSubscriptions.values()) { + for (const u of list) u(); + } + pvSubscriptions.clear(); + for (const l of statusListeners) { + try { + l('closed'); + } catch (err) { + console.error('[ophyd-sim] status listener threw on close:', err); + } + } + }, + }; +} + +function metadataToFields(metadata: PVMetadata | undefined): { + units: string; + lower_ctrl_limit: number | null; + upper_ctrl_limit: number | null; + precision: number; + enum_strs: null | string[]; + read_access: boolean; + write_access: boolean; +} { + return { + units: metadata?.units ?? '', + lower_ctrl_limit: metadata?.lower_ctrl_limit ?? null, + upper_ctrl_limit: metadata?.upper_ctrl_limit ?? null, + precision: metadata?.precision ?? 0, + enum_strs: metadata?.enum_strs ?? null, + read_access: metadata?.read_access ?? true, + write_access: metadata?.write_access ?? false, + }; +} + +function buildMetaMessage(pv: string, sim: OphydSim): MetaUpdateResponse { + const value = sim.get(pv); + const meta = metadataToFields(sim.metadata(pv)); + return { + sub_type: 'meta', + pv, + value: (value ?? 0) as SimValue, + timestamp: Date.now(), + connected: value !== undefined, + status: 0, + severity: 0, + setpoint_timestamp: null, + setpoint_status: null, + setpoint_severity: null, + setpoint_precision: null, + ...meta, + }; +} + +function buildValueMessage( + pv: string, + value: SimValue, + timestamp: number, + sim: OphydSim, +): { + pv: string; + value: SimValue; + timestamp: number; + connected: boolean; + read_access: boolean; + write_access: boolean; +} { + const meta = sim.metadata(pv); + return { + pv, + value, + timestamp, + connected: true, + read_access: meta?.read_access ?? true, + write_access: meta?.write_access ?? false, + }; +} diff --git a/src/stories/DeviceControllerBox.stories.tsx b/src/stories/DeviceControllerBox.stories.tsx new file mode 100644 index 00000000..344d1de3 --- /dev/null +++ b/src/stories/DeviceControllerBox.stories.tsx @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import DeviceControllerBox from '../components/DeviceControllerBox'; +import useOphydPVSocket from '../api/ophyd/useOphydPVSocket'; + +/** + * Story that proves shared-state behavior of the ophyd-sim transport. + * + * The Storybook global decorator (configured in .storybook/preview.ts) wraps + * every story in an OphydSimProvider + OphydTransportProvider bound to a + * single simulator instance. Two DeviceControllerBox instances below each + * call useOphydPVSocket independently for IOC:m1; moving via one updates + * the readback on both because they share the same simulator. + */ +const meta = { + title: 'Bluesky Components/DeviceControllerBox', + component: DeviceControllerBox, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function ConnectedBox() { + const { devices, handleSetValueRequest, toggleDeviceLock } = useOphydPVSocket([ + 'IOC:m1', + 'IOC:m1.RBV', + ]); + return ( + + ); +} + +export const Default: Story = { + render: () => , +}; + +export const SharedStateWithTwoConsumers: Story = { + name: 'Shared state across two consumers', + render: () => ( +
+ + +
+ ), +}; diff --git a/src/testing/tests/BeamstopModel.test.tsx b/src/testing/tests/BeamstopModel.test.tsx new file mode 100644 index 00000000..d42ec8b6 --- /dev/null +++ b/src/testing/tests/BeamstopModel.test.tsx @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import { braggAngle } from '../../lib/ophyd-sim/generators/bragg'; +import { + beamstopCurrentModel, + beamYAtEnergy, + CENTER_X, + CENTER_Y, + PEAK_CURRENT, + ENERGY_MIN_EV, + ENERGY_MAX_EV, + ENERGY_REF_EV, +} from '../../lib/ophyd-sim/scenarios/beamstopModel'; + +describe('braggAngle', () => { + it('returns an angle in (0, π/2] for in-range energies', () => { + const theta = braggAngle({ energyEV: ENERGY_REF_EV }); + expect(theta).toBeGreaterThan(0); + expect(theta).toBeLessThanOrEqual(Math.PI / 2); + }); + + it('decreases as energy increases (higher energy → smaller angle)', () => { + const low = braggAngle({ energyEV: ENERGY_MIN_EV }); + const high = braggAngle({ energyEV: ENERGY_MAX_EV }); + expect(high).toBeLessThan(low); + }); + + it('saturates at π/2 rather than NaN for energies below the crystal cutoff', () => { + const theta = braggAngle({ energyEV: 1 }); + expect(Number.isNaN(theta)).toBe(false); + expect(theta).toBeCloseTo(Math.PI / 2, 5); + }); +}); + +describe('beamYAtEnergy', () => { + it('sits exactly at CENTER_Y at the reference energy', () => { + expect(beamYAtEnergy(ENERGY_REF_EV)).toBeCloseTo(CENTER_Y, 10); + }); + + it('shifts monotonically with energy', () => { + const ys = [ENERGY_MIN_EV, 3000, 4000, 5000, 6000, ENERGY_MAX_EV].map(beamYAtEnergy); + const ascending = ys.every((v, i) => i === 0 || v >= ys[i - 1]); + expect(ascending).toBe(true); + }); + + it('keeps the beam within motor travel [-10, 10] across the range', () => { + for (let e = ENERGY_MIN_EV; e <= ENERGY_MAX_EV; e += 100) { + const y = beamYAtEnergy(e); + expect(y).toBeGreaterThanOrEqual(-10); + expect(y).toBeLessThanOrEqual(10); + } + }); +}); + +describe('beamstopCurrentModel', () => { + it('peaks at PEAK_CURRENT when the stop sits on the energy-dependent beam', () => { + const energyEV = ENERGY_REF_EV; + const peak = beamstopCurrentModel({ + x: CENTER_X, + y: beamYAtEnergy(energyEV), + energyEV, + }); + expect(peak).toBeCloseTo(PEAK_CURRENT, 6); + }); + + it('falls off as the stop moves away from the beam', () => { + const energyEV = ENERGY_REF_EV; + const onBeam = beamstopCurrentModel({ x: CENTER_X, y: beamYAtEnergy(energyEV), energyEV }); + const offBeam = beamstopCurrentModel({ + x: CENTER_X + 5, + y: beamYAtEnergy(energyEV) + 5, + energyEV, + }); + expect(offBeam).toBeLessThan(onBeam); + }); + + it('changes with energy at a fixed beamstop position', () => { + const atRef = beamstopCurrentModel({ x: CENTER_X, y: CENTER_Y, energyEV: ENERGY_REF_EV }); + const atMax = beamstopCurrentModel({ x: CENTER_X, y: CENTER_Y, energyEV: ENERGY_MAX_EV }); + // At the reference energy the beam is at CENTER_Y, so current is maximal; + // shifting energy moves the beam off the stop and lowers the current. + expect(atMax).toBeLessThan(atRef); + }); +}); diff --git a/src/testing/tests/EnergyVsCurrentPlotPV.test.tsx b/src/testing/tests/EnergyVsCurrentPlotPV.test.tsx new file mode 100644 index 00000000..3d5dc0c1 --- /dev/null +++ b/src/testing/tests/EnergyVsCurrentPlotPV.test.tsx @@ -0,0 +1,226 @@ +import { render, screen, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import EnergyVsCurrentPlotPV from '../../features/EnergyVsCurrentPlotPV'; +import type { PlotlyScatterProps } from '../../components/PlotlyScatter'; +import { beamstopCurrentModel, ENERGY_MIN_EV, ENERGY_MAX_EV } from '@/lib/ophyd-sim'; + +// Capture props forwarded to PlotlyScatter so we can inspect the traces the +// component builds without rendering real Plotly. +const ScatterMock = vi.fn(({ xAxisTitle, yAxisTitle }: PlotlyScatterProps) => ( +
+)); + +vi.mock('../../components/PlotlyScatter', () => ({ + default: (props: unknown) => ScatterMock(props as PlotlyScatterProps), +})); + +vi.mock('@/api/ophyd/useOphydPVSocket', () => ({ default: vi.fn() })); + +import useOphydPVSocket from '@/api/ophyd/useOphydPVSocket'; + +const ENERGY_PV = 'ENERGY'; +const CURRENT_PV = 'CURRENT'; +const BSX_PV = 'BSX'; +const BSY_PV = 'BSY'; + +type Trace = { x: number[]; y: number[]; name?: string; mode?: string }; + +const makeDevice = (value: number | string, units = 'A') => ({ value, units, connected: true }); + +/** Point PVs at the given values (any subset). Missing PVs stay absent. */ +const setDevices = (values: Partial>) => { + const devices: Record> = {}; + for (const [pv, value] of Object.entries(values)) { + if (value !== undefined) devices[pv] = makeDevice(value); + } + vi.mocked(useOphydPVSocket).mockReturnValue({ devices } as unknown as ReturnType< + typeof useOphydPVSocket + >); +}; + +/** The two traces from the most recent PlotlyScatter render: [expected, measured]. */ +const lastTraces = () => { + const props = ScatterMock.mock.calls[ScatterMock.mock.calls.length - 1][0]; + return props.data as unknown as [Trace, Trace]; +}; + +/** Recompute the expected curve exactly as the component does, for assertions. */ +const computeExpected = (x: number, y: number, steps: number) => { + const xs: number[] = []; + const ys: number[] = []; + for (let i = 0; i < steps; i++) { + const energyEV = ENERGY_MIN_EV + ((ENERGY_MAX_EV - ENERGY_MIN_EV) * i) / (steps - 1); + xs.push(energyEV); + ys.push(beamstopCurrentModel({ x, y, energyEV })); + } + return { xs, ys }; +}; + +const renderPlot = (props: Partial> = {}) => + render( + , + ); + +beforeEach(() => { + ScatterMock.mockClear(); + vi.useFakeTimers(); + setDevices({}); +}); + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe('EnergyVsCurrentPlotPV', () => { + // ─── Rendering & static props ───────────────────────────────────────────── + + it('renders PlotlyScatter', () => { + renderPlot(); + expect(screen.getByTestId('plotly-scatter')).toBeInTheDocument(); + }); + + it('labels the axes for energy (x) and current (y)', () => { + renderPlot(); + const scatter = screen.getByTestId('plotly-scatter'); + expect(scatter).toHaveAttribute('data-x-title', 'Beam Energy (eV)'); + expect(scatter).toHaveAttribute('data-y-title', 'Beamstop Current (A)'); + }); + + it('pins the x-axis range to the energy bounds', () => { + renderPlot(); + const props = ScatterMock.mock.calls[ScatterMock.mock.calls.length - 1][0]; + expect(props.xAxisRange).toEqual([ENERGY_MIN_EV, ENERGY_MAX_EV]); + }); + + it('forwards className to PlotlyScatter alongside the default height', () => { + renderPlot({ className: 'my-class' }); + const props = ScatterMock.mock.calls[ScatterMock.mock.calls.length - 1][0]; + expect(props.className).toContain('my-class'); + expect(props.className).toContain('h-96'); + }); + + it('renders Expected and Measured traces in that order', () => { + renderPlot(); + const [expected, measured] = lastTraces(); + expect(expected.name).toBe('Expected'); + expect(expected.mode).toBe('lines'); + expect(measured.name).toBe('Measured'); + expect(measured.mode).toBe('markers'); + }); + + // ─── Expected curve ─────────────────────────────────────────────────────── + + it('spans the full energy range with the default number of points', () => { + renderPlot(); + const [expected] = lastTraces(); + expect(expected.x).toHaveLength(100); + expect(expected.x[0]).toBe(ENERGY_MIN_EV); + expect(expected.x[expected.x.length - 1]).toBe(ENERGY_MAX_EV); + }); + + it('respects a custom expectedCurvePoints count', () => { + renderPlot({ expectedCurvePoints: 10 }); + const [expected] = lastTraces(); + expect(expected.x).toHaveLength(10); + }); + + it('defaults the beamstop position to (0,0) when the readback PVs are absent', () => { + renderPlot({ expectedCurvePoints: 5 }); + const [expected] = lastTraces(); + expect(expected.y).toEqual(computeExpected(0, 0, 5).ys); + }); + + it('shapes the expected curve from beamstopCurrentModel at the current beamstop position', () => { + setDevices({ [BSX_PV]: 3, [BSY_PV]: -2 }); + renderPlot({ expectedCurvePoints: 8 }); + const [expected] = lastTraces(); + expect(expected.y).toEqual(computeExpected(3, -2, 8).ys); + }); + + it('reshapes the expected curve when the beamstop moves', () => { + setDevices({ [BSX_PV]: 0, [BSY_PV]: 0 }); + const { rerender } = renderPlot({ expectedCurvePoints: 8 }); + const before = [...lastTraces()[0].y]; + + setDevices({ [BSX_PV]: 5, [BSY_PV]: 5 }); + rerender( + , + ); + const after = lastTraces()[0].y; + expect(after).not.toEqual(before); + expect(after).toEqual(computeExpected(5, 5, 8).ys); + }); + + // ─── Measured samples (polling) ──────────────────────────────────────────── + + it('starts with no measured points', () => { + setDevices({ [ENERGY_PV]: 1000, [CURRENT_PV]: 0.5 }); + renderPlot(); + const [, measured] = lastTraces(); + expect(measured.x).toHaveLength(0); + }); + + it('records an (energy, current) sample on each polling tick', () => { + setDevices({ [ENERGY_PV]: 1000, [CURRENT_PV]: 0.5 }); + renderPlot({ pollingIntervalMs: 100 }); + act(() => { + vi.advanceTimersByTime(100); + }); + const [, measured] = lastTraces(); + expect(measured.x).toEqual([1000]); + expect(measured.y).toEqual([0.5]); + }); + + it('does not record samples while energy or current is non-numeric', () => { + setDevices({ [ENERGY_PV]: 1000, [CURRENT_PV]: 'n/a' }); + renderPlot({ pollingIntervalMs: 100 }); + act(() => { + vi.advanceTimersByTime(500); + }); + const [, measured] = lastTraces(); + expect(measured.x).toHaveLength(0); + }); + + it('caps the measured window at numVisiblePoints, dropping the oldest', () => { + setDevices({ [ENERGY_PV]: 1000, [CURRENT_PV]: 0.5 }); + renderPlot({ pollingIntervalMs: 100, numVisiblePoints: 3 }); + act(() => { + vi.advanceTimersByTime(1000); // ~10 ticks + }); + const [, measured] = lastTraces(); + expect(measured.x).toHaveLength(3); + }); + + it('stops sampling after unmount', () => { + setDevices({ [ENERGY_PV]: 1000, [CURRENT_PV]: 0.5 }); + const { unmount } = renderPlot({ pollingIntervalMs: 100 }); + act(() => { + vi.advanceTimersByTime(100); + }); + unmount(); + // Advancing further must not throw (interval cleared) — a state update + // after unmount would surface here. + expect(() => + act(() => { + vi.advanceTimersByTime(500); + }), + ).not.toThrow(); + }); +}); diff --git a/src/testing/tests/createFallbackTransport.test.ts b/src/testing/tests/createFallbackTransport.test.ts new file mode 100644 index 00000000..a524d830 --- /dev/null +++ b/src/testing/tests/createFallbackTransport.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createFallbackTransport } from '../../api/ophyd/transport/createFallbackTransport'; +import type { OphydTransportStatus, Unsubscribe } from '../../api/ophyd/transport/types'; + +/** + * A controllable fake transport for exercising the fallback wrapper. Records + * everything sent to it and lets the test drive its message/status channels. + */ +function makeFakeTransport() { + const messageListeners = new Set<(m: unknown) => void>(); + const statusListeners = new Set<(s: OphydTransportStatus) => void>(); + let status: OphydTransportStatus = 'connecting'; + const sent: unknown[] = []; + let closed = false; + + return { + sent, + get closed() { + return closed; + }, + emitMessage(m: unknown) { + for (const l of messageListeners) l(m); + }, + emitStatus(s: OphydTransportStatus) { + status = s; + for (const l of statusListeners) l(s); + }, + transport: { + send(m: unknown) { + sent.push(m); + }, + onMessage(listener: (m: unknown) => void): Unsubscribe { + messageListeners.add(listener); + return () => messageListeners.delete(listener); + }, + onStatus(listener: (s: OphydTransportStatus) => void): Unsubscribe { + statusListeners.add(listener); + listener(status); + return () => statusListeners.delete(listener); + }, + close() { + closed = true; + }, + }, + }; +} + +describe('createFallbackTransport', () => { + it('switches to the fallback on primary error and replays active subscriptions', () => { + const primary = makeFakeTransport(); + const fallback = makeFakeTransport(); + + const wrapper = createFallbackTransport(primary.transport, () => fallback.transport); + + const received: unknown[] = []; + wrapper.onMessage((m) => received.push(m)); + + // Subscribe through the wrapper; the primary should receive it. + wrapper.send({ action: 'subscribe', pv: 'IOC:m1' } as never); + expect(primary.sent).toEqual([{ action: 'subscribe', pv: 'IOC:m1' }]); + + // Primary fails -> wrapper switches to fallback and replays the subscribe. + primary.emitStatus('error'); + expect(primary.closed).toBe(true); + expect(fallback.sent).toEqual([{ action: 'subscribe', pv: 'IOC:m1' }]); + + // Messages from the fallback reach the original listener... + fallback.emitMessage({ pv: 'IOC:m1', value: 42 }); + expect(received).toContainEqual({ pv: 'IOC:m1', value: 42 }); + + // ...and subsequent sends route to the fallback. + wrapper.send({ action: 'set', pv: 'IOC:m1', value: 1 } as never); + expect(fallback.sent).toContainEqual({ action: 'set', pv: 'IOC:m1', value: 1 }); + }); + + it('never forwards the primary terminal status to consumers', () => { + const primary = makeFakeTransport(); + const fallback = makeFakeTransport(); + const wrapper = createFallbackTransport(primary.transport, () => fallback.transport); + + const statuses: OphydTransportStatus[] = []; + wrapper.onStatus((s) => statuses.push(s)); + + primary.emitStatus('error'); + expect(statuses).not.toContain('error'); + + // The fallback's status supersedes from the switch onward. + fallback.emitStatus('open'); + expect(statuses).toContain('open'); + }); + + it('does not replay subscriptions that were unsubscribed before the switch', () => { + const primary = makeFakeTransport(); + const fallback = makeFakeTransport(); + const wrapper = createFallbackTransport(primary.transport, () => fallback.transport); + + wrapper.send({ action: 'subscribe', pv: 'IOC:m1' } as never); + wrapper.send({ action: 'unsubscribe', pv: 'IOC:m1' } as never); + + primary.emitStatus('closed'); + expect(fallback.sent).toEqual([]); + }); + + it('does not switch when the user closes the wrapper', () => { + const primary = makeFakeTransport(); + const makeFallback = vi.fn(() => makeFakeTransport().transport); + const wrapper = createFallbackTransport(primary.transport, makeFallback); + + wrapper.close(); + expect(primary.closed).toBe(true); + + // A close-driven 'closed' from the primary must not trigger a fallback. + primary.emitStatus('closed'); + expect(makeFallback).not.toHaveBeenCalled(); + }); +}); diff --git a/src/testing/tests/ophyd-sim/dependencyGraph.test.ts b/src/testing/tests/ophyd-sim/dependencyGraph.test.ts new file mode 100644 index 00000000..f529eebc --- /dev/null +++ b/src/testing/tests/ophyd-sim/dependencyGraph.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { DependencyGraph } from '../../../lib/ophyd-sim/core/dependencyGraph'; + +describe('DependencyGraph', () => { + it('returns direct dependents in topological order', () => { + const g = new DependencyGraph(); + g.register('I0', ['x']); + g.register('I1', ['x']); + // Both I0 and I1 should appear (no order constraint between them). + const order = g.dependentsOf('x'); + expect(order.sort()).toEqual(['I0', 'I1']); + }); + + it('returns transitive dependents with dependencies before dependents', () => { + const g = new DependencyGraph(); + g.register('mid', ['x']); + g.register('leaf', ['mid']); + const order = g.dependentsOf('x'); + expect(order).toEqual(['mid', 'leaf']); + }); + + it('throws on direct cycle', () => { + const g = new DependencyGraph(); + g.register('a', ['b']); + expect(() => g.register('b', ['a'])).toThrow(/cycle/); + }); + + it('throws on indirect cycle', () => { + const g = new DependencyGraph(); + g.register('a', ['b']); + g.register('b', ['c']); + expect(() => g.register('c', ['a'])).toThrow(/cycle/); + }); + + it('returns empty list for a source with no dependents', () => { + const g = new DependencyGraph(); + expect(g.dependentsOf('orphan')).toEqual([]); + }); +}); diff --git a/src/testing/tests/ophyd-sim/detector.test.ts b/src/testing/tests/ophyd-sim/detector.test.ts new file mode 100644 index 00000000..a38447f9 --- /dev/null +++ b/src/testing/tests/ophyd-sim/detector.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect } from 'vitest'; +import { createOphydSim } from '../../../lib/ophyd-sim/core/createOphydSim'; +import { signal } from '../../../lib/ophyd-sim/devices/signal'; +import { + detector, + mapLinearClamped, + type DetectorConfig, +} from '../../../lib/ophyd-sim/devices/detector'; +import { ENERGY_PV } from '../../../lib/ophyd-sim/scenarios/beamstopModel'; +import simDetectorConfig from '../../../lib/ophyd-sim/scenarios/simDetector.json'; + +const config: DetectorConfig = { + prefix: '13SIM1', + image: { mode: 'noisy', sizeX: 640, sizeY: 480 }, + modulations: [ + { + source: ENERGY_PV, + effect: 'opacity', + from: { in: 2000, out: 0.1 }, + to: { in: 7000, out: 1.0 }, + }, + ], +}; + +function makeSim(detectorConfig: DetectorConfig = config) { + return createOphydSim({ + devices: [ + signal({ name: ENERGY_PV, initialValue: 4500, writeAccess: true }), + detector(detectorConfig), + ], + }); +} + +describe('detector', () => { + it('seeds the standard camera PVs under the prefix', () => { + const sim = makeSim(); + + expect(sim.get('13SIM1:cam1:SizeX')).toBe(640); + expect(sim.get('13SIM1:cam1:SizeY')).toBe(480); + expect(sim.get('13SIM1:cam1:MinX')).toBe(0); + expect(sim.get('13SIM1:cam1:MinY')).toBe(0); + expect(sim.get('13SIM1:cam1:ColorMode')).toBe(0); + expect(sim.get('13SIM1:cam1:DataType')).toBe(1); + expect(sim.get('13SIM1:cam1:Acquire')).toBe(0); + }); + + it('exposes enum metadata for ColorMode and the Acquire flag', () => { + const sim = makeSim(); + expect(sim.metadata('13SIM1:cam1:ColorMode')?.enum_strs).toEqual(['Mono', 'RGB1']); + expect(sim.metadata('13SIM1:cam1:Acquire')?.enum_strs).toEqual(['Done', 'Acquire']); + expect(sim.metadata('13SIM1:cam1:SizeX')?.units).toBe('px'); + }); + + it('seeds the image value mode from config', () => { + expect(makeSim().get('13SIM1:image1:Mode')).toBe('noisy'); + const imageSim = makeSim({ ...config, image: { ...config.image, mode: 'image_file' } }); + expect(imageSim.get('13SIM1:image1:Mode')).toBe('image_file'); + }); + + it('derives image opacity from the energy modulation and tracks changes', () => { + const sim = makeSim(); + + // Initial energy 4500 → halfway → 0.1 + 0.5 * 0.9 = 0.55. + expect(sim.get('13SIM1:image1:Opacity')).toBeCloseTo(0.55, 6); + + sim.set(ENERGY_PV, 7000); + expect(sim.get('13SIM1:image1:Opacity')).toBeCloseTo(1.0, 6); + + sim.set(ENERGY_PV, 2000); + expect(sim.get('13SIM1:image1:Opacity')).toBeCloseTo(0.1, 6); + }); + + it('clamps opacity outside the modulation input range', () => { + const sim = makeSim(); + + sim.set(ENERGY_PV, 100000); + expect(sim.get('13SIM1:image1:Opacity')).toBeCloseTo(1.0, 6); + + sim.set(ENERGY_PV, 0); + expect(sim.get('13SIM1:image1:Opacity')).toBeCloseTo(0.1, 6); + }); + + it('omits the opacity PV when there are no modulations', () => { + const sim = makeSim({ ...config, modulations: [] }); + expect(sim.get('13SIM1:image1:Opacity')).toBeUndefined(); + }); + + it('seeds overlay center PVs and binds them to a source PV', () => { + const overlayConfig: DetectorConfig = { + prefix: '13SIM1', + image: { + mode: 'image_file', + sizeX: 512, + sizeY: 512, + file: '/base.png', + overlays: [ + { + file: '/dot.png', + width: 48, + height: 48, + x: 256, + y: 256, + positionX: { + source: 'bl531_xps2:beamstop_x_mm.RBV', + from: { in: -10, out: 0 }, + to: { in: 10, out: 512 }, + }, + }, + ], + }, + }; + const sim = createOphydSim({ + devices: [ + signal({ + name: 'bl531_xps2:beamstop_x_mm.RBV', + initialValue: 0, + writeAccess: true, + }), + detector(overlayConfig), + ], + }); + + // Bound X: 0 mm maps to the canvas center; the unbound Y keeps its static seed. + expect(sim.get('13SIM1:image1:Overlay1:CenterX')).toBeCloseTo(256, 6); + expect(sim.get('13SIM1:image1:Overlay1:CenterY')).toBe(256); + + // Moving the source PV drives the bound coordinate (clamped to the px range). + sim.set('bl531_xps2:beamstop_x_mm.RBV', 10); + expect(sim.get('13SIM1:image1:Overlay1:CenterX')).toBeCloseTo(512, 6); + sim.set('bl531_xps2:beamstop_x_mm.RBV', 100); + expect(sim.get('13SIM1:image1:Overlay1:CenterX')).toBeCloseTo(512, 6); + }); +}); + +describe('simDetector.json overlay', () => { + it('ties the overlay center to the beamstop motor readbacks', () => { + const json = simDetectorConfig as DetectorConfig; + const overlay = json.image.overlays?.[0]; + expect(overlay?.positionX?.source).toBe('bl531_xps2:beamstop_x_mm.RBV'); + expect(overlay?.positionY?.source).toBe('bl531_xps2:beamstop_y_mm.RBV'); + }); +}); + +describe('simDetector.json', () => { + it('modulation source matches the sim energy PV', () => { + const json = simDetectorConfig as DetectorConfig; + expect(json.modulations?.[0]?.source).toBe(ENERGY_PV); + }); + + it('maps the documented energy → opacity endpoints', () => { + const json = simDetectorConfig as DetectorConfig; + const mod = json.modulations![0]; + expect(mapLinearClamped(7000, mod.from, mod.to)).toBeCloseTo(1.0, 6); + expect(mapLinearClamped(2000, mod.from, mod.to)).toBeCloseTo(0.1, 6); + }); +}); diff --git a/src/testing/tests/ophyd-sim/hexapod.test.ts b/src/testing/tests/ophyd-sim/hexapod.test.ts new file mode 100644 index 00000000..144c1ca9 --- /dev/null +++ b/src/testing/tests/ophyd-sim/hexapod.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; +import { createOphydSim } from '../../../lib/ophyd-sim/core/createOphydSim'; +import { hexapod } from '../../../lib/ophyd-sim/devices/hexapod'; + +const P = 'SYM:HEX01'; +const rbv = (axis: string) => `${P}:s_uto_${axis}_RBV`; +const setpoint = (Axis: string) => `${P}:MOVE_PTP:${Axis}`; +const EXECUTE = `${P}:MOVE_PTP`; +const MOVE_TYPE = `${P}:MOVE_PTP:MoveType`; +const IN_POSITION = `${P}:s_hexa:InPosition_RBV`; +const STOP = `${P}:STOP`; + +const makeSim = () => createOphydSim({ devices: [hexapod({ prefix: P })] }); + +/** Run enough simulated time for any in-range move to settle. */ +const settle = (sim: ReturnType) => { + for (let i = 0; i < 300; i++) sim.advance(50); +}; + +describe('hexapod', () => { + it('seeds all six axes at the zero home pose, in position', () => { + const sim = makeSim(); + for (const axis of ['tx', 'ty', 'tz', 'rx', 'ry', 'rz']) { + expect(sim.get(rbv(axis))).toBe(0); + } + expect(sim.get(IN_POSITION)).toBe(1); + }); + + it('exposes translation axes in mm and rotation axes in deg with their limits', () => { + const sim = makeSim(); + const tx = sim.metadata(rbv('tx')); + expect(tx?.units).toBe('mm'); + expect(tx?.lower_ctrl_limit).toBe(-10); + expect(tx?.upper_ctrl_limit).toBe(10); + + const rx = sim.metadata(rbv('rx')); + expect(rx?.units).toBe('deg'); + expect(rx?.lower_ctrl_limit).toBe(-5); + expect(rx?.upper_ctrl_limit).toBe(5); + }); + + it('does not move until MOVE_PTP is executed', () => { + const sim = makeSim(); + sim.set(setpoint('Tx'), 5); + sim.advance(500); + expect(sim.get(rbv('tx'))).toBe(0); + }); + + it('moves readbacks to the staged setpoints on execute, then settles', () => { + const sim = makeSim(); + sim.set(setpoint('Tx'), 5); + sim.set(setpoint('Rx'), 3); + + sim.set(EXECUTE, 1); + // The command PV is momentary — it returns to 0 so the next write re-triggers. + expect(sim.get(EXECUTE)).toBe(0); + expect(sim.get(IN_POSITION)).toBe(0); + + settle(sim); + expect(sim.get(rbv('tx'))).toBeCloseTo(5, 3); + expect(sim.get(rbv('rx'))).toBeCloseTo(3, 3); + expect(sim.get(IN_POSITION)).toBe(1); + }); + + it('adds to the current readback when MoveType is relative', () => { + const sim = makeSim(); + sim.set(setpoint('Tx'), 4); + sim.set(EXECUTE, 1); + settle(sim); + expect(sim.get(rbv('tx'))).toBeCloseTo(4, 3); + + sim.set(MOVE_TYPE, 1); // relative + sim.set(setpoint('Tx'), 3); + sim.set(EXECUTE, 1); + settle(sim); + expect(sim.get(rbv('tx'))).toBeCloseTo(7, 3); + }); + + it('clamps captured targets to the axis limits', () => { + const sim = makeSim(); + sim.set(setpoint('Tx'), 999); + sim.set(setpoint('Rx'), -999); + sim.set(EXECUTE, 1); + settle(sim); + expect(sim.get(rbv('tx'))).toBeCloseTo(10, 3); + expect(sim.get(rbv('rx'))).toBeCloseTo(-5, 3); + }); + + it('halts motion where it is when STOP is written', () => { + const sim = makeSim(); + sim.set(setpoint('Tx'), 10); + sim.set(EXECUTE, 1); + sim.advance(200); // partial travel: 4 mm/s * 0.2s = 0.8 mm + + const mid = sim.get(rbv('tx')) as number; + expect(mid).toBeGreaterThan(0); + expect(mid).toBeLessThan(10); + + sim.set(STOP, 1); + expect(sim.get(STOP)).toBe(0); // momentary + expect(sim.get(IN_POSITION)).toBe(1); + + settle(sim); + expect(sim.get(rbv('tx'))).toBeCloseTo(mid, 3); + }); +}); diff --git a/src/testing/tests/ophyd-sim/motor.test.ts b/src/testing/tests/ophyd-sim/motor.test.ts new file mode 100644 index 00000000..ba63e9e8 --- /dev/null +++ b/src/testing/tests/ophyd-sim/motor.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import { createOphydSim } from '../../../lib/ophyd-sim/core/createOphydSim'; +import { motor } from '../../../lib/ophyd-sim/devices/motor'; + +describe('motor', () => { + it('clamps setpoint writes into the configured limits', () => { + const sim = createOphydSim({ + devices: [motor({ name: 'IOC:m1', limits: [-1, 1], velocity: 10 })], + }); + + sim.set('IOC:m1', 100); + expect(sim.get('IOC:m1')).toBe(1); + + sim.set('IOC:m1', -100); + expect(sim.get('IOC:m1')).toBe(-1); + }); + + it('seeds readback and moving flag from initialPosition', () => { + const sim = createOphydSim({ + devices: [motor({ name: 'IOC:m1', initialPosition: 3 })], + }); + + expect(sim.get('IOC:m1')).toBe(3); + expect(sim.get('IOC:m1.RBV')).toBe(3); + expect(sim.get('IOC:m1.MOVN')).toBe(0); + }); + + it('advances readback toward setpoint and clears the moving flag at target', () => { + const sim = createOphydSim({ + devices: [ + motor({ + name: 'IOC:m1', + initialPosition: 0, + velocity: 1, // 1 unit / second + limits: [-10, 10], + }), + ], + }); + + sim.set('IOC:m1', 5); + + // 1s @ 1 unit/s → readback should reach 1, moving flag set. + sim.advance(1000); + expect(sim.get('IOC:m1.RBV')).toBeCloseTo(1, 4); + expect(sim.get('IOC:m1.MOVN')).toBe(1); + + // 4 more seconds → readback should land at the setpoint. + sim.advance(4000); + expect(sim.get('IOC:m1.RBV')).toBeCloseTo(5, 4); + + // One more tick to settle: readback equals setpoint, MOVN clears. + sim.advance(16); + expect(sim.get('IOC:m1.RBV')).toBe(5); + expect(sim.get('IOC:m1.MOVN')).toBe(0); + }); + + it('reverses direction when setpoint moves past current readback', () => { + const sim = createOphydSim({ + devices: [motor({ name: 'IOC:m1', initialPosition: 0, velocity: 1 })], + }); + + sim.set('IOC:m1', 5); + sim.advance(2000); // readback ≈ 2 + sim.set('IOC:m1', -3); + sim.advance(1000); + // Was around 2, now moving toward -3 at 1 unit/s → ~ 1 after 1s + expect(sim.get('IOC:m1.RBV')).toBeLessThan(2); + }); +}); diff --git a/src/testing/tests/ophyd-sim/shutter.test.ts b/src/testing/tests/ophyd-sim/shutter.test.ts new file mode 100644 index 00000000..ae79190b --- /dev/null +++ b/src/testing/tests/ophyd-sim/shutter.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { createOphydSim } from '../../../lib/ophyd-sim/core/createOphydSim'; +import { signal } from '../../../lib/ophyd-sim/devices/signal'; +import { + shutter, + SHUTTER_OPEN_VALUE, + SHUTTER_CLOSED_VALUE, +} from '../../../lib/ophyd-sim/devices/shutter'; + +const PV = 'bl531:LJT4:1:AO0'; + +describe('shutter', () => { + it('seeds a writable analog PV, open by default', () => { + const sim = createOphydSim({ devices: [shutter({ name: PV })] }); + + expect(sim.get(PV)).toBe(SHUTTER_OPEN_VALUE); + expect(sim.metadata(PV)?.write_access).toBe(true); + expect(sim.metadata(PV)?.units).toBe('V'); + }); + + it('seeds the closed value when initial is "closed"', () => { + const sim = createOphydSim({ devices: [shutter({ name: PV, initial: 'closed' })] }); + + expect(sim.get(PV)).toBe(SHUTTER_CLOSED_VALUE); + }); + + it('honors custom open/closed values', () => { + const sim = createOphydSim({ + devices: [shutter({ name: PV, openValue: 1, closedValue: 10, initial: 'closed' })], + }); + + expect(sim.get(PV)).toBe(10); + }); + + it('gates a downstream signal off unless the shutter is open', () => { + const sim = createOphydSim({ + devices: [ + shutter({ name: PV, initial: 'open' }), + signal({ + name: 'diode', + dependsOn: [PV], + // Beam reaches the diode only at the open value. + value: ({ get }) => (get.number(PV) === SHUTTER_OPEN_VALUE ? 100 : 0), + }), + ], + }); + + expect(sim.get('diode')).toBe(100); + + sim.set(PV, SHUTTER_CLOSED_VALUE); + expect(sim.get('diode')).toBe(0); + + sim.set(PV, SHUTTER_OPEN_VALUE); + expect(sim.get('diode')).toBe(100); + }); +}); diff --git a/src/testing/tests/ophyd-sim/signal.test.ts b/src/testing/tests/ophyd-sim/signal.test.ts new file mode 100644 index 00000000..fa728b39 --- /dev/null +++ b/src/testing/tests/ophyd-sim/signal.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createOphydSim } from '../../../lib/ophyd-sim/core/createOphydSim'; +import { signal } from '../../../lib/ophyd-sim/devices/signal'; + +describe('signal', () => { + it('seeds a static initial value', () => { + const sim = createOphydSim({ + devices: [signal({ name: 'ring_current', initialValue: 400, units: 'mA' })], + }); + + expect(sim.get('ring_current')).toBe(400); + expect(sim.metadata('ring_current')?.units).toBe('mA'); + }); + + it('recomputes a derived signal when a dependency changes', () => { + const sim = createOphydSim({ + devices: [ + signal({ name: 'x', initialValue: 0, writeAccess: true }), + signal({ + name: 'I0', + dependsOn: ['x'], + value: ({ get }) => get.number('x') * 2, + }), + ], + }); + + expect(sim.get('I0')).toBe(0); + + sim.set('x', 5); + expect(sim.get('I0')).toBe(10); + + sim.set('x', -3); + expect(sim.get('I0')).toBe(-6); + }); + + it('notifies subscribers of a derived signal when its dependency changes', () => { + const sim = createOphydSim({ + devices: [ + signal({ name: 'x', initialValue: 0, writeAccess: true }), + signal({ + name: 'y', + dependsOn: ['x'], + value: ({ get }) => get.number('x') + 1, + }), + ], + }); + + const listener = vi.fn(); + sim.subscribe('y', listener); + listener.mockClear(); + + sim.set('x', 10); + expect(listener).toHaveBeenCalledWith(expect.objectContaining({ name: 'y', value: 11 })); + }); +}); diff --git a/src/testing/tests/ophyd-sim/state.test.ts b/src/testing/tests/ophyd-sim/state.test.ts new file mode 100644 index 00000000..f3db3192 --- /dev/null +++ b/src/testing/tests/ophyd-sim/state.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, vi } from 'vitest'; +import { SimState } from '../../../lib/ophyd-sim/core/state'; + +describe('SimState', () => { + it('subscribe-then-set delivers the new value', () => { + const state = new SimState(() => 1000); + const listener = vi.fn(); + state.subscribe('IOC:m1', listener); + listener.mockClear(); + + state.set('IOC:m1', 5); + expect(listener).toHaveBeenCalledWith({ + name: 'IOC:m1', + value: 5, + timestamp: 1000, + }); + }); + + it('subscribing after seed replays the latest value synchronously', () => { + const state = new SimState(() => 1000); + state.seed('IOC:m1', 42); + + const listener = vi.fn(); + state.subscribe('IOC:m1', listener); + + expect(listener).toHaveBeenCalledWith({ + name: 'IOC:m1', + value: 42, + timestamp: 1000, + }); + }); + + it('set returns false and does not notify when value is unchanged', () => { + const state = new SimState(() => 0); + state.seed('IOC:m1', 5); + const listener = vi.fn(); + state.subscribe('IOC:m1', listener); + listener.mockClear(); + + const changed = state.set('IOC:m1', 5); + expect(changed).toBe(false); + expect(listener).not.toHaveBeenCalled(); + }); + + it('unsubscribe stops further notifications', () => { + const state = new SimState(() => 0); + const listener = vi.fn(); + const unsub = state.subscribe('IOC:m1', listener); + unsub(); + listener.mockClear(); + + state.set('IOC:m1', 5); + expect(listener).not.toHaveBeenCalled(); + }); + + it('one subscriber throwing does not stop others', () => { + const state = new SimState(() => 0); + const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const ok = vi.fn(); + state.subscribe('IOC:m1', () => { + throw new Error('boom'); + }); + state.subscribe('IOC:m1', ok); + + state.set('IOC:m1', 5); + expect(ok).toHaveBeenCalled(); + errSpy.mockRestore(); + }); +}); diff --git a/src/testing/tests/ophyd-sim/transport.test.ts b/src/testing/tests/ophyd-sim/transport.test.ts new file mode 100644 index 00000000..19f0aeb6 --- /dev/null +++ b/src/testing/tests/ophyd-sim/transport.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createOphydSim } from '../../../lib/ophyd-sim/core/createOphydSim'; +import { signal } from '../../../lib/ophyd-sim/devices/signal'; +import { motor } from '../../../lib/ophyd-sim/devices/motor'; +import { createOphydSimTransport } from '../../../lib/ophyd-sim/transport/createOphydSimTransport'; +import type { OphydPVIncomingMessage } from '../../../api/ophyd/transport/types'; + +describe('createOphydSimTransport', () => { + it('emits a meta message immediately on subscribe', () => { + const sim = createOphydSim({ + devices: [signal({ name: 'I0', initialValue: 42, units: 'arb.' })], + }); + const transport = createOphydSimTransport(sim); + + const received: OphydPVIncomingMessage[] = []; + transport.onMessage((m) => received.push(m)); + + transport.send({ action: 'subscribe', pv: 'I0' }); + + // First message must be the meta + initial value (subscribe replays the + // latest known value synchronously). + expect(received.length).toBeGreaterThanOrEqual(2); + expect(received[0]).toMatchObject({ + sub_type: 'meta', + pv: 'I0', + value: 42, + units: 'arb.', + connected: true, + }); + expect(received[1]).toMatchObject({ pv: 'I0', value: 42 }); + }); + + it('forwards value updates after subscribe', () => { + const sim = createOphydSim({ + devices: [signal({ name: 'x', initialValue: 0, writeAccess: true })], + }); + const transport = createOphydSimTransport(sim); + + const received: OphydPVIncomingMessage[] = []; + transport.onMessage((m) => received.push(m)); + transport.send({ action: 'subscribe', pv: 'x' }); + received.length = 0; + + sim.set('x', 7); + expect(received[0]).toMatchObject({ pv: 'x', value: 7 }); + }); + + it('routes set messages to the simulator', () => { + const sim = createOphydSim({ + devices: [motor({ name: 'IOC:m1', limits: [-10, 10], velocity: 5 })], + }); + const transport = createOphydSimTransport(sim); + + transport.send({ action: 'set', pv: 'IOC:m1', value: 3 }); + expect(sim.get('IOC:m1')).toBe(3); + }); + + it('reports open status synchronously when a status listener attaches', () => { + const sim = createOphydSim({ devices: [] }); + const transport = createOphydSimTransport(sim); + + const listener = vi.fn(); + transport.onStatus(listener); + expect(listener).toHaveBeenCalledWith('open'); + }); + + it('reports closed after close()', () => { + const sim = createOphydSim({ devices: [] }); + const transport = createOphydSimTransport(sim); + const listener = vi.fn(); + transport.onStatus(listener); + listener.mockClear(); + + transport.close(); + expect(listener).toHaveBeenCalledWith('closed'); + }); + + it('unsubscribe stops further value messages for that PV', () => { + const sim = createOphydSim({ + devices: [signal({ name: 'x', initialValue: 0, writeAccess: true })], + }); + const transport = createOphydSimTransport(sim); + + const received: OphydPVIncomingMessage[] = []; + transport.onMessage((m) => received.push(m)); + transport.send({ action: 'subscribe', pv: 'x' }); + transport.send({ action: 'unsubscribe', pv: 'x' }); + received.length = 0; + + sim.set('x', 1); + expect(received).toHaveLength(0); + }); +}); diff --git a/src/testing/tests/ophyd-sim/useOphydPVSocket.integration.test.tsx b/src/testing/tests/ophyd-sim/useOphydPVSocket.integration.test.tsx new file mode 100644 index 00000000..fce68f64 --- /dev/null +++ b/src/testing/tests/ophyd-sim/useOphydPVSocket.integration.test.tsx @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { render, act, screen } from '@testing-library/react'; +import { createOphydSim } from '../../../lib/ophyd-sim/core/createOphydSim'; +import { motor } from '../../../lib/ophyd-sim/devices/motor'; +import { createOphydSimTransport } from '../../../lib/ophyd-sim/transport/createOphydSimTransport'; +import { OphydTransportProvider } from '../../../api/ophyd/OphydTransportProvider'; +import useOphydPVSocket from '../../../api/ophyd/useOphydPVSocket'; + +function MotorReadout({ label }: { label: string }) { + const { devices } = useOphydPVSocket(['IOC:m1.RBV']); + const value = devices['IOC:m1.RBV']?.value; + return ( +
+ {String(value)} +
+ ); +} + +function MotorControl({ initialTarget }: { initialTarget: number }) { + const { devices, handleSetValueRequest } = useOphydPVSocket(['IOC:m1', 'IOC:m1.RBV']); + return ( +
+ {String(devices['IOC:m1.RBV']?.value)} + +
+ ); +} + +describe('useOphydPVSocket + ophyd-sim integration', () => { + it('two hooks subscribed to the same PV observe the same motion', async () => { + const sim = createOphydSim({ + devices: [motor({ name: 'IOC:m1', velocity: 1, limits: [-10, 10] })], + }); + const transport = createOphydSimTransport(sim); + + render( + + + + , + ); + + // Both consumers start at 0. + expect(screen.getByTestId('control-rbv').textContent).toBe('0'); + expect(screen.getByTestId('readout-rbv').textContent).toBe('0'); + + // Trigger a move via one hook. + act(() => { + screen.getByText('move').click(); + }); + + // Advance the simulator past the time it needs to reach the target. + act(() => { + sim.advance(5000); + sim.advance(16); + }); + + expect(Number(screen.getByTestId('control-rbv').textContent)).toBeCloseTo(3, 4); + expect(Number(screen.getByTestId('readout-rbv').textContent)).toBeCloseTo(3, 4); + }); +}); diff --git a/src/testing/tests/simCameraSocket.test.ts b/src/testing/tests/simCameraSocket.test.ts new file mode 100644 index 00000000..4b06cf64 --- /dev/null +++ b/src/testing/tests/simCameraSocket.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { + SimCameraSocket, + type CameraFramePayload, + type CameraSocketMessageEvent, +} from '../../lib/ophyd-sim'; + +/** + * Drive the sim socket with a synchronous stub frame generator so frame + * delivery is deterministic under fake timers — the real generator depends on + * `OffscreenCanvas`, which jsdom lacks. + */ +function makeSocket(overrides = {}) { + const frame = new ArrayBuffer(8); + const generateFrame = vi.fn((): CameraFramePayload => frame); + const socket = new SimCameraSocket({ fps: 10, generateFrame, ...overrides }); + return { socket, frame, generateFrame }; +} + +describe('SimCameraSocket', () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + it('opens asynchronously and emits an initial dimensions message', async () => { + const { socket } = makeSocket({ width: 320, height: 240 }); + const onopen = vi.fn(); + const messages: CameraSocketMessageEvent['data'][] = []; + socket.onopen = onopen; + socket.onmessage = (e) => messages.push(e.data); + + // Nothing fires until the deferred "connect" timer runs. + expect(onopen).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(0); + + expect(onopen).toHaveBeenCalledTimes(1); + expect(socket.readyState).toBe(1); // OPEN + expect(messages).toContainEqual(JSON.stringify({ x: 320, y: 240 })); + }); + + it('emits frames at the configured fps once open', async () => { + const { socket, frame, generateFrame } = makeSocket(); // fps: 10 -> 100ms + const frames: CameraSocketMessageEvent['data'][] = []; + socket.onmessage = (e) => { + if (e.data instanceof ArrayBuffer) frames.push(e.data); + }; + + await vi.advanceTimersByTimeAsync(0); // open + expect(frames).toHaveLength(0); + + await vi.advanceTimersByTimeAsync(350); // ~3 frame intervals + expect(generateFrame).toHaveBeenCalled(); + expect(frames.length).toBeGreaterThanOrEqual(3); + expect(frames[0]).toBe(frame); + }); + + it('echoes logNormalization in response to toggleLogNormalization', async () => { + const { socket } = makeSocket(); + const messages: string[] = []; + socket.onmessage = (e) => { + if (typeof e.data === 'string') messages.push(e.data); + }; + + await vi.advanceTimersByTimeAsync(0); // open + + socket.send(JSON.stringify({ toggleLogNormalization: false })); + expect(messages).toContainEqual(JSON.stringify({ logNormalization: false })); + }); + + it('stops emitting frames and fires onclose after close', async () => { + const { socket } = makeSocket(); + const onclose = vi.fn(); + let frameCount = 0; + socket.onclose = onclose; + socket.onmessage = (e) => { + if (e.data instanceof ArrayBuffer) frameCount += 1; + }; + + await vi.advanceTimersByTimeAsync(0); // open + await vi.advanceTimersByTimeAsync(250); + const beforeClose = frameCount; + expect(beforeClose).toBeGreaterThan(0); + + socket.close(); + expect(onclose).toHaveBeenCalledTimes(1); + expect(socket.readyState).toBe(3); // CLOSED + + await vi.advanceTimersByTimeAsync(500); + expect(frameCount).toBe(beforeClose); // no frames after close + }); + + it('ignores sends and frame ticks before the socket opens', () => { + const { socket } = makeSocket(); + const messages: unknown[] = []; + socket.onmessage = (e) => messages.push(e.data); + + // Still CONNECTING — send is a no-op, nothing emitted. + socket.send(JSON.stringify({ toggleLogNormalization: true })); + expect(messages).toHaveLength(0); + expect(socket.readyState).toBe(0); // CONNECTING + }); +}); diff --git a/src/utils/plotGenerators.ts b/src/utils/plotGenerators.ts index ff78e3b9..2ed06a07 100644 --- a/src/utils/plotGenerators.ts +++ b/src/utils/plotGenerators.ts @@ -23,3 +23,19 @@ export const blankScatterData: PlotlyScatterData = { mode: 'lines+markers', marker: { color: 'grey', size: [] }, }; + +/** + * Per-point marker opacities for a time-ordered series (oldest first, newest + * last). The oldest point gets `minOpacity` and the newest gets 1. Recency is + * raised to `exponent` before interpolating, so `exponent > 1` makes older + * points fade out faster (the curve hugs full opacity near the newest point and + * drops off sooner for older ones). A single point is always fully opaque. + */ +export function recencyOpacities(count: number, minOpacity = 0, exponent = 4): number[] { + if (count <= 0) return []; + if (count === 1) return [1]; + return Array.from({ length: count }, (_, i) => { + const recency = i / (count - 1); + return minOpacity + Math.pow(recency, exponent) * (1 - minOpacity); + }); +} diff --git a/tsconfig.app.json b/tsconfig.app.json index f1bb298f..b88b023a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -11,6 +11,7 @@ "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", + "resolveJsonModule": true, "skipLibCheck": true, /* Bundler mode */