diff --git a/.gitignore b/.gitignore index 4332968fb..76fb01fe6 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,7 @@ style/tailwind_converted.css # Configs config.yaml +frontend/viewers.config.yaml # Claude Code .claude diff --git a/CLAUDE.md b/CLAUDE.md index 770f7648e..eb6b27456 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -169,6 +169,23 @@ Key settings: - `db_url`: Database connection string - SSL certificates for HTTPS mode +## Viewers Configuration + +Fileglancer uses a manifest-based viewer configuration system. Each viewer is defined by a **capability manifest** (a YAML file describing the viewer's name, URL template, and capabilities). The config file lists manifest URLs and optional overrides. + +- **Configuration file**: `frontend/src/config/viewers.config.yaml` -- lists viewers by `manifest_url` with optional `instance_template_url` and `label` overrides +- **Manifest files**: `frontend/public/viewers/*.yaml` -- capability manifest YAML files defining each viewer's identity and supported features +- **Compatibility**: Handled by the `@bioimagetools/capability-manifest` library, which checks dataset metadata against manifest capabilities at runtime +- **Documentation**: See `docs/ViewersConfiguration.md` + +To customize viewers: + +1. Copy `frontend/src/config/viewers.config.yaml` to `frontend/viewers.config.yaml` +2. Edit `frontend/viewers.config.yaml` (add/remove `manifest_url` entries, override URLs or labels) +3. Rebuild application: `pixi run node-build` + +`frontend/viewers.config.yaml` is gitignored so customizations do not conflict with upstream updates. When it exists, it takes precedence over the committed default at `frontend/src/config/viewers.config.yaml`. The config file is bundled at build time. + ## Pixi Environments - `default`: Standard development diff --git a/docs/Development.md b/docs/Development.md index dd973b338..117b52162 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -45,16 +45,16 @@ By default, Fileglancer provides access to each user's home directory without re ```yaml file_share_mounts: - - "~/" # User's home directory (default) + - "~/" # User's home directory (default) ``` You can add additional file share paths by editing your `config.yaml`: ```yaml file_share_mounts: - - "~/" # User's home directory - - "/groups/scicomp/data" # Shared data directory - - "/opt/data" # Another shared directory + - "~/" # User's home directory + - "/groups/scicomp/data" # Shared data directory + - "/opt/data" # Another shared directory ``` **How Home Directories Work:** @@ -68,8 +68,24 @@ file_share_mounts: Instead of using the `file_share_mounts` setting, you can configure file share paths in the database. This is useful for production deployments where you want centralized management of file share paths. To use the paths in the database, set `file_share_mounts: []`. See [fileglancer-janelia](https://github.com/JaneliaSciComp/fileglancer-janelia) for an example of populating the file share paths in the database, using a private wiki source. +### Viewers Configuration -### Testing configuration +Fileglancer supports dynamic configuration of OME-Zarr viewers through `viewers.config.yaml`. This allows you to customize which viewers are available in your deployment and configure custom viewer URLs. No configuration is required to use the default viewers defined in `frontend/src/config/viewers.config.yaml`. + +**To customize viewers:** + +1. Copy the default config: `cp frontend/src/config/viewers.config.yaml frontend/viewers.config.yaml` + +2. Edit `frontend/viewers.config.yaml` to enable/disable viewers or customize URLs + +3. Rebuild the application: `pixi run node-build` or use watch mode in development: `pixi run dev-watch` + +**Note:** The configuration file is bundled at build time, so changes require rebuilding the application. `frontend/viewers.config.yaml` is gitignored so your customizations will not conflict with upstream updates. The default configuration includes Neuroglancer, Avivator, OME-Zarr Validator, and Vol-E viewers. + +For detailed configuration options, examples, and documentation on adding custom viewers, see [ViewersConfiguration.md](ViewersConfiguration.md). + + +### Testing Configuration Optionally, to run Playwright tests against a development deployment with OKTA authentication enabled, add the below to the configuration file. **Note:** Do NOT add this configuration to a production deployment. diff --git a/docs/ViewersConfiguration.md b/docs/ViewersConfiguration.md new file mode 100644 index 000000000..52542957f --- /dev/null +++ b/docs/ViewersConfiguration.md @@ -0,0 +1,173 @@ +# Viewers Configuration Guide + +Fileglancer supports dynamic configuration of OME-Zarr viewers. This allows administrators to customize which viewers are available in their deployment, override viewer URLs, and control how compatibility is determined. + +## Overview + +The viewer system is built on capability manifests: + +- **`viewers.config.yaml`**: Configuration file listing viewers and their manifest URLs +- **Capability manifest files**: YAML files describing each viewer's name, URL template, and capabilities +- **`@bioimagetools/capability-manifest`**: Library that loads manifests and checks dataset compatibility +- **`ViewersContext`**: React context that provides viewer information to the application + +Each viewer is defined by a **capability manifest** hosted at a URL. The configuration file simply lists manifest URLs and optional overrides. At runtime, the manifests are fetched, and the `@bioimagetools/capability-manifest` library determines which viewers are compatible with a given dataset based on the manifest's declared capabilities. + +## Customize Viewers + +**Note:** No configuration is required to use the default viewers defined in `frontend/src/config/viewers.config.yaml`. + +1. Copy the default config: `cp frontend/src/config/viewers.config.yaml frontend/viewers.config.yaml` +2. Edit `frontend/viewers.config.yaml` to customize viewers +3. Rebuild the application: `pixi run node-build` + +## Runtime Configuration (System Deployments) + +For deployments where Fileglancer is installed from PyPI and the frontend is pre-built, you can override the viewers configuration at runtime without rebuilding. + +Set the `FGC_VIEWERS_CONFIG` environment variable (or `viewers_config` in `config.yaml`) to the absolute path of a `viewers.config.yaml` file on disk: + +```env +FGC_VIEWERS_CONFIG=/opt/deploy/viewers.config.yaml +``` + +Or in `config.yaml`: + +```yaml +viewers_config: /opt/deploy/viewers.config.yaml +``` + +When set, the application serves this file via the API and the frontend uses it instead of the bundled config. The file follows the same format as the build-time `viewers.config.yaml`. + +### Precedence + +The viewers configuration is resolved in the following order (highest priority first): + +1. **Runtime API config** — served from the path in `FGC_VIEWERS_CONFIG` (no rebuild required) +2. **Build-time override** — `frontend/viewers.config.yaml` (requires rebuild) +3. **Build-time default** — `frontend/src/config/viewers.config.yaml` (requires rebuild) + +## Configuration File + +### Location + +There are three config locations, resolved in order of precedence: + +| Location | Purpose | +| -------- | ------- | +| Path in `FGC_VIEWERS_CONFIG` | **Runtime override** — served via API, no rebuild required; ideal for system deployments | +| `frontend/viewers.config.yaml` | **Build-time override** — gitignored, safe to customize without merge conflicts | +| `frontend/src/config/viewers.config.yaml` | **Default config** — committed source file, used when no override exists | + +Copy `frontend/src/config/viewers.config.yaml` to `frontend/viewers.config.yaml` to create a local override. This file is listed in `.gitignore` so your customizations will not conflict with upstream updates. + +**Important:** The build-time configs are bundled at build time and changes require rebuilding the application. Runtime config via `FGC_VIEWERS_CONFIG` does **not** require rebuilding. + +### Structure + +The configuration file has a single top-level key, `viewers`, containing a list of viewer entries. Each entry requires a `manifest_url` and supports optional overrides. + +#### Viewer Entry Fields + +| Field | Required | Description | +| ----------------------- | -------- | -------------------------------------------------------------------------------- | +| `manifest_url` | Yes | URL to a capability manifest YAML file | +| `instance_template_url` | No | Override the viewer's `template_url` from the manifest | +| `label` | No | Custom tooltip text (defaults to "View in {Name}") | + +### Default Configuration + +The default `viewers.config.yaml` configures four viewers: + +```yaml +viewers: + - manifest_url: "https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/neuroglancer.yaml" + + - manifest_url: "https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/avivator.yaml" + + - manifest_url: "https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/validator.yaml" + + - manifest_url: "https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/vole.yaml" +``` + +## Capability Manifest Files + +Manifest files describe a viewer's identity and capabilities. The default manifests are hosted in the [`@bioimagetools/capability-manifest`](https://github.com/BioImageTools/capability-manifest) repository. You can host your own manifest files anywhere accessible via URL. See the [`@bioimagetools/capability-manifest`](https://github.com/BioImageTools/capability-manifest) repository for information on how to format a viewer manifest. + +## Configuration Examples + +### Minimal: single viewer + +```yaml +viewers: + - manifest_url: "https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/neuroglancer.yaml" +``` + +### Override a viewer's URL + +Use `instance_template_url` to point to a custom deployment of a viewer while still using its manifest for capability matching: + +```yaml +viewers: + - manifest_url: "https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/avivator.yaml" + instance_template_url: "https://my-avivator-instance.example.com/?image_url={dataLink}" + +``` + +### Add a custom viewer + +To add a new viewer, create a capability manifest YAML file, host it at a URL, and reference it in the config: + +1. Create a manifest file (e.g., `my-viewer.yaml`). Follow the format guidelines in the [`@bioimagetools/capability-manifest`](https://github.com/BioImageTools/capability-manifest) repository. + +2. Host the manifest at an accessible URL (e.g., on GitHub or any web server). + +3. Reference it in `viewers.config.yaml`: + +```yaml +viewers: + - manifest_url: "https://example.com/manifests/my-viewer.yaml" + label: "Open in My Viewer" +``` + +## How Compatibility Works + +The `@bioimagetools/capability-manifest` library handles all compatibility checking. When a user views an OME-Zarr dataset: + +1. The application reads the dataset's metadata (OME-Zarr version, axes, codecs, etc.) +2. For each registered viewer, the library's `validateViewer()` function compares the dataset metadata against the manifest's declared capabilities +3. Only viewers whose capabilities match the dataset are shown to the user +4. Incompatibility reasons (e.g., "Viewer does not support OME-Zarr v3") are logged to the browser console for debugging + +This replaces the previous system where `valid_ome_zarr_versions` was a global config setting and custom viewers used simple version matching. Now all compatibility logic is driven by the detailed capabilities declared in each viewer's manifest. + +## Viewer Logos + +Viewer logos are managed by the `@bioimagetools/capability-manifest` library. Logo resolution follows this order: + +1. **Override**: If the manifest includes a `viewer.logo` field, that URL is used directly +2. **Convention-based**: Otherwise, the logo URL is derived from the viewer name (lowercased, spaces replaced with hyphens, e.g. "OME-Zarr Validator" → `ome-zarr-validator.png`) and hosted alongside the manifests +3. **Fallback**: If the logo fails to load at runtime, a bundled fallback image is shown + +## Development + +When developing with custom configurations: + +1. Copy the default config: `cp frontend/src/config/viewers.config.yaml frontend/viewers.config.yaml` +2. Edit `frontend/viewers.config.yaml` +3. Rebuild frontend: `pixi run node-build` or use watch mode: `pixi run dev-watch` +4. Check the browser console for viewer initialization messages + +### Validation + +The configuration is validated at build time using Zod schemas (see `frontend/src/config/viewersConfig.ts`). Validation enforces: + +- The `viewers` array must contain at least one entry +- Each entry must have a valid `manifest_url` (a properly formed URL) +- Optional fields (`instance_template_url`, `label`) must be strings if present + +At runtime, manifests that fail to load are skipped with a warning. If a viewer has no `template_url` (neither from its manifest nor from `instance_template_url` in the config), it is also skipped. + +## Copy URL Tool + +The "Copy data URL" tool is always available when a data URL exists, regardless of viewer configuration. diff --git a/fileglancer/server.py b/fileglancer/server.py index c761acf2d..3f07bf46f 100644 --- a/fileglancer/server.py +++ b/fileglancer/server.py @@ -442,6 +442,22 @@ async def version_endpoint(): return {"version": APP_VERSION} + @app.get("/api/viewers-config", include_in_schema=False) + async def get_viewers_config(): + if not settings.viewers_config: + raise HTTPException(status_code=404, detail="No viewers configuration") + + config_path = PathLib(settings.viewers_config) + if not config_path.exists() or not config_path.is_file(): + logger.warning(f"Viewers config file not found: {settings.viewers_config}") + raise HTTPException(status_code=404, detail="Viewers configuration file not found") + + return PlainTextResponse( + content=config_path.read_text(encoding="utf-8"), + media_type="text/yaml" + ) + + # Authentication routes @app.get("/api/auth/login", include_in_schema=settings.enable_okta_auth, description="Initiate OKTA OAuth login flow") diff --git a/fileglancer/settings.py b/fileglancer/settings.py index 3b21bdef8..8d05e69a2 100644 --- a/fileglancer/settings.py +++ b/fileglancer/settings.py @@ -113,6 +113,11 @@ class Settings(BaseSettings): # Username used when creating a session via the test-login endpoint. test_login_username: str = "jacs" + # Optional path to a viewers configuration YAML file. + # When set, the file is served at GET /api/viewers-config, allowing + # runtime customization of OME-Zarr viewers without rebuilding the frontend. + viewers_config: Optional[str] = None + model_config = SettingsConfigDict( yaml_file="config.yaml", env_file='.env', diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 5cd965e0d..aa0b85e80 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -1,231 +1,35 @@ # CLAUDE.md - Frontend -This file provides guidance to Claude Code when working with the frontend code in this directory. - > **Note**: This is a subdirectory-specific guide. For full project context, look for a CLAUDE.md in the root directory. -## Directory Overview - -This directory contains the React/TypeScript frontend application for Fileglancer. The built output is copied to `../fileglancer/ui/` and served by the FastAPI backend. - -## Quick Start - -```bash -# From project root -cd .. -pixi run dev-install # Install and build -pixi run dev-watch # Watch mode for frontend changes -pixi run dev-launch # Launch backend + serve frontend -pixi run test-frontend # Vitest frontend unit tests (npm test) -pixi run test-ui # Playwright integration tests -pixi run test-ui -- tests/specific.spec.ts # Run specific test -pixi run node-eslint-check # Check eslint rules -pixi run node-eslint-write # Modify according to eslint rules -pixi run node-prettier-write # Modify according to prettier rules -./clean.sh # Clean all build directories -``` - -## Directory Structure - -``` -frontend/ -├── src/ -│ ├── main.tsx # Application entry point -│ ├── App.tsx # Root component with routing -│ ├── index.css # Global styles -│ ├── logger.ts # Logging utility (loglevel) -│ ├── omezarr-helper.ts # OME-Zarr/NGFF utilities -│ ├── shared.types.ts # Shared TypeScript types -│ │ -│ ├── components/ # Page-level components -│ │ ├── Browse.tsx -│ │ ├── Help.tsx -│ │ ├── Jobs.tsx -│ │ ├── Links.tsx -│ │ ├── Preferences.tsx -│ │ ├── Notifications.tsx -│ │ └── ui/ # Feature-specific UI components -│ │ ├── BrowsePage/ # File browser components -│ │ ├── Dialogs/ # Modal dialogs -│ │ ├── Menus/ # Context and action menus -│ │ ├── Navbar/ # Top navigation -│ │ ├── Sidebar/ # File browser sidebar -│ │ ├── Table/ # Table components -│ │ ├── widgets/ # Reusable UI widgets -│ │ ├── PreferencesPage/ -│ │ ├── PropertiesDrawer/ -│ │ └── Notifications/ -│ │ -│ ├── contexts/ # React Context providers -│ │ ├── FileBrowserContext.tsx -│ │ ├── ZonesAndFspMapContext.tsx -│ │ ├── PreferencesContext.tsx -│ │ ├── TicketsContext.tsx -│ │ ├── ProxiedPathContext.tsx -│ │ ├── ExternalBucketContext.tsx -│ │ ├── ProfileContext.tsx -│ │ ├── ServerHealthContext.tsx -│ │ ├── NotificationsContext.tsx -│ │ ├── OpenFavoritesContext.tsx -│ │ └── CookiesContext.tsx -│ │ -│ ├── hooks/ # Custom React hooks -│ │ -│ ├── queries/ # TanStack Query hooks -│ │ └── (API query hooks) -│ │ -│ ├── layouts/ # Layout components -│ │ ├── MainLayout.tsx -│ │ ├── BrowsePageLayout.tsx -│ │ └── OtherPagesLayout.tsx -│ │ -│ ├── utils/ # Utility functions -│ │ -│ ├── constants/ # Application constants -│ │ -│ ├── assets/ # Static assets (images, icons) -│ │ -│ └── __tests__/ # Test files -│ ├── setup.ts -│ ├── test-utils.tsx -│ ├── componentTests/ -│ ├── unitTests/ -│ └── mocks/ -│ └── handlers.ts # MSW mock handlers -│ -├── ui-tests/ # Playwright E2E tests -├── public/ # Static public assets -├── index.html # HTML entry point -├── vite.config.ts # Vite configuration -├── tailwind.config.js # Tailwind CSS theme -├── eslint.config.mjs # ESLint configuration -├── prettier.config.mjs # Prettier configuration -├── tsconfig.json # TypeScript configuration -└── package.json # NPM dependencies and scripts -``` - -## Technology Stack - -### Core - -- **React 18.3.1** - UI framework with hooks and concurrent features -- **TypeScript 5.8** - Type-safe JavaScript -- **Vite** (Rolldown) - Fast Rust-based bundler (Rollup alternative) -- **React Router 7.4** - Client-side routing - -### State Management - -- **React Context API** - Application state (see `src/contexts/`) -- **TanStack Query v5** - Server state management, data fetching, caching - - Query hooks in `src/queries/` - - DevTools available in development mode - -### UI & Styling - -- **Material Tailwind v3** (beta) - Component library -- **Tailwind CSS 3.4** - Utility-first CSS framework -- **React Icons 5.5** - Icon library -- **React Hot Toast 2.5** - Notifications/toasts -- **React Resizable Panels 3.0** - Resizable layout panels - -### Data & Visualization - -- **TanStack Table v8** - Headless table/data grid -- **ome-zarr.js 0.0.14** - OME-NGFF/Zarr visualization -- **zarrita 0.5** - Zarr file format support -- **React Syntax Highlighter 15.6** - Code display - -### Testing - -- **Vitest 3.1** - Fast unit test runner (Vite-native) -- **React Testing Library 16.3** - Component testing utilities -- **Happy DOM 18.0** - Fast DOM implementation for tests -- **MSW 2.10** - API mocking for tests -- **@testing-library/jest-dom 6.6** - DOM matchers -- **@testing-library/user-event 14.6** - User interaction simulation -- **@types/react 18.3** - React type definitions -- **@types/react-dom 18.3** - React DOM type definitions -- **Playwright** (in ui-tests/) - E2E browser testing - -### Development Tools - -- **ESLint 9.26** - Linting with TypeScript/React plugins -- **Prettier 3.5** - Code formatting -- **Lefthook 1.12** - Git hooks manager - ## Development Patterns ### Import Formatting -**Separate imports for functions and types:** - -When importing both functions/values and types from the same namespace, use separate import lines: - -```typescript -// Good - separate imports -import { useState, useEffect } from 'react'; -import type { FC, ReactNode } from 'react'; - -import { useQuery } from '@tanstack/react-query'; -import type { QueryClient } from '@tanstack/react-query'; - -// Avoid - mixing functions and types -import { useState, useEffect, type FC, type ReactNode } from 'react'; -``` - -This improves readability and makes it clear which imports are type-only. +Use separate import lines for values and types from the same package (e.g. `import { useQuery } from '...'` and `import type { UseQueryResult } from '...'` on separate lines). ### URL Construction and Encoding -**Key Principle**: URL encoding must happen at the point of URL construction using utility functions, not manually. - -**Data Flow**: User-controlled data (file paths, FSP names, etc.) flows through the application in raw, unencoded form. Encoding is applied by URL construction utilities. - -**URL Construction Utilities** (`src/utils/index.ts` and `src/utils/pathHandling.ts`): +Never manually construct URLs with template strings. Use utility functions (`src/utils/index.ts`, `src/utils/pathHandling.ts`): -1. **`buildApiUrl(basePath, pathSegments?, queryParams?)`** - For internal API requests - - Encodes path segments with `encodeURIComponent()` (including `/`) - - Uses `URLSearchParams` for query parameters - - Returns relative URLs (e.g., `/api/files/myFSP?subpath=file.txt`) - - **Why it encodes `/`**: FastAPI automatically URL-decodes path parameters, so full encoding is required - - **Use for**: All `sendFetchRequest()` calls to internal APIs +1. **`buildUrl`** (`src/utils/index.ts`) - General-purpose URL builder with two overloads: + - **Overload 1**: `buildUrl(baseUrl, singlePathSegment | null, queryParams | null)` — single path segment encoded with `encodeURIComponent`, plus optional query params via `URLSearchParams` + ```typescript + buildUrl('/api/files/', 'myFSP', { subpath: 'folder/file.txt' }); + // → '/api/files/myFSP?subpath=folder%2Ffile.txt' + buildUrl('/api/endpoint', null, { key: 'value' }); + // → '/api/endpoint?key=value' + ``` + - **Overload 2**: `buildUrl(baseUrl, multiSegmentPathString)` — multi-segment path encoded with `escapePathForUrl` (preserves `/`) + ```typescript + buildUrl('https://s3.example.com/bucket', 'folder/file 100%.zarr'); + // → 'https://s3.example.com/bucket/folder/file%20100%25.zarr' + ``` + - **Use for**: All internal API calls and any external URL that needs path or query encoding -2. **`buildExternalUrlWithQuery(baseUrl, queryParams?)`** - For form/query-based external URLs - - Takes absolute URLs as base - - Only supports query parameters (no path segments) - - Uses `URLSearchParams` for query encoding - - Returns absolute URLs (e.g., `https://viewer.com?url=...`) - - **Use for**: External form submissions, validators, and web apps that accept data as query params +2. **`getFileURL(fspName, filePath?)`** - Absolute `/api/content/` URL for file content; use when passing URLs to OME-Zarr viewers -3. **`buildExternalUrlWithPath(baseUrl, pathSegment?, queryParams?)`** - For S3-style external URLs - - Takes absolute URLs as base - - Path segments are encoded while preserving `/` as path separator - - Optional query parameters using `URLSearchParams` - - Returns absolute URLs (e.g., `https://s3.example.com/bucket/folder/file.zarr`) - - **Use for**: S3-compatible storage, cloud bucket URLs with path-based resource access - -4. **`getFileURL(fspName, filePath?)`** - For browser-accessible file content URLs - - Uses `escapePathForUrl()` which preserves `/` as path separator - - Returns absolute URLs using `window.location.origin` - - Specifically for `/api/content/` endpoint - - **Use for**: File content URLs displayed to users or used in OME-Zarr viewers - -5. **`escapePathForUrl(path)`** - For path-style URLs (preserves `/`) - - Encodes each path segment separately - - Preserves forward slashes as path separators - - **Use for**: Constructing file paths within URLs - -**Best Practices**: - -- **Always use utility functions** - Never manually construct URLs with template strings -- **Choose the right utility**: - - Internal API calls → `buildApiUrl` - - Query-based external URLs → `buildExternalUrlWithQuery` - - S3-style external URLs → `buildExternalUrlWithPath` - - File content URLs → `getFileURL` - - Manual path construction → `escapePathForUrl` -- **No double encoding**: Functions that receive URLs (like `sendFetchRequest`) do not re-encode -- **Backend URLs are ready**: URLs from backend API responses are already encoded +3. **`escapePathForUrl(path)`** - Encodes path segments while preserving `/`; use when `buildUrl` overload 2 isn't sufficient ### Component Guidelines @@ -246,272 +50,74 @@ This improves readability and makes it clear which imports are type-only. - Good: `function MyComponent() { ... }` - Avoid: `function MyComponent(): JSX.Element { ... }` -### State Management Patterns - -**When to use what:** - -- **React Context** (`src/contexts/`) - Global UI state, dependency injection (e.g., of server state) - - Follow provider pattern used in existing contexts -- **TanStack Query** (`src/queries/`) - Server data fetching, caching, synchronization - - Use for all API calls - - Define query/mutation hooks in `src/queries/` - - Leverage automatic refetching, caching, and background updates -- **Component State** (`useState`) - Local UI state that doesn't need to be shared -- **URL State** (React Router) - Navigation state, filters, search params - ### API Integration with TanStack Query -**Pattern for data fetching:** +**URL construction:** Use `buildUrl` from `@/utils` for all API URLs: -```typescript -// In src/queries/useMyData.ts -// default `staleTime` set to 30 seconds in /src/main.tsx -// only override if good reason to (see example below) -import { useQuery } from '@tanstack/react-query'; -import { buildApiUrl, sendFetchRequest } from '@/utils'; +- `buildUrl('/api/files/', fspName, { subpath: path })` — path segment + query params +- `buildUrl('/api/resource/', id, null)` — path segment only -export function useMyData(fspName: string, filePath?: string) { - return useQuery({ - queryKey: ['my-data', fspName, filePath], - queryFn: async ({ signal }) => { - // Use buildApiUrl for proper URL encoding - const url = buildApiUrl( - '/api/files/', - [fspName], - filePath ? { subpath: filePath } : undefined - ); +**Query utilities** (`src/queries/queryUtils.ts`): - // Use sendFetchRequest for session handling and health checks - const response = await sendFetchRequest(url, 'GET', undefined, { - signal - }); +- `sendRequestAndThrowForNotOk(url, method, body?)` — sends request, throws on non-2xx; use for most mutations +- `getResponseJsonOrError(response)` + `throwResponseNotOkError(response, body)` — use together when you need custom status-code handling (e.g. treat 404 as empty result, not an error) - if (!response.ok) { - throw new Error('Failed to fetch'); - } - return response.json(); - }, - staleTime: 1000 * 60 * 5 // 5 minutes, data not expected to change frequently - }); -} +**Query key factories:** Define a key factory object alongside each query file for consistent cache management: -// In component -const { data, isLoading, error } = useMyData(fspName, filePath); +```typescript +export const myQueryKeys = { + all: ['myResource'] as const, + list: () => ['myResource', 'list'] as const, + detail: (id: string) => ['myResource', 'detail', id] as const +}; ``` -**Pattern for mutations:** +**Pattern for data fetching:** ```typescript -// In src/queries/useUpdateMyData.ts -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { buildApiUrl, sendFetchRequest } from '@/utils'; - -export function useUpdateMyData() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async (payload: { fspName: string; data: MyData }) => { - // Use buildApiUrl for proper URL encoding - const url = buildApiUrl('/api/files/', [payload.fspName]); - - // Use sendFetchRequest - it handles headers, credentials, and error handling - const response = await sendFetchRequest(url, 'PUT', payload.data); - - if (!response.ok) { - throw new Error('Update failed'); - } - return response.json(); - }, - onSuccess: () => { - // Invalidate and refetch - queryClient.invalidateQueries({ queryKey: ['my-data'] }); - } +// In src/queries/myQueries.ts +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { buildUrl, sendFetchRequest } from '@/utils'; +import { getResponseJsonOrError, throwResponseNotOkError } from './queryUtils'; + +// Extract fetch logic outside the hook for reuse and testability +const fetchMyData = async ( + fspName: string, + path: string, + signal?: AbortSignal +): Promise => { + const url = buildUrl('/api/files/', fspName, { subpath: path }); + const response = await sendFetchRequest(url, 'GET', undefined, { signal }); + const body = await getResponseJsonOrError(response); + + if (response.status === 404) { + return null; // treat 404 as empty, not an error + } + if (!response.ok) { + throwResponseNotOkError(response, body); + } + return body as MyData; +}; + +export function useMyDataQuery( + fspName: string | undefined, + path: string | undefined +): UseQueryResult { + return useQuery({ + queryKey: myQueryKeys.detail(fspName ?? '', path ?? ''), + queryFn: ({ signal }) => fetchMyData(fspName!, path!, signal), + enabled: !!fspName && !!path, + staleTime: 5 * 60 * 1000, // override default only when there's a good reason + retry: false // omit to use the default; set false when retrying won't help }); } ``` -**Why use `sendFetchRequest`?** - -- Automatically includes credentials for session management -- Handles session expiration (401/403) with automatic logout -- Reports failed requests to health check monitoring -- Consistent error handling across the application -- Sets appropriate headers based on HTTP method - -### Routing - -- **Base path**: `/fg/` (configured in `vite.config.ts`) -- **Routes**: - - `/fg/` - Dashboard/Browse (default) - - `/fg/browse` - File browser - - `/fg/jobs` - Background jobs - - `/fg/links` - Data links management - - `/fg/help` - Help/support - - `/fg/preferences` - User preferences - - `/fg/notifications` - Notifications -- **Route definitions**: See `src/App.tsx` -- **Navigation**: Use React Router's `useNavigate()`, `Link`, or `NavLink` +**Mutations** use `useMutation` + `sendRequestAndThrowForNotOk`. Use `onMutate`/`onError` for optimistic updates with rollback, and `onSuccess` to call `queryClient.invalidateQueries` or `setQueryData`. See `proxiedPathQueries.ts` for a full example. ### Error Handling -- **React Error Boundary**: Wraps app to catch React errors -- **TanStack Query**: Built-in error handling for async operations - - Check `error` property from `useQuery`/`useMutation` -- **Toast notifications**: Use `react-hot-toast` for user-facing errors -- **Logging**: Use `logger` from `src/logger.ts` for debugging. Leave minimal loggers in final version of code - -### Testing Strategy - -**Unit Tests** (`src/__tests__/`) - -- **Setup**: `setup.ts` configures test environment (Happy DOM, React Testing Library) -- **Test utilities**: `test-utils.tsx` provides custom render functions with providers -- **API mocking**: MSW handlers in `mocks/handlers.ts` -- **Component tests**: `componentTests/` - test UI components in isolation -- **Unit tests**: `unitTests/` - test utility functions, helpers, hooks -- **Coverage**: Run with `npm test -- --coverage` - -**E2E Tests** (`ui-tests/`) - -- **Playwright** browser tests for full application flows -- Run from project root with `pixi run test-ui` - -**Testing best practices:** - -- Mock API calls with MSW handlers -- Use `screen.getByRole()` over `getByTestId()` -- Test user interactions, not implementation details -- Keep tests isolated and independent - -## Common Workflows - -### Adding a New Feature - -1. **Plan the feature** - Identify components, contexts, API calls needed -2. **Check for existing patterns** - Look for similar features to reuse/adapt -3. **Create components** - In appropriate `src/components/ui/` subdirectory -4. **Add state management** - Context for UI state and dependency injection, TanStack Query for server data -5. **Define types** - TypeScript interfaces in component files or `shared.types.ts` -6. **Add API integration** - Query/mutation hooks in `src/queries/` -7. **Update routing** - If needed, add route in `src/App.tsx` -8. **Add tests** - Unit tests in `__tests__/`, E2E tests in `ui-tests/` -9. **Update docs** - Document new patterns or conventions - -### Modifying Existing Components - -1. **Read the component** - Understand current implementation -2. **Check dependencies** - See what contexts/hooks it uses -3. **Update types** - Modify TypeScript interfaces as needed -4. **Make changes** - Follow existing patterns and conventions -5. **Update tests** - Ensure existing tests work with new behavior - -### Debugging - -**Development tools:** - -- **React DevTools** - Browser extension for component inspection -- **TanStack Query DevTools** - Automatically available in dev mode (bottom-left icon) -- **Browser DevTools** - Console, Network tab, React tab -- **Vite HMR** - Hot Module Replacement for fast feedback - -**Common issues:** - -- **Build errors**: Check TypeScript types, import paths -- **Runtime errors**: Check browser console, React Error Boundary -- **API errors**: Check TanStack Query DevTools, Network tab -- **State issues**: Check React Context providers, TanStack Query cache -- **Style issues**: Check Tailwind classes, `tailwind.config.js` - -### Working with Backend - -**API base URL:** - -- Development: `http://localhost:7878` (FastAPI backend) -- Production: Same origin as frontend (served by FastAPI) - -**API endpoints:** All under `/api/` prefix - -- `/api/files` - File operations -- `/api/proxied-paths` - Data links -- `/api/tickets` - Background jobs -- `/api/external-buckets` - S3 buckets -- `/api/file-share-paths` - File shares -- `/api/profile` - User profile -- `/api/health` - Backend health check - -**API integration:** - -- Use TanStack Query hooks in `src/queries/` -- Define query keys consistently for cache management -- Handle loading, error, and success states - -## Import Aliases - -- `@/` - Resolves to `./src/` (configured in `vite.config.ts` and `tsconfig.json`) -- Example: `import { logger } from '@/logger'` - -## Build Output - -- **Development build**: Outputs to `../fileglancer/ui/` -- **Served by**: FastAPI backend at `/fg/` path -- **Static assets**: `/fg/assets/` (Vite asset hashing applied) - -## Environment Variables - -- `.env` - Local environment configuration -- Variables must be prefixed with `VITE_` to be exposed to frontend -- Access via `import.meta.env.VITE_VAR_NAME` - -## Troubleshooting - -**Build issues:** - -- Clear cache: `rm -rf node_modules .eslintcache && npm install` or use `./clean.sh` from the root directory -- Check Node version: Should be v22.12+ -- Verify output directory: `../fileglancer/ui/` should exist after build - -**Import errors:** - -- Check import paths and file extensions (.tsx, .ts) -- Verify `@/` alias resolves correctly -- Check `tsconfig.json` paths configuration - -**Type errors:** - -- Run `pixi run node-eslint-check` to see all type errors -- Check TypeScript version matches project (5.8+) -- Verify all dependencies have type definitions - -**Vite/Rolldown issues:** - -- Check `vite.config.ts` for plugin configuration -- Clear Vite cache: `rm -rf node_modules/.vite` -- Note: Using Rolldown (Rust alternative to Rollup) via `rolldown-vite` package - -**Test failures:** - -- Check MSW handlers are properly configured -- Verify test setup in `src/__tests__/setup.ts` - -**TanStack Query issues:** - -- Check query keys are unique and consistent -- Use TanStack Query DevTools to inspect cache -- Verify `queryClient` configuration in `src/main.tsx` - -## Additional Resources - -- [React 18 Docs](https://react.dev) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/) -- [Vite Guide](https://vite.dev/guide/) -- [React Router Docs](https://reactrouter.com) -- [TanStack Query Docs](https://tanstack.com/query/latest/docs/react/overview) -- [Material Tailwind Docs](https://www.material-tailwind.com) -- [Tailwind CSS Docs](https://tailwindcss.com/docs) -- [Vitest Docs](https://vitest.dev) -- [React Testing Library Docs](https://testing-library.com/react) -- [MSW Docs](https://mswjs.io) - ---- +- Either handle error or throw. Do not log and then throw. For backend development, database migrations, or full-stack workflows, look for a CLAUDE.md in the root directory. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5514953a4..c60f4537c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,15 +9,18 @@ "version": "2.7.0", "license": "BSD-3-Clause", "dependencies": { + "@bioimagetools/capability-manifest": "0.5.0", "@material-tailwind/react": "^3.0.0-beta.24", "@tanstack/react-hotkeys": "^0.4.1", "@tanstack/react-query": "^5.90.2", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.23", + "@types/js-yaml": "^4.0.9", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", "fracturedjsonjs": "^5.0.1", + "js-yaml": "^4.1.1", "loglevel": "^1.9.2", "npm-run-all2": "^7.0.2", "ome-zarr.js": "^0.0.17", @@ -34,7 +37,8 @@ "react-syntax-highlighter": "^16.1.0", "shepherd.js": "^14.5.1", "tailwindcss": "^3.4.17", - "zarrita": "^0.5.1" + "zarrita": "^0.5.1", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/css": "^0.8.1", @@ -102,12 +106,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -116,29 +120,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -164,13 +168,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -180,12 +184,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -214,27 +218,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -244,9 +248,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -280,25 +284,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -338,40 +342,40 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -379,9 +383,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -401,41 +405,53 @@ "node": ">=18" } }, + "node_modules/@bioimagetools/capability-manifest": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@bioimagetools/capability-manifest/-/capability-manifest-0.5.0.tgz", + "integrity": "sha512-P8H/74W6qcogeYqgFkkEroRnlpuxPZU8HLJLmp+zGbK3AKmjhN/CuHJPOuclAr5i9x2Yurxd7gn2jh2O5Ksr/w==", + "license": "ISC", + "dependencies": { + "js-yaml": "^4.1.1" + } + }, "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -462,24 +478,31 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -488,9 +511,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "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": { @@ -555,9 +578,9 @@ } }, "node_modules/@eslint/css-tree": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-3.6.6.tgz", - "integrity": "sha512-C3YiJMY9OZyZ/3vEMFWJIesdGaRY6DmIYvmtyxMT934CbrOKqRs+Iw7NWSRlJQEaK4dPYy2lZ2y1zkaj8z0p5A==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-3.6.9.tgz", + "integrity": "sha512-3D5/OHibNEGk+wKwNwMbz63NMf367EoR4mVNNpxddCHKEb2Nez7z62J2U6YjtErSsZDoY0CsccmoUpdEbkogNA==", "dev": true, "license": "MIT", "dependencies": { @@ -569,20 +592,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "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.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -592,10 +615,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -617,9 +647,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "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": { @@ -630,9 +660,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -759,40 +789,28 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, - "node_modules/@floating-ui/core/node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, - "node_modules/@floating-ui/dom/node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, "node_modules/@floating-ui/utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", - "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -949,6 +967,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -966,6 +985,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -978,6 +998,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -992,9 +1013,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -1079,22 +1100,28 @@ } }, "node_modules/@material-tailwind/react/node_modules/@floating-ui/react/node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, + "node_modules/@material-tailwind/react/node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==", + "license": "MIT" + }, "node_modules/@mswjs/interceptors": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", - "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", "dev": true, "license": "MIT", "dependencies": { @@ -1110,15 +1137,21 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -1182,18 +1215,18 @@ "license": "MIT" }, "node_modules/@oxc-project/runtime": { - "version": "0.97.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.97.0.tgz", - "integrity": "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==", + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.101.0.tgz", + "integrity": "sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@oxc-project/types": { - "version": "0.97.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.97.0.tgz", - "integrity": "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==", + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.101.0.tgz", + "integrity": "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -1203,6 +1236,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1223,9 +1257,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz", - "integrity": "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==", "cpu": [ "arm64" ], @@ -1239,9 +1273,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.50.tgz", - "integrity": "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==", "cpu": [ "arm64" ], @@ -1255,9 +1289,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.50.tgz", - "integrity": "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==", "cpu": [ "x64" ], @@ -1271,9 +1305,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.50.tgz", - "integrity": "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==", "cpu": [ "x64" ], @@ -1287,9 +1321,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.50.tgz", - "integrity": "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.53.tgz", + "integrity": "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==", "cpu": [ "arm" ], @@ -1303,9 +1337,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.50.tgz", - "integrity": "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==", "cpu": [ "arm64" ], @@ -1319,9 +1353,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.50.tgz", - "integrity": "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==", "cpu": [ "arm64" ], @@ -1335,9 +1369,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.50.tgz", - "integrity": "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==", "cpu": [ "x64" ], @@ -1351,9 +1385,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.50.tgz", - "integrity": "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==", "cpu": [ "x64" ], @@ -1367,9 +1401,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.50.tgz", - "integrity": "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==", "cpu": [ "arm64" ], @@ -1383,25 +1417,25 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.50.tgz", - "integrity": "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.53.tgz", + "integrity": "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.7" + "@napi-rs/wasm-runtime": "^1.1.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.50.tgz", - "integrity": "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==", "cpu": [ "arm64" ], @@ -1414,26 +1448,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.50.tgz", - "integrity": "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.50.tgz", - "integrity": "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==", "cpu": [ "x64" ], @@ -1447,9 +1465,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", - "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", "license": "MIT" }, "node_modules/@rollup/plugin-inject": { @@ -1533,26 +1551,32 @@ "license": "Apache-2.0" }, "node_modules/@tanstack/eslint-plugin-query": { - "version": "5.91.2", - "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.91.2.tgz", - "integrity": "sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw==", + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.99.0.tgz", + "integrity": "sha512-jVp1AEL7S7BeuQvH5SN1F5UdrNW/AbryKDeWUUMeAKNzh9C+Ik/bRSa/HeuJLlmaN+WOUkdDFbtCK0go7BxnUQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.44.1" + "@typescript-eslint/utils": "^8.58.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "^5.4.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@tanstack/hotkeys": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tanstack/hotkeys/-/hotkeys-0.4.1.tgz", - "integrity": "sha512-EGHqcdKP2jzy0dEkahA3ABtEXohMqPlU3Ac04sBQjgesJqr9xWuesJotOfWPh3P68kQQg8krNAtFTydIN3+WSw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tanstack/hotkeys/-/hotkeys-0.4.2.tgz", + "integrity": "sha512-dCCu6Q91wZ2Mz7Vb+tzzpbKH0cSY9JXqJS7ZyouxewNL8oVmI228P9BmP94/1255g5WjPS+njenyrbWVeEQP5Q==", "license": "MIT", "dependencies": { "@tanstack/store": "^0.9.2" @@ -1566,9 +1590,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.90.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz", - "integrity": "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==", + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.0.tgz", + "integrity": "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==", "license": "MIT", "funding": { "type": "github", @@ -1589,12 +1613,12 @@ } }, "node_modules/@tanstack/react-hotkeys": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-hotkeys/-/react-hotkeys-0.4.1.tgz", - "integrity": "sha512-hFh/kKQODn4kSytfIsEE/Vf1AaAb+NAFi4lx+OB49NmKY5z/BNH1/uEdYlVgOEvnDm4QrCISIMBOVpMgK5QNQg==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-hotkeys/-/react-hotkeys-0.4.2.tgz", + "integrity": "sha512-7AMAmX+l1k0tHCaDgT5lHHF0cmtrmK0aaKxR6DliSdAVVjfmyraPZQapLwpyNKoTKNYIROj+2Wg+OvWYP52Oog==", "license": "MIT", "dependencies": { - "@tanstack/hotkeys": "0.4.1", + "@tanstack/hotkeys": "0.4.2", "@tanstack/react-store": "^0.9.2" }, "engines": { @@ -1610,12 +1634,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.10", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz", - "integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==", + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.0.tgz", + "integrity": "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.10" + "@tanstack/query-core": "5.99.0" }, "funding": { "type": "github", @@ -1642,12 +1666,12 @@ } }, "node_modules/@tanstack/react-store": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.2.tgz", - "integrity": "sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", + "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.9.2", + "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "funding": { @@ -1697,9 +1721,9 @@ } }, "node_modules/@tanstack/store": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.2.tgz", - "integrity": "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", + "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", "license": "MIT", "funding": { "type": "github", @@ -1777,9 +1801,9 @@ "license": "MIT" }, "node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1888,9 +1912,9 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "dev": true, "license": "MIT", "dependencies": { @@ -1920,6 +1944,12 @@ "@types/unist": "*" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1945,19 +1975,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", "license": "MIT" }, "node_modules/@types/prop-types": { @@ -1967,9 +1997,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2026,21 +2056,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", - "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/type-utils": "8.47.0", - "@typescript-eslint/utils": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2050,9 +2079,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.47.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.58.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -2066,17 +2095,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", - "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2086,20 +2115,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", - "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.47.0", - "@typescript-eslint/types": "^8.47.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2109,18 +2138,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", - "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0" + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2131,9 +2160,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", - "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", "dev": true, "license": "MIT", "engines": { @@ -2144,21 +2173,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", - "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2168,14 +2197,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", - "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", "dev": true, "license": "MIT", "engines": { @@ -2187,22 +2216,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", - "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.47.0", - "@typescript-eslint/tsconfig-utils": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2212,20 +2240,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", - "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2235,19 +2263,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", - "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.58.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2258,28 +2286,28 @@ } }, "node_modules/@typescript-eslint/visitor-keys/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": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", - "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "license": "MIT", "dependencies": { - "@babel/core": "^7.28.5", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.47", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -2287,7 +2315,7 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitest/coverage-v8": { @@ -2440,19 +2468,19 @@ } }, "node_modules/@zarrita/storage": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.1.3.tgz", - "integrity": "sha512-ZyCMYN3LuCNtKxro9876r/KyHyXV+ie2Bhk1qYsJR4Jp+sAjoVRRNNSJPsJxk64ZgFFezayO5S2hCu88/1Odwg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.1.4.tgz", + "integrity": "sha512-qURfJAQcQGRfDQ4J9HaCjGaj3jlJKc66bnRk6G/IeLUsM7WKyG7Bzsuf1EZurSXyc0I4LVcu6HaeQQ4d3kZ16g==", "license": "MIT", "dependencies": { "reference-spec-reader": "^0.2.0", - "unzipit": "^1.4.3" + "unzipit": "1.4.3" } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2473,9 +2501,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2493,6 +2521,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2502,6 +2531,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2542,7 +2572,6 @@ "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" }, "node_modules/aria-query": { @@ -2706,9 +2735,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -2737,21 +2766,21 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", - "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" + "js-tokens": "^10.0.0" } }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -2766,9 +2795,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.22", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", - "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", "funding": [ { "type": "opencollective", @@ -2785,10 +2814,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.27.0", - "caniuse-lite": "^1.0.30001754", + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", - "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, @@ -2819,11 +2847,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -2847,12 +2878,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", - "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/binary-extensions": { @@ -2868,9 +2902,9 @@ } }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true, "license": "MIT" }, @@ -2878,6 +2912,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2886,15 +2921,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/brace-expansion/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==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2925,12 +2951,13 @@ } }, "node_modules/browser-resolve/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -3079,9 +3106,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -3098,11 +3125,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "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" }, "bin": { "browserslist": "cli.js" @@ -3161,15 +3188,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -3230,9 +3257,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", "funding": [ { "type": "opencollective", @@ -3325,9 +3352,9 @@ } }, "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", "engines": { @@ -3467,6 +3494,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3479,6 +3507,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/comma-separated-tokens": { @@ -3527,12 +3556,16 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/core-util-is": { @@ -3554,9 +3587,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -3734,9 +3767,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -3880,9 +3913,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -3944,12 +3977,13 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.255", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.255.tgz", - "integrity": "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ==", + "version": "1.5.336", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", + "integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==", "license": "ISC" }, "node_modules/elliptic": { @@ -3969,9 +4003,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -3979,6 +4013,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/entities": { @@ -3995,9 +4030,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -4077,35 +4112,34 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.2", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4201,25 +4235,25 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -4238,7 +4272,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4277,14 +4311,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4354,19 +4388,26 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", - "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { "eslint": ">=8.40" } }, + "node_modules/eslint-plugin-react/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4375,9 +4416,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "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": { @@ -4454,10 +4495,17 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4479,9 +4527,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "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": { @@ -4523,9 +4571,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4600,9 +4648,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4666,9 +4714,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -4777,6 +4825,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -4967,6 +5016,8 @@ "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", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4995,6 +5046,39 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -5047,17 +5131,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "dev": true, "license": "MIT", "engines": { @@ -5065,9 +5142,9 @@ } }, "node_modules/happy-dom": { - "version": "20.8.9", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.9.tgz", - "integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==", + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.9.0.tgz", + "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5606,6 +5683,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5973,6 +6051,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -6003,7 +6082,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6270,9 +6348,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -6285,23 +6363,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -6319,9 +6397,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -6339,9 +6417,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -6359,9 +6437,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -6379,9 +6457,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -6399,9 +6477,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -6419,9 +6497,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -6439,9 +6517,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -6459,9 +6537,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -6479,9 +6557,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -6499,9 +6577,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -6756,9 +6834,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7630,9 +7708,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -7661,25 +7739,27 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", - "integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==", - "license": "ISC", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -7691,29 +7771,29 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.2.tgz", - "integrity": "sha512-Fsr8AR5Yu6C0thoWa1Z8qGBFQLDvLsWlAn/v3CNLiUizoRqBYArK3Ex3thXpMWRr1Li5/MKLOEZ5mLygUmWi1A==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.13.3.tgz", + "integrity": "sha512-/F49bxavkNGfreMlrKmTxZs6YorjfMbbDLd89Q3pWi+cXGtQQNXXaHt4MkXN7li91xnQJ24HWXqW9QDm5id33w==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.40.0", + "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", - "@types/statuses": "^2.0.4", + "@types/statuses": "^2.0.6", "cookie": "^1.0.2", - "graphql": "^16.8.1", + "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", - "rettime": "^0.7.0", + "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", - "type-fest": "^4.26.1", + "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, @@ -7781,10 +7861,39 @@ "dev": true, "license": "MIT" }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", "license": "MIT" }, "node_modules/node-stdlib-browser": { @@ -7842,15 +7951,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-normalize-package-bin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", @@ -7898,13 +7998,43 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/npm-run-all2/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/npm-run-all2/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/npm-run-all2/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-all2/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", + "dependencies": { + "brace-expansion": "^2.0.2" + }, "engines": { - "node": ">=16" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm-run-all2/node_modules/which": { @@ -8159,6 +8289,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { @@ -8259,6 +8390,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -8275,6 +8407,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { @@ -8391,9 +8524,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "funding": [ { "type": "opencollective", @@ -8436,11 +8569,12 @@ } }, "node_modules/postcss-import/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -8577,9 +8711,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -8593,9 +8727,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -8704,9 +8838,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -8721,9 +8855,9 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8812,15 +8946,12 @@ } }, "node_modules/react-error-boundary": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", - "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.1.tgz", + "integrity": "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, "peerDependencies": { - "react": ">=16.13.1" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-hot-toast": { @@ -8841,9 +8972,9 @@ } }, "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", "license": "MIT", "peerDependencies": { "react": "*" @@ -8876,9 +9007,9 @@ } }, "node_modules/react-router": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", - "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -8898,12 +9029,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", - "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", "license": "MIT", "dependencies": { - "react-router": "7.12.0" + "react-router": "7.14.1" }, "engines": { "node": ">=20.0.0" @@ -8928,9 +9059,9 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz", - "integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -9087,19 +9218,25 @@ } }, "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9115,9 +9252,9 @@ } }, "node_modules/rettime": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", - "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.7.tgz", + "integrity": "sha512-DoAm1WjR1eH7z8sHPtvvUMIZh4/CSKkGCz6CxPqOrEAnOGtOuHSnSE9OC+razqxKuf4ub7pAYyl/vZV0vGs5tg==", "dev": true, "license": "MIT" }, @@ -9225,13 +9362,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.50.tgz", - "integrity": "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", + "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.97.0", - "@rolldown/pluginutils": "1.0.0-beta.50" + "@oxc-project/types": "=0.101.0", + "@rolldown/pluginutils": "1.0.0-beta.53" }, "bin": { "rolldown": "bin/cli.mjs" @@ -9240,26 +9377,25 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-beta.50", - "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", - "@rolldown/binding-darwin-x64": "1.0.0-beta.50", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", - "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" + "@rolldown/binding-android-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-x64": "1.0.0-beta.53", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz", - "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "license": "MIT" }, "node_modules/run-parallel": { @@ -9371,9 +9507,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -9534,14 +9670,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -9600,6 +9736,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -9710,6 +9847,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -9728,6 +9866,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9742,12 +9881,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9855,12 +9996,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -9874,6 +10016,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9886,6 +10029,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9941,17 +10085,17 @@ "license": "MIT" }, "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { @@ -9988,9 +10132,9 @@ } }, "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10004,11 +10148,24 @@ } }, "node_modules/tabbable": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", - "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.8.1.tgz", @@ -10016,9 +10173,9 @@ "license": "MIT" }, "node_modules/tailwindcss": { - "version": "3.4.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -10053,11 +10210,12 @@ } }, "node_modules/tailwindcss/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -10073,15 +10231,15 @@ } }, "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", - "minimatch": "^9.0.4" + "minimatch": "^10.2.2" }, "engines": { "node": ">=18" @@ -10136,13 +10294,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -10211,22 +10369,22 @@ } }, "node_modules/tldts": { - "version": "7.0.18", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.18.tgz", - "integrity": "sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==", + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", + "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.18" + "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.18", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.18.tgz", - "integrity": "sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q==", + "version": "7.0.28", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", + "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", "dev": true, "license": "MIT" }, @@ -10258,9 +10416,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -10271,9 +10429,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -10317,13 +10475,16 @@ } }, "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "dev": true, "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10421,16 +10582,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.47.0.tgz", - "integrity": "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==", + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", + "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.47.0", - "@typescript-eslint/parser": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0" + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10440,8 +10601,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/unbox-primitive": { @@ -10464,9 +10625,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "devOptional": true, "license": "MIT" }, @@ -10499,9 +10660,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "dev": true, "license": "MIT", "dependencies": { @@ -10552,9 +10713,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "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==", "funding": [ { "type": "opencollective", @@ -10649,17 +10810,18 @@ }, "node_modules/vite": { "name": "rolldown-vite", - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.5.tgz", - "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.3.1.tgz", + "integrity": "sha512-LYzdNAjRHhF2yA4JUQm/QyARyi216N2rpJ0lJZb8E9FU2y5v6Vk+xq/U4XBOxMefpWixT5H3TslmAHm1rqIq2w==", + "deprecated": "Use this package to migrate from Vite 7 to Vite 8. For the most recent updates, migrate to Vite 8 once you're ready.", "license": "MIT", "dependencies": { - "@oxc-project/runtime": "0.97.0", + "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rolldown": "1.0.0-beta.50", + "rolldown": "1.0.0-beta.53", "tinyglobby": "^0.2.15" }, "bin": { @@ -10676,7 +10838,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -10978,9 +11140,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -11046,6 +11208,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -11063,12 +11226,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11083,6 +11248,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11274,6 +11440,15 @@ "numcodecs": "^0.3.2" } }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 62ff71dc9..4cb6f1919 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,15 +30,18 @@ "test": "vitest" }, "dependencies": { + "@bioimagetools/capability-manifest": "0.5.0", "@material-tailwind/react": "^3.0.0-beta.24", "@tanstack/react-hotkeys": "^0.4.1", "@tanstack/react-query": "^5.90.2", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.23", + "@types/js-yaml": "^4.0.9", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", "fracturedjsonjs": "^5.0.1", + "js-yaml": "^4.1.1", "loglevel": "^1.9.2", "npm-run-all2": "^7.0.2", "ome-zarr.js": "^0.0.17", @@ -55,7 +58,8 @@ "react-syntax-highlighter": "^16.1.0", "shepherd.js": "^14.5.1", "tailwindcss": "^3.4.17", - "zarrita": "^0.5.1" + "zarrita": "^0.5.1", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/css": "^0.8.1", diff --git a/frontend/src/__tests__/componentTests/DataToolLinks.test.tsx b/frontend/src/__tests__/componentTests/DataToolLinks.test.tsx new file mode 100644 index 000000000..82610c34b --- /dev/null +++ b/frontend/src/__tests__/componentTests/DataToolLinks.test.tsx @@ -0,0 +1,410 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { waitFor } from '@testing-library/react'; +import { render, screen } from '@/__tests__/test-utils'; +import DataToolLinks from '@/components/ui/BrowsePage/DataToolLinks'; +import type { OpenWithToolUrls, PendingToolKey } from '@/hooks/useZarrMetadata'; +import { ViewersProvider } from '@/contexts/ViewersContext'; + +// Mock logger to capture console warnings +const mockLogger = vi.hoisted(() => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn() +})); + +vi.mock('@/logger', () => ({ + default: mockLogger +})); + +// Mock capability manifest to avoid network requests in tests +const mockCapabilityManifest = vi.hoisted(() => ({ + loadManifestsFromUrls: vi.fn(), + validateViewer: vi.fn(), + getLogoUrl: vi.fn( + (manifest: { viewer: { name: string } }) => + `https://icons.example.com/${manifest.viewer.name.toLowerCase().replace(/\s+/g, '-')}.png` + ) +})); + +vi.mock('@bioimagetools/capability-manifest', () => mockCapabilityManifest); + +const mockOpenWithToolUrls: OpenWithToolUrls = { + copy: 'http://localhost:3000/test/copy/url', + neuroglancer: 'http://localhost:3000/test/neuroglancer/url', + avivator: 'http://localhost:3000/test/avivator/url' +}; + +// Helper component to wrap DataToolLinks with ViewersProvider +function TestDataToolLinksComponent({ + urls = mockOpenWithToolUrls, + onToolClick = vi.fn() +}: { + urls?: OpenWithToolUrls | null; + onToolClick?: (toolKey: PendingToolKey) => Promise; +}) { + return ( + + + + ); +} + +// Wrapper function for rendering with proper route context +function renderDataToolLinks( + urls?: OpenWithToolUrls | null, + onToolClick?: (toolKey: PendingToolKey) => Promise +) { + return render( + , + { initialEntries: ['/browse/test_fsp/test_file'] } + ); +} + +describe('DataToolLinks - Error Scenarios', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Default mock: return empty Map (no manifests loaded) + mockCapabilityManifest.loadManifestsFromUrls.mockResolvedValue(new Map()); + mockCapabilityManifest.validateViewer.mockReturnValue({ + dataCompatible: false, + dataFeaturesSupported: false, + errors: [ + { + capability: 'test', + message: 'Not compatible', + required: null, + found: null + } + ], + warnings: [] + }); + }); + + describe('Invalid YAML syntax', () => { + it('should log error when YAML parsing fails in ViewersContext', async () => { + // This test verifies that the ViewersContext logs errors appropriately + // The actual YAML parsing error is tested in the ViewersContext initialization + + // Import the parseViewersConfig function to test it directly + const { parseViewersConfig } = await import('@/config/viewersConfig'); + + const invalidYaml = 'viewers:\n - manifest_url: test\n invalid: [[['; + + expect(() => parseViewersConfig(invalidYaml)).toThrow( + /Failed to parse viewers configuration YAML/ + ); + }); + + it('should still render when ViewersContext fails to initialize', async () => { + // When ViewersContext fails to initialize, it sets error state + // and logs to console. The component should still render but with empty viewers. + renderDataToolLinks(); + + await waitFor( + () => { + // The component should still be initialized (to prevent hanging) + // but viewers may be empty if there was an error + expect(screen.getByText('Test Tools')).toBeInTheDocument(); + }, + { timeout: 3000 } + ); + }); + }); + + describe('Missing required fields', () => { + it('should throw error when viewer lacks required manifest_url field', async () => { + const { parseViewersConfig } = await import('@/config/viewersConfig'); + + const configMissingManifestUrl = ` +viewers: + - label: Custom Label + # Missing manifest_url +`; + + expect(() => parseViewersConfig(configMissingManifestUrl)).toThrow( + /Each viewer must have a "manifest_url" field/ + ); + }); + + it('should throw error when viewers array is empty', async () => { + const { parseViewersConfig } = await import('@/config/viewersConfig'); + + const configEmptyViewers = ` +viewers: [] +`; + + expect(() => parseViewersConfig(configEmptyViewers)).toThrow( + /"viewers" must contain at least one viewer/ + ); + }); + }); +}); + +describe('DataToolLinks - Edge Cases', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Mock loadManifestsFromUrls to return manifests keyed by the URLs it receives. + // This decouples the test from the exact manifest_url values in viewers.config.yaml. + const manifestsByName: Record = { + neuroglancer: { + viewer: { + name: 'Neuroglancer', + template_url: 'https://neuroglancer.com/#!{DATA_URL}' + } + }, + avivator: { + viewer: { + name: 'Avivator', + template_url: 'https://avivator.com/?url={DATA_URL}' + } + } + }; + + mockCapabilityManifest.loadManifestsFromUrls.mockImplementation( + async (urls: string[]) => { + const map = new Map(); + for (const url of urls) { + for (const [name, manifest] of Object.entries(manifestsByName)) { + if (url.includes(name)) { + map.set(url, manifest); + break; + } + } + } + return map; + } + ); + + // Mock validateViewer to return compatible for all viewers + mockCapabilityManifest.validateViewer.mockReturnValue({ + dataCompatible: true, + dataFeaturesSupported: true, + errors: [], + warnings: [] + }); + }); + + describe('Logo rendering in components', () => { + it('should render viewer logos in component', async () => { + // Test that viewers with known logo files render correctly in the component + renderDataToolLinks(); + + // Wait for viewer logos to load (async: config query + manifest loading) + await waitFor( + () => { + const images = screen.getAllByRole('img'); + + // Check for neuroglancer logo (known viewer with logo) + const neuroglancerLogo = images.find( + img => img.getAttribute('alt') === 'View in Neuroglancer' + ); + expect(neuroglancerLogo).toBeTruthy(); + expect(neuroglancerLogo?.getAttribute('src')).toContain( + 'neuroglancer' + ); + + // Check for avivator logo (name for viewer in avivator.yaml) + const vizarrLogo = images.find( + img => img.getAttribute('alt') === 'View in Avivator' + ); + expect(vizarrLogo).toBeTruthy(); + expect(vizarrLogo?.getAttribute('src')).toContain('avivator'); + }, + { timeout: 3000 } + ); + }); + }); + + describe('Custom viewer compatibility', () => { + it('should exclude viewer URL when set to null in OpenWithToolUrls', async () => { + const urls: OpenWithToolUrls = { + copy: 'http://localhost:3000/copy', + neuroglancer: 'http://localhost:3000/neuroglancer', + customviewer: null // Custom viewer not compatible (explicitly null) + }; + + renderDataToolLinks(urls); + + // Wait for viewer logos to load asynchronously + await waitFor( + () => { + const images = screen.getAllByRole('img'); + const neuroglancerLogo = images.find( + img => img.getAttribute('alt') === 'View in Neuroglancer' + ); + expect(neuroglancerLogo).toBeTruthy(); + }, + { timeout: 3000 } + ); + + // Copy button is an SVG icon, find by aria-label + const copyButton = screen.getByLabelText('Copy data URL'); + expect(copyButton).toBeInTheDocument(); + }); + }); + + describe('Component behavior with null urls', () => { + it('should render nothing when urls is null', () => { + renderDataToolLinks(null); + + // Component should not render when urls is null + expect(screen.queryByText('Test Tools')).not.toBeInTheDocument(); + }); + }); +}); + +describe('DataToolLinks - Expected Behavior', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Mock loadManifestsFromUrls to return manifests keyed by the URLs it receives. + // This decouples the test from the exact manifest_url values in viewers.config.yaml. + const manifestsByName: Record = { + neuroglancer: { + viewer: { + name: 'Neuroglancer', + template_url: 'https://neuroglancer.com/#!{DATA_URL}' + } + }, + avivator: { + viewer: { + name: 'Avivator', + template_url: 'https://avivator.com/?url={DATA_URL}' + } + } + }; + + mockCapabilityManifest.loadManifestsFromUrls.mockImplementation( + async (urls: string[]) => { + const map = new Map(); + for (const url of urls) { + for (const [name, manifest] of Object.entries(manifestsByName)) { + if (url.includes(name)) { + map.set(url, manifest); + break; + } + } + } + return map; + } + ); + + // Mock validateViewer to return compatible for all viewers + mockCapabilityManifest.validateViewer.mockReturnValue({ + dataCompatible: true, + dataFeaturesSupported: true, + errors: [], + warnings: [] + }); + }); + + describe('Component behavior with valid viewers', () => { + it('should render valid viewer icons and copy icon', async () => { + renderDataToolLinks(); + + // Wait for viewer logos to load asynchronously + await waitFor( + () => { + const images = screen.getAllByRole('img'); + expect(images.length).toBeGreaterThanOrEqual(1); + }, + { timeout: 3000 } + ); + + // Copy button is an SVG icon, find by aria-label + const copyButton = screen.getByLabelText('Copy data URL'); + expect(copyButton).toBeInTheDocument(); + }); + + it('should call onToolClick when copy icon is clicked', async () => { + const onToolClick = vi.fn(async () => {}); + renderDataToolLinks(undefined, onToolClick); + + await waitFor( + () => { + expect(screen.getByText('Test Tools')).toBeInTheDocument(); + }, + { timeout: 3000 } + ); + + // Click the copy button (SVG icon, not img — find by aria-label) + const copyButton = screen.getByLabelText('Copy data URL'); + expect(copyButton).toBeInTheDocument(); + + copyButton.click(); + + await waitFor(() => { + expect(onToolClick).toHaveBeenCalledWith('copy'); + }); + }); + + it('should render multiple viewer logos when URLs are provided', async () => { + renderDataToolLinks(); + + // Wait for viewer logos to load asynchronously + await waitFor( + () => { + const images = screen.getAllByRole('img'); + expect(images.length).toBeGreaterThanOrEqual(2); + + const neuroglancerLogo = images.find( + img => img.getAttribute('alt') === 'View in Neuroglancer' + ); + const vizarrLogo = images.find( + img => img.getAttribute('alt') === 'View in Avivator' + ); + + expect(neuroglancerLogo).toBeTruthy(); + expect(vizarrLogo).toBeTruthy(); + }, + { timeout: 3000 } + ); + + // Copy icon is an SVG, not an img — verify separately + const copyButton = screen.getByLabelText('Copy data URL'); + expect(copyButton).toBeInTheDocument(); + }); + }); + + describe('Tooltip behavior', () => { + it('should show "Copy data URL" tooltip by default', async () => { + renderDataToolLinks(); + + await waitFor( + () => { + expect(screen.getByText('Test Tools')).toBeInTheDocument(); + }, + { timeout: 3000 } + ); + + // The copy button should have the correct aria-label + const copyButton = screen.getByLabelText('Copy data URL'); + expect(copyButton).toBeInTheDocument(); + }); + + it('should show viewer tooltip labels', async () => { + renderDataToolLinks(); + + // Wait for viewer logos to load asynchronously + await waitFor( + () => { + const neuroglancerButton = screen.getByAltText( + 'View in Neuroglancer' + ); + expect(neuroglancerButton).toBeInTheDocument(); + }, + { timeout: 3000 } + ); + + const vizarrButton = screen.getByAltText('View in Avivator'); + expect(vizarrButton).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/__tests__/componentTests/ZarrMetadataTable.test.tsx b/frontend/src/__tests__/componentTests/ZarrMetadataTable.test.tsx index 4a5791843..f78f8d759 100644 --- a/frontend/src/__tests__/componentTests/ZarrMetadataTable.test.tsx +++ b/frontend/src/__tests__/componentTests/ZarrMetadataTable.test.tsx @@ -26,7 +26,8 @@ vi.mock('@/hooks/useZarrMetadata', async () => { // Test component that uses the actual useZarrMetadata hook function ZarrMetadataTableTestWrapper() { - const { availableVersions, layerType, zarrMetadataQuery } = useZarrMetadata(); + const { availableZarrVersions, layerType, zarrMetadataQuery } = + useZarrMetadata(); // Don't render until we have metadata if (!zarrMetadataQuery.data?.metadata) { @@ -35,7 +36,7 @@ function ZarrMetadataTableTestWrapper() { return ( @@ -43,14 +44,15 @@ function ZarrMetadataTableTestWrapper() { } describe('ZarrMetadataTable', () => { - it('should display "v2, v3" when both versions are available', async () => { + it('should display "3, 2" when both zarr versions are available', async () => { render(, { initialEntries: ['/browse/test_fsp/my_folder/ome_zarr_both_versions'] }); // Wait for the metadata table to render with version info + // zarr.json (v3) is checked first, then .zattrs (v2), so order is 3, 2 await waitFor(() => { - expect(screen.getByText('v2, v3')).toBeInTheDocument(); + expect(screen.getByText('3, 2')).toBeInTheDocument(); }); }); diff --git a/frontend/src/__tests__/mocks/handlers.ts b/frontend/src/__tests__/mocks/handlers.ts index 609be7049..926928f27 100644 --- a/frontend/src/__tests__/mocks/handlers.ts +++ b/frontend/src/__tests__/mocks/handlers.ts @@ -281,6 +281,11 @@ export const handlers = [ return HttpResponse.json({ authenticated: true }); }), + // Viewers config - 404 means no runtime config, fall through to bundled default + http.get('/api/viewers-config', () => { + return HttpResponse.json(null, { status: 404 }); + }), + // File content for Zarr metadata files http.get('/api/content/:fspName', ({ params, request }) => { const url = new URL(request.url); diff --git a/frontend/src/__tests__/mocks/viewers.config.yaml b/frontend/src/__tests__/mocks/viewers.config.yaml new file mode 100644 index 000000000..7062d5f69 --- /dev/null +++ b/frontend/src/__tests__/mocks/viewers.config.yaml @@ -0,0 +1,4 @@ +# Test fixture: mock viewers.config.yaml used to verify the override mechanism. +# This file simulates a custom frontend/viewers.config.yaml provided by the user. +viewers: + - manifest_url: 'https://example.com/mock-viewer.yaml' diff --git a/frontend/src/__tests__/test-utils.tsx b/frontend/src/__tests__/test-utils.tsx index a54d0a43d..f3bcce0c6 100644 --- a/frontend/src/__tests__/test-utils.tsx +++ b/frontend/src/__tests__/test-utils.tsx @@ -15,6 +15,7 @@ import { TicketProvider } from '@/contexts/TicketsContext'; import { ProfileContextProvider } from '@/contexts/ProfileContext'; import { ExternalBucketProvider } from '@/contexts/ExternalBucketContext'; import { ServerHealthProvider } from '@/contexts/ServerHealthContext'; +import { ViewersProvider } from '@/contexts/ViewersContext'; import ErrorFallback from '@/components/ErrorFallback'; interface CustomRenderOptions extends Omit { @@ -40,13 +41,15 @@ const Browse = ({ children }: { children: ReactNode }) => { - - - - {children} - - - + + + + + {children} + + + + diff --git a/frontend/src/__tests__/unitTests/viewersConfig.test.ts b/frontend/src/__tests__/unitTests/viewersConfig.test.ts new file mode 100644 index 000000000..e0f137c1e --- /dev/null +++ b/frontend/src/__tests__/unitTests/viewersConfig.test.ts @@ -0,0 +1,361 @@ +import { describe, it, expect } from 'vitest'; +import { parseViewersConfig } from '@/config/viewersConfig'; + +describe('parseViewersConfig', () => { + describe('Valid configurations', () => { + it('should parse config with single manifest_url viewer', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/neuroglancer.yaml +`; + const result = parseViewersConfig(yaml); + + expect(result.viewers).toHaveLength(1); + expect(result.viewers[0].manifest_url).toBe( + 'https://example.com/neuroglancer.yaml' + ); + expect(result.viewers[0].instance_template_url).toBeUndefined(); + expect(result.viewers[0].label).toBeUndefined(); + }); + + it('should parse config with multiple viewers', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/neuroglancer.yaml + - manifest_url: https://example.com/avivator.yaml + - manifest_url: https://example.com/validator.yaml +`; + const result = parseViewersConfig(yaml); + + expect(result.viewers).toHaveLength(3); + expect(result.viewers[0].manifest_url).toBe( + 'https://example.com/neuroglancer.yaml' + ); + expect(result.viewers[1].manifest_url).toBe( + 'https://example.com/avivator.yaml' + ); + expect(result.viewers[2].manifest_url).toBe( + 'https://example.com/validator.yaml' + ); + }); + + it('should parse config with all optional fields', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/viewer.yaml + instance_template_url: https://example.com/viewer?url={dataLink} + label: Custom Viewer Label +`; + const result = parseViewersConfig(yaml); + + expect(result.viewers).toHaveLength(1); + expect(result.viewers[0].manifest_url).toBe( + 'https://example.com/viewer.yaml' + ); + expect(result.viewers[0].instance_template_url).toBe( + 'https://example.com/viewer?url={dataLink}' + ); + expect(result.viewers[0].label).toBe('Custom Viewer Label'); + }); + + it('should parse config with manifest_url only (no optional fields)', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/simple-viewer.yaml +`; + const result = parseViewersConfig(yaml); + + expect(result.viewers).toHaveLength(1); + expect(result.viewers[0]).toEqual({ + manifest_url: 'https://example.com/simple-viewer.yaml' + }); + }); + }); + + describe('Invalid YAML syntax', () => { + it('should throw error for malformed YAML', () => { + const invalidYaml = 'viewers:\n - manifest_url: test\n invalid: [[['; + + expect(() => parseViewersConfig(invalidYaml)).toThrow( + /Failed to parse viewers configuration YAML/ + ); + }); + + it('should throw error for non-object YAML (string)', () => { + const invalidYaml = 'just a string'; + + expect(() => parseViewersConfig(invalidYaml)).toThrow( + /Configuration must have a "viewers" field/ + ); + }); + + it('should throw error for non-object YAML (number)', () => { + const invalidYaml = '123'; + + expect(() => parseViewersConfig(invalidYaml)).toThrow( + /Configuration must have a "viewers" field/ + ); + }); + + it('should throw error for non-object YAML (array)', () => { + const invalidYaml = '[1, 2, 3]'; + + expect(() => parseViewersConfig(invalidYaml)).toThrow( + /Configuration must have a "viewers" field/ + ); + }); + + it('should throw error for empty YAML', () => { + const invalidYaml = ''; + + expect(() => parseViewersConfig(invalidYaml)).toThrow( + /Configuration must have a "viewers" field/ + ); + }); + + it('should throw error for null YAML', () => { + const invalidYaml = 'null'; + + expect(() => parseViewersConfig(invalidYaml)).toThrow( + /Configuration must have a "viewers" field/ + ); + }); + }); + + describe('Missing required fields', () => { + it('should throw error when viewers array is missing', () => { + const yaml = ` +name: some-config +other_field: value +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /Configuration must have a "viewers" field containing an array/ + ); + }); + + it('should throw error when viewer is missing manifest_url', () => { + const yaml = ` +viewers: + - label: Custom Label +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /Each viewer must have a "manifest_url" field/ + ); + }); + + it('should throw error when viewers array is empty', () => { + const yaml = ` +viewers: [] +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /"viewers" must contain at least one viewer/ + ); + }); + }); + + describe('Invalid field types', () => { + it('should throw error when manifest_url is not a string', () => { + const yaml = ` +viewers: + - manifest_url: 123 +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /Each viewer must have a "manifest_url" field/ + ); + }); + + it('should throw error when manifest_url is not a valid URL or absolute path', () => { + const yaml = ` +viewers: + - manifest_url: not-a-valid-url +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /"manifest_url" must be a valid URL or an absolute path starting with \// + ); + }); + + it('should throw error when label is not a string', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/viewer.yaml + label: 123 +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /"label" must be a string/ + ); + }); + + it('should throw error when instance_template_url is not a string', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/viewer.yaml + instance_template_url: 123 +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /"instance_template_url" must be a string/ + ); + }); + + it('should throw error when viewer entry is not an object (string in array)', () => { + const yaml = ` +viewers: + - just-a-string +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /Each viewer must be an object with a "manifest_url" field/ + ); + }); + + it('should throw error when viewer entry is not an object (number in array)', () => { + const yaml = ` +viewers: + - 123 +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /Each viewer must be an object with a "manifest_url" field/ + ); + }); + + it('should throw error when viewers is not an array', () => { + const yaml = ` +viewers: not-an-array +`; + + expect(() => parseViewersConfig(yaml)).toThrow( + /Configuration must have a "viewers" field containing an array/ + ); + }); + }); + + describe('Edge cases', () => { + it('should handle single viewer with only manifest_url', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/viewer.yaml +`; + + const result = parseViewersConfig(yaml); + + expect(result.viewers).toHaveLength(1); + expect(result.viewers[0]).toEqual({ + manifest_url: 'https://example.com/viewer.yaml' + }); + }); + + it('should preserve all valid optional fields in output', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/viewer.yaml + instance_template_url: https://example.com/viewer?url={dataLink} + label: Custom Label +`; + + const result = parseViewersConfig(yaml); + + expect(result.viewers[0]).toEqual({ + manifest_url: 'https://example.com/viewer.yaml', + instance_template_url: 'https://example.com/viewer?url={dataLink}', + label: 'Custom Label' + }); + }); + + it('should strip/ignore unknown fields', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/viewer.yaml + unknown_field: some-value + another_unknown: 123 +`; + + const result = parseViewersConfig(yaml); + + // Zod should strip unknown fields + expect(result.viewers[0]).toEqual({ + manifest_url: 'https://example.com/viewer.yaml' + }); + expect(result.viewers[0]).not.toHaveProperty('unknown_field'); + expect(result.viewers[0]).not.toHaveProperty('another_unknown'); + }); + + it('should accept http and https URLs', () => { + const yaml = ` +viewers: + - manifest_url: http://example.com/viewer.yaml + - manifest_url: https://example.com/viewer.yaml +`; + + const result = parseViewersConfig(yaml); + + expect(result.viewers).toHaveLength(2); + expect(result.viewers[0].manifest_url).toBe( + 'http://example.com/viewer.yaml' + ); + expect(result.viewers[1].manifest_url).toBe( + 'https://example.com/viewer.yaml' + ); + }); + + it('should accept absolute paths starting with /', () => { + const yaml = ` +viewers: + - manifest_url: /viewers/neuroglancer.yaml + - manifest_url: /viewers/avivator.yaml +`; + + const result = parseViewersConfig(yaml); + + expect(result.viewers).toHaveLength(2); + expect(result.viewers[0].manifest_url).toBe('/viewers/neuroglancer.yaml'); + expect(result.viewers[1].manifest_url).toBe('/viewers/avivator.yaml'); + }); + + it('should handle URL with special characters', () => { + const yaml = ` +viewers: + - manifest_url: https://example.com/viewer-config_v2.yaml?version=1.0&format=yaml +`; + + const result = parseViewersConfig(yaml); + + expect(result.viewers[0].manifest_url).toBe( + 'https://example.com/viewer-config_v2.yaml?version=1.0&format=yaml' + ); + }); + + it('should handle empty optional strings', () => { + // Empty label is allowed (it's a display string) + const yamlValid = ` +viewers: + - manifest_url: https://example.com/viewer.yaml + label: "" +`; + + const result = parseViewersConfig(yamlValid); + + expect(result.viewers[0]).toEqual({ + manifest_url: 'https://example.com/viewer.yaml', + label: '' + }); + + // Empty instance_template_url is rejected (must be a valid URL or path) + const yamlInvalid = ` +viewers: + - manifest_url: https://example.com/viewer.yaml + instance_template_url: "" +`; + + expect(() => parseViewersConfig(yamlInvalid)).toThrow( + /"instance_template_url" must be a valid URL or an absolute path starting with \// + ); + }); + }); +}); diff --git a/frontend/src/__tests__/unitTests/viewersConfigOverride.test.ts b/frontend/src/__tests__/unitTests/viewersConfigOverride.test.ts new file mode 100644 index 000000000..9fde5168d --- /dev/null +++ b/frontend/src/__tests__/unitTests/viewersConfigOverride.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import path from 'path'; +import { resolveViewersConfigPath } from '@/config/resolveViewersConfigPath'; + +// Use the mocks directory as a stand-in for the frontend root in override tests. +// It contains a viewers.config.yaml fixture, so existsSync returns true there — +// exactly as it would for a real user-provided override at frontend/viewers.config.yaml. +const mocksDir = path.resolve(process.cwd(), 'src/__tests__/mocks'); + +// A directory guaranteed not to contain viewers.config.yaml, to test the fallback. +const noOverrideDir = path.resolve(process.cwd(), 'src/__tests__/unitTests'); + +describe('resolveViewersConfigPath', () => { + it('returns the override path when viewers.config.yaml exists in the frontend root', () => { + const result = resolveViewersConfigPath(mocksDir); + expect(result).toBe(path.resolve(mocksDir, 'viewers.config.yaml')); + }); + + it('returns the default config path when no override file is present', () => { + const result = resolveViewersConfigPath(noOverrideDir); + expect(result).toBe( + path.resolve(noOverrideDir, 'src/config/viewers.config.yaml') + ); + }); +}); diff --git a/frontend/src/__tests__/unitTests/zarrVersionDetection.test.ts b/frontend/src/__tests__/unitTests/zarrVersionDetection.test.ts index 1468039b1..5918917ba 100644 --- a/frontend/src/__tests__/unitTests/zarrVersionDetection.test.ts +++ b/frontend/src/__tests__/unitTests/zarrVersionDetection.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { detectZarrVersions } from '@/queries/zarrQueries'; +import { + areZarrMetadataFilesPresent, + getOmeNgffVersion, + getEffectiveZarrStorageVersion +} from '@/queries/zarrQueries'; import { FileOrFolder } from '@/shared.types'; // Helper to create minimal FileOrFolder objects for testing @@ -14,36 +18,106 @@ const createFile = (name: string): FileOrFolder => ({ last_modified: Date.now() }); -describe('detectZarrVersions', () => { - it('should detect only zarr v3 when only zarr.json exists', () => { +describe('areZarrMetadataFilesPresent', () => { + it('should return true when zarr.json exists', () => { const files = [ createFile('zarr.json'), createFile('arrays/data/chunk_key_1') ]; - const result = detectZarrVersions(files); - expect(result).toEqual(['v3']); + expect(areZarrMetadataFilesPresent(files)).toBe(true); }); - it('should detect only zarr v2 when only .zarray exists', () => { + it('should return true when .zarray exists', () => { const files = [createFile('.zarray'), createFile('.zattrs')]; - const result = detectZarrVersions(files); - expect(result).toEqual(['v2']); + expect(areZarrMetadataFilesPresent(files)).toBe(true); }); - it('should detect both versions when both zarr.json and .zarray exist', () => { + it('should return true when .zattrs exists', () => { + const files = [createFile('.zattrs')]; + expect(areZarrMetadataFilesPresent(files)).toBe(true); + }); + + it('should return true when both zarr.json and .zarray exist', () => { const files = [ createFile('zarr.json'), createFile('.zarray'), createFile('.zattrs'), createFile('arrays/data/chunk_key_1') ]; - const result = detectZarrVersions(files); - expect(result).toEqual(['v2', 'v3']); + expect(areZarrMetadataFilesPresent(files)).toBe(true); }); - it('should return empty array when neither version files exist', () => { + it('should return false when no zarr metadata files exist', () => { const files = [createFile('file.txt'), createFile('other.json')]; - const result = detectZarrVersions(files); - expect(result).toEqual([]); + expect(areZarrMetadataFilesPresent(files)).toBe(false); + }); + + it('should return false for empty file list', () => { + expect(areZarrMetadataFilesPresent([])).toBe(false); + }); +}); + +describe('getOmeNgffVersion', () => { + it('should return version from attributes.ome.version', () => { + const data = { attributes: { ome: { version: '0.5' } } }; + expect(getOmeNgffVersion(data)).toBe('0.5'); + }); + + it('should return version from ome.version', () => { + const data = { ome: { version: '0.5' } }; + expect(getOmeNgffVersion(data)).toBe('0.5'); + }); + + it('should return version from top-level version', () => { + const data = { version: '0.3' }; + expect(getOmeNgffVersion(data)).toBe('0.3'); + }); + + it('should return version from multiscales[0].version', () => { + const data = { multiscales: [{ version: '0.4' }] }; + expect(getOmeNgffVersion(data)).toBe('0.4'); + }); + + it('should return version from plate.version', () => { + const data = { plate: { version: '0.4' } }; + expect(getOmeNgffVersion(data)).toBe('0.4'); + }); + + it('should return version from well.version', () => { + const data = { well: { version: '0.4' } }; + expect(getOmeNgffVersion(data)).toBe('0.4'); + }); + + it('should return 0.4 when no version is found anywhere', () => { + const data = { someOtherField: 'value' }; + expect(getOmeNgffVersion(data)).toBe('0.4'); + }); + + it('should strip pre-release suffix from version', () => { + const data = { attributes: { ome: { version: '0.5-dev2' } } }; + expect(getOmeNgffVersion(data)).toBe('0.5'); + }); + + it('should return 0.4 when attributes.ome exists but has no version', () => { + const data = { attributes: { ome: { multiscales: [] } } }; + expect(getOmeNgffVersion(data)).toBe('0.4'); + }); +}); + +describe('getEffectiveZarrStorageVersion', () => { + it('should return 3 when only v3 is available', () => { + expect(getEffectiveZarrStorageVersion([3])).toBe(3); + }); + + it('should return 2 when only v2 is available', () => { + expect(getEffectiveZarrStorageVersion([2])).toBe(2); + }); + + it('should prefer v3 when both v2 and v3 are available', () => { + expect(getEffectiveZarrStorageVersion([2, 3])).toBe(3); + }); + + it('should return 2 when no versions are available', () => { + expect(getEffectiveZarrStorageVersion([])).toBe(2); }); }); diff --git a/frontend/src/assets/aics_website-3d-cell-viewer.png b/frontend/src/assets/aics_website-3d-cell-viewer.png deleted file mode 100644 index e5e00744c..000000000 Binary files a/frontend/src/assets/aics_website-3d-cell-viewer.png and /dev/null differ diff --git a/frontend/src/assets/fallback_logo.png b/frontend/src/assets/fallback_logo.png new file mode 100644 index 000000000..da5fcc42a Binary files /dev/null and b/frontend/src/assets/fallback_logo.png differ diff --git a/frontend/src/assets/neuroglancer.png b/frontend/src/assets/neuroglancer.png deleted file mode 100644 index bb39db661..000000000 Binary files a/frontend/src/assets/neuroglancer.png and /dev/null differ diff --git a/frontend/src/assets/ome-ngff-validator.png b/frontend/src/assets/ome-ngff-validator.png deleted file mode 100644 index b96a53096..000000000 Binary files a/frontend/src/assets/ome-ngff-validator.png and /dev/null differ diff --git a/frontend/src/assets/vizarr_logo.png b/frontend/src/assets/vizarr_logo.png deleted file mode 100644 index 2d07b8cfc..000000000 Binary files a/frontend/src/assets/vizarr_logo.png and /dev/null differ diff --git a/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx b/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx index 18f745c23..7598bf688 100644 --- a/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx +++ b/frontend/src/components/ui/BrowsePage/DataToolLinks.tsx @@ -1,16 +1,14 @@ import { Typography } from '@material-tailwind/react'; import { Link } from 'react-router'; +import fallback_logo from '@/assets/fallback_logo.png'; import { HiOutlineClipboardCopy } from 'react-icons/hi'; import { HiOutlineEllipsisHorizontalCircle } from 'react-icons/hi2'; -import neuroglancer_logo from '@/assets/neuroglancer.png'; -import validator_logo from '@/assets/ome-ngff-validator.png'; -import volE_logo from '@/assets/aics_website-3d-cell-viewer.png'; -import avivator_logo from '@/assets/vizarr_logo.png'; import type { OpenWithToolUrls, PendingToolKey } from '@/hooks/useZarrMetadata'; import FgTooltip from '@/components/ui/widgets/FgTooltip'; import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn'; import DataLinkUsageDialog from '@/components/ui/Dialogs/dataLinkUsage/DataLinkUsageDialog'; +import { useViewersContext } from '@/contexts/ViewersContext'; const CIRCLE_CLASSES = 'rounded-full bg-surface-light dark:bg-primary/15 hover:bg-surface dark:hover:bg-primary/25 w-12 h-12 flex items-center justify-center cursor-pointer transform active:scale-90 transition-all duration-75'; @@ -49,6 +47,9 @@ function ToolLink({ {logoAlt} { + e.currentTarget.src = fallback_logo; + }} src={logoSrc} /> @@ -77,6 +78,8 @@ export default function DataToolLinks({ readonly title: string; readonly urls: OpenWithToolUrls | null; }) { + const { validViewers } = useViewersContext(); + if (!urls) { return null; } @@ -89,53 +92,27 @@ export default function DataToolLinks({
- {urls.neuroglancer !== null ? ( - - ) : null} + {validViewers.map(viewer => { + const url = urls[viewer.key]; - {urls.vole !== null ? ( - - ) : null} + // null means incompatible, don't show + if (url === null || url === undefined) { + return null; + } - {urls.avivator !== null ? ( - - ) : null} - - {urls.validator !== null ? ( - - ) : null} + return ( + + ); + })}
0; + const isZarrDir = areZarrMetadataFilesPresent(files); const isN5Dir = detectN5(files); const hasZarrExt = currentName.endsWith('.zarr'); @@ -95,8 +95,6 @@ export default function FileBrowser({ const { hasAttributesJson, hasS0Folder } = getN5DetectionSignals(files); - const isN5Ext = hasN5Ext; - // 1st case Zarr hint req'd: query fired, no error, but no metadata returned (indicating a Zarr without expected metadata structure) const isZarrNullMetadata = isZarrDir && @@ -128,9 +126,9 @@ export default function FileBrowser({ const propertiesTarget = fileBrowserState.propertiesTarget; const isFavorite = Boolean( fspName && - folderPreferenceMap[ - makeMapKey('folder', `${fspName}_${propertiesTarget.path}`) - ] + folderPreferenceMap[ + makeMapKey('folder', `${fspName}_${propertiesTarget.path}`) + ] ); return [ @@ -233,7 +231,7 @@ export default function FileBrowser({ /> ) : zarrMetadataQuery.data?.metadata ? ( - ) : isN5Ext && !hasAttributesJson && hasS0Folder ? ( + ) : hasN5Ext && !hasAttributesJson && hasS0Folder ? ( - ) : isN5Ext && hasAttributesJson && !hasS0Folder ? ( + ) : hasN5Ext && hasAttributesJson && !hasS0Folder ? ( - ) : isN5Ext && !hasAttributesJson && !hasS0Folder ? ( + ) : hasN5Ext && !hasAttributesJson && !hasS0Folder ? ( ) : null ) : null} diff --git a/frontend/src/components/ui/BrowsePage/ZarrMetadataTable.tsx b/frontend/src/components/ui/BrowsePage/ZarrMetadataTable.tsx index 86f4fdf3d..6edbf6c13 100644 --- a/frontend/src/components/ui/BrowsePage/ZarrMetadataTable.tsx +++ b/frontend/src/components/ui/BrowsePage/ZarrMetadataTable.tsx @@ -1,6 +1,7 @@ import * as zarr from 'zarrita'; import { Axis } from 'ome-zarr.js'; import { HiQuestionMarkCircle } from 'react-icons/hi'; +import { default as log } from '@/logger'; import { usePreferencesContext } from '@/contexts/PreferencesContext'; import { @@ -13,7 +14,7 @@ import FgTooltip from '@/components/ui/widgets/FgTooltip'; type ZarrMetadataTableProps = { readonly metadata: Metadata; readonly layerType: 'auto' | 'image' | 'segmentation' | null; - readonly availableVersions?: ('v2' | 'v3')[]; + readonly availableZarrVersions?: number[]; }; function getSizeString(shapes: number[][] | undefined) { @@ -58,7 +59,7 @@ function getAxisData(metadata: Metadata) { }; }); } catch (error) { - console.error('Error getting axis data: ', error); + log.error('Error getting axis data: ', error); return []; } } @@ -66,7 +67,7 @@ function getAxisData(metadata: Metadata) { export default function ZarrMetadataTable({ metadata, layerType, - availableVersions + availableZarrVersions }: ZarrMetadataTableProps) { const { disableHeuristicalLayerTypeDetection } = usePreferencesContext(); const { zarrVersion, multiscale, shapes } = metadata; @@ -85,8 +86,8 @@ export default function ZarrMetadataTable({ Zarr Version - {availableVersions && availableVersions.length > 1 - ? availableVersions.join(', ') + {availableZarrVersions && availableZarrVersions.length > 0 + ? availableZarrVersions.join(', ') : zarrVersion} diff --git a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx index eac0a69fb..fac230b76 100644 --- a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx +++ b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx @@ -20,7 +20,7 @@ type ZarrPreviewProps = { readonly mainPanelWidth: number; readonly openWithToolUrls: OpenWithToolUrls | null; readonly path: string; - readonly availableVersions: ('v2' | 'v3')[]; + readonly availableZarrVersions: number[]; readonly thumbnailQuery: UseQueryResult; readonly zarrMetadataQuery: UseQueryResult<{ metadata: ZarrMetadata; @@ -29,7 +29,7 @@ type ZarrPreviewProps = { }; export default function ZarrPreview({ - availableVersions, + availableZarrVersions, fspName, layerType, mainPanelWidth, @@ -125,7 +125,7 @@ export default function ZarrPreview({ className={`flex ${mainPanelWidth > 1000 ? 'gap-6' : 'flex-col gap-4'} h-fit`} > diff --git a/frontend/src/components/ui/Dialogs/ChangePermissions.tsx b/frontend/src/components/ui/Dialogs/ChangePermissions.tsx index 6994366d5..e51d5bb3d 100644 --- a/frontend/src/components/ui/Dialogs/ChangePermissions.tsx +++ b/frontend/src/components/ui/Dialogs/ChangePermissions.tsx @@ -153,8 +153,7 @@ export default function ChangePermissions({ className="!rounded-md" disabled={Boolean( mutations.changePermissions.isPending || - localPermissions === - fileBrowserState.propertiesTarget.permissions + localPermissions === fileBrowserState.propertiesTarget.permissions )} type="submit" > diff --git a/frontend/src/components/ui/Dialogs/dataLinkUsage/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/dataLinkUsage/DataLinkUsageDialog.tsx index 2e704529a..70802641a 100644 --- a/frontend/src/components/ui/Dialogs/dataLinkUsage/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/dataLinkUsage/DataLinkUsageDialog.tsx @@ -7,7 +7,8 @@ import DataLinkTabs from '@/components/ui/Dialogs/dataLinkUsage/tabsContent/Data import CopyTooltip from '@/components/ui/widgets/CopyTooltip'; import useFileQuery from '@/queries/fileQueries'; import { - detectZarrVersions, + areZarrMetadataFilesPresent, + getEffectiveZarrStorageVersion, useZarrMetadataQuery } from '@/queries/zarrQueries'; import { detectN5 } from '@/queries/n5Queries'; @@ -36,8 +37,7 @@ export default function DataLinkUsageDialog({ const targetFileQuery = useFileQuery(fspName, path); const files = targetFileQuery.data?.files ?? []; - const zarrVersions = detectZarrVersions(files); - const isZarr = zarrVersions.length > 0; + const isZarr = areZarrMetadataFilesPresent(files); const isN5 = detectN5(files); // Reuse the zarr metadata query — TanStack Query caches by key, @@ -48,11 +48,12 @@ export default function DataLinkUsageDialog({ files }); - const zarrVersion: ZarrVersion | undefined = isZarr - ? zarrVersions.includes('v3') - ? 3 - : 2 - : undefined; + const zarrVersion: ZarrVersion | undefined = + isZarr && zarrMetadataQuery.data + ? getEffectiveZarrStorageVersion( + zarrMetadataQuery.data.availableZarrVersions + ) + : undefined; // Determine data type: for zarr, wait for metadata query to distinguish OME vs plain let dataType: DataLinkType; diff --git a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index b94e9bae4..6bf874969 100644 --- a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -279,7 +279,7 @@ export default function PropertiesDrawer({ } disabled={Boolean( externalDataUrlQuery.data || - fileBrowserState.propertiesTarget.hasRead === false + fileBrowserState.propertiesTarget.hasRead === false )} id="share-switch" label={ diff --git a/frontend/src/config/resolveViewersConfigPath.ts b/frontend/src/config/resolveViewersConfigPath.ts new file mode 100644 index 000000000..2ed2d0aa0 --- /dev/null +++ b/frontend/src/config/resolveViewersConfigPath.ts @@ -0,0 +1,18 @@ +import { existsSync } from 'fs'; +import path from 'path'; + +/** + * Returns the path to the viewers config YAML to use. + * If a custom viewers.config.yaml exists at the frontend root it takes + * precedence over the committed default in src/config/. + * + * @param frontendDir - Absolute path to the frontend/ directory. + */ +export function resolveViewersConfigPath(frontendDir: string): string { + const overridePath = path.resolve(frontendDir, 'viewers.config.yaml'); + const defaultPath = path.resolve( + frontendDir, + 'src/config/viewers.config.yaml' + ); + return existsSync(overridePath) ? overridePath : defaultPath; +} diff --git a/frontend/src/config/viewers.config.yaml b/frontend/src/config/viewers.config.yaml new file mode 100644 index 000000000..a869702e7 --- /dev/null +++ b/frontend/src/config/viewers.config.yaml @@ -0,0 +1,29 @@ +# Fileglancer OME-Zarr Viewers Configuration +# +# To customize, copy this file to frontend/viewers.config.yaml (which is +# gitignored) and edit your copy. The override takes precedence at build time. +# +# After editing, rebuild with: pixi run node-build +# (or use watch mode: pixi run dev-watch) +# +# For system deployments (e.g. installed from PyPI), you can also set +# FGC_VIEWERS_CONFIG=/path/to/viewers.config.yaml to override viewers at +# runtime without rebuilding. See docs/ViewersConfiguration.md for details. +# +# Each viewer entry requires: +# - manifest_url: URL or absolute path to a capability manifest YAML file +# Use absolute paths (e.g. /viewers/neuroglancer.yaml) for manifests bundled +# in the public/ directory, or full URLs for externally hosted manifests. +# +# Optional: +# - instance_template_url: Override the viewer's template_url from the manifest +# - label: Custom tooltip text (defaults to "View in {Name}") + +viewers: + - manifest_url: 'https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/neuroglancer.yaml' + + - manifest_url: 'https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/avivator.yaml' + + - manifest_url: 'https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/validator.yaml' + + - manifest_url: 'https://raw.githubusercontent.com/BioImageTools/capability-manifest/host-manifests-and-docs/manifests/vole.yaml' diff --git a/frontend/src/config/viewersConfig.ts b/frontend/src/config/viewersConfig.ts new file mode 100644 index 000000000..e9d77bc9a --- /dev/null +++ b/frontend/src/config/viewersConfig.ts @@ -0,0 +1,112 @@ +import yaml from 'js-yaml'; +import { z } from 'zod'; + +/** + * Zod schema for viewer entry from viewers.config.yaml + */ +const ViewerConfigEntrySchema = z.object( + { + manifest_url: z + .string({ + message: 'Each viewer must have a "manifest_url" field (string)' + }) + .refine(val => val.startsWith('/') || URL.canParse(val), { + message: + '"manifest_url" must be a valid URL or an absolute path starting with /' + }), + instance_template_url: z + .string({ message: '"instance_template_url" must be a string' }) + .refine(val => val.startsWith('/') || URL.canParse(val), { + message: + '"instance_template_url" must be a valid URL or an absolute path starting with /' + }) + .optional(), + label: z.string({ message: '"label" must be a string' }).optional() + }, + { + error: iss => { + if (iss.code === 'invalid_type' && iss.expected === 'object') { + return 'Each viewer must be an object with a "manifest_url" field'; + } + return undefined; + } + } +); + +/** + * Zod schema for viewers.config.yaml structure + */ +const ViewersConfigYamlSchema = z.object( + { + viewers: z + .array(ViewerConfigEntrySchema, { + message: + 'Configuration must have a "viewers" field containing an array of viewers' + }) + .min(1, { + message: '"viewers" must contain at least one viewer' + }) + }, + { + error: iss => { + if (iss.code === 'invalid_type') { + return { + message: 'Configuration must have a "viewers" field' + }; + } + } + } +); + +// exported for use in ViewersContext +export type ViewerConfigEntry = z.infer; + +type ViewersConfigYaml = z.infer; + +/** + * Parse and validate viewers configuration YAML + * @param yamlContent - The YAML content to parse + */ +export function parseViewersConfig(yamlContent: string): ViewersConfigYaml { + let parsed: unknown; + + try { + parsed = yaml.load(yamlContent); + } catch (error) { + throw new Error( + `Failed to parse viewers configuration YAML: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } + + const result = ViewersConfigYamlSchema.safeParse(parsed); + + if (!result.success) { + const firstError = result.error.issues[0]; + + // Check if the error is nested within a specific viewer + if (firstError.path.length > 0 && firstError.path[0] === 'viewers') { + const viewerIndex = firstError.path[1]; + + if ( + typeof viewerIndex === 'number' && + parsed && + typeof parsed === 'object' + ) { + const configData = parsed as { viewers?: unknown[] }; + const viewer = configData.viewers?.[viewerIndex]; + + if (viewer && typeof viewer === 'object' && 'manifest_url' in viewer) { + const manifestUrl = (viewer as { manifest_url: unknown }) + .manifest_url; + if (typeof manifestUrl === 'string') { + throw new Error(`Viewer "${manifestUrl}": ${firstError.message}`); + } + } + } + } + + throw new Error(firstError.message); + } + + return result.data; +} diff --git a/frontend/src/contexts/ViewersContext.tsx b/frontend/src/contexts/ViewersContext.tsx new file mode 100644 index 000000000..bff4bc007 --- /dev/null +++ b/frontend/src/contexts/ViewersContext.tsx @@ -0,0 +1,231 @@ +import { + createContext, + useContext, + useState, + useEffect, + useCallback, + type ReactNode +} from 'react'; +import { + loadManifestsFromUrls, + validateViewer, + getLogoUrl, + type ViewerManifest, + type OmeZarrMetadata +} from '@bioimagetools/capability-manifest'; +import { default as log } from '@/logger'; +import { useViewersConfigQuery } from '@/queries/viewersConfigQueries'; + +/** + * Validated viewer with all necessary information + */ +export interface ValidViewer { + /** Internal key for this viewer (normalized name) */ + key: string; + /** Display name */ + displayName: string; + /** URL template (may contain {dataLink} placeholder) */ + urlTemplate: string; + /** Logo path */ + logoPath: string; + /** Tooltip/alt text label */ + label: string; + /** Associated capability manifest (required) */ + manifest: ViewerManifest; +} + +interface ViewersContextType { + validViewers: ValidViewer[]; + isInitialized: boolean; + error: string | null; + getViewersCompatibleWithImage: (metadata: OmeZarrMetadata) => ValidViewer[]; +} + +const ViewersContext = createContext(undefined); + +/** + * Normalize viewer name to a valid key + */ +function normalizeViewerName(name: string): string { + return name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); +} + +export function ViewersProvider({ + children +}: { + readonly children: ReactNode; +}) { + const [validViewers, setValidViewers] = useState([]); + const [isInitialized, setIsInitialized] = useState(false); + const [error, setError] = useState(null); + + const { + data: configEntries, + isError: isConfigError, + error: configError + } = useViewersConfigQuery(); + + useEffect(() => { + if (!configEntries) { + return; + } + const entries = configEntries; + + async function loadManifests() { + try { + log.info(`Loaded configuration for ${entries.length} viewers`); + + // Extract manifest URLs + const manifestUrls = entries.map(entry => entry.manifest_url); + + // Load capability manifests (with a 10s timeout to avoid hanging on unreachable URLs) + let manifestsMap: Map; + try { + const manifestsPromise = loadManifestsFromUrls(manifestUrls); + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error('Manifest loading timed out after 10s')), + 10_000 + ) + ); + manifestsMap = await Promise.race([manifestsPromise, timeoutPromise]); + log.info(`Loaded ${manifestsMap.size} viewer capability manifests`); + } catch (manifestError) { + throw new Error( + `Failed to load viewer manifests: ${manifestError instanceof Error ? manifestError.message : 'Unknown error'}` + ); + } + + const validated: ValidViewer[] = []; + + // Map through viewer config entries to validate + for (const entry of entries) { + const manifest = manifestsMap.get(entry.manifest_url); + + if (!manifest) { + log.warn( + `Viewer manifest from "${entry.manifest_url}" failed to load, skipping` + ); + continue; + } + + // Determine URL template + const urlTemplate = + entry.instance_template_url ?? manifest.viewer.template_url; + + if (!urlTemplate) { + log.warn( + `Viewer "${manifest.viewer.name}" has no template_url in manifest and no instance_template_url override, skipping` + ); + continue; + } + + // Replace {DATA_URL} with {dataLink} for consistency with existing code + const normalizedUrlTemplate = urlTemplate.replace( + /{DATA_URL}/g, + '{dataLink}' + ); + + // Create valid viewer entry + const key = normalizeViewerName(manifest.viewer.name); + const displayName = manifest.viewer.name; + const label = entry.label || `View in ${displayName}`; + const logoPath = getLogoUrl(manifest); + + validated.push({ + key, + displayName, + urlTemplate: normalizedUrlTemplate, + logoPath, + label, + manifest + }); + + log.info(`Viewer "${manifest.viewer.name}" registered successfully`); + } + + if (validated.length === 0) { + throw new Error( + 'No valid viewers configured. Check viewers.config.yaml or console for errors.' + ); + } + + setValidViewers(validated); + setIsInitialized(true); + log.info( + `Viewers initialization complete: ${validated.length} viewers available` + ); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Unknown error'; + log.error('Failed to initialize viewers:', errorMessage); + log.error( + 'Application will continue with no viewers available. Check viewers.config.yaml for errors.' + ); + setError(errorMessage); + setValidViewers([]); // Ensure empty viewer list on error + setIsInitialized(true); // Still mark as initialized to prevent hanging + } + } + + loadManifests(); + }, [configEntries]); + + // Handle query-level errors + useEffect(() => { + if (isConfigError && configError) { + const errorMessage = configError.message; + log.error('Failed to load viewers configuration:', errorMessage); + setError(errorMessage); + setIsInitialized(true); + } + }, [isConfigError, configError]); + + const getViewersCompatibleWithImage = useCallback( + (metadata: OmeZarrMetadata): ValidViewer[] => { + if (!isInitialized || !metadata) { + return []; + } + + return validViewers.filter(viewer => { + const result = validateViewer(viewer.manifest, metadata); + if (!result.dataCompatible) { + log.info( + `Viewer "${viewer.displayName}" is not compatible with this dataset: ${result.errors.map(e => e.message).join('; ')}` + ); + } + if (result.warnings.length > 0) { + log.info( + `Viewer "${viewer.displayName}" warnings: ${result.warnings.map(w => w.message).join('; ')}` + ); + } + return result.dataCompatible; + }); + }, + [validViewers, isInitialized] + ); + + return ( + + {children} + + ); +} + +export function useViewersContext() { + const context = useContext(ViewersContext); + if (!context) { + throw new Error('useViewersContext must be used within ViewersProvider'); + } + return context; +} diff --git a/frontend/src/hooks/useN5Metadata.ts b/frontend/src/hooks/useN5Metadata.ts index b56bf9554..aa6653680 100644 --- a/frontend/src/hooks/useN5Metadata.ts +++ b/frontend/src/hooks/useN5Metadata.ts @@ -73,10 +73,7 @@ export default function useN5Metadata() { const toolUrls: N5OpenWithToolUrls = { copy: url || '', - neuroglancer: '', - validator: null, - vole: null, - avivator: null + neuroglancer: '' }; if (url) { diff --git a/frontend/src/hooks/useZarrMetadata.ts b/frontend/src/hooks/useZarrMetadata.ts index c0f590652..0e95d271b 100644 --- a/frontend/src/hooks/useZarrMetadata.ts +++ b/frontend/src/hooks/useZarrMetadata.ts @@ -4,9 +4,12 @@ import { useFileBrowserContext } from '@/contexts/FileBrowserContext'; import { usePreferencesContext } from '@/contexts/PreferencesContext'; import { useProxiedPathContext } from '@/contexts/ProxiedPathContext'; import { useExternalBucketContext } from '@/contexts/ExternalBucketContext'; +import { useViewersContext } from '@/contexts/ViewersContext'; +import type { OmeZarrMetadata } from '@bioimagetools/capability-manifest'; import { useZarrMetadataQuery, - useOmeZarrThumbnailQuery + useOmeZarrThumbnailQuery, + getEffectiveZarrStorageVersion } from '@/queries/zarrQueries'; import type { OpenWithToolUrls, ZarrMetadata } from '@/queries/zarrQueries'; import { @@ -31,6 +34,11 @@ export default function useZarrMetadata() { disableHeuristicalLayerTypeDetection, useLegacyMultichannelApproach } = usePreferencesContext(); + const { + validViewers, + isInitialized: viewersInitialized, + getViewersCompatibleWithImage + } = useViewersContext(); // Fetch Zarr metadata const zarrMetadataQuery = useZarrMetadataQuery({ @@ -39,8 +47,9 @@ export default function useZarrMetadata() { files: fileQuery.data?.files }); - const effectiveZarrVersion = - zarrMetadataQuery.data?.availableVersions.includes('v3') ? 3 : 2; + const effectiveZarrVersion = getEffectiveZarrStorageVersion( + zarrMetadataQuery.data?.availableZarrVersions ?? [] + ); const metadata = zarrMetadataQuery.data?.metadata || null; const omeZarrUrl = zarrMetadataQuery.data?.omeZarrUrl || null; @@ -74,7 +83,7 @@ export default function useZarrMetadata() { setLayerType(determinedLayerType); } catch (error) { if (!signal.aborted) { - console.error('Error determining layer type:', error); + log.error('Error determining layer type:', error); setLayerType('image'); // Default fallback } } @@ -88,105 +97,142 @@ export default function useZarrMetadata() { }, [thumbnailSrc, disableHeuristicalLayerTypeDetection]); const openWithToolUrls = useMemo(() => { - if (!metadata) { + if (!metadata || !viewersInitialized) { return null; } - const validatorBaseUrl = 'https://ome.github.io/ome-ngff-validator/'; - const neuroglancerBaseUrl = 'https://neuroglancer-demo.appspot.com/#!'; - const voleBaseUrl = 'https://volumeviewer.allencell.org/viewer'; - const avivatorBaseUrl = 'https://janeliascicomp.github.io/viv/'; const url = externalDataUrlQuery.data || currentDirProxiedPathQuery.data?.url; + const openWithToolUrls = { copy: url || '' } as OpenWithToolUrls; - // Determine which tools should be available based on metadata type + // Get compatible viewers for this dataset + let compatibleViewers = validViewers; + + // If we have multiscales metadata (OME-Zarr), use capability checking to filter if (metadata?.multiscale) { - // OME-Zarr - all urls for v2; no avivator for v3 - if (url) { - if (effectiveZarrVersion === 2) { - openWithToolUrls.avivator = buildUrl(avivatorBaseUrl, null, { - image_url: url - }); - } else { - openWithToolUrls.avivator = null; + // Convert our metadata to OmeZarrMetadata format for capability checking + const omeZarrMetadata: OmeZarrMetadata = { + version: zarrMetadataQuery.data?.availableOmeZarrVersions.sort( + (a, b) => parseFloat(b) - parseFloat(a) + )[0], + axes: metadata.multiscale?.axes as OmeZarrMetadata['axes'], + multiscales: metadata.multiscale + ? ([metadata.multiscale] as OmeZarrMetadata['multiscales']) + : undefined, + omero: metadata.omero as OmeZarrMetadata['omero'], + labels: metadata.labels + }; + + compatibleViewers = getViewersCompatibleWithImage(omeZarrMetadata); + + // Create a Set for lookup of compatible viewer keys + // Needed to mark incompatible but valid (as defined by the viewer config) viewers as null in openWithToolUrls + const compatibleKeys = new Set(compatibleViewers.map(v => v.key)); + + for (const viewer of validViewers) { + if (!compatibleKeys.has(viewer.key)) { + openWithToolUrls[viewer.key] = null; } - // Populate with actual URLs when proxied path is available - openWithToolUrls.validator = buildUrl(validatorBaseUrl, null, { - source: url - }); - openWithToolUrls.vole = buildUrl(voleBaseUrl, null, { - url - }); - if (disableNeuroglancerStateGeneration) { - openWithToolUrls.neuroglancer = - neuroglancerBaseUrl + - generateNeuroglancerStateForDataURL(url, effectiveZarrVersion); - } else if (layerType) { - try { - openWithToolUrls.neuroglancer = + } + + // For compatible viewers, generate URLs + for (const viewer of compatibleViewers) { + if (!url) { + // Compatible but no data URL yet - show as available (empty string) + openWithToolUrls[viewer.key] = ''; + continue; + } + + // Generate the viewer URL + let viewerUrl = viewer.urlTemplate; + + // Special handling for Neuroglancer to maintain existing state generation logic + if (viewer.key === 'neuroglancer') { + // Extract base URL from template (everything before #!) + const neuroglancerBaseUrl = viewer.urlTemplate.split('#!')[0] + '#!'; + if (disableNeuroglancerStateGeneration) { + viewerUrl = neuroglancerBaseUrl + - generateNeuroglancerStateForOmeZarr( - url, - effectiveZarrVersion, - layerType, - metadata.multiscale, - metadata.arr, - metadata.labels, - metadata.omero, - useLegacyMultichannelApproach + generateNeuroglancerStateForDataURL(url, effectiveZarrVersion); + } else if (layerType) { + try { + viewerUrl = + neuroglancerBaseUrl + + generateNeuroglancerStateForOmeZarr( + url, + effectiveZarrVersion, + layerType, + metadata.multiscale, + metadata.arr, + metadata.labels, + metadata.omero, + useLegacyMultichannelApproach + ); + } catch (error) { + log.error( + 'Error generating Neuroglancer state for OME-Zarr:', + error ); - } catch (error) { - log.error( - 'Error generating Neuroglancer state for OME-Zarr:', - error + viewerUrl = + neuroglancerBaseUrl + + generateNeuroglancerStateForDataURL(url, effectiveZarrVersion); + } + } + } else { + // For other viewers, replace {dataLink} placeholder if present + if (viewerUrl.includes('{dataLink}')) { + viewerUrl = viewerUrl.replace( + /{dataLink}/g, + encodeURIComponent(url) ); - openWithToolUrls.neuroglancer = - neuroglancerBaseUrl + - generateNeuroglancerStateForDataURL(url, effectiveZarrVersion); + } else { + // If no placeholder, use buildUrl with 'url' query param + viewerUrl = buildUrl(viewerUrl, null, { url }); } } - } else { - // No proxied URL - show all tools as available but empty - openWithToolUrls.validator = ''; - openWithToolUrls.vole = ''; - // if this is a zarr version 2, then set the url to blank which will show - // the icon before a data link has been generated. Setting it to null for - // all other versions, eg zarr v3 means the icon will not be present before - // a data link is generated. - openWithToolUrls.avivator = effectiveZarrVersion === 2 ? '' : null; - openWithToolUrls.neuroglancer = ''; + + openWithToolUrls[viewer.key] = viewerUrl; } } else { // Non-OME Zarr - only Neuroglancer available - if (url) { - openWithToolUrls.validator = null; - openWithToolUrls.vole = null; - openWithToolUrls.avivator = null; - if (disableNeuroglancerStateGeneration) { - openWithToolUrls.neuroglancer = - neuroglancerBaseUrl + - generateNeuroglancerStateForDataURL(url, effectiveZarrVersion); - } else if (layerType) { - openWithToolUrls.neuroglancer = - neuroglancerBaseUrl + - generateNeuroglancerStateForZarrArray( - url, - effectiveZarrVersion, - layerType - ); + // Mark all non-Neuroglancer viewers as incompatible + for (const viewer of validViewers) { + if (viewer.key !== 'neuroglancer') { + openWithToolUrls[viewer.key] = null; + } else { + // Neuroglancer + if (url) { + // Extract base URL from template (everything before #!) + const neuroglancerBaseUrl = + viewer.urlTemplate.split('#!')[0] + '#!'; + if (disableNeuroglancerStateGeneration) { + openWithToolUrls.neuroglancer = + neuroglancerBaseUrl + + generateNeuroglancerStateForDataURL(url, effectiveZarrVersion); + } else if (layerType) { + openWithToolUrls.neuroglancer = + neuroglancerBaseUrl + + generateNeuroglancerStateForZarrArray( + url, + effectiveZarrVersion, + layerType + ); + } else { + // layerType not yet determined - use fallback + openWithToolUrls.neuroglancer = + neuroglancerBaseUrl + + generateNeuroglancerStateForDataURL(url, effectiveZarrVersion); + } + } else { + // No proxied URL - show Neuroglancer as available but empty + openWithToolUrls.neuroglancer = ''; + } } - } else { - // No proxied URL - only show Neuroglancer as available but empty - openWithToolUrls.validator = null; - openWithToolUrls.vole = null; - openWithToolUrls.avivator = null; - openWithToolUrls.neuroglancer = ''; } } - return openWithToolUrls; }, [ metadata, @@ -195,7 +241,11 @@ export default function useZarrMetadata() { disableNeuroglancerStateGeneration, useLegacyMultichannelApproach, layerType, - effectiveZarrVersion + effectiveZarrVersion, + zarrMetadataQuery.data?.availableOmeZarrVersions, + validViewers, + viewersInitialized, + getViewersCompatibleWithImage ]); return { @@ -203,6 +253,6 @@ export default function useZarrMetadata() { thumbnailQuery, openWithToolUrls, layerType, - availableVersions: zarrMetadataQuery.data?.availableVersions || [] + availableZarrVersions: zarrMetadataQuery.data?.availableZarrVersions || [] }; } diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index 88d0b33c1..09306c415 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -18,6 +18,7 @@ import { ExternalBucketProvider } from '@/contexts/ExternalBucketContext'; import { ProfileContextProvider } from '@/contexts/ProfileContext'; import { NotificationProvider } from '@/contexts/NotificationsContext'; import { ServerHealthProvider } from '@/contexts/ServerHealthContext'; +import { ViewersProvider } from '@/contexts/ViewersContext'; import FileglancerNavbar from '@/components/ui/Navbar/Navbar'; import Notifications from '@/components/ui/Notifications/Notifications'; import ErrorFallback from '@/components/ErrorFallback'; @@ -64,25 +65,27 @@ export const MainLayout = () => { return ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/frontend/src/queries/n5Queries.ts b/frontend/src/queries/n5Queries.ts index 4d0e09b2e..c1d4980cc 100644 --- a/frontend/src/queries/n5Queries.ts +++ b/frontend/src/queries/n5Queries.ts @@ -49,9 +49,6 @@ export type N5Metadata = { export type N5OpenWithToolUrls = { copy: string; neuroglancer: string; - validator: null; - vole: null; - avivator: null; }; type N5MetadataQueryParams = { diff --git a/frontend/src/queries/viewersConfigQueries.ts b/frontend/src/queries/viewersConfigQueries.ts new file mode 100644 index 000000000..a48f6c82e --- /dev/null +++ b/frontend/src/queries/viewersConfigQueries.ts @@ -0,0 +1,50 @@ +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { sendFetchRequest } from '@/utils'; +import { default as log } from '@/logger'; +import { + parseViewersConfig, + type ViewerConfigEntry +} from '@/config/viewersConfig'; + +export const viewersConfigKeys = { + all: ['viewersConfig'] as const +}; + +const fetchViewersConfig = async (): Promise => { + // Try runtime config from the server first. + try { + const response = await sendFetchRequest('/api/viewers-config', 'GET'); + if (response.ok) { + const configYaml = await response.text(); + const config = parseViewersConfig(configYaml); + log.info('Using runtime viewers configuration from server'); + return config.viewers; + } else if (response.status !== 404) { + log.warn( + `Unexpected status ${response.status} from /api/viewers-config, falling back to bundled config` + ); + } + // 404 means no runtime config — fall through to bundled default + } catch { + // Network error — fall through to bundled default + log.info('Runtime viewers config not available, using bundled default'); + } + + // Fall back to build-time bundled config + const module = await import('@/config/viewers.config.yaml?raw'); + const config = parseViewersConfig(module.default); + return config.viewers; +}; + +export function useViewersConfigQuery(): UseQueryResult< + ViewerConfigEntry[], + Error +> { + return useQuery({ + queryKey: viewersConfigKeys.all, + queryFn: fetchViewersConfig, + staleTime: Infinity, // Config won't change during a session + retry: false // If both sources fail, don't retry + }); +} diff --git a/frontend/src/queries/zarrQueries.ts b/frontend/src/queries/zarrQueries.ts index ec32ca48b..d739808f2 100644 --- a/frontend/src/queries/zarrQueries.ts +++ b/frontend/src/queries/zarrQueries.ts @@ -1,4 +1,5 @@ -import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryResult } from '@tanstack/react-query'; import { default as log } from '@/logger'; import { getOmeZarrMetadata, @@ -12,11 +13,11 @@ import { FileOrFolder } from '@/shared.types'; export type OpenWithToolUrls = { copy: string; - validator: string | null; - neuroglancer: string; - vole: string | null; - avivator: string | null; -}; +} & Record; + +// The 'copy' key is always present, all other keys are viewer-specific +// null means the viewer is incompatible with this dataset +// empty string means the viewer is compatible but no data URL is available yet export type ZarrMetadata = Metadata | null; @@ -29,15 +30,18 @@ type ZarrMetadataQueryParams = { export type ZarrMetadataResult = { metadata: ZarrMetadata; omeZarrUrl: string | null; - availableVersions: ('v2' | 'v3')[]; + availableZarrVersions: number[]; + availableOmeZarrVersions: string[]; isOmeZarr: boolean; }; // Zarr v3 zarr.json structure type ZarrV3Attrs = { + zarr_format?: number; node_type: 'array' | 'group'; attributes?: { ome?: { + version?: string; multiscales?: unknown; labels?: string[]; }; @@ -51,32 +55,63 @@ type ZarrV2Attrs = { }; /** - * Detects which Zarr versions are supported by checking for version-specific marker files. - * @returns Array of supported versions: ['v2'], ['v3'], or ['v2', 'v3'] + * Extracts the OME-NGFF spec version from parsed metadata. + * Logic follows the OME-NGFF Validator. */ -export function detectZarrVersions(files: FileOrFolder[]): ('v2' | 'v3')[] { - if (!files || files.length === 0) { - return []; +export function getOmeNgffVersion(ngffData: Record): string { + let version: string | undefined; + + if (ngffData.attributes?.ome) { + version = ngffData.attributes.ome.version; + if (!version) { + log.warn('No version found in attributes.ome, defaulting to 0.4'); + } + // Used if 'attributes' is at the root + } else if (ngffData.ome?.version) { + version = ngffData.ome.version; + } else if (ngffData.version) { + version = ngffData.version; + } else { + // 0.4 and earlier: check multiscales, plate, or well + version = + ngffData.multiscales?.[0]?.version ?? + ngffData.plate?.version ?? + ngffData.well?.version; } - const hasFile = (name: string) => files.some(f => f.name === name); - const versions: ('v2' | 'v3')[] = []; + // for 0.4 and earlier, version wasn't MUST and we defaulted + // to using v0.4 for validation. To preserve that behaviour + // return "0.4" if no version found. + version = version || '0.4'; + // remove any -dev2 etc. + return version.split('-')[0]; +} - // Check for Zarr v2 indicators - if (hasFile('.zarray') || hasFile('.zattrs')) { - versions.push('v2'); +export function areZarrMetadataFilesPresent(files: FileOrFolder[]): boolean { + if (!files || files.length === 0) { + return false; } + const hasFile = (name: string) => files.some(f => f.name === name); + return hasFile('zarr.json') || hasFile('.zattrs') || hasFile('.zarray'); +} - // Check for Zarr v3 indicator - if (hasFile('zarr.json')) { - versions.push('v3'); +/** + * Returns the preferred Zarr storage version from available versions. + * Prefers v3 if available, otherwise v2. + */ +export function getEffectiveZarrStorageVersion( + availableZarrVersions: number[] +): 2 | 3 { + if (availableZarrVersions.includes(3)) { + return 3; } - - return versions; + return 2; } /** - * Fetches Zarr metadata by checking for zarr.json, .zarray, or .zattrs files + * Fetches Zarr metadata by checking for zarr.json, .zattrs, and .zarray files. + * Always checks all metadata sources to build complete version arrays. + * Start with zarr.json for Zarr v3 metadata, then .zattrs for Zarr v2 metadata, then .zarray as fallback. */ async function fetchZarrMetadata({ fspName, @@ -88,7 +123,8 @@ async function fetchZarrMetadata({ return { metadata: null, omeZarrUrl: null, - availableVersions: [], + availableZarrVersions: [], + availableOmeZarrVersions: [], isOmeZarr: false }; } @@ -99,21 +135,44 @@ async function fetchZarrMetadata({ const getFile = (fileName: string) => files.find((file: FileOrFolder) => file.name === fileName); - const availableVersions = detectZarrVersions(files); + const availableZarrVersions: number[] = []; + const availableOmeZarrVersions: string[] = []; + + // Track whether we found primary metadata from zarr.json + let primaryMetadata: ZarrMetadataResult | null = null; - // Default to Zarr v3 when available - if (availableVersions.includes('v3')) { - const zarrJsonFile = getFile('zarr.json') as FileOrFolder; + // Step 1: Try zarr.json + const zarrJsonFile = getFile('zarr.json'); + if (zarrJsonFile) { const attrs = (await fetchFileAsJson( fspName, zarrJsonFile.path )) as ZarrV3Attrs; + // Read zarr_format field for Zarr storage version + const zarrStorageVersion = attrs.zarr_format; + if (zarrStorageVersion === undefined || zarrStorageVersion === null) { + log.warn('zarr.json missing zarr_format field, defaulting to 3'); + availableZarrVersions.push(3); + } else { + availableZarrVersions.push(zarrStorageVersion); + } + + const effectiveVersion: 2 | 3 = + zarrStorageVersion === 2 || zarrStorageVersion === 3 + ? zarrStorageVersion + : 3; + if (attrs.node_type === 'array') { - log.info('Getting Zarr array for', imageUrl, 'with Zarr version', 3); - const arr = await getZarrArray(imageUrl, 3); + log.info( + 'Getting Zarr array for', + imageUrl, + 'with Zarr version', + effectiveVersion + ); + const arr = await getZarrArray(imageUrl, effectiveVersion); const shapes = [arr.shape]; - return { + primaryMetadata = { metadata: { arr, shapes, @@ -121,19 +180,25 @@ async function fetchZarrMetadata({ scales: undefined, omero: undefined, labels: undefined, - zarrVersion: 3 + zarrVersion: effectiveVersion }, omeZarrUrl: null, - availableVersions, + availableZarrVersions, + availableOmeZarrVersions, isOmeZarr: false }; } else if (attrs.node_type === 'group') { if (attrs.attributes?.ome?.multiscales) { + const ngffVersion = getOmeNgffVersion(attrs); + if (!availableOmeZarrVersions.includes(ngffVersion)) { + availableOmeZarrVersions.push(ngffVersion); + } + log.info( 'Getting OME-Zarr metadata for', imageUrl, 'with Zarr version', - 3 + effectiveVersion ); const metadata = await getOmeZarrMetadata(imageUrl); // Check for labels (optional - may not exist) @@ -146,114 +211,116 @@ async function fetchZarrMetadata({ } catch { // Labels directory doesn't exist - that's fine } - return { + primaryMetadata = { metadata, omeZarrUrl: imageUrl, - availableVersions, + availableZarrVersions, + availableOmeZarrVersions, isOmeZarr: true }; } else { - return { - metadata: null, - omeZarrUrl: null, - availableVersions, - isOmeZarr: false - }; + log.info('Zarrv3 group has no multiscales', attrs.attributes); + // Don't return yet - continue to check .zattrs } } else { - return { - metadata: null, - omeZarrUrl: null, - availableVersions, - isOmeZarr: false - }; + log.warn('Unknown Zarrv3 node type', attrs.node_type); } - // v3 not available, now check for v2 - } else { - // v2 present - if (availableVersions.includes('v2')) { - const zarrayFile = getFile('.zarray'); - const zattrsFile = getFile('.zattrs'); - - // Check for .zarray (Zarr v2 array) - if (zarrayFile) { - log.info('Getting Zarr array for', imageUrl, 'with Zarr version', 2); - const arr = await getZarrArray(imageUrl, 2); - const shapes = [arr.shape]; - return { - metadata: { - arr, - shapes, - multiscale: undefined, - scales: undefined, - omero: undefined, - labels: undefined, - zarrVersion: 2 - }, - omeZarrUrl: null, - availableVersions, - isOmeZarr: false - }; - // Check for .zattrs (Zarr v2 OME-Zarr) - } else if (zattrsFile) { - const attrs = (await fetchFileAsJson( - fspName, - zattrsFile.path - )) as ZarrV2Attrs; - if (attrs.multiscales) { - log.info( - 'Getting OME-Zarr metadata for', - imageUrl, - 'with Zarr version', - 2 - ); - const metadata = await getOmeZarrMetadata(imageUrl); - // Check for labels (optional - may not exist) - try { - const labelsAttrs = (await fetchFileAsJson( - fspName, - currentFileOrFolder.path + '/labels/.zattrs' - )) as ZarrV2Attrs; - metadata.labels = labelsAttrs?.labels; - } catch { - // Labels directory doesn't exist - that's fine + } + + // Step 2: Always also check .zattrs + const zattrsFile = getFile('.zattrs'); + if (zattrsFile) { + if (!availableZarrVersions.includes(2)) { + availableZarrVersions.push(2); + } + + const attrs = (await fetchFileAsJson( + fspName, + zattrsFile.path + )) as ZarrV2Attrs; + + if (attrs.multiscales) { + const ngffVersion = getOmeNgffVersion(attrs); + if (!availableOmeZarrVersions.includes(ngffVersion)) { + availableOmeZarrVersions.push(ngffVersion); + } + + // If we don't already have primary metadata from zarr.json, use .zattrs + if (!primaryMetadata) { + log.info( + 'Getting OME-Zarr metadata for', + imageUrl, + 'with Zarr version', + 2 + ); + const metadata = await getOmeZarrMetadata(imageUrl); + // Check for labels + try { + const labelsAttrs = (await fetchFileAsJson( + fspName, + currentFileOrFolder.path + '/labels/.zattrs' + )) as ZarrV2Attrs; + metadata.labels = labelsAttrs?.labels; + if (metadata.labels) { + log.info('OME-Zarr Labels found: ', metadata.labels); } - return { - metadata, - omeZarrUrl: imageUrl, - availableVersions, - isOmeZarr: true - }; - } else { - log.debug('Zarrv2 .zattrs has no multiscales', attrs); - return { - metadata: null, - omeZarrUrl: null, - availableVersions, - isOmeZarr: false - }; + } catch (error) { + log.trace('Could not fetch labels attrs: ', error); } - // No Zarr metadata found - } else { - log.debug('No Zarr metadata files found for', imageUrl); - return { - metadata: null, - omeZarrUrl: null, - availableVersions, - isOmeZarr: false + primaryMetadata = { + metadata, + omeZarrUrl: imageUrl, + availableZarrVersions, + availableOmeZarrVersions, + isOmeZarr: true }; } - // No Zarr metadata found } else { - log.debug('No supported Zarr versions detected for', imageUrl); + log.debug('Zarrv2 .zattrs has no multiscales', attrs); + } + } + + // Step 3: If neither zarr.json nor .zattrs had data, check .zarray + if (!primaryMetadata) { + const zarrayFile = getFile('.zarray'); + if (zarrayFile) { + if (!availableZarrVersions.includes(2)) { + availableZarrVersions.push(2); + } + log.info('Getting Zarr array for', imageUrl, 'with Zarr version', 2); + const arr = await getZarrArray(imageUrl, 2); + const shapes = [arr.shape]; return { - metadata: null, + metadata: { + arr, + shapes, + multiscale: undefined, + scales: undefined, + omero: undefined, + labels: undefined, + zarrVersion: 2 + }, omeZarrUrl: null, - availableVersions: [], + availableZarrVersions, + availableOmeZarrVersions, isOmeZarr: false }; } } + + // Return primary metadata if found, otherwise return empty result + if (primaryMetadata) { + return primaryMetadata; + } + + log.debug('No Zarr metadata found for', imageUrl); + return { + metadata: null, + omeZarrUrl: null, + availableZarrVersions, + availableOmeZarrVersions, + isOmeZarr: false + }; } /** @@ -277,7 +344,7 @@ export function useZarrMetadataQuery( !!currentFileOrFolder && !!files && files.length > 0 && - detectZarrVersions(files).length > 0, + areZarrMetadataFilesPresent(files), staleTime: 5 * 60 * 1000, // 5 minutes - Zarr metadata doesn't change often retry: false // Don't retry if no Zarr files found }); diff --git a/frontend/ui-tests/playwright.config.js b/frontend/ui-tests/playwright.config.js index 412788710..260d78734 100644 --- a/frontend/ui-tests/playwright.config.js +++ b/frontend/ui-tests/playwright.config.js @@ -36,6 +36,9 @@ export default defineConfig({ }, timeout: process.env.CI ? 180_000 : 20_000, navigationTimeout: process.env.CI ? 90_000 : 10_000, + expect: { + timeout: 20_000 + }, workers: 1, webServer: { command: 'pixi run test-launch', diff --git a/frontend/ui-tests/tests/data-link-operations.spec.ts b/frontend/ui-tests/tests/data-link-operations.spec.ts index 58c900d4e..2e889c3e0 100644 --- a/frontend/ui-tests/tests/data-link-operations.spec.ts +++ b/frontend/ui-tests/tests/data-link-operations.spec.ts @@ -10,6 +10,8 @@ const navigateToZarrDir = async ( await page.goto('/browse', { waitUntil: 'domcontentloaded' }); + // Make sure the full page content has loaded before interacting with the file browser + await expect(page.getByText('Recently viewed')).toBeVisible(); await navigateToScratchFsp(page); const testDirName = testDir.split('/').pop() || testDir; const fullTestPath = testDirName.startsWith('test-') @@ -18,7 +20,7 @@ const navigateToZarrDir = async ( await navigateToTestDir(page, fullTestPath); await page.getByRole('link', { name: zarrDirName }).click(); // Wait for zarr metadata to load - await page.waitForSelector('text=zarr.json', { timeout: 10000 }); + await page.waitForSelector('text=zarr.json'); }; test.describe('Data Link Operations', () => { @@ -42,9 +44,7 @@ test.describe('Data Link Operations', () => { // Wait for zarr metadata to load await expect(page.getByText('zarr.json')).toBeVisible({ timeout: 10000 }); - await expect( - page.getByRole('link', { name: 'Neuroglancer logo' }) - ).toBeVisible({ timeout: 10000 }); + await expect(page.getByAltText(/neuroglancer/i)).toBeVisible(); const dataLinkToggle = page.getByRole('checkbox', { name: /data link/i }); const confirmButton = page.getByRole('button', { @@ -55,9 +55,7 @@ test.describe('Data Link Operations', () => { }); await test.step('Turn on automatic data links via the data link dialog', async () => { - const neuroglancerLink = page.getByRole('link', { - name: 'Neuroglancer logo' - }); + const neuroglancerLink = page.getByAltText(/neuroglancer/i); await neuroglancerLink.click(); // Confirm the data link creation in the dialog @@ -95,25 +93,25 @@ test.describe('Data Link Operations', () => { await test.step('Delete data link via properties panel', async () => { await dataLinkToggle.click(); - await expect(confirmDeleteButton).toBeVisible({ timeout: 5000 }); + await expect(confirmDeleteButton).toBeVisible(); await confirmDeleteButton.click(); await expect( page.getByText('Successfully deleted data link') ).toBeVisible(); - await expect(dataLinkToggle).not.toBeChecked({ timeout: 10000 }); + await expect(dataLinkToggle).not.toBeChecked(); }); await test.step('Recreate data link via properties panel', async () => { await expect( page.getByText('Successfully deleted data link') - ).not.toBeVisible({ timeout: 10000 }); + ).not.toBeVisible(); await dataLinkToggle.click(); // Navigate back to the zarr directory to check data link status; the above click takes you to Neuroglancer await navigateToZarrDir(page, testDir, zarrDirName); - await page.waitForLoadState('domcontentloaded', { timeout: 10000 }); + await page.waitForLoadState('domcontentloaded'); - await expect(dataLinkToggle).toBeChecked({ timeout: 10000 }); + await expect(dataLinkToggle).toBeChecked(); }); await test.step('Delete the link via action menu on links page', async () => { @@ -131,21 +129,21 @@ test.describe('Data Link Operations', () => { const deleteLinkOption = page.getByRole('menuitem', { name: /unshare/i }); await deleteLinkOption.click(); // Confirm deletion - await expect(confirmDeleteButton).toBeVisible({ timeout: 10000 }); + await expect(confirmDeleteButton).toBeVisible(); await confirmDeleteButton.click(); // Verify the link is removed from the table - await expect(linkRow).not.toBeVisible({ timeout: 10000 }); + await expect(linkRow).not.toBeVisible(); }); await test.step('Copy link works when automatic links is on and no data link exists yet', async () => { await navigateToZarrDir(page, testDir, zarrDirName); - await page.waitForLoadState('domcontentloaded', { timeout: 10000 }); + await page.waitForLoadState('domcontentloaded'); - await expect(page.getByText('zarr.json')).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('zarr.json')).toBeVisible(); const copyLinkIcon = page.getByRole('button', { name: 'Copy data URL' }); - await expect(copyLinkIcon).toBeVisible({ timeout: 10000 }); + await expect(copyLinkIcon).toBeVisible(); await copyLinkIcon.click(); await expect(page.getByText('Copied!')).toBeVisible(); @@ -212,10 +210,8 @@ test.describe('Data Link Operations', () => { // Navigate into the zarr directory await page.getByRole('link', { name: zarrDirName }).click(); - await expect(page.getByText('zarr.json')).toBeVisible({ timeout: 10000 }); - await expect( - page.getByRole('link', { name: 'Neuroglancer logo' }) - ).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('zarr.json')).toBeVisible(); + await expect(page.getByAltText(/neuroglancer/i)).toBeVisible(); // Click on the s0 subdirectory row to select it as the properties target const s0Row = page.getByRole('row').filter({ hasText: 's0' }); @@ -225,22 +221,20 @@ test.describe('Data Link Operations', () => { const propertiesPanel = page .locator('[role="complementary"]') .filter({ hasText: 'Properties' }); - await expect(propertiesPanel.getByText('s0', { exact: true })).toBeVisible({ - timeout: 10000 - }); + await expect( + propertiesPanel.getByText('s0', { exact: true }) + ).toBeVisible(); // Click the Neuroglancer viewer icon — this should create a data link // for the zarr directory (currentFileOrFolder), not for s0 (propertiesTarget) - const neuroglancerLink = page.getByRole('link', { - name: 'Neuroglancer logo' - }); + const neuroglancerLink = page.getByAltText(/neuroglancer/i); await neuroglancerLink.click(); // Confirm in dialog const confirmButton = page.getByRole('button', { name: /confirm|create|yes/i }); - await expect(confirmButton).toBeVisible({ timeout: 5000 }); + await expect(confirmButton).toBeVisible(); await confirmButton.click(); await expect( @@ -255,9 +249,7 @@ test.describe('Data Link Operations', () => { await expect(page.getByRole('heading', { name: /links/i })).toBeVisible(); // The data link should be for the zarr directory, not the s0 subdirectory - await expect(page.getByText(zarrDirName, { exact: true })).toBeVisible({ - timeout: 10000 - }); + await expect(page.getByText(zarrDirName, { exact: true })).toBeVisible(); await expect(page.getByText('s0', { exact: true })).not.toBeVisible(); }); }); diff --git a/frontend/ui-tests/tests/load-zarr-files.spec.ts b/frontend/ui-tests/tests/load-zarr-files.spec.ts index deaeb46ed..1ae7a036a 100644 --- a/frontend/ui-tests/tests/load-zarr-files.spec.ts +++ b/frontend/ui-tests/tests/load-zarr-files.spec.ts @@ -32,10 +32,8 @@ test.describe('Zarr File Type Representation', () => { // Wait for zarr metadata to load (zarr.json file present indicates loaded) await expect(page.getByText('zarr.json')).toBeVisible({ timeout: 10000 }); - await expect( - page.getByRole('link', { name: 'Neuroglancer logo' }) - ).toBeVisible({ timeout: 10000 }); - await expect(page.getByRole('link', { name: 'Vol-E logo' })).toHaveCount(0); + await expect(page.getByAltText(/neuroglancer/i)).toBeVisible(); + await expect(page.getByAltText(/vol-e/i)).toHaveCount(0); }); test('Zarr V3 OME-Zarr should show all viewers except avivator', async ({ @@ -51,16 +49,10 @@ test.describe('Zarr File Type Representation', () => { // Wait for zarr metadata to load await expect(page.getByText('zarr.json')).toBeVisible({ timeout: 10000 }); - await expect( - page.getByRole('link', { name: 'Neuroglancer logo' }) - ).toBeVisible({ timeout: 10000 }); - await expect(page.getByRole('link', { name: 'Vol-E logo' })).toBeVisible(); - await expect( - page.getByRole('link', { name: 'OME-Zarr Validator logo' }) - ).toBeVisible(); - await expect(page.getByRole('link', { name: 'Avivator logo' })).toHaveCount( - 0 - ); + await expect(page.getByAltText(/neuroglancer/i)).toBeVisible(); + await expect(page.getByAltText(/vol-e/i)).toBeVisible(); + await expect(page.getByAltText(/validator/i)).toBeVisible(); + await expect(page.getByAltText(/avivator/i)).toHaveCount(0); }); test('Zarr V2 Array should show only neuroglancer', async ({ @@ -76,10 +68,8 @@ test.describe('Zarr File Type Representation', () => { // Wait for zarr metadata to load await expect(page.getByText('.zarray')).toBeVisible({ timeout: 10000 }); - await expect( - page.getByRole('link', { name: 'Neuroglancer logo' }) - ).toBeVisible({ timeout: 10000 }); - await expect(page.getByRole('link', { name: 'Vol-E logo' })).toHaveCount(0); + await expect(page.getByAltText(/neuroglancer/i)).toBeVisible(); + await expect(page.getByAltText(/vol-e/i)).toHaveCount(0); }); test('Zarr V2 OME-Zarr should display all viewers including avivator', async ({ @@ -95,16 +85,10 @@ test.describe('Zarr File Type Representation', () => { // Wait for zarr metadata to load await expect(page.getByText('.zattrs')).toBeVisible({ timeout: 10000 }); - await expect( - page.getByRole('link', { name: 'Neuroglancer logo' }) - ).toBeVisible({ timeout: 10000 }); - await expect(page.getByRole('link', { name: 'Vol-E logo' })).toBeVisible(); - await expect( - page.getByRole('link', { name: 'OME-Zarr Validator logo' }) - ).toBeVisible(); - await expect( - page.getByRole('link', { name: 'Avivator logo' }) - ).toBeVisible(); + await expect(page.getByAltText(/neuroglancer/i)).toBeVisible(); + await expect(page.getByAltText(/vol-e/i)).toBeVisible(); + await expect(page.getByAltText(/validator/i)).toBeVisible(); + await expect(page.getByAltText(/avivator/i)).toBeVisible(); }); test('Refresh button should update zarr metadata when .zattrs is modified', async ({ diff --git a/frontend/ui-tests/utils/navigation.ts b/frontend/ui-tests/utils/navigation.ts index e07f1f34d..db84ebb68 100644 --- a/frontend/ui-tests/utils/navigation.ts +++ b/frontend/ui-tests/utils/navigation.ts @@ -5,6 +5,8 @@ const navigateToScratchFsp = async (page: Page) => { const localZone = page .getByLabel('List of file share paths') .getByRole('button', { name: 'Local' }); + // Wait for the Local zone to be visible before clicking + await expect(localZone).toBeVisible(); // Click specifically on the text to avoid clicking the favorite button await localZone.getByText('Local').click(); @@ -14,7 +16,7 @@ const navigateToScratchFsp = async (page: Page) => { .filter({ hasNotText: 'zarr' }) .nth(0); - await expect(scratchFsp).toBeVisible({ timeout: 10000 }); + await expect(scratchFsp).toBeVisible(); // Wait for file directory to load by waiting for the API response await Promise.all([ diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5b0c70176..ce37b0110 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,15 +3,22 @@ import fs from 'fs'; import path from 'path'; import react from '@vitejs/plugin-react'; import { nodePolyfills } from 'vite-plugin-node-polyfills'; +import { resolveViewersConfigPath } from './src/config/resolveViewersConfigPath'; + +const viewersConfigPath = resolveViewersConfigPath(__dirname); // https://vite.dev/config/ export default defineConfig({ base: '/', plugins: [react(), nodePolyfills({ include: ['path'] })], resolve: { - alias: { - '@': path.resolve(__dirname, './src') - } + alias: [ + { + find: /^@\/config\/viewers\.config\.yaml(\?.*)?$/, + replacement: viewersConfigPath + '$1' + }, + { find: '@', replacement: path.resolve(__dirname, './src') } + ] }, css: { lightningcss: { diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 11f4c5bfd..c730ca260 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1426,3 +1426,86 @@ def test_broken_symlink_in_file_listing(test_client, temp_dir): regular = next((f for f in files if f["name"] == "regular.txt"), None) assert regular is not None, "Regular file should be in response" assert regular["is_symlink"] is False, "Regular file should not be marked as symlink" + + +def test_viewers_config_not_set(test_client): + """Test that /api/viewers-config returns 404 when viewers_config is not set""" + response = test_client.get("/api/viewers-config") + assert response.status_code == 404 + + +def test_viewers_config_file_exists(temp_dir): + """Test that /api/viewers-config returns file contents as text/yaml when file exists""" + # Create a test viewers config file + config_content = "viewers:\n - manifest_url: 'https://example.com/manifest.yaml'\n" + config_path = os.path.join(temp_dir, "viewers.config.yaml") + with open(config_path, 'w') as f: + f.write(config_content) + + # Create app with viewers_config set + db_path = os.path.join(temp_dir, "test_vc.db") + db_url = f"sqlite:///{db_path}" + engine = create_engine(db_url) + Session = sessionmaker(bind=engine) + db_session = Session() + Base.metadata.create_all(engine) + + settings = Settings(db_url=db_url, file_share_mounts=[], viewers_config=config_path) + + import fileglancer.settings + import fileglancer.database + original_get_settings = fileglancer.settings.get_settings + fileglancer.settings.get_settings = lambda: settings + fileglancer.database.get_settings = lambda: settings + + app = create_app(settings) + from fileglancer.server import get_current_user + app.dependency_overrides[get_current_user] = lambda: "testuser" + client = TestClient(app) + + try: + response = client.get("/api/viewers-config") + assert response.status_code == 200 + assert "text/yaml" in response.headers["content-type"] + assert response.text == config_content + finally: + db_session.close() + engine.dispose() + from fileglancer.database import dispose_engine + dispose_engine(db_url) + fileglancer.settings.get_settings = original_get_settings + fileglancer.database.get_settings = original_get_settings + + +def test_viewers_config_file_missing(temp_dir): + """Test that /api/viewers-config returns 404 when configured file doesn't exist""" + db_path = os.path.join(temp_dir, "test_vcm.db") + db_url = f"sqlite:///{db_path}" + engine = create_engine(db_url) + Session = sessionmaker(bind=engine) + db_session = Session() + Base.metadata.create_all(engine) + + settings = Settings(db_url=db_url, file_share_mounts=[], viewers_config="/nonexistent/viewers.config.yaml") + + import fileglancer.settings + import fileglancer.database + original_get_settings = fileglancer.settings.get_settings + fileglancer.settings.get_settings = lambda: settings + fileglancer.database.get_settings = lambda: settings + + app = create_app(settings) + from fileglancer.server import get_current_user + app.dependency_overrides[get_current_user] = lambda: "testuser" + client = TestClient(app) + + try: + response = client.get("/api/viewers-config") + assert response.status_code == 404 + finally: + db_session.close() + engine.dispose() + from fileglancer.database import dispose_engine + dispose_engine(db_url) + fileglancer.settings.get_settings = original_get_settings + fileglancer.database.get_settings = original_get_settings