diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml new file mode 100644 index 00000000..f97d8f91 --- /dev/null +++ b/.github/workflows/dashboard.yml @@ -0,0 +1,47 @@ +name: Dashboard +permissions: + contents: read +on: + schedule: + # Every day at 5 AM UTC+8 + - cron: '0 21 * * *' + + workflow_dispatch: + + repository_dispatch: + types: [ci-dashboard, ci-all] + +run-name: >- + ${{ github.event_name == 'repository_dispatch' + && format( + 'PR #{0} - Label {1} - {2}', + github.event.client_payload.pull_number, + github.event.client_payload.ci_label, + github.event.client_payload.correlation_id + ) + || format('Dashboard - {0}', github.event_name) }} + +jobs: + dashboard: + if: > + github.event_name != 'repository_dispatch' || + github.event.action == 'ci-dashboard' || + github.event.action == 'ci-all' + name: Chromatic + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v6 + with: + node-version: '22' + - name: Install JavaScript dependencies + run: cd dashboard && npm ci + - name: Run Chromatic + uses: chromaui/action@latest + with: + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + workingDir: dashboard + exitZeroOnChanges: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e638275b..2bad497b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,10 +45,11 @@ jobs: --group agents \ --no-default-groups if: matrix.setup == 'slow' + # This pre-commit skips JavaScript on purpose. - name: Run pre-commit uses: pre-commit/action@v3.0.1 - name: Check Python headers - run: uv run --locked --no-sync scripts/check_python_headers.py + run: uv run --locked --no-sync scripts/check_headers.py - name: Run Black run: uv run --locked --no-sync black --check . - name: Run isort @@ -60,6 +61,28 @@ jobs: run: uv run --locked --no-sync pyright -p pyrightconfig.json if: matrix.setup == 'slow' + lint-js: + name: Lint - JavaScript + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v6 + with: + node-version: '22' + - name: Install dependencies + run: cd dashboard && npm ci + - name: Run ESLint + run: cd dashboard && npm run eslint + - name: Run Prettier + run: cd dashboard && npm run prettier + - name: Run Stylelint + run: cd dashboard && npm run stylelint + - name: Run Typecheck + run: cd dashboard && npm run typecheck + - name: Verify build + run: cd dashboard && npm run build + docs: name: Build documentation runs-on: ubuntu-latest @@ -137,3 +160,25 @@ jobs: uv run pytest -v --durations=0 tests env: PYTEST_ADDOPTS: "--color=yes" + + test-js: + name: Test - JavaScript + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v6 + with: + node-version: '22' + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: '3.12' + - name: Sync Python dependencies + run: uv sync --frozen --no-default-groups --extra apo --group dev --group agents --group core-stable + - name: Install JavaScript dependencies + run: cd dashboard && npm ci + - name: Run vitest + run: cd dashboard && npm run vitest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3d5ebf1..5349a1fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,3 +24,53 @@ repos: pass_filenames: false always_run: true args: ["."] + + - repo: local + hooks: + - id: prettier + name: prettier (dashboard) + language: system + pass_filenames: false + always_run: true + entry: > + bash -c ' + cd dashboard || exit 1 + if [ -d node_modules ]; then + echo "✅ node_modules already exists" + npx prettier --cache --write "**/*.{ts,tsx,mjs,cjs}" + else + echo "⚠️ node_modules not found — npx is not reliable. Skipping." + fi + ' + + - id: eslint + name: eslint (dashboard) + language: system + pass_filenames: false + always_run: true + entry: > + bash -c ' + cd dashboard || exit 1 + if [ -d node_modules ]; then + echo "✅ node_modules already exists" + npx eslint --cache --fix . + else + echo "⚠️ node_modules not found — npx is not reliable. Skipping." + fi + ' + + - id: stylelint + name: stylelint (dashboard) + language: system + pass_filenames: false + always_run: true + entry: > + bash -c ' + cd dashboard || exit 1 + if [ -d node_modules ]; then + echo "✅ node_modules already exists" + npx stylelint --cache --fix "**/*.css" + else + echo "⚠️ node_modules not found — npx is not reliable. Skipping." + fi + ' diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..b295889c --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,132 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +.DS_Store diff --git a/dashboard/.prettierrc.mjs b/dashboard/.prettierrc.mjs new file mode 100644 index 00000000..16e86a8f --- /dev/null +++ b/dashboard/.prettierrc.mjs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */ +const config = { + printWidth: 120, + singleQuote: true, + tabWidth: 2, + useTabs: false, + semi: true, + quoteProps: 'consistent', + jsxSingleQuote: true, + trailingComma: 'all', + bracketSpacing: true, + objectWrap: 'preserve', + arrowParens: 'always', + proseWrap: 'preserve', + endOfLine: 'lf', + plugins: ['@ianvs/prettier-plugin-sort-imports'], + importOrder: [ + '.*styles.css$', + '', + 'dayjs', + '^react$', + '^next$', + '^next/.*$', + '', + '', + '^@mantine/(.*)$', + '^@mantinex/(.*)$', + '^@mantine-tests/(.*)$', + '^@docs/(.*)$', + '^@/.*$', + '^../(?!.*.css$).*$', + '^./(?!.*.css$).*$', + '\\.css$', + ], + overrides: [ + { + files: '*.mdx', + options: { + printWidth: 120, + }, + }, + ], +}; + +export default config; diff --git a/dashboard/.storybook/constants.ts b/dashboard/.storybook/constants.ts new file mode 100644 index 00000000..f10165e1 --- /dev/null +++ b/dashboard/.storybook/constants.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Centralized constants that keep Storybook fixtures deterministic so Chromatic +// snapshots do not drift when the build environment changes. +export const STORY_DATE_NOW_MS = 1762775145209; +export const STORY_DATE_NOW_SECONDS = Math.floor(STORY_DATE_NOW_MS / 1000); + +// Use a fixed origin so any code that would normally read window.location.* +// in the app can rely on the same value from Storybook fixtures. Prefer HTTPS +// so Chromatic (which is served over HTTPS) avoids mixed-content fetch errors. +export const STORY_BASE_URL = 'https://storybook.agentlightning.invalid'; +export const STORY_LOCATION_HREF = `${STORY_BASE_URL}/storybook`; diff --git a/dashboard/.storybook/main.ts b/dashboard/.storybook/main.ts new file mode 100644 index 00000000..518184c9 --- /dev/null +++ b/dashboard/.storybook/main.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + core: { + disableWhatsNewNotifications: true, + disableTelemetry: true, + enableCrashReports: false, + }, + stories: ['../src/**/*.mdx', '../src/**/*.story.@(js|jsx|ts|tsx)'], + staticDirs: ['../static'], + addons: ['@storybook/addon-themes'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, +}; + +export default config; diff --git a/dashboard/.storybook/modes.ts b/dashboard/.storybook/modes.ts new file mode 100644 index 00000000..9c475747 --- /dev/null +++ b/dashboard/.storybook/modes.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +export const allModes = { + MD: { + viewport: 'md', + }, + LG: { + viewport: 'lg', + }, + XL: { + viewport: 'xl', + }, +} as const; diff --git a/dashboard/.storybook/preview.tsx b/dashboard/.storybook/preview.tsx new file mode 100644 index 00000000..067f5a05 --- /dev/null +++ b/dashboard/.storybook/preview.tsx @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +import '@mantine/core/styles.css'; +import 'mantine-datatable/styles.css'; +import '../src/styles/theme.css'; +import '../src/styles/app.css'; + +import { initialize, mswLoader } from 'msw-storybook-addon'; +import { ColorSchemeScript, MantineProvider } from '@mantine/core'; +import { shadcnCssVariableResolver } from '../src/cssVariableResolver'; +import { theme as mantineTheme } from '../src/theme'; +import { STORY_DATE_NOW_MS } from './constants'; + +type ColorSchemeValue = 'light' | 'dark'; + +initialize({ + onUnhandledRequest: 'bypass', + serviceWorker: { + url: '/mockServiceWorker.js', + }, +}); + +const fixedDateNow = (() => { + const patched = Date.now as typeof Date.now & { __storybookPatched?: boolean }; + if (patched.__storybookPatched) { + return patched; + } + const replacement = (() => STORY_DATE_NOW_MS) as typeof Date.now & { __storybookPatched?: boolean }; + replacement.__storybookPatched = true; + return replacement; +})(); + +Date.now = fixedDateNow; + +export const parameters = { + layout: 'fullscreen', + options: { + showPanel: false, + // @ts-expect-error – storybook throws build error for (a: any, b: any) + storySort: (a, b) => a.title.localeCompare(b.title, undefined, { numeric: true }), + }, + backgrounds: { disable: true }, + viewport: { + options: { + md: { name: 'md', styles: { width: '1280px', height: '800px' } }, + lg: { name: 'lg', styles: { width: '1920px', height: '1080px' } }, + xl: { name: 'xl', styles: { width: '2560px', height: '1440px' } }, + }, + }, +}; + +export const globalTypes = { + theme: { + name: 'Theme', + description: 'Mantine color scheme', + defaultValue: 'light', + toolbar: { + icon: 'mirror', + items: [ + { value: 'light', title: 'Light' }, + { value: 'dark', title: 'Dark' }, + ], + }, + }, +}; + +export const decorators = [ + (Story: any, context: any) => { + const scheme = (context.parameters.theme ?? context.globals.theme ?? 'light') as ColorSchemeValue; + return ( + + + + + ); + }, +]; + +export const loaders = [mswLoader]; diff --git a/dashboard/.stylelintignore b/dashboard/.stylelintignore new file mode 100644 index 00000000..f316aa71 --- /dev/null +++ b/dashboard/.stylelintignore @@ -0,0 +1,5 @@ +# Generated files +dist + +# Theme files +theme.css diff --git a/dashboard/.stylelintrc.json b/dashboard/.stylelintrc.json new file mode 100644 index 00000000..4ea6506d --- /dev/null +++ b/dashboard/.stylelintrc.json @@ -0,0 +1,28 @@ +{ + "extends": ["stylelint-config-standard-scss"], + "rules": { + "custom-property-pattern": null, + "selector-class-pattern": null, + "scss/no-duplicate-mixins": null, + "declaration-empty-line-before": null, + "declaration-block-no-redundant-longhand-properties": null, + "alpha-value-notation": null, + "custom-property-empty-line-before": null, + "property-no-vendor-prefix": null, + "color-function-notation": null, + "length-zero-no-unit": null, + "selector-not-notation": null, + "no-descending-specificity": null, + "comment-empty-line-before": null, + "scss/at-mixin-pattern": null, + "scss/at-rule-no-unknown": null, + "value-keyword-case": null, + "media-feature-range-notation": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["global"] + } + ] + } +} diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 00000000..cdb4382c --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,27 @@ +# Agent-lightning Dashboard + +This is the dashboard for Agent-lightning. It is a web application that allows you to inspect your Agent-lightning store and debug running experiments. + +The dashboard is built with React, Mantine UI, and Storybook. + +## npm scripts + +## Build and dev scripts + +- `dev` – start development server +- `build` – build production version of the app +- `preview` – locally preview production build + +### Testing scripts + +- `eslint` - runs ESLint +- `stylelint` - runs Stylelint +- `prettier` - runs Prettier +- `typecheck` - runs TypeScript typecheck +- `vitest` – runs vitest tests +- `chromatic` – runs chromatic tests + +### Other scripts + +- `storybook` – starts storybook dev server +- `build-storybook` – build production storybook bundle to `storybook-static` diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js new file mode 100644 index 00000000..e4970e49 --- /dev/null +++ b/dashboard/eslint.config.js @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +// @ts-check +import stylistic from '@stylistic/eslint-plugin'; +import mantine from 'eslint-config-mantine'; +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; + +export default defineConfig([ + // These are arrays → safe to spread + ...tseslint.configs.recommended, + stylistic.configs.customize({ semi: true }), + + // mantine is often a single object → include as-is (or spread only if it's actually an array) + ...(Array.isArray(mantine) ? mantine : [mantine]), + + // ignores go as their own entry + { ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}'] }, + + // file-specific rules + { + files: ['**/*.story.tsx'], + rules: { 'no-console': 'off' }, + }, + + // project/TS settings + your custom rules + { + languageOptions: { + parserOptions: { + tsconfigRootDir: process.cwd(), + project: ['./tsconfig.json'], + }, + }, + rules: { + // Disabling conflict rules with prettier + '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }], + '@stylistic/no-trailing-spaces': 'error', + '@stylistic/no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1 }], + '@stylistic/jsx-quotes': ['error', 'prefer-single'], + '@stylistic/multiline-ternary': 'off', + '@stylistic/arrow-parens': ['error', 'always'], + '@stylistic/jsx-closing-bracket-location': 'off', + '@stylistic/operator-linebreak': 'off', + '@stylistic/jsx-newline': 'off', + '@stylistic/jsx-one-expression-per-line': 'off', + '@stylistic/indent': 'off', + '@stylistic/indent-binary-ops': 'off', + }, + }, +]); diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 00000000..de5c4c39 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,10306 @@ +{ + "name": "agent-lightning-dashboard", + "version": "0.2.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-lightning-dashboard", + "version": "0.2.2", + "dependencies": { + "@mantine/core": "8.3.5", + "@mantine/hooks": "8.3.5", + "@monaco-editor/react": "^4.7.0", + "@reduxjs/toolkit": "^2.9.2", + "@tabler/icons-react": "^3.35.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.18", + "mantine-datatable": "^8.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.4" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@ianvs/prettier-plugin-sort-imports": "^4.7.0", + "@storybook/addon-themes": "^9.1.10", + "@storybook/react": "^9.1.10", + "@storybook/react-vite": "^9.1.10", + "@stylistic/eslint-plugin": "^5.5.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.7.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.1", + "@vitejs/plugin-react": "^5.0.4", + "chromatic": "^13.3.3", + "eslint": "^9.37.0", + "eslint-config-mantine": "^4.0.3", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "identity-obj-proxy": "^3.0.0", + "jsdom": "^27.0.0", + "msw": "^2.11.6", + "msw-storybook-addon": "^2.0.6", + "postcss": "^8.5.6", + "postcss-preset-mantine": "1.18.0", + "postcss-simple-vars": "^7.0.1", + "prettier": "^3.6.2", + "prop-types": "^15.8.1", + "storybook": "^9.1.10", + "stylelint": "^16.25.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.0", + "vite": "^7.1.9", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.3.tgz", + "integrity": "sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/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/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memoize": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.3.tgz", + "integrity": "sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.0.3" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.3.tgz", + "integrity": "sha512-R3UKy/CKOyb1LZG/VRCTMcpiMDyLH7SH3JrraRdK6kf3GweWCOU3sgvE13W3TiDRbxnDKylzKJvhUAvWl9LQOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memoize": "^2.0.3", + "@cacheable/utils": "^2.0.3", + "@keyv/bigmap": "^1.0.2", + "hookified": "^1.12.1", + "keyv": "^5.5.3" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.1.0.tgz", + "integrity": "sha512-MX7XIUNwVRK+hjZcAbNJ0Z8DREo+Weu9vinBOjGU1thEi9F6vPhICzBbk4CCf3eEefKRz7n6TfZXwUFZTSgj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.12.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.5.3" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.1.0.tgz", + "integrity": "sha512-ZdxfOiaarMqMj+H7qwlt5EBKWaeGihSYVHdQv5lUsbn8MJJOTW82OIwirQ39U5tMZkNvy3bQE+ryzC+xTAb9/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "keyv": "^5.5.3" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/JounQin" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "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/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.7.0.tgz", + "integrity": "sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.2", + "@babel/parser": "^7.26.2", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@prettier/plugin-oxc": "^0.0.4", + "@vue/compiler-sfc": "2.7.x || 3.x", + "content-tag": "^4.0.0", + "prettier": "2 || 3 || ^4.0.0-0", + "prettier-plugin-ember-template-tag": "^2.1.0" + }, + "peerDependenciesMeta": { + "@prettier/plugin-oxc": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "content-tag": { + "optional": true + }, + "prettier-plugin-ember-template-tag": { + "optional": true + } + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.19", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", + "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "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/@inquirer/core/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", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "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", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz", + "integrity": "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "magic-string": "^0.30.0", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mantine/core": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.5.tgz", + "integrity": "sha512-PdVNLMgOS2vFhOujRi6/VC9ic8w3UDyKX7ftwDeJ7yQT8CiepUxfbWWYpVpnq23bdWh/7fIT2Pn1EY8r8GOk7g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "clsx": "^2.1.1", + "react-number-format": "^5.4.4", + "react-remove-scroll": "^2.7.1", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.41.0" + }, + "peerDependencies": { + "@mantine/hooks": "8.3.5", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/hooks": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz", + "integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz", + "integrity": "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", + "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "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": { + "node": ">=14" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz", + "integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@storybook/addon-themes": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/@storybook/addon-themes/-/addon-themes-9.1.16.tgz", + "integrity": "sha512-wAB11HfXmK7KcYI6an1+WQi2m9VPfFnM4EV66VOWR+1e1PUThfwr0LhaPXj1g32lFBWdmTZp/9YLGXTyJqSQwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.16" + } + }, + "node_modules/@storybook/builder-vite": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.16.tgz", + "integrity": "sha512-CyvYA5w1BKeSVaRavKi+euWxLffshq0v9Rz/5E9MKCitbYtjwkDH6UMIYmcbTs906mEBuYqrbz3nygDP0ppodw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "9.1.16", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.16", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.16.tgz", + "integrity": "sha512-GKlNNlmWeFBQxhQY5hZOSnFGbeKq69jal0dYNWoSImTjor28eYRHb9iQkDzRpijLPizBaB9MlxLsLrgFDp7adA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.16" + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/react": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.16.tgz", + "integrity": "sha512-M/SkHJJdtiGpodBJq9+DYmSkEOD+VqlPxKI+FvbHESTNs//1IgqFIjEWetd8quhd9oj/gvo4ICBAPu+UmD6M9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "9.1.16" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.16", + "typescript": ">= 4.9.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.16.tgz", + "integrity": "sha512-MsI4qTxdT6lMXQmo3IXhw3EaCC+vsZboyEZBx4pOJ+K/5cDJ6ZoQ3f0d4yGpVhumDxaxlnNAg954+f8WWXE1rQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.16" + } + }, + "node_modules/@storybook/react-vite": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.16.tgz", + "integrity": "sha512-WRKSq0XfQ/Qx66aKisQCfa/1UKwN9HjVbY6xrmsX7kI5zBdITxIcKInq6PWoPv91SJD7+Et956yX+F86R1aEXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "9.1.16", + "@storybook/react": "9.1.16", + "find-up": "^7.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^8.0.0", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.16", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.5.0.tgz", + "integrity": "sha512-IeZF+8H0ns6prg4VrkhgL+yrvDXWDH2cKchrbh80ejG9dQgZWp10epHMbgRuQvgchLII/lfh6Xn3lu6+6L86Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.0", + "@typescript-eslint/types": "^8.46.1", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "node_modules/@tabler/icons": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.35.0.tgz", + "integrity": "sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.35.0.tgz", + "integrity": "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.35.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "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" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", + "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.43", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.4.tgz", + "integrity": "sha512-99EDqiCkncCmvIZj3qJXBZbyoQ35ghOwVWNnQ5nj0Hnsv4Qm40HmrMJrceewjLVvsxV/JSU4qyx2CGcfMBmXJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.4.tgz", + "integrity": "sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/utils": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.4.tgz", + "integrity": "sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.4", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.4.tgz", + "integrity": "sha512-XICqf5Gi4648FGoBIeRgnHWSNDp+7R5tpclGosFaUUFzY6SfcpsfHNMnC7oDu/iOLBxYfxVzaQpylEvpgii3zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.4", + "magic-string": "^0.30.19", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.4.tgz", + "integrity": "sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "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" + } + }, + "node_modules/ansi-styles": { + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "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": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/baseline-browser-mapping": { + "version": "2.8.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", + "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacheable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.1.1.tgz", + "integrity": "sha512-LmF4AXiSNdiRbI2UjH8pAp9NIXxeQsTotpEaegPiDcnN0YPygDJDV3l/Urc0mL72JWdATEorKqIHEx55nDlONg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memoize": "^2.0.3", + "@cacheable/memory": "^2.0.3", + "@cacheable/utils": "^2.1.0", + "hookified": "^1.12.2", + "keyv": "^5.5.3", + "qified": "^0.5.0" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chromatic": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-13.3.3.tgz", + "integrity": "sha512-89w0hiFzIRqLbwGSkqSQzhbpuqaWpXYZuevSIF+570Wb+T/apeAkp3px8nMJcFw+zEdqw/i6soofkJtfirET1Q==", + "dev": true, + "license": "MIT", + "bin": { + "chroma": "dist/bin.js", + "chromatic": "dist/bin.js", + "chromatic-cli": "dist/bin.js" + }, + "peerDependencies": { + "@chromatic-com/cypress": "^0.*.* || ^1.0.0", + "@chromatic-com/playwright": "^0.*.* || ^1.0.0" + }, + "peerDependenciesMeta": { + "@chromatic-com/cypress": { + "optional": true + }, + "@chromatic-com/playwright": { + "optional": true + } + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "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/cliui/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", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "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", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "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" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "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/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-functions-list": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", + "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12 || >=16" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", + "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "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.241", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", + "integrity": "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "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": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.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", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "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-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-mantine": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-config-mantine/-/eslint-config-mantine-4.0.3.tgz", + "integrity": "sha512-NLU8aso+AtqRwXJhaB50y3gtrcW8XGJGqXYvMsWQur5YssD86xD+nyq9QX25dY2em15GfD4H4T6OmtW1StRRmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@eslint/js": "^9.9.1", + "eslint": "^9.9.1", + "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-react": "^7.35.0", + "typescript-eslint": "^8.3.0" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/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/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "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", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hookified": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.2.tgz", + "integrity": "sha512-aokUX1VdTpI0DUsndvW+OiwmBpKCu/NgRsSSkuSY0zq8PY6Q6a+lmOfAFDXAAOtBqJELvcWY9L1EVtzjbQcMdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "license": "MIT", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "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" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", + "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.7.2", + "cssstyle": "^5.3.1", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mantine-datatable": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/mantine-datatable/-/mantine-datatable-8.2.0.tgz", + "integrity": "sha512-dkdOnDw1DF9t5kxl4VEPM0baubNW2Tc+lvbc0bkVU1bNaDpXphSeoTGU6lai0sQ21hrKSMbfiK0ACczdrSOKyg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/icflorescu" + }, + "peerDependencies": { + "@mantine/core": ">=8.1", + "@mantine/hooks": ">=8.1", + "clsx": ">=2", + "react": ">=19", + "react-dom": ">=19" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/monaco-editor": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", + "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.1.7", + "marked": "14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.6.tgz", + "integrity": "sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.40.0", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.4", + "cookie": "^1.0.2", + "graphql": "^16.8.1", + "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", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^4.26.1", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw-storybook-addon": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.6.tgz", + "integrity": "sha512-ExCwDbcJoM2V3iQU+fZNp+axVfNc7DWMRh4lyTXebDO8IbpUNYKGFUrA8UqaeWiRGKVuS7+fU+KXEa9b0OP6uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.0.1" + }, + "peerDependencies": { + "msw": "^2.0.0" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "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", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "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": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-mixins": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-12.1.2.tgz", + "integrity": "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-js": "^4.0.1", + "postcss-simple-vars": "^7.0.1", + "sugarss": "^5.0.0", + "tinyglobby": "^0.2.14" + }, + "engines": { + "node": "^20.0 || ^22.0 || >=24.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", + "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-preset-mantine": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.18.0.tgz", + "integrity": "sha512-sP6/s1oC7cOtBdl4mw/IRKmKvYTuzpRrH/vT6v9enMU/EQEQ31eQnHcWtFghOXLH87AAthjL/Q75rLmin1oZoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-mixins": "^12.0.0", + "postcss-nested": "^7.0.2" + }, + "peerDependencies": { + "postcss": ">=8.0.0" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-simple-vars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", + "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.1" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.1.tgz", + "integrity": "sha512-+BtFN3dCP+IaFA6IYNOu/f/uK1B8xD2QWyOeCse0rjtAebBmkzgd2d1OAXi3ikAzJMIBSdzZDNZ3wZKEUDQs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.12.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-docgen": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.2.tgz", + "integrity": "sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.20.7", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, + "node_modules/react-docgen/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-number-format": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", + "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "license": "MIT", + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", + "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", + "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.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" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rettime": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", + "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/storybook": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.16.tgz", + "integrity": "sha512-339U14K6l46EFyRvaPS2ZlL7v7Pb+LlcXT8KAETrGPxq8v1sAjj2HAOB6zrlAK3M+0+ricssfAwsLCwt7Eg8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/user-event": "^14.6.1", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/spy": "3.2.4", + "better-opn": "^3.0.2", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", + "esbuild-register": "^3.5.0", + "recast": "^0.23.5", + "semver": "^7.6.2", + "ws": "^8.18.0" + }, + "bin": { + "storybook": "bin/index.cjs" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "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", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint": { + "version": "16.25.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz", + "integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.1.0", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^10.1.4", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^7.0.5", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.2.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-17.0.0.tgz", + "integrity": "sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.23.0" + } + }, + "node_modules/stylelint-config-recommended-scss": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-16.0.2.tgz", + "integrity": "sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-scss": "^4.0.9", + "stylelint-config-recommended": "^17.0.0", + "stylelint-scss": "^6.12.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.24.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-config-standard": { + "version": "39.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-39.0.1.tgz", + "integrity": "sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "stylelint-config-recommended": "^17.0.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.23.0" + } + }, + "node_modules/stylelint-config-standard-scss": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard-scss/-/stylelint-config-standard-scss-16.0.0.tgz", + "integrity": "sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stylelint-config-recommended-scss": "^16.0.1", + "stylelint-config-standard": "^39.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.23.1" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-scss": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.12.1.tgz", + "integrity": "sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.1", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.36.0", + "mdn-data": "^2.21.0", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-selector-parser": "^7.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.0.2" + } + }, + "node_modules/stylelint-scss/node_modules/known-css-properties": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz", + "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylelint-scss/node_modules/mdn-data": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.24.0.tgz", + "integrity": "sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylelint/node_modules/emoji-regex": { + "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/stylelint/node_modules/file-entry-cache": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz", + "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.13" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.18.tgz", + "integrity": "sha512-JUPnFgHMuAVmLmoH9/zoZ6RHOt5n9NlUw/sDXsTbROJ2SFoS2DS4s+swAV6UTeTbGH/CAsZIE6M8TaG/3jVxgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.1.0", + "flatted": "^3.3.3", + "hookified": "^1.12.0" + } + }, + "node_modules/stylelint/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/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", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sugarss": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-5.0.1.tgz", + "integrity": "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "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/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/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", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", + "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.4.tgz", + "integrity": "sha512-hV31h0/bGbtmDQc0KqaxsTO1v4ZQeF8ojDFuy4sZhFadwAqqvJA0LDw68QUocctI5EDpFMql/jVWKuPYHIf2Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.4", + "@vitest/mocker": "4.0.4", + "@vitest/pretty-format": "4.0.4", + "@vitest/runner": "4.0.4", + "@vitest/snapshot": "4.0.4", + "@vitest/spy": "4.0.4", + "@vitest/utils": "4.0.4", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.19", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.4", + "@vitest/browser-preview": "4.0.4", + "@vitest/browser-webdriverio": "4.0.4", + "@vitest/ui": "4.0.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/expect": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.4.tgz", + "integrity": "sha512-0ioMscWJtfpyH7+P82sGpAi3Si30OVV73jD+tEqXm5+rIx9LgnfdaOn45uaFkKOncABi/PHL00Yn0oW/wK4cXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.4", + "@vitest/utils": "4.0.4", + "chai": "^6.0.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.4.tgz", + "integrity": "sha512-UTtKgpjWj+pvn3lUM55nSg34098obGhSHH+KlJcXesky8b5wCUgg7s60epxrS6yAG8slZ9W8T9jGWg4PisMf5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.19" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/pretty-format": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.4.tgz", + "integrity": "sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/spy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.4.tgz", + "integrity": "sha512-G9L13AFyYECo40QG7E07EdYnZZYCKMTSp83p9W8Vwed0IyCG1GnpDLxObkx8uOGPXfDpdeVf24P1Yka8/q1s9g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/utils": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.4.tgz", + "integrity": "sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.4", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/chai": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "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", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "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", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "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", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "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/yargs/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", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 00000000..6c9997e2 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,70 @@ +{ + "name": "agent-lightning-dashboard", + "type": "module", + "version": "0.2.2", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "eslint": "eslint .", + "stylelint": "stylelint '**/*.css'", + "prettier": "prettier --check \"**/*.{ts,tsx,mjs,cjs}\"", + "vitest": "vitest run", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "chromatic": "chromatic" + }, + "dependencies": { + "@mantine/core": "8.3.5", + "@mantine/hooks": "8.3.5", + "@monaco-editor/react": "^4.7.0", + "@reduxjs/toolkit": "^2.9.2", + "@tabler/icons-react": "^3.35.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.18", + "mantine-datatable": "^8.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.4" + }, + "devDependencies": { + "@eslint/js": "^9.37.0", + "@ianvs/prettier-plugin-sort-imports": "^4.7.0", + "@storybook/addon-themes": "^9.1.10", + "@storybook/react": "^9.1.10", + "@storybook/react-vite": "^9.1.10", + "@stylistic/eslint-plugin": "^5.5.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.7.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.1", + "@vitejs/plugin-react": "^5.0.4", + "chromatic": "^13.3.3", + "eslint": "^9.37.0", + "eslint-config-mantine": "^4.0.3", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "identity-obj-proxy": "^3.0.0", + "jsdom": "^27.0.0", + "msw": "^2.11.6", + "msw-storybook-addon": "^2.0.6", + "postcss": "^8.5.6", + "postcss-preset-mantine": "1.18.0", + "postcss-simple-vars": "^7.0.1", + "prettier": "^3.6.2", + "prop-types": "^15.8.1", + "storybook": "^9.1.10", + "stylelint": "^16.25.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.0", + "vite": "^7.1.9", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.0" + } +} diff --git a/dashboard/postcss.config.cjs b/dashboard/postcss.config.cjs new file mode 100644 index 00000000..3d9f0c56 --- /dev/null +++ b/dashboard/postcss.config.cjs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +module.exports = { + plugins: { + 'postcss-preset-mantine': {}, + 'postcss-simple-vars': { + variables: { + 'mantine-breakpoint-xs': '36em', + 'mantine-breakpoint-sm': '48em', + 'mantine-breakpoint-md': '62em', + 'mantine-breakpoint-lg': '75em', + 'mantine-breakpoint-xl': '88em', + }, + }, + }, +}; diff --git a/dashboard/public/index.html b/dashboard/public/index.html new file mode 100644 index 00000000..c6700c0b --- /dev/null +++ b/dashboard/public/index.html @@ -0,0 +1,13 @@ + + + + + + + Agent-lightning Dashboard + + +
+ + + diff --git a/dashboard/public/main.tsx b/dashboard/public/main.tsx new file mode 100644 index 00000000..3565a5a2 --- /dev/null +++ b/dashboard/public/main.tsx @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +import '../src/main.js'; diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx new file mode 100644 index 00000000..641a9d4c --- /dev/null +++ b/dashboard/src/App.tsx @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +import '@mantine/core/styles.css'; +import 'mantine-datatable/styles.css'; +import './styles/theme.css'; +import './styles/app.css'; + +import { MantineProvider } from '@mantine/core'; +import { useColorScheme } from '@mantine/hooks'; +import { shadcnCssVariableResolver } from './cssVariableResolver'; +import { selectThemePreference } from './features/config/selectors'; +import { Router } from './Router'; +import { useAppSelector } from './store/hooks'; +import { shadcnTheme } from './theme'; + +export default function App() { + const themePreference = useAppSelector(selectThemePreference); + const systemColorScheme = useColorScheme(); + const resolvedColorScheme = themePreference === 'system' ? systemColorScheme : themePreference; + + return ( + + + + ); +} diff --git a/dashboard/src/Router.tsx b/dashboard/src/Router.tsx new file mode 100644 index 00000000..c5f1afd5 --- /dev/null +++ b/dashboard/src/Router.tsx @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; +import { AppLayoutWithState } from './layouts/AppLayout'; +import { ResourcesPage } from './pages/Resources.page'; +import { RolloutsPage } from './pages/Rollouts.page'; +import { SettingsPage } from './pages/Settings.page'; +import { TracesPage } from './pages/Traces.page'; + +const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'rollouts', + element: , + }, + { + path: 'resources', + element: , + }, + { + path: 'traces', + element: , + }, + { + path: 'settings', + element: , + }, + ], + }, +]); + +export function Router() { + return ; +} diff --git a/dashboard/src/components/AppAlertBanner.story.tsx b/dashboard/src/components/AppAlertBanner.story.tsx new file mode 100644 index 00000000..050c34bd --- /dev/null +++ b/dashboard/src/components/AppAlertBanner.story.tsx @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { Provider } from 'react-redux'; +import { initialConfigState } from '@/features/config/slice'; +import { initialRolloutsUiState } from '@/features/rollouts/slice'; +import type { AlertsState, AlertTone } from '@/features/ui/alert'; +import { initialDrawerState } from '@/features/ui/drawer/slice'; +import { createAppStore } from '@/store'; +import { STORY_BASE_URL, STORY_DATE_NOW_MS } from '../../.storybook/constants'; +import { AppAlertBanner } from './AppAlertBanner'; + +const meta: Meta = { + title: 'Components/AppAlertBanner', + component: AppAlertBanner, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; + +type Story = StoryObj; + +function renderWithAlert(message: string, tone: AlertTone) { + const alertState: AlertsState = { + alerts: [ + { + id: 'storybook-alert', + message, + tone, + isVisible: true, + createdAt: STORY_DATE_NOW_MS, + }, + ], + }; + + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + }, + drawer: initialDrawerState, + rollouts: initialRolloutsUiState, + alert: alertState, + }); + + return ( + +
+ +
+
+ ); +} + +export const InfoAlert: Story = { + render: () => renderWithAlert('Background synchronization completed successfully.', 'info'), +}; + +export const WarningAlert: Story = { + render: () => + renderWithAlert('Rollout data may be stale. Check your network connection before continuing.', 'warning'), +}; + +export const ErrorAlert: Story = { + render: () => + renderWithAlert('Unable to reach the Agent-lightning API. Retry or adjust the backend settings.', 'error'), +}; + +export const NoAlert: Story = { + render: () => { + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + }, + drawer: initialDrawerState, + rollouts: initialRolloutsUiState, + alert: { alerts: [] }, + }); + + return ( + +
+ +
+
+ ); + }, +}; diff --git a/dashboard/src/components/AppAlertBanner.tsx b/dashboard/src/components/AppAlertBanner.tsx new file mode 100644 index 00000000..92dca91f --- /dev/null +++ b/dashboard/src/components/AppAlertBanner.tsx @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useEffect, useState } from 'react'; +import { IconAlertCircle, IconAlertTriangle, IconInfoCircle } from '@tabler/icons-react'; +import { Notification, Portal, Transition } from '@mantine/core'; +import { hideAlert, selectHighestPriorityAlert, type AppAlert } from '@/features/ui/alert'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; + +const ALERT_META = { + info: { + color: 'blue', + icon: IconInfoCircle, + }, + warning: { + color: 'yellow', + icon: IconAlertTriangle, + }, + error: { + color: 'red', + icon: IconAlertCircle, + }, +} as const; + +export function AppAlertBanner() { + const dispatch = useAppDispatch(); + const alert = useAppSelector(selectHighestPriorityAlert); + const [transitionAlert, setTransitionAlert] = useState(alert); + + useEffect(() => { + if (alert) { + setTransitionAlert(alert); + } + }, [alert]); + + const handleClose = (id?: string) => { + if (id) { + dispatch(hideAlert({ id })); + } + }; + + const currentAlert = alert ?? transitionAlert; + const mounted = Boolean(alert); + + if (!currentAlert) { + return null; + } + + const meta = ALERT_META[currentAlert.tone]; + const IconComponent = meta.icon; + + return ( + + setTransitionAlert(null)} + > + {(styles) => ( + } + color={meta.color} + variant='light' + withCloseButton + onClose={() => handleClose(currentAlert.id)} + style={{ + position: 'fixed', + top: 16, + right: 16, + maxWidth: 450, + width: 'calc(100% - 32px)', + zIndex: 2000, + boxShadow: 'var(--mantine-shadow-md)', + ...styles, + }} + > + {currentAlert.message} + + )} + + + ); +} diff --git a/dashboard/src/components/AppDrawer.component.tsx b/dashboard/src/components/AppDrawer.component.tsx new file mode 100644 index 00000000..1891d3f9 --- /dev/null +++ b/dashboard/src/components/AppDrawer.component.tsx @@ -0,0 +1,448 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; +import { Editor } from '@monaco-editor/react'; +import { IconCheck, IconCopy } from '@tabler/icons-react'; +import type { DataTableSortStatus } from 'mantine-datatable'; +import { + ActionIcon, + Badge, + Box, + CopyButton, + Drawer, + Group, + Stack, + Text, + Tooltip, + useMantineColorScheme, +} from '@mantine/core'; +import { useGetSpansQuery } from '@/features/rollouts'; +import { closeDrawer, openDrawer, selectDrawerContent, selectDrawerIsOpen } from '@/features/ui/drawer'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import type { Attempt, AttemptStatus, Rollout, RolloutStatus, Span } from '@/types'; +import { formatStatusLabel } from '@/utils/format'; +import { TracesTable, type TracesTableRecord } from './TracesTable.component'; + +const ATTEMPT_STATUS_COLORS: Record = { + failed: 'red', + preparing: 'violet', + running: 'blue', + succeeded: 'teal', + timeout: 'orange', + unresponsive: 'orange', +}; + +const ROLLOUT_STATUS_COLORS: Record = { + cancelled: 'gray', + failed: 'red', + preparing: 'violet', + queuing: 'blue', + requeuing: 'cyan', + running: 'blue', + succeeded: 'teal', +}; + +const SPAN_STATUS_COLORS: Record = { + UNSET: 'gray', + OK: 'teal', + ERROR: 'red', +}; + +const TRACES_SORT_FIELD_MAP: Record = { + name: 'name', + traceId: 'trace_id', + spanId: 'span_id', + parentId: 'parent_id', + statusCode: 'status_code', + startTime: 'start_time', + duration: 'duration', +}; + +type SortDirection = 'asc' | 'desc'; + +type LocalSortState = { + column: string; + direction: SortDirection; +}; + +function resolveTracesSortField(column: string): string { + return TRACES_SORT_FIELD_MAP[column] ?? 'start_time'; +} + +function getStatusBadgeColor(status: RolloutStatus | AttemptStatus, isAttempt: boolean) { + if (isAttempt) { + return ATTEMPT_STATUS_COLORS[status as AttemptStatus] ?? 'gray'; + } + + return ROLLOUT_STATUS_COLORS[status as RolloutStatus] ?? 'gray'; +} + +function formatJson(value: unknown) { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +export type AppDrawerProps = { + opened: boolean; + onClose: () => void; + title?: ReactNode; + body?: ReactNode; +}; + +export function AppDrawer({ opened, onClose, title, body }: AppDrawerProps) { + return ( + + + {body} + + + ); +} + +type TraceDrawerTitleProps = { + span: Span; +}; + +export function TraceDrawerTitle({ span }: TraceDrawerTitleProps) { + const spanStatusCode = span.status?.status_code ?? null; + const spanBadgeColor = spanStatusCode ? (SPAN_STATUS_COLORS[spanStatusCode] ?? 'gray') : undefined; + + return ( + + + {span.name ?? span.spanId} + {spanStatusCode ? ( + + {spanStatusCode} + + ) : null} + + + + {span.spanId} + + + {({ copied, copy }) => ( + + { + event.stopPropagation(); + copy(); + }} + > + {copied ? : } + + + )} + + + + + + Rollout + + + {span.rolloutId} + + + + + Attempt + + + {span.attemptId ?? '—'} + + + + + ); +} + +type RolloutAttemptDrawerTitleProps = { + rollout: Rollout; + attempt: Attempt | null; +}; + +export function RolloutAttemptDrawerTitle({ rollout, attempt }: RolloutAttemptDrawerTitleProps) { + const rolloutId = rollout.rolloutId; + const attemptId = attempt?.attemptId ?? null; + const rolloutStatus = rollout.status ?? null; + const attemptStatus = attempt?.status ?? null; + const rolloutStatusLabel = rolloutStatus ? formatStatusLabel(rolloutStatus) : null; + const attemptStatusLabel = attemptStatus ? formatStatusLabel(attemptStatus) : null; + const hasStatusMismatch = rolloutStatus !== null && attemptStatus !== null && rolloutStatus !== attemptStatus; + const rolloutBadgeColor = rolloutStatus ? getStatusBadgeColor(rolloutStatus, false) : undefined; + const attemptBadgeColor = attemptStatus ? getStatusBadgeColor(attemptStatus, true) : undefined; + const showRolloutBadgeInHeading = Boolean(rolloutStatusLabel && (!attemptStatus || hasStatusMismatch)); + const showAttemptBadge = Boolean(attemptStatusLabel && attemptStatus); + + return ( + + + {rolloutId} + + {({ copied, copy }) => ( + + { + event.stopPropagation(); + copy(); + }} + > + {copied ? : } + + + )} + + {showRolloutBadgeInHeading && rolloutStatusLabel ? ( + + {rolloutStatusLabel} + + ) : null} + + + {attemptId ? ( + + + Attempt + + + {attemptId} + + + ) : null} + {showAttemptBadge && attemptStatusLabel ? ( + + {attemptStatusLabel} + + ) : null} + {!showRolloutBadgeInHeading && !attemptStatus && rolloutStatusLabel ? ( + + {rolloutStatusLabel} + + ) : null} + + + ); +} + +type JsonEditorProps = { + value: unknown; +}; + +export function JsonEditor({ value }: JsonEditorProps) { + const { colorScheme } = useMantineColorScheme(); + const editorTheme = colorScheme === 'dark' ? 'vs-dark' : 'vs-light'; + + return ( + + + + ); +} + +type RolloutTracesDrawerBodyProps = { + rollout: Rollout; + attempt: Attempt | null; + onShowRollout: (record: TracesTableRecord) => void; + onShowSpanDetail: (record: TracesTableRecord) => void; +}; + +function RolloutTracesDrawerBody({ rollout, attempt, onShowRollout, onShowSpanDetail }: RolloutTracesDrawerBodyProps) { + const [page, setPage] = useState(1); + const [recordsPerPage, setRecordsPerPage] = useState(100); + const [sort, setSort] = useState({ + column: 'startTime', + direction: 'desc', + }); + + useEffect(() => { + setPage(1); + }, [rollout.rolloutId, attempt?.attemptId]); + + const queryArgs = useMemo( + () => ({ + rolloutId: rollout.rolloutId, + attemptId: attempt?.attemptId ?? undefined, + limit: recordsPerPage, + offset: Math.max(0, (page - 1) * recordsPerPage), + sortBy: resolveTracesSortField(sort.column), + sortOrder: sort.direction, + }), + [rollout.rolloutId, attempt?.attemptId, recordsPerPage, page, sort], + ); + + const { data, isFetching, isError, error, refetch } = useGetSpansQuery(queryArgs); + const spans = data?.items ?? []; + const totalRecords = data?.total ?? 0; + + const handleSortStatusChange = useCallback((status: DataTableSortStatus) => { + setSort({ + column: status.columnAccessor as string, + direction: status.direction, + }); + }, []); + + const handlePageChange = useCallback((nextPage: number) => { + setPage(nextPage); + }, []); + + const handleRecordsPerPageChange = useCallback((value: number) => { + setRecordsPerPage(value); + setPage(1); + }, []); + + return ( + + + Showing spans for rollout {rollout.rolloutId} + {attempt ? ` · Attempt ${attempt.sequenceId} (${attempt.attemptId})` : ' · Latest attempt'} + + + {}} + onRefetch={refetch} + onShowRollout={onShowRollout} + onShowSpanDetail={onShowSpanDetail} + recordsPerPageOptions={[50, 100, 200, 500]} + /> + + + ); +} + +export function AppDrawerContainer() { + const dispatch = useAppDispatch(); + const isOpen = useAppSelector(selectDrawerIsOpen); + const content = useAppSelector(selectDrawerContent); + + const handleClose = useCallback(() => { + dispatch(closeDrawer()); + }, [dispatch]); + + const derivedContent = useMemo(() => { + if (!content) { + return null; + } + + if (content.type === 'trace-detail') { + const { span } = content; + const title = ; + const body = ; + + return { title, body }; + } + + const rollout = content.rollout; + const attempt = content.attempt; + const title = ; + + if (content.type === 'rollout-json') { + const jsonValue = content.isNested && content.attempt ? content.attempt : rollout; + const body = jsonValue ? : null; + return { title, body }; + } + + if (content.type === 'rollout-traces') { + const body = ( + { + const attemptForRecord = attempt ?? rollout.attempt ?? null; + dispatch( + openDrawer({ + type: 'rollout-json', + rollout, + attempt: attemptForRecord, + isNested: content.isNested, + }), + ); + }} + onShowSpanDetail={(record) => { + const attemptForRecord = attempt ?? rollout.attempt ?? null; + dispatch( + openDrawer({ + type: 'trace-detail', + span: record, + rollout, + attempt: attemptForRecord, + }), + ); + }} + /> + ); + + return { title, body }; + } + + return null; + }, [content, dispatch]); + + if (!content || !derivedContent) { + return null; + } + + const { title, body } = derivedContent; + + return ; +} diff --git a/dashboard/src/components/AppDrawer.story.tsx b/dashboard/src/components/AppDrawer.story.tsx new file mode 100644 index 00000000..65f665d9 --- /dev/null +++ b/dashboard/src/components/AppDrawer.story.tsx @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { Provider } from 'react-redux'; +import { initialConfigState } from '@/features/config/slice'; +import { initialResourcesUiState } from '@/features/resources/slice'; +import { rolloutsApi } from '@/features/rollouts'; +import { initialRolloutsUiState } from '@/features/rollouts/slice'; +import { initialTracesUiState } from '@/features/traces/slice'; +import type { DrawerContent } from '@/features/ui/drawer'; +import { createAppStore } from '@/store'; +import type { Attempt, Rollout, Span } from '@/types'; +import { STORY_BASE_URL, STORY_DATE_NOW_SECONDS } from '../../.storybook/constants'; +import { AppDrawerContainer } from './AppDrawer.component'; + +const meta = { + title: 'Components/AppDrawer', + component: AppDrawerContainer, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const now = STORY_DATE_NOW_SECONDS; + +const baseAttempt: Attempt = { + rolloutId: 'ro-story-001', + attemptId: 'at-story-001', + sequenceId: 1, + startTime: now - 3600, + endTime: null, + status: 'running', + workerId: 'worker-story', + lastHeartbeatTime: now - 42, + metadata: { info: 'Sample metadata', runId: 'run-123' }, +}; + +const baseRollout: Rollout = { + rolloutId: 'ro-story-001', + input: { + task: 'Generate daily summary', + payload: { account: 'enterprise', date: '2024-02-19' }, + }, + startTime: now - 4000, + endTime: null, + mode: 'train', + resourcesId: 'rs-story-001', + status: 'running', + config: { retries: 1, priority: 'high' }, + metadata: { owner: 'storybook' }, + attempt: baseAttempt, +}; + +const noAttemptRollout: Rollout = { + ...baseRollout, + status: 'queuing', + attempt: null, +}; + +const mismatchRollout: Rollout = { + ...baseRollout, + status: 'running', + attempt: { + ...baseAttempt, + status: 'failed', + endTime: now - 1200, + metadata: { info: 'Latest attempt failed', reason: 'Timeout' }, + }, +}; + +const sampleSpan: Span = { + rolloutId: 'ro-story-001', + attemptId: 'at-story-001', + sequenceId: 2, + traceId: 'tr-story-001', + spanId: 'sp-story-001', + parentId: null, + name: 'Fetch Resources', + status: { status_code: 'OK', description: 'Completed successfully' }, + attributes: { + 'http.method': 'GET', + 'http.url': 'https://api.example.com/resources', + 'duration_ms': 120, + }, + startTime: now - 240, + endTime: now - 120, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, +}; + +const sampleTraces: Span[] = [ + sampleSpan, + { + ...sampleSpan, + spanId: 'sp-story-002', + name: 'Process Response', + parentId: 'sp-story-001', + sequenceId: 3, + status: { status_code: 'ERROR', description: 'Unexpected response code' }, + attributes: { + ...sampleSpan.attributes, + duration_ms: 240, + }, + startTime: now - 120, + endTime: now - 30, + }, +]; + +function renderWithDrawer(content: DrawerContent, options?: { spans?: Span[] }) { + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + }, + rollouts: initialRolloutsUiState, + resources: initialResourcesUiState, + traces: initialTracesUiState, + drawer: { + isOpen: true, + content, + }, + }); + + if (content.type === 'rollout-traces' && options?.spans) { + const defaultLimit = 100; + const queryArgs = { + rolloutId: content.rollout.rolloutId, + attemptId: content.attempt?.attemptId ?? undefined, + limit: defaultLimit, + offset: 0, + sortBy: 'start_time', + sortOrder: 'desc' as const, + }; + + store.dispatch( + rolloutsApi.util.upsertQueryData('getSpans', queryArgs, { + items: options.spans, + total: options.spans.length, + limit: defaultLimit, + offset: 0, + }), + ); + } + + return ( + + + + ); +} + +export const RolloutJson: Story = { + render: () => + renderWithDrawer({ + type: 'rollout-json', + rollout: baseRollout, + attempt: baseRollout.attempt, + isNested: false, + }), +}; + +export const NestedAttemptJson: Story = { + render: () => + renderWithDrawer({ + type: 'rollout-json', + rollout: baseRollout, + attempt: { + ...baseAttempt, + attemptId: 'at-story-002', + sequenceId: 2, + status: 'failed', + endTime: now - 1200, + metadata: { info: 'Secondary attempt', reason: 'Timeout' }, + }, + isNested: true, + }), +}; + +export const RolloutTraces: Story = { + render: () => + renderWithDrawer( + { + type: 'rollout-traces', + rollout: baseRollout, + attempt: baseRollout.attempt, + isNested: false, + }, + { spans: sampleTraces }, + ), +}; + +export const NoAttempt: Story = { + render: () => + renderWithDrawer({ + type: 'rollout-json', + rollout: noAttemptRollout, + attempt: null, + isNested: false, + }), +}; + +export const StatusMismatch: Story = { + render: () => + renderWithDrawer({ + type: 'rollout-json', + rollout: mismatchRollout, + attempt: mismatchRollout.attempt, + isNested: false, + }), +}; + +export const SpanDetail: Story = { + render: () => + renderWithDrawer({ + type: 'trace-detail', + span: sampleSpan, + rollout: mismatchRollout, + attempt: mismatchRollout.attempt, + }), +}; + +export const LightTheme: Story = { + render: () => + renderWithDrawer({ + type: 'rollout-json', + rollout: baseRollout, + attempt: baseRollout.attempt, + isNested: false, + }), + parameters: { + theme: 'light', + }, +}; + +export const DarkTheme: Story = { + render: () => + renderWithDrawer({ + type: 'trace-detail', + span: { + ...sampleSpan, + spanId: 'sp-story-002', + name: 'Process Response', + status: { status_code: 'ERROR', description: 'Unexpected response code' }, + }, + rollout: mismatchRollout, + attempt: mismatchRollout.attempt, + }), + parameters: { + theme: 'dark', + }, +}; diff --git a/dashboard/src/components/ResourcesTable.component.tsx b/dashboard/src/components/ResourcesTable.component.tsx new file mode 100644 index 00000000..716474b0 --- /dev/null +++ b/dashboard/src/components/ResourcesTable.component.tsx @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useCallback, useEffect, useMemo, useState, type ReactNode, type SetStateAction } from 'react'; +import { IconCheck, IconCopy, IconRefresh } from '@tabler/icons-react'; +import { DataTable, type DataTableColumn, type DataTableSortStatus } from 'mantine-datatable'; +import { ActionIcon, Box, Button, CopyButton, Group, Stack, Text, Tooltip } from '@mantine/core'; +import { useElementSize } from '@mantine/hooks'; +import type { Resources } from '@/types'; +import { getErrorDescriptor } from '@/utils/error'; +import { formatDateTime, safeStringify } from '@/utils/format'; +import { createResponsiveColumns, type ColumnVisibilityConfig } from '@/utils/table'; + +const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [50, 100, 200, 500]; + +const COLUMN_VISIBILITY: Record = { + resourcesId: { fixedWidth: 12, priority: 0 }, + version: { fixedWidth: 8, priority: 1 }, + createTime: { fixedWidth: 14, priority: 2 }, + updateTime: { fixedWidth: 14, priority: 2 }, + resourceCount: { fixedWidth: 8, priority: 3 }, + resourcesPreview: { minWidth: 16, priority: 4 }, +}; + +export type ResourcesTableRecord = Resources & { + resourceCount: number; + canExpand: boolean; + resourcesPreview: string; +}; + +function buildResourcesRecord(resources: Resources): ResourcesTableRecord { + const resourceCount = Object.keys(resources.resources ?? {}).length; + const resourcesValue = + resources.resources === null || typeof resources.resources === 'undefined' + ? '—' + : typeof resources.resources === 'string' + ? resources.resources + : safeStringify(resources.resources); + + return { + ...resources, + resourceCount, + canExpand: resourceCount > 0, + resourcesPreview: resourcesValue, + }; +} + +type ResourcesColumnsOptions = Record; + +function createResourcesColumns(_options: ResourcesColumnsOptions): DataTableColumn[] { + return [ + { + accessor: 'resourcesId', + title: 'Resources ID', + sortable: true, + render: ({ resourcesId }) => ( + + + {resourcesId} + + + {({ copied, copy }) => ( + + { + event.stopPropagation(); + copy(); + }} + > + {copied ? : } + + + )} + + + ), + }, + { + accessor: 'version', + title: 'Version', + sortable: true, + textAlign: 'left', + render: ({ version }) => {version}, + }, + { + accessor: 'createTime', + title: 'Created', + sortable: true, + textAlign: 'left', + render: ({ createTime }) => {formatDateTime(createTime)}, + }, + { + accessor: 'updateTime', + title: 'Updated', + sortable: true, + textAlign: 'left', + render: ({ updateTime }) => {formatDateTime(updateTime)}, + }, + { + accessor: 'resourceCount', + title: 'Count', + sortable: true, + textAlign: 'left', + render: ({ resourceCount }) => {resourceCount}, + }, + { + accessor: 'resourcesPreview', + title: 'Preview', + render: ({ resourcesPreview }) => ( + + {resourcesPreview} + + ), + }, + ]; +} + +type RowExpansionRenderer = (context: { + resources: Resources; + columns: DataTableColumn[]; +}) => ReactNode; + +export type ResourcesTableProps = { + resourcesList: Resources[] | undefined; + totalRecords: number; + isFetching: boolean; + isError: boolean; + error: unknown; + searchTerm: string; + sort: { column: string; direction: 'asc' | 'desc' }; + page: number; + recordsPerPage: number; + onSortStatusChange: (status: DataTableSortStatus) => void; + onPageChange: (page: number) => void; + onRecordsPerPageChange: (value: number) => void; + onResetFilters: () => void; + onRefetch: () => void; + recordsPerPageOptions?: number[]; + renderRowExpansion?: RowExpansionRenderer; +}; + +export function ResourcesTable({ + resourcesList, + totalRecords, + isFetching, + isError, + error, + searchTerm, + sort, + page, + recordsPerPage, + onSortStatusChange, + onPageChange, + onRecordsPerPageChange, + onResetFilters, + onRefetch, + recordsPerPageOptions = DEFAULT_RECORDS_PER_PAGE_OPTIONS, + renderRowExpansion, +}: ResourcesTableProps) { + const [expandedRecordIds, setExpandedRecordIds] = useState([]); + const { ref: tableContainerRef, width: containerWidth } = useElementSize(); + + const resourcesRecords = useMemo(() => { + if (!resourcesList) { + return []; + } + return resourcesList.map((resourcesItem) => buildResourcesRecord(resourcesItem)); + }, [resourcesList]); + + const columns = useMemo(() => createResourcesColumns({}), []); + + const responsiveColumns = useMemo( + () => createResponsiveColumns(columns, containerWidth, COLUMN_VISIBILITY), + [columns, containerWidth], + ); + + const totalPages = useMemo( + () => Math.max(1, Math.ceil(Math.max(0, totalRecords) / Math.max(1, recordsPerPage))), + [recordsPerPage, totalRecords], + ); + + useEffect(() => { + if (page > totalPages) { + onPageChange(totalPages); + } + }, [onPageChange, page, totalPages]); + + useEffect(() => { + setExpandedRecordIds((current) => + current.filter((id) => resourcesRecords.some((record) => record.resourcesId === id && record.canExpand)), + ); + }, [resourcesRecords]); + + const hasActiveFilters = searchTerm.trim().length > 0; + + const sortStatus: DataTableSortStatus = { + columnAccessor: sort.column, + direction: sort.direction, + }; + + const handleSortStatusChange = useCallback( + (status: DataTableSortStatus) => { + onSortStatusChange(status); + }, + [onSortStatusChange], + ); + + const errorDescriptor = isError ? getErrorDescriptor(error) : null; + const errorMessage = isError + ? `Resources are temporarily unavailable${errorDescriptor ? ` (${errorDescriptor})` : ''}.` + : 'Resources are temporarily unavailable.'; + + const emptyState = ( + + {isError ? ( + <> + + {errorMessage} + + + Use the retry button to try again, or adjust the filters to broaden the results. + + + + {hasActiveFilters ? ( + + ) : null} + + + ) : ( + <> + + No resources found + + + {hasActiveFilters + ? 'Try adjusting the search to see more results.' + : 'Try refreshing to fetch the latest resources.'} + + + + {hasActiveFilters ? ( + + ) : null} + + + )} + + ); + + return ( + + + classNames={{ root: 'resources-table' }} + withTableBorder + withColumnBorders + highlightOnHover + verticalAlign='center' + minHeight={resourcesRecords.length === 0 ? 500 : undefined} + idAccessor='resourcesId' + records={resourcesRecords} + columns={responsiveColumns} + totalRecords={totalRecords} + recordsPerPage={recordsPerPage} + page={page} + onPageChange={onPageChange} + onRecordsPerPageChange={onRecordsPerPageChange} + recordsPerPageOptions={recordsPerPageOptions} + sortStatus={sortStatus} + onSortStatusChange={handleSortStatusChange} + fetching={isFetching} + loaderSize='sm' + emptyState={resourcesRecords.length === 0 ? emptyState : undefined} + rowExpansion={ + renderRowExpansion + ? { + allowMultiple: true, + expandable: ({ record }) => record.canExpand, + expanded: { + recordIds: expandedRecordIds, + onRecordIdsChange: (nextRecordIds: SetStateAction) => { + setExpandedRecordIds((previous) => { + const resolved = + typeof nextRecordIds === 'function' + ? nextRecordIds(previous) + : ((nextRecordIds ?? []) as (string | number)[]); + return resolved + .map(String) + .filter((id) => + resourcesRecords.some( + (tableRecord) => tableRecord.resourcesId === id && tableRecord.canExpand, + ), + ); + }); + }, + }, + content: ({ record }) => renderRowExpansion({ resources: record, columns: responsiveColumns }), + } + : undefined + } + /> + + ); +} diff --git a/dashboard/src/components/ResourcesTable.story.tsx b/dashboard/src/components/ResourcesTable.story.tsx new file mode 100644 index 00000000..835e9a78 --- /dev/null +++ b/dashboard/src/components/ResourcesTable.story.tsx @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@tabler/icons-react'; +import { Box, Stack, TextInput, Title } from '@mantine/core'; +import type { Resources } from '@/types'; +import { ResourcesTable } from './ResourcesTable.component'; +import { ResourcesTree } from './ResourcesTree.component'; + +const meta: Meta = { + title: 'Components/ResourcesTable', + component: ResourcesTable, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; + +type Story = StoryObj; + +const sampleResources: Resources[] = [ + { + resourcesId: 'rs-story-001', + version: 1, + createTime: 1710806400, + updateTime: 1713412800, + resources: { + model: { + name: 'gpt-4', + version: '2024-01-01', + temperature: 0.7, + maxTokens: 2048, + topP: 0.9, + }, + database: { + host: 'db.example.com', + port: 5432, + name: 'production', + pool: { + min: 2, + max: 10, + idle: 30000, + }, + }, + cache: { + type: 'redis', + host: 'cache.example.com', + port: 6379, + ttl: 3600, + }, + }, + }, + { + resourcesId: 'rs-story-002', + version: 2, + createTime: 1712217600, + updateTime: 1714823200, + resources: { + model: { + name: 'claude-3-opus', + version: '2024-02-01', + temperature: 0.5, + maxTokens: 4096, + }, + storage: { + type: 's3', + bucket: 'training-data', + region: 'us-east-1', + credentials: { + accessKeyId: 'AKIA***', + encrypted: true, + }, + }, + compute: { + instances: [ + { id: 'i-001', type: 't3.large', zone: 'us-east-1a' }, + { id: 'i-002', type: 't3.large', zone: 'us-east-1b' }, + ], + autoScaling: { + min: 2, + max: 10, + targetCpu: 70, + }, + }, + }, + }, + { + resourcesId: 'rs-story-003', + version: 3, + createTime: 1709251200, + updateTime: 1711856800, + resources: { + model: { + name: 'gpt-3.5-turbo', + version: '2023-12-01', + temperature: 0.8, + maxTokens: 1024, + }, + monitoring: { + enabled: true, + interval: 60, + metrics: ['cpu', 'memory', 'disk', 'network'], + alerts: { + email: 'ops@example.com', + slack: '#alerts', + pagerduty: true, + }, + }, + }, + }, + { + resourcesId: 'rs-story-004', + version: 1, + createTime: 1706745600, + updateTime: 1709347200, + resources: { + apiKeys: { + openai: 'sk-***', + anthropic: 'sk-ant-***', + replicate: 'r8-***', + }, + rateLimits: { + requestsPerMinute: 100, + tokensPerDay: 1000000, + concurrent: 5, + }, + }, + }, + { + resourcesId: 'rs-story-005', + version: 1, + createTime: 1704067200, + updateTime: 1706668800, + resources: {}, + }, +]; + +type WrapperProps = { + maxWidth: number; + resourcesList?: Resources[] | undefined; + isFetching?: boolean; + isError?: boolean; + error?: unknown; +}; + +function ResourcesTableStoryWrapper({ + maxWidth, + resourcesList = sampleResources, + isFetching = false, + isError = false, + error = null, +}: WrapperProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [page, setPage] = useState(1); + const [recordsPerPage, setRecordsPerPage] = useState(5); + const [sort, setSort] = useState<{ column: string; direction: 'asc' | 'desc' }>({ + column: 'resourcesId', + direction: 'asc', + }); + + const baseResources = resourcesList ?? []; + + const filteredResources = useMemo(() => { + const normalized = searchTerm.trim().toLowerCase(); + if (normalized.length === 0) { + return baseResources; + } + return baseResources.filter((resource) => resource.resourcesId.toLowerCase().includes(normalized)); + }, [baseResources, searchTerm]); + + const sortedResources = useMemo(() => { + const items = filteredResources.slice(); + const resolveSortValue = (resource: Resources, column: string) => { + switch (column) { + case 'version': + return resource.version; + case 'createTime': + return resource.createTime; + case 'updateTime': + return resource.updateTime; + case 'resourceCount': + return Object.keys(resource.resources ?? {}).length; + case 'resourcesId': + default: + return resource.resourcesId; + } + }; + + items.sort((a, b) => { + const aValue = resolveSortValue(a, sort.column); + const bValue = resolveSortValue(b, sort.column); + + if (aValue === bValue) { + return 0; + } + + if (typeof aValue === 'number' && typeof bValue === 'number') { + return aValue - bValue; + } + + return String(aValue).localeCompare(String(bValue)); + }); + + if (sort.direction === 'desc') { + items.reverse(); + } + + return items; + }, [filteredResources, sort]); + + const totalRecordsValue = sortedResources.length; + + const pagedResources = useMemo(() => { + const startIndex = (page - 1) * recordsPerPage; + return sortedResources.slice(startIndex, startIndex + recordsPerPage); + }, [page, recordsPerPage, sortedResources]); + + return ( + + + Resources + { + setSearchTerm(event.currentTarget.value); + setPage(1); + }} + leftSection={} + data-testid='resources-search-input' + w='100%' + style={{ maxWidth: 360 }} + /> + { + setSort({ + column: status.columnAccessor as string, + direction: status.direction, + }); + }} + onPageChange={setPage} + onRecordsPerPageChange={(value) => { + setRecordsPerPage(value); + setPage(1); + }} + onResetFilters={() => { + setSearchTerm(''); + setSort({ column: 'resourcesId', direction: 'asc' }); + setPage(1); + }} + onRefetch={() => undefined} + recordsPerPageOptions={[5, 10, 20]} + renderRowExpansion={({ resources }) => } + /> + + + ); +} + +export const WideContainer: Story = { + render: () => , +}; + +export const MediumContainer: Story = { + render: () => , +}; + +export const NarrowContainer: Story = { + render: () => , +}; + +export const DrawerWidth: Story = { + render: () => , +}; + +export const ErrorState: Story = { + render: () => ( + + ), +}; + +export const EmptyResources: Story = { + render: () => ( + + ), +}; + +export const LoadingState: Story = { + render: () => , +}; diff --git a/dashboard/src/components/ResourcesTree.component.tsx b/dashboard/src/components/ResourcesTree.component.tsx new file mode 100644 index 00000000..37b37f11 --- /dev/null +++ b/dashboard/src/components/ResourcesTree.component.tsx @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useMemo } from 'react'; +import { IconAlertCircle, IconChevronRight } from '@tabler/icons-react'; +import { Box, Group, Stack, Text, Tree, type TreeNodeData } from '@mantine/core'; +import type { Resources } from '@/types'; +import { safeStringify } from '@/utils/format'; + +function convertToTreeData(obj: any, key: string = 'root', parentPath = ''): TreeNodeData { + const isObject = obj !== null && typeof obj === 'object' && !Array.isArray(obj); + const isArray = Array.isArray(obj); + const currentPath = parentPath ? `${parentPath}.${key}` : key; + + if (isObject) { + const children = Object.entries(obj).map(([childKey, childValue]) => + convertToTreeData(childValue, childKey, currentPath), + ); + + return { + value: currentPath, + label: ( + + + {key} + + + (Object) + + + ), + children: children.length > 0 ? children : undefined, + }; + } + + if (isArray) { + const children = obj.map((item: any, index: number) => convertToTreeData(item, `[${index}]`, currentPath)); + + return { + value: currentPath, + label: ( + + + {key} + + + (Array[ + {obj.length} + ]) + + + ), + children: children.length > 0 ? children : undefined, + }; + } + + // Primitive value + return { + value: currentPath, + label: ( + + + {key}: + + + {safeStringify(obj)} + + + ), + }; +} + +export type ResourcesTreeProps = { + resources: Resources; +}; + +export function ResourcesTree({ resources }: ResourcesTreeProps) { + const resourcesDict = resources.resources ?? {}; + + const treeData = useMemo(() => { + const entries = Object.entries(resourcesDict); + + if (entries.length === 0) { + return []; + } + + return entries.map(([key, value]) => convertToTreeData(value, key)); + }, [resourcesDict]); + + if (treeData.length === 0) { + return ( + + + + No resources found + + + ); + } + + return ( + + ( + + {hasChildren && ( + + + + )} + {node.label} + + )} + /> + + ); +} diff --git a/dashboard/src/components/ResourcesTree.story.tsx b/dashboard/src/components/ResourcesTree.story.tsx new file mode 100644 index 00000000..fd0c0366 --- /dev/null +++ b/dashboard/src/components/ResourcesTree.story.tsx @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { Box, Stack, Title } from '@mantine/core'; +import type { Resources } from '@/types'; +import { ResourcesTree } from './ResourcesTree.component'; + +const meta: Meta = { + title: 'Components/ResourcesTree', + component: ResourcesTree, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; + +type Story = StoryObj; + +const simpleResources: Resources = { + resourcesId: 'rs-simple-001', + version: 1, + createTime: 1704067200, + updateTime: 1706668800, + resources: { + apiKey: { value: 'sk-test-key-123', type: 'secret' }, + maxRetries: { value: 3, description: 'Maximum retry attempts' }, + timeout: { value: 30000, unit: 'ms' }, + enabled: { value: true }, + }, +}; + +const nestedResources: Resources = { + resourcesId: 'rs-nested-001', + version: 2, + createTime: 1709251200, + updateTime: 1711856800, + resources: { + model: { + name: 'gpt-4', + version: '2024-01-01', + temperature: 0.7, + maxTokens: 2048, + topP: 0.9, + }, + database: { + host: 'db.example.com', + port: 5432, + name: 'production', + pool: { + min: 2, + max: 10, + idle: 30000, + }, + }, + cache: { + type: 'redis', + host: 'cache.example.com', + port: 6379, + ttl: 3600, + }, + }, +}; + +const arrayResources: Resources = { + resourcesId: 'rs-array-001', + version: 3, + createTime: 1712217600, + updateTime: 1714823200, + resources: { + compute: { + instances: [ + { id: 'i-001', type: 't3.large', zone: 'us-east-1a', status: 'running' }, + { id: 'i-002', type: 't3.large', zone: 'us-east-1b', status: 'running' }, + { id: 'i-003', type: 't3.xlarge', zone: 'us-east-1c', status: 'stopped' }, + ], + autoScaling: { + min: 2, + max: 10, + targetCpu: 70, + }, + }, + tags: ['production', 'ml-training', 'auto-scale'], + ports: [80, 443, 8080], + }, +}; + +const complexResources: Resources = { + resourcesId: 'rs-complex-001', + version: 4, + createTime: 1706745600, + updateTime: 1710000000, + resources: { + model: { + name: 'claude-3-opus', + version: '2024-02-01', + temperature: 0.5, + maxTokens: 4096, + providers: [ + { name: 'anthropic', priority: 1, enabled: true }, + { name: 'aws-bedrock', priority: 2, enabled: false }, + ], + }, + storage: { + type: 's3', + bucket: 'training-data', + region: 'us-east-1', + credentials: { + accessKeyId: 'AKIA***', + encrypted: true, + }, + lifecycle: { + transitionToIA: 30, + transitionToGlacier: 90, + expiration: 365, + }, + }, + monitoring: { + enabled: true, + interval: 60, + metrics: ['cpu', 'memory', 'disk', 'network'], + alerts: { + email: 'ops@example.com', + slack: '#alerts', + pagerduty: true, + thresholds: { + cpu: { warning: 70, critical: 90 }, + memory: { warning: 80, critical: 95 }, + disk: { warning: 75, critical: 90 }, + }, + }, + }, + apiKeys: { + openai: 'sk-***', + anthropic: 'sk-ant-***', + replicate: 'r8-***', + }, + rateLimits: { + requestsPerMinute: 100, + tokensPerDay: 1000000, + concurrent: 5, + burstMultiplier: 1.5, + }, + }, +}; + +const emptyResources: Resources = { + resourcesId: 'rs-empty-001', + version: 1, + createTime: 1702000000, + updateTime: 1704600000, + resources: {}, +}; + +type WrapperProps = { + resources: Resources; + maxWidth?: number; +}; + +function ResourcesTreeStoryWrapper({ resources, maxWidth = 800 }: WrapperProps) { + return ( + + + + Resources: + {resources.resourcesId} + + + + + ); +} + +export const SimpleValues: Story = { + render: () => , +}; + +export const NestedObjects: Story = { + render: () => , +}; + +export const WithArrays: Story = { + render: () => , +}; + +export const ComplexStructure: Story = { + render: () => , +}; + +export const EmptyResources: Story = { + render: () => , +}; + +export const NarrowContainer: Story = { + render: () => , +}; + +export const WideContainer: Story = { + render: () => , +}; diff --git a/dashboard/src/components/RolloutTable.component.tsx b/dashboard/src/components/RolloutTable.component.tsx new file mode 100644 index 00000000..a7026861 --- /dev/null +++ b/dashboard/src/components/RolloutTable.component.tsx @@ -0,0 +1,774 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useCallback, useEffect, useMemo, useState, type ReactNode, type SetStateAction } from 'react'; +import { + IconAlertCircle, + IconCheck, + IconCopy, + IconFileDescription, + IconRefresh, + IconReload, + IconTimeline, +} from '@tabler/icons-react'; +import { DataTable, type DataTableColumn, type DataTableSortStatus } from 'mantine-datatable'; +import { + ActionIcon, + Alert, + Badge, + Box, + Button, + CopyButton, + Group, + MultiSelect, + Stack, + Text, + Tooltip, +} from '@mantine/core'; +import { useElementSize } from '@mantine/hooks'; +import { + type Attempt, + type AttemptStatus, + type Rollout, + type RolloutMode, + type RolloutsSortState, + type RolloutStatus, +} from '@/features/rollouts'; +import { + clampToNow, + formatDateTime, + formatDuration, + formatRelativeTime, + formatStatusLabel, + safeStringify, + toTimestamp, +} from '@/utils/format'; +import { createResponsiveColumns, type ColumnVisibilityConfig } from '@/utils/table'; + +const ROLLOUT_STATUS_OPTIONS: RolloutStatus[] = [ + 'queuing', + 'preparing', + 'running', + 'failed', + 'succeeded', + 'cancelled', + 'requeuing', +]; + +const ATTEMPT_STATUS_COLORS: Record = { + failed: 'red', + preparing: 'violet', + running: 'blue', + succeeded: 'teal', + timeout: 'orange', + unresponsive: 'orange', +}; + +const ROLLOUT_STATUS_COLORS: Record = { + cancelled: 'gray', + failed: 'red', + preparing: 'violet', + queuing: 'gray', + requeuing: 'gray', + running: 'blue', + succeeded: 'teal', +}; + +const ROLLOUT_MODE_OPTIONS: RolloutMode[] = ['train', 'val', 'test']; + +const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [50, 100, 200, 500]; + +const COLUMN_VISIBILITY: Record = { + rolloutId: { fixedWidth: 10, priority: 0 }, + actionsPlaceholder: { fixedWidth: 6.5, priority: 0 }, + inputText: { minWidth: 14, priority: 1 }, + statusValue: { fixedWidth: 10, priority: 1 }, + startTimestamp: { fixedWidth: 12, priority: 2 }, + durationSeconds: { fixedWidth: 10, priority: 2 }, + attemptId: { fixedWidth: 12, priority: 3 }, + resourcesId: { fixedWidth: 8, priority: 3 }, + mode: { fixedWidth: 8, priority: 3 }, + lastHeartbeatTimestamp: { fixedWidth: 10, priority: 3 }, + workerId: { fixedWidth: 10, priority: 3 }, +}; + +export type RolloutTableRecord = Rollout & { + attemptId: string | null; + attemptSequence: number | null; + isNested: boolean; + canExpand: boolean; + inputText: string; + attemptStatus?: AttemptStatus; + statusValue: string; + startTimestamp: number | null; + durationSeconds: number | null; + lastHeartbeatTimestamp: number | null; + workerId: string | null; + actionsPlaceholder?: null; +}; + +function selectHeartbeatTimestamp(attempt?: Attempt | null): number | null { + if (!attempt || attempt.lastHeartbeatTime == null || Number.isNaN(attempt.lastHeartbeatTime)) { + return null; + } + + return attempt.lastHeartbeatTime; +} + +export function buildRolloutRecord(rollout: Rollout): RolloutTableRecord { + const latestAttempt = rollout.attempt; + const inputValue = + rollout.input === null || typeof rollout.input === 'undefined' + ? '—' + : typeof rollout.input === 'string' + ? rollout.input + : safeStringify(rollout.input); + const startTimestamp = toTimestamp(latestAttempt?.startTime ?? rollout.startTime); + const endTimestamp = toTimestamp(latestAttempt?.endTime ?? rollout.endTime); + const durationSeconds = clampToNow(startTimestamp, endTimestamp); + const attemptStatus = latestAttempt?.status; + const sequenceId = latestAttempt?.sequenceId; + const statusValue = + attemptStatus && attemptStatus !== rollout.status ? `${rollout.status}-${attemptStatus}` : rollout.status; + + return { + ...rollout, + attempt: latestAttempt ?? null, + attemptId: latestAttempt?.attemptId ?? null, + attemptSequence: latestAttempt?.sequenceId ?? null, + isNested: false, + canExpand: Boolean(sequenceId && sequenceId > 1), + inputText: inputValue, + attemptStatus, + statusValue, + startTimestamp, + durationSeconds, + lastHeartbeatTimestamp: rollout.attempt?.lastHeartbeatTime ?? null, + workerId: latestAttempt?.workerId ?? null, + actionsPlaceholder: null, + }; +} + +function buildAttemptRecord(rollout: Rollout, attempt: Attempt): RolloutTableRecord { + const inputValue = + rollout.input === null || typeof rollout.input === 'undefined' + ? '—' + : typeof rollout.input === 'string' + ? rollout.input + : safeStringify(rollout.input); + const startTimestamp = toTimestamp(attempt.startTime ?? rollout.startTime); + const endTimestamp = toTimestamp(attempt.endTime); + const durationSeconds = clampToNow(startTimestamp, endTimestamp); + const lastHeartbeatTimestamp = selectHeartbeatTimestamp(attempt); + + return { + ...rollout, + attempt, + attemptId: attempt.attemptId, + attemptSequence: attempt.sequenceId, + isNested: true, + canExpand: false, + inputText: inputValue, + attemptStatus: attempt.status, + statusValue: attempt.status, + startTimestamp, + durationSeconds, + lastHeartbeatTimestamp, + workerId: attempt.workerId ?? null, + actionsPlaceholder: null, + }; +} + +function getStatusBadge(status: string, kind: 'rollout' | 'attempt') { + const color = + kind === 'rollout' + ? (ROLLOUT_STATUS_COLORS[status as RolloutStatus] ?? 'gray') + : (ATTEMPT_STATUS_COLORS[status as AttemptStatus] ?? 'gray'); + + return ( + + {formatStatusLabel(status)} + + ); +} + +type RolloutColumnsOptions = { + statusFilters: RolloutStatus[]; + onStatusFilterChange: (values: RolloutStatus[]) => void; + onStatusFilterReset: () => void; + modeFilters: RolloutMode[]; + onModeFilterChange: (values: RolloutMode[]) => void; + onModeFilterReset: () => void; + onViewRawJson?: (record: RolloutTableRecord) => void; + onViewTraces?: (record: RolloutTableRecord) => void; +}; + +function createRolloutColumns({ + statusFilters, + onStatusFilterChange, + onStatusFilterReset, + modeFilters, + onModeFilterChange, + onModeFilterReset, + onViewRawJson, + onViewTraces, +}: RolloutColumnsOptions): DataTableColumn[] { + const statusOptions = ROLLOUT_STATUS_OPTIONS.map((status) => ({ + value: status, + label: formatStatusLabel(status), + })); + const modeOptions = ROLLOUT_MODE_OPTIONS.map((mode) => ({ + value: mode, + label: formatStatusLabel(mode), + })); + + return [ + { + accessor: 'rolloutId', + title: 'Rollout', + sortable: true, + render: ({ rolloutId }) => ( + + + {rolloutId} + + + {({ copied, copy }) => ( + + { + event.stopPropagation(); + copy(); + }} + > + {copied ? : } + + + )} + + + ), + }, + { + accessor: 'attemptId', + title: 'Attempt', + sortable: true, + render: ({ attemptId, attemptSequence, isNested }) => ( + + + {attemptId ?? '—'} + + {attemptId && ( + + {({ copied, copy }) => ( + + { + event.stopPropagation(); + copy(); + }} + > + {copied ? : } + + + )} + + )} + {attemptSequence && (isNested || attemptSequence > 1) && ( + } pl={6} pr={6}> + {attemptSequence} + + )} + + ), + }, + { + accessor: 'inputText', + title: 'Input', + render: ({ inputText }) => ( + + {inputText} + + ), + }, + { + accessor: 'statusValue', + title: 'Status', + sortable: true, + filter: ({ close }) => ( + + onStatusFilterChange(values as RolloutStatus[])} + /> + + + ), + filtering: statusFilters.length > 0, + render: ({ status, attemptStatus, isNested }) => { + if (isNested) { + return {getStatusBadge(attemptStatus ?? 'unknown', 'attempt')}; + } + + if (attemptStatus && attemptStatus !== status) { + return ( + + {getStatusBadge(status, 'rollout')} + + — + + {getStatusBadge(attemptStatus, 'attempt')} + + ); + } + + return getStatusBadge(status, 'rollout'); + }, + }, + { + accessor: 'resourcesId', + title: 'Resources', + sortable: true, + render: ({ resourcesId }) => ( + + {resourcesId ?? '—'} + + ), + }, + { + accessor: 'mode', + title: 'Mode', + sortable: true, + filter: ({ close }) => ( + + onModeFilterChange(values as RolloutMode[])} + /> + + + ), + filtering: modeFilters.length > 0, + render: ({ mode }) => ( + + {mode ?? '—'} + + ), + }, + { + accessor: 'startTimestamp', + title: 'Start Time', + sortable: true, + textAlign: 'left', + render: ({ startTimestamp }) => {formatDateTime(startTimestamp)}, + }, + { + accessor: 'durationSeconds', + title: 'Duration', + sortable: true, + textAlign: 'left', + render: ({ durationSeconds }) => {formatDuration(durationSeconds)}, + }, + { + accessor: 'lastHeartbeatTimestamp', + title: 'Last Heartbeat', + sortable: true, + textAlign: 'left', + render: ({ lastHeartbeatTimestamp, attempt, isNested }) => { + if (!attempt && isNested) { + return ( + + — + + ); + } + return {formatRelativeTime(lastHeartbeatTimestamp)}; + }, + }, + { + accessor: 'workerId', + title: 'Worker', + sortable: true, + render: ({ workerId }) => ( + + {workerId ?? '—'} + + ), + }, + { + accessor: 'actionsPlaceholder', + title: 'Actions', + render: (record) => ( + + + { + event.stopPropagation(); + onViewRawJson?.(record); + }} + > + + + + + { + event.stopPropagation(); + onViewTraces?.(record); + }} + > + + + + + ), + }, + ]; +} + +type RowExpansionRenderer = (context: { + rollout: Rollout; + columns: DataTableColumn[]; +}) => ReactNode; + +export type RolloutTableProps = { + rollouts: Rollout[] | undefined; + totalRecords: number; + isFetching: boolean; + isError: boolean; + error: unknown; + searchTerm: string; + statusFilters: RolloutStatus[]; + modeFilters: RolloutMode[]; + sort: RolloutsSortState; + page: number; + recordsPerPage: number; + onStatusFilterChange: (values: RolloutStatus[]) => void; + onStatusFilterReset: () => void; + onModeFilterChange: (values: RolloutMode[]) => void; + onModeFilterReset: () => void; + onSortStatusChange: (status: DataTableSortStatus) => void; + onPageChange: (page: number) => void; + onRecordsPerPageChange: (value: number) => void; + onResetFilters: () => void; + onRefetch: () => void; + onViewRawJson?: (record: RolloutTableRecord) => void; + onViewTraces?: (record: RolloutTableRecord) => void; + recordsPerPageOptions?: number[]; + renderRowExpansion?: RowExpansionRenderer; +}; + +export function RolloutTable({ + rollouts, + totalRecords, + isFetching, + isError, + error, + searchTerm, + statusFilters, + modeFilters, + sort, + page, + recordsPerPage, + onStatusFilterChange, + onStatusFilterReset, + onModeFilterChange, + onModeFilterReset, + onSortStatusChange, + onPageChange, + onRecordsPerPageChange, + onResetFilters, + onRefetch, + onViewRawJson, + onViewTraces, + recordsPerPageOptions = DEFAULT_RECORDS_PER_PAGE_OPTIONS, + renderRowExpansion, +}: RolloutTableProps) { + const [expandedRecordIds, setExpandedRecordIds] = useState([]); + const { ref: tableContainerRef, width: containerWidth } = useElementSize(); + + const rolloutRecords = useMemo(() => { + if (!rollouts) { + return []; + } + return rollouts.map((rolloutItem) => buildRolloutRecord(rolloutItem)); + }, [rollouts]); + + const columns = useMemo( + () => + createRolloutColumns({ + statusFilters, + onStatusFilterChange, + onStatusFilterReset, + modeFilters, + onModeFilterChange, + onModeFilterReset, + onViewRawJson, + onViewTraces, + }), + [ + statusFilters, + onStatusFilterChange, + onStatusFilterReset, + modeFilters, + onModeFilterChange, + onModeFilterReset, + onViewRawJson, + onViewTraces, + ], + ); + + const responsiveColumns = useMemo( + () => createResponsiveColumns(columns, containerWidth, COLUMN_VISIBILITY), + [columns, containerWidth], + ); + + const totalPages = useMemo( + () => Math.max(1, Math.ceil(Math.max(0, totalRecords) / Math.max(1, recordsPerPage))), + [recordsPerPage, totalRecords], + ); + + useEffect(() => { + if (page > totalPages) { + onPageChange(totalPages); + } + }, [onPageChange, page, totalPages]); + + useEffect(() => { + setExpandedRecordIds((current) => + current.filter((id) => rolloutRecords.some((record) => record.rolloutId === id && record.canExpand)), + ); + }, [rolloutRecords]); + + const hasActiveFilters = searchTerm.trim().length > 0 || statusFilters.length > 0 || modeFilters.length > 0; + + const sortStatus: DataTableSortStatus = { + columnAccessor: sort.column, + direction: sort.direction, + }; + + const handleSortStatusChange = useCallback( + (status: DataTableSortStatus) => { + onSortStatusChange(status); + }, + [onSortStatusChange], + ); + + const errorMessage = + isError && error && typeof error === 'object' && 'status' in (error as Record) + ? `Rollouts are temporarily unavailable (status: ${String((error as Record).status)}).` + : 'Rollouts are temporarily unavailable.'; + + const emptyState = ( + + {isError ? ( + <> + + {errorMessage} + + + Use the retry button to try again, or adjust the filters to broaden the results. + + + + {hasActiveFilters ? ( + + ) : null} + + + ) : ( + <> + + No rollouts found + + + {hasActiveFilters + ? 'Try adjusting the search or filters to see more results.' + : 'Try refreshing to fetch the latest rollouts.'} + + + + {hasActiveFilters ? ( + + ) : null} + + + )} + + ); + + return ( + + + classNames={{ root: 'rollouts-table' }} + withTableBorder + withColumnBorders + highlightOnHover + verticalAlign='center' + minHeight={rolloutRecords.length === 0 ? 500 : undefined} + idAccessor='rolloutId' + records={rolloutRecords} + columns={responsiveColumns} + totalRecords={totalRecords} + recordsPerPage={recordsPerPage} + page={page} + onPageChange={onPageChange} + onRecordsPerPageChange={onRecordsPerPageChange} + recordsPerPageOptions={recordsPerPageOptions} + sortStatus={sortStatus} + onSortStatusChange={handleSortStatusChange} + fetching={isFetching} + loaderSize='sm' + emptyState={rolloutRecords.length === 0 ? emptyState : undefined} + rowExpansion={ + renderRowExpansion + ? { + allowMultiple: true, + expandable: ({ record }) => record.canExpand, + expanded: { + recordIds: expandedRecordIds, + onRecordIdsChange: (nextRecordIds: SetStateAction) => { + setExpandedRecordIds((previous) => { + const resolved = + typeof nextRecordIds === 'function' + ? nextRecordIds(previous) + : ((nextRecordIds ?? []) as (string | number)[]); + return resolved + .map(String) + .filter((id) => + rolloutRecords.some((tableRecord) => tableRecord.rolloutId === id && tableRecord.canExpand), + ); + }); + }, + }, + content: ({ record }) => renderRowExpansion({ rollout: record, columns: responsiveColumns }), + } + : undefined + } + /> + + ); +} + +export type RolloutAttemptsTableProps = { + rollout: Rollout; + attempts: Attempt[] | undefined; + isFetching: boolean; + isError: boolean; + onRetry: () => void; + columns: DataTableColumn[]; +}; + +export function RolloutAttemptsTable({ + rollout, + attempts, + isFetching, + isError, + onRetry, + columns, +}: RolloutAttemptsTableProps) { + const attemptRecords = useMemo(() => { + if (!attempts) { + return []; + } + return attempts + .map((attempt) => buildAttemptRecord(rollout, attempt)) + .sort((a, b) => (b.attemptSequence ?? 0) - (a.attemptSequence ?? 0)) + .filter((record) => record.attemptSequence !== rollout.attempt?.sequenceId); + }, [attempts, rollout]); + + if (isError && !attemptRecords.length) { + return ( + }> + + Unable to load attempts for this rollout. + + + + ); + } + + const emptyState = ( + + + No attempts found for this rollout. + + + + ); + + return ( + + classNames={{ root: 'rollouts-table rollouts-table--nested' }} + withColumnBorders + noHeader + minHeight={0} + idAccessor='attemptId' + verticalAlign='center' + fetching={isFetching} + loaderSize='sm' + records={attemptRecords} + columns={columns} + emptyState={attemptRecords.length === 0 ? emptyState : undefined} + /> + ); +} diff --git a/dashboard/src/components/RolloutTable.story.tsx b/dashboard/src/components/RolloutTable.story.tsx new file mode 100644 index 00000000..911a27f2 --- /dev/null +++ b/dashboard/src/components/RolloutTable.story.tsx @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@tabler/icons-react'; +import { Box, Stack, TextInput, Title } from '@mantine/core'; +import type { RolloutsSortState } from '@/features/rollouts'; +import type { Rollout, RolloutMode, RolloutStatus } from '@/types'; +import { compareRecords } from '@/utils/table'; +import { STORY_DATE_NOW_SECONDS } from '../../.storybook/constants'; +import { buildRolloutRecord, RolloutTable, type RolloutTableRecord } from './RolloutTable.component'; + +const meta: Meta = { + title: 'Components/RolloutTable', + component: RolloutTable, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; + +type Story = StoryObj; + +const now = STORY_DATE_NOW_SECONDS; + +const sampleRollouts: Rollout[] = [ + { + rolloutId: 'ro-story-001', + input: { task: 'Generate onboarding summary' }, + startTime: now - 3200, + endTime: null, + mode: 'train', + resourcesId: 'rs-story-001', + status: 'running', + config: { retries: 1 }, + metadata: { owner: 'alice' }, + attempt: { + rolloutId: 'ro-story-001', + attemptId: 'at-story-010', + sequenceId: 1, + startTime: now - 3200, + endTime: null, + status: 'running', + workerId: 'worker-east', + lastHeartbeatTime: now - 45, + metadata: { info: 'Worker is processing' }, + }, + }, + { + rolloutId: 'ro-story-002', + input: { task: 'Classify feedback tickets' }, + startTime: now - 7200, + endTime: now - 5400, + mode: 'val', + resourcesId: 'rs-story-002', + status: 'succeeded', + config: { retries: 2 }, + metadata: { owner: 'bob' }, + attempt: { + rolloutId: 'ro-story-002', + attemptId: 'at-story-011', + sequenceId: 2, + startTime: now - 6200, + endTime: now - 5400, + status: 'succeeded', + workerId: 'worker-north', + lastHeartbeatTime: now - 5400, + metadata: { previousAttempt: 'at-story-010' }, + }, + }, + { + rolloutId: 'ro-story-003', + input: { task: 'Analyze experiment results' }, + startTime: now - 10800, + endTime: now - 9600, + mode: 'test', + resourcesId: 'rs-story-003', + status: 'failed', + config: { retries: 1 }, + metadata: { owner: 'carol' }, + attempt: { + rolloutId: 'ro-story-003', + attemptId: 'at-story-012', + sequenceId: 3, + startTime: now - 10200, + endTime: now - 9600, + status: 'failed', + workerId: 'worker-west', + lastHeartbeatTime: now - 9600, + metadata: { reason: 'Timeout' }, + }, + }, + { + rolloutId: 'ro-story-004', + input: { task: 'Evaluate prompt variants' }, + startTime: now - 3600, + endTime: null, + mode: 'train', + resourcesId: null, + status: 'preparing', + config: { retries: 0 }, + metadata: { owner: 'dave' }, + attempt: null, + }, + { + rolloutId: 'ro-story-005', + input: { task: 'Generate quick answers' }, + startTime: now - 1800, + endTime: null, + mode: 'val', + resourcesId: 'rs-story-004', + status: 'running', + config: { retries: 0 }, + metadata: { owner: 'eva' }, + attempt: { + rolloutId: 'ro-story-005', + attemptId: 'at-story-013', + sequenceId: 1, + startTime: now - 1800, + endTime: null, + status: 'running', + workerId: null, + lastHeartbeatTime: now - 75, + metadata: null, + }, + }, + { + rolloutId: 'ro-story-006', + input: { task: 'Compile release notes' }, + startTime: now - 9600, + endTime: now - 9000, + mode: null, + resourcesId: 'rs-story-005', + status: 'cancelled', + config: { retries: 3 }, + metadata: null, + attempt: { + rolloutId: 'ro-story-006', + attemptId: 'at-story-014', + sequenceId: 1, + startTime: now - 9600, + endTime: now - 9000, + status: 'timeout', + workerId: 'worker-south', + lastHeartbeatTime: now - 9000, + metadata: { info: 'Cancelled by operator' }, + }, + }, +]; + +type WrapperProps = { + maxWidth: number; + rollouts?: Rollout[] | undefined; + isFetching?: boolean; + isError?: boolean; + error?: unknown; +}; + +function RolloutTableStoryWrapper({ + maxWidth, + rollouts = sampleRollouts, + isFetching = false, + isError = false, + error = null, +}: WrapperProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilters, setStatusFilters] = useState([]); + const [modeFilters, setModeFilters] = useState([]); + const [page, setPage] = useState(1); + const [recordsPerPage, setRecordsPerPage] = useState(5); + const [sort, setSort] = useState({ + column: 'startTimestamp', + direction: 'desc', + }); + + const tableRecords = useMemo(() => { + if (!rollouts) { + return []; + } + return rollouts.map((rolloutItem) => buildRolloutRecord(rolloutItem)); + }, [rollouts]); + + const filteredRecords = useMemo(() => { + const normalizedSearch = searchTerm.trim().toLowerCase(); + return tableRecords.filter((record) => { + const matchesSearch = normalizedSearch.length === 0 || record.rolloutId.toLowerCase().includes(normalizedSearch); + const matchesStatus = statusFilters.length === 0 || statusFilters.includes(record.status); + const matchesMode = modeFilters.length === 0 || (record.mode !== null && modeFilters.includes(record.mode)); + return matchesSearch && matchesStatus && matchesMode; + }); + }, [modeFilters, searchTerm, statusFilters, tableRecords]); + + const sortedRecords = useMemo(() => { + const sorted = filteredRecords.slice(); + if (!sorted.length) { + return sorted; + } + const comparatorKey = sort.column as keyof RolloutTableRecord; + if (!(comparatorKey in sorted[0])) { + return sorted; + } + sorted.sort((a, b) => compareRecords(a, b, comparatorKey)); + if (sort.direction === 'desc') { + sorted.reverse(); + } + return sorted; + }, [filteredRecords, sort]); + + const totalRecordsValue = sortedRecords.length; + + const pagedRecords = useMemo(() => { + const startIndex = (page - 1) * recordsPerPage; + const endIndex = startIndex + recordsPerPage; + return sortedRecords.slice(startIndex, endIndex); + }, [page, recordsPerPage, sortedRecords]); + + const pagedRollouts = useMemo(() => pagedRecords.map((record) => record as Rollout), [pagedRecords]); + + return ( + + + Rollouts + setSearchTerm(event.currentTarget.value)} + leftSection={} + data-testid='rollouts-search-input' + w='100%' + style={{ maxWidth: 360 }} + /> + { + setStatusFilters(values); + setPage(1); + }} + onStatusFilterReset={() => { + setStatusFilters([]); + setPage(1); + }} + onModeFilterChange={(values) => { + setModeFilters(values); + setPage(1); + }} + onModeFilterReset={() => { + setModeFilters([]); + setPage(1); + }} + onSortStatusChange={(status) => { + setSort({ + column: status.columnAccessor as string, + direction: status.direction, + }); + }} + onPageChange={setPage} + onRecordsPerPageChange={(value) => { + setRecordsPerPage(value); + setPage(1); + }} + onResetFilters={() => { + setSearchTerm(''); + setStatusFilters([]); + setModeFilters([]); + setSort({ column: 'startTimestamp', direction: 'desc' }); + setPage(1); + }} + onRefetch={() => undefined} + recordsPerPageOptions={[5, 10, 20]} + /> + + + ); +} + +export const WideContainer: Story = { + render: () => , +}; + +export const MediumContainer: Story = { + render: () => , +}; + +export const NarrowContainer: Story = { + render: () => , +}; + +export const DrawerWidth: Story = { + render: () => , +}; + +export const ErrorState: Story = { + render: () => ( + + ), +}; diff --git a/dashboard/src/components/TracesTable.component.tsx b/dashboard/src/components/TracesTable.component.tsx new file mode 100644 index 00000000..7147a68c --- /dev/null +++ b/dashboard/src/components/TracesTable.component.tsx @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useCallback, useEffect, useMemo } from 'react'; +import { + IconAlertCircle, + IconCheck, + IconCopy, + IconFileDescription, + IconRefresh, + IconRouteSquare, +} from '@tabler/icons-react'; +import { DataTable, type DataTableColumn, type DataTableSortStatus } from 'mantine-datatable'; +import { ActionIcon, Badge, Box, Button, CopyButton, Group, Stack, Text, Tooltip } from '@mantine/core'; +import { useElementSize } from '@mantine/hooks'; +import type { Span } from '@/types'; +import { getErrorDescriptor } from '@/utils/error'; +import { formatDateTime, formatDuration, toTimestamp } from '@/utils/format'; +import { createResponsiveColumns, type ColumnVisibilityConfig } from '@/utils/table'; + +const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [50, 100, 200, 500]; + +const COLUMN_VISIBILITY: Record = { + name: { minWidth: 12.5, priority: 0 }, + spanId: { fixedWidth: 12, priority: 1 }, + traceId: { fixedWidth: 12, priority: 2 }, + parentId: { fixedWidth: 12, priority: 2 }, + statusCode: { fixedWidth: 8, priority: 2 }, + attributeKeys: { minWidth: 12.5, priority: 2 }, + startTime: { fixedWidth: 12, priority: 1 }, + duration: { fixedWidth: 10, priority: 2 }, + actionsPlaceholder: { fixedWidth: 6, priority: 0 }, +}; + +const STATUS_COLORS: Record = { + UNSET: 'gray', + OK: 'teal', + ERROR: 'red', +}; + +export type TracesTableRecord = Span & { + statusCode: string; + attributeKeys: string; + duration: number; + actionsPlaceholder?: null; +}; + +export function buildTraceRecord(span: Span): TracesTableRecord { + const statusCode = span.status.status_code; + const attributeKeys = Object.keys(span.attributes ?? {}).join(', ') || ''; + const startTimestamp = toTimestamp(span.startTime); + const endTimestamp = toTimestamp(span.endTime); + const duration = endTimestamp && startTimestamp ? endTimestamp - startTimestamp : 0; + + return { + ...span, + statusCode, + attributeKeys, + duration, + actionsPlaceholder: null, + }; +} + +type TracesColumnsOptions = { + onShowRollout?: (record: TracesTableRecord) => void; + onShowSpanDetail?: (record: TracesTableRecord) => void; + onParentIdClick?: (parentId: string) => void; + spanIds: Set; +}; + +function createTracesColumns({ + onShowRollout, + onShowSpanDetail, + onParentIdClick, + spanIds, +}: TracesColumnsOptions): DataTableColumn[] { + return [ + { + accessor: 'name', + title: 'Name', + sortable: true, + render: ({ name }) => ( + + {name} + + ), + }, + { + accessor: 'traceId', + title: 'Trace ID', + sortable: true, + render: ({ traceId }) => ( + + {traceId} + + {({ copied, copy }) => ( + + { + event.stopPropagation(); + copy(); + }} + > + {copied ? : } + + + )} + + + ), + }, + { + accessor: 'spanId', + title: 'Span ID', + sortable: true, + render: ({ spanId }) => ( + + {spanId} + + {({ copied, copy }) => ( + + { + event.stopPropagation(); + copy(); + }} + > + {copied ? : } + + + )} + + + ), + }, + { + accessor: 'parentId', + title: 'Parent ID', + sortable: true, + render: ({ parentId }) => { + if (!parentId) { + return ( + + — + + ); + } + + const parentExists = spanIds.has(parentId); + const isInteractive = parentExists && typeof onParentIdClick === 'function'; + + return ( + + { + if (isInteractive) { + event.stopPropagation(); + onParentIdClick?.(parentId); + } + }} + > + {parentId.slice(0, 8)} + + {!parentExists && ( + + + + )} + + ); + }, + }, + + { + accessor: 'statusCode', + title: 'Status', + sortable: true, + render: ({ statusCode }) => ( + + {statusCode} + + ), + }, + { + accessor: 'attributeKeys', + title: 'Attribute Keys', + render: ({ attributeKeys }) => + attributeKeys ? ( + + {/* TODO: dim "." and "," and other characters are just normal text */} + {attributeKeys} + + ) : ( + + — + + ), + }, + { + accessor: 'startTime', + title: 'Start Time', + sortable: true, + textAlign: 'left', + render: ({ startTime }) => {formatDateTime(toTimestamp(startTime))}, + }, + { + accessor: 'duration', + title: 'Duration', + sortable: true, + textAlign: 'left', + render: ({ duration }) => {formatDuration(duration)}, + }, + { + accessor: 'actionsPlaceholder', + title: 'Actions', + render: (record) => ( + + + { + event.stopPropagation(); + onShowRollout?.(record); + }} + > + + + + + { + event.stopPropagation(); + onShowSpanDetail?.(record); + }} + > + + + + + ), + }, + ]; +} + +export type TracesTableProps = { + spans: Span[] | undefined; + totalRecords: number; + isFetching: boolean; + isError: boolean; + error: unknown; + selectionMessage?: string; + searchTerm: string; + sort: { column: string; direction: 'asc' | 'desc' }; + page: number; + recordsPerPage: number; + onSortStatusChange: (status: DataTableSortStatus) => void; + onPageChange: (page: number) => void; + onRecordsPerPageChange: (value: number) => void; + onResetFilters: () => void; + onRefetch: () => void; + onShowRollout?: (record: TracesTableRecord) => void; + onShowSpanDetail?: (record: TracesTableRecord) => void; + onParentIdClick?: (parentId: string) => void; + recordsPerPageOptions?: number[]; +}; + +export function TracesTable({ + spans, + totalRecords, + isFetching, + isError, + error, + selectionMessage, + searchTerm, + sort, + page, + recordsPerPage, + onSortStatusChange, + onPageChange, + onRecordsPerPageChange, + onResetFilters, + onRefetch, + onShowRollout, + onShowSpanDetail, + onParentIdClick, + recordsPerPageOptions = DEFAULT_RECORDS_PER_PAGE_OPTIONS, +}: TracesTableProps) { + const { ref: tableContainerRef, width: containerWidth } = useElementSize(); + + const traceRecords = useMemo(() => { + if (!spans) { + return []; + } + return spans.map((span) => buildTraceRecord(span)); + }, [spans]); + + const spanIds = useMemo(() => { + return new Set(traceRecords.map((record) => record.spanId)); + }, [traceRecords]); + + const columns = useMemo( + () => + createTracesColumns({ + onShowRollout, + onShowSpanDetail, + onParentIdClick, + spanIds, + }), + [onShowRollout, onShowSpanDetail, onParentIdClick, spanIds], + ); + + const responsiveColumns = useMemo( + () => createResponsiveColumns(columns, containerWidth, COLUMN_VISIBILITY), + [columns, containerWidth], + ); + + const totalPages = useMemo( + () => Math.max(1, Math.ceil(Math.max(0, totalRecords) / Math.max(1, recordsPerPage))), + [recordsPerPage, totalRecords], + ); + + useEffect(() => { + if (page > totalPages) { + onPageChange(totalPages); + } + }, [onPageChange, page, totalPages]); + + const hasActiveFilters = searchTerm.trim().length > 0; + + const sortStatus: DataTableSortStatus = { + columnAccessor: sort.column, + direction: sort.direction, + }; + + const handleSortStatusChange = useCallback( + (status: DataTableSortStatus) => { + onSortStatusChange(status); + }, + [onSortStatusChange], + ); + + const errorDescriptor = isError ? getErrorDescriptor(error) : null; + const errorMessage = isError + ? `Traces are temporarily unavailable${errorDescriptor ? ` (${errorDescriptor})` : ''}.` + : 'Traces are temporarily unavailable.'; + + const selectionEmptyState = selectionMessage ? ( + + + {selectionMessage} + + + Choose a rollout and attempt from the controls above to load trace results. + + + ) : null; + + const fallbackEmptyState = ( + + {isError ? ( + <> + + {errorMessage} + + + Use the retry button to try again, or adjust the filters to broaden the results. + + + + {hasActiveFilters ? ( + + ) : null} + + + ) : ( + <> + + No traces found + + + {hasActiveFilters + ? 'Try adjusting the search to see more results.' + : 'Try refreshing to fetch the latest traces.'} + + + + {hasActiveFilters ? ( + + ) : null} + + + )} + + ); + + const emptyState = selectionEmptyState ?? fallbackEmptyState; + + return ( + + + classNames={{ root: 'traces-table' }} + withTableBorder + withColumnBorders + highlightOnHover + verticalAlign='center' + minHeight={traceRecords.length === 0 ? 500 : undefined} + idAccessor='spanId' + records={traceRecords} + columns={responsiveColumns} + totalRecords={totalRecords} + recordsPerPage={recordsPerPage} + page={page} + onPageChange={onPageChange} + onRecordsPerPageChange={onRecordsPerPageChange} + recordsPerPageOptions={recordsPerPageOptions} + sortStatus={sortStatus} + onSortStatusChange={handleSortStatusChange} + fetching={isFetching} + loaderSize='sm' + emptyState={traceRecords.length === 0 ? emptyState : undefined} + /> + + ); +} diff --git a/dashboard/src/components/TracesTable.story.tsx b/dashboard/src/components/TracesTable.story.tsx new file mode 100644 index 00000000..ffb3ca9e --- /dev/null +++ b/dashboard/src/components/TracesTable.story.tsx @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useMemo, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@tabler/icons-react'; +import { Box, Stack, TextInput, Title } from '@mantine/core'; +import type { Span } from '@/types'; +import { compareRecords } from '@/utils/table'; +import { buildTraceRecord, TracesTable, type TracesTableRecord } from './TracesTable.component'; + +const meta: Meta = { + title: 'Components/TracesTable', + component: TracesTable, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; + +type Story = StoryObj; + +const now = Math.floor(1762775145209 / 1000); + +const sampleSpans: Span[] = [ + { + rolloutId: 'ro-trace-001', + attemptId: 'at-trace-001', + sequenceId: 1, + traceId: 'trace-abc123def456', + spanId: 'span-root-001', + parentId: null, + name: 'main_task', + status: { status_code: 'OK', description: null }, + attributes: { + 'task.type': 'generation', + 'task.priority': 'high', + 'user.id': 'user-123', + }, + startTime: now - 100, + endTime: now - 10, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-trace-001', + attemptId: 'at-trace-001', + sequenceId: 1, + traceId: 'trace-abc123def456', + spanId: 'span-child-001', + parentId: 'span-root-001', + name: 'llm_call', + status: { status_code: 'OK', description: null }, + attributes: { + 'llm.model': 'gpt-4', + 'llm.temperature': 0.7, + 'llm.max_tokens': 2048, + }, + startTime: now - 90, + endTime: now - 50, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-trace-001', + attemptId: 'at-trace-001', + sequenceId: 1, + traceId: 'trace-abc123def456', + spanId: 'span-child-002', + parentId: 'span-root-001', + name: 'database_query', + status: { status_code: 'OK', description: null }, + attributes: { + 'db.system': 'postgresql', + 'db.operation': 'SELECT', + 'db.table': 'users', + }, + startTime: now - 80, + endTime: now - 70, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-trace-002', + attemptId: 'at-trace-002', + sequenceId: 1, + traceId: 'trace-xyz789ghi012', + spanId: 'span-error-001', + parentId: 'span-missing-parent', + name: 'failed_operation', + status: { status_code: 'ERROR', description: 'Connection timeout' }, + attributes: { + 'error.type': 'TimeoutError', + 'error.message': 'Connection timed out after 30s', + }, + startTime: now - 150, + endTime: now - 120, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-trace-003', + attemptId: 'at-trace-003', + sequenceId: 1, + traceId: 'trace-unset123', + spanId: 'span-unset-001', + parentId: null, + name: 'pending_task', + status: { status_code: 'UNSET', description: null }, + attributes: {}, + startTime: now - 30, + endTime: now - 5, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-trace-004', + attemptId: 'at-trace-004', + sequenceId: 2, + traceId: 'trace-nested456', + spanId: 'span-parent-001', + parentId: null, + name: 'workflow_execution', + status: { status_code: 'OK', description: null }, + attributes: { + 'workflow.name': 'data_processing', + 'workflow.version': '2.1.0', + }, + startTime: now - 200, + endTime: now - 50, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-trace-004', + attemptId: 'at-trace-004', + sequenceId: 2, + traceId: 'trace-nested456', + spanId: 'span-child-nested-001', + parentId: 'span-parent-001', + name: 'step_1_validation', + status: { status_code: 'OK', description: null }, + attributes: { + 'step.name': 'validation', + 'step.index': 1, + }, + startTime: now - 195, + endTime: now - 180, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-trace-004', + attemptId: 'at-trace-004', + sequenceId: 2, + traceId: 'trace-nested456', + spanId: 'span-child-nested-002', + parentId: 'span-parent-001', + name: 'step_2_processing', + status: { status_code: 'ERROR', description: 'Validation failed' }, + attributes: { + 'step.name': 'processing', + 'step.index': 2, + 'error.type': 'ValidationError', + }, + startTime: now - 175, + endTime: now - 160, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, +]; + +type WrapperProps = { + maxWidth: number; + spans?: Span[] | undefined; + isFetching?: boolean; + isError?: boolean; + error?: unknown; +}; + +function TracesTableStoryWrapper({ + maxWidth, + spans = sampleSpans, + isFetching = false, + isError = false, + error = null, +}: WrapperProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [page, setPage] = useState(1); + const [recordsPerPage, setRecordsPerPage] = useState(10); + const [sort, setSort] = useState<{ column: string; direction: 'asc' | 'desc' }>({ + column: 'startTime', + direction: 'desc', + }); + + const tableRecords = useMemo(() => { + if (!spans) { + return []; + } + return spans.map((span) => buildTraceRecord(span)); + }, [spans]); + + const filteredRecords = useMemo(() => { + const normalizedSearch = searchTerm.trim().toLowerCase(); + if (normalizedSearch.length === 0) { + return tableRecords; + } + return tableRecords.filter( + (record) => + record.traceId.toLowerCase().includes(normalizedSearch) || + record.spanId.toLowerCase().includes(normalizedSearch) || + record.name.toLowerCase().includes(normalizedSearch), + ); + }, [searchTerm, tableRecords]); + + const sortedRecords = useMemo(() => { + const sorted = filteredRecords.slice(); + if (!sorted.length) { + return sorted; + } + const comparatorKey = sort.column as keyof TracesTableRecord; + if (!(comparatorKey in sorted[0])) { + return sorted; + } + sorted.sort((a, b) => compareRecords(a, b, comparatorKey)); + if (sort.direction === 'desc') { + sorted.reverse(); + } + return sorted; + }, [filteredRecords, sort]); + + const totalRecordsValue = sortedRecords.length; + + const pagedRecords = useMemo(() => { + const startIndex = (page - 1) * recordsPerPage; + const endIndex = startIndex + recordsPerPage; + return sortedRecords.slice(startIndex, endIndex); + }, [page, recordsPerPage, sortedRecords]); + + const pagedSpans = useMemo(() => pagedRecords.map((record) => record as Span), [pagedRecords]); + + const handleShowRollout = (record: any) => { + console.log('Show rollout for:', record.rolloutId); + }; + + const handleShowSpanDetail = (record: any) => { + console.log('Show span detail for:', record.spanId, record); + }; + + const handleParentIdClick = (parentId: string) => { + console.log('Navigate to parent span:', parentId); + setSearchTerm(parentId); + }; + + return ( + + + Traces + setSearchTerm(event.currentTarget.value)} + leftSection={} + data-testid='traces-search-input' + w='100%' + style={{ maxWidth: 360 }} + /> + { + setSort({ + column: status.columnAccessor as string, + direction: status.direction, + }); + }} + onPageChange={setPage} + onRecordsPerPageChange={(value) => { + setRecordsPerPage(value); + setPage(1); + }} + onResetFilters={() => { + setSearchTerm(''); + setSort({ column: 'startTime', direction: 'desc' }); + setPage(1); + }} + onRefetch={() => undefined} + onShowRollout={handleShowRollout} + onShowSpanDetail={handleShowSpanDetail} + onParentIdClick={handleParentIdClick} + recordsPerPageOptions={[10, 20, 50]} + /> + + + ); +} + +export const WideContainer: Story = { + render: () => , +}; + +export const MediumContainer: Story = { + render: () => , +}; + +export const NarrowContainer: Story = { + render: () => , +}; + +export const DrawerWidth: Story = { + render: () => , +}; + +export const ErrorState: Story = { + render: () => , +}; + +export const LoadingState: Story = { + render: () => , +}; + +export const EmptyState: Story = { + render: () => , +}; + +export const WithMissingParent: Story = { + render: () => ( + s.spanId === 'span-error-001')} /> + ), +}; + +export const NestedSpans: Story = { + render: () => ( + s.traceId === 'trace-nested456')} /> + ), +}; diff --git a/dashboard/src/cssVariableResolver.ts b/dashboard/src/cssVariableResolver.ts new file mode 100644 index 00000000..953e817b --- /dev/null +++ b/dashboard/src/cssVariableResolver.ts @@ -0,0 +1,436 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { alpha, CSSVariablesResolver } from '@mantine/core'; + +export const shadcnCssVariableResolver: CSSVariablesResolver = () => ({ + variables: { + // variables that do not depend on color scheme + '--mantine-heading-font-weight': '600', + '--mantine-primary-color-filled-hover': alpha('var(--mantine-primary-color-filled)', 0.9), + '--mantine-primary-color-light': 'var(--mantine-color-zinc-light)', + '--mantine-primary-color-light-hover': 'var(--mantine-color-zinc-light-hover)', + '--mantine-primary-color-light-color': 'var(--mantine-color-zinc-light-color)', + }, + light: { + // all variables that depend on light color scheme + '--mantine-primary-color-contrast': 'var(--mantine-color-zinc-0)', // used as primary color contrast + '--mantine-color-text': 'var(--mantine-color-secondary-9)', // used as text color + '--mantine-color-body': 'var(--mantine-color-white)', // used as body color + '--mantine-color-error': 'var(--mantine-color-error-10)', // used as error color + '--mantine-color-placeholder': 'var(--mantine-color-secondary-10)', // used as placeholder color + '--mantine-color-anchor': 'var(--mantine-color-secondary-10)', // used as anchor color + + '--mantine-color-default': 'var(--mantine-color-secondary-0)', // used as default surface color + '--mantine-color-default-hover': 'var(--mantine-color-secondary-1)', // used as default hover color + '--mantine-color-default-color': 'var(--mantine-color-secondary-9)', // used as default text color + '--mantine-color-default-border': 'var(--mantine-color-secondary-2)', // used as default border color + '--mantine-color-dimmed': 'var(--mantine-color-secondary-10)', // used as dimmed text color + + '--mantine-color-secondary-filled': 'var(--mantine-color-white)', // used as secondary surface color + '--mantine-color-secondary-filled-hover': 'var(--mantine-color-secondary-1)', // used as secondary hover color + + '--mantine-color-secondary-light': 'var(--mantine-color-secondary-1)', // used as primary light color + '--mantine-color-secondary-light-hover': alpha('var(--mantine-color-secondary-light)', 0.8), // used as primary light hover color + + '--mantine-color-secondary-text': 'var(--mantine-primary-color-contrast)', // can be used as secondary text color + '--mantine-color-secondary-light-color': 'var(--mantine-color-secondary-8)', // used as primary light variant's text color + + '--mantine-color-secondary-outline': 'var(--mantine-color-secondary-2)', + '--mantine-color-secondary-outline-hover': 'var(--mantine-color-secondary-1)', + + // all filled colors + '--mantine-color-zinc-filled': 'var(--mantine-color-zinc-8)', + '--mantine-color-zinc-filled-hover': alpha('var(--mantine-color-zinc-8)', 0.9), + '--mantine-color-slate-filled': 'var(--mantine-color-slate-8)', + '--mantine-color-slate-filled-hover': alpha('var(--mantine-color-slate-8)', 0.9), + '--mantine-color-gray-filled': 'var(--mantine-color-gray-8)', + '--mantine-color-gray-filled-hover': alpha('var(--mantine-color-gray-8)', 0.9), + '--mantine-color-neutral-filled': 'var(--mantine-color-neutral-8)', + '--mantine-color-neutral-filled-hover': alpha('var(--mantine-color-neutral-8)', 0.9), + '--mantine-color-stone-filled': 'var(--mantine-color-stone-8)', + '--mantine-color-stone-filled-hover': alpha('var(--mantine-color-stone-8)', 0.9), + '--mantine-color-red-filled': 'var(--mantine-color-red-5)', + '--mantine-color-red-filled-hover': alpha('var(--mantine-color-red-5)', 0.9), + '--mantine-color-rose-filled': 'var(--mantine-color-rose-5)', + '--mantine-color-rose-filled-hover': alpha('var(--mantine-color-rose-5)', 0.9), + '--mantine-color-orange-filled': 'var(--mantine-color-orange-5)', + '--mantine-color-orange-filled-hover': alpha('var(--mantine-color-orange-5)', 0.9), + '--mantine-color-amber-filled': 'var(--mantine-color-amber-5)', + '--mantine-color-amber-filled-hover': alpha('var(--mantine-color-amber-5)', 0.9), + '--mantine-color-yellow-filled': 'var(--mantine-color-yellow-4)', + '--mantine-color-yellow-filled-hover': alpha('var(--mantine-color-yellow-4)', 0.9), + '--mantine-color-lime-filled': 'var(--mantine-color-lime-5)', + '--mantine-color-lime-filled-hover': alpha('var(--mantine-color-lime-5)', 0.9), + '--mantine-color-green-filled': 'var(--mantine-color-green-6)', + '--mantine-color-green-filled-hover': alpha('var(--mantine-color-green-6)', 0.9), + '--mantine-color-emerald-filled': 'var(--mantine-color-emerald-5)', + '--mantine-color-emerald-filled-hover': alpha('var(--mantine-color-emerald-5)', 0.9), + '--mantine-color-teal-filled': 'var(--mantine-color-teal-5)', + '--mantine-color-teal-filled-hover': alpha('var(--mantine-color-teal-5)', 0.9), + '--mantine-color-cyan-filled': 'var(--mantine-color-cyan-5)', + '--mantine-color-cyan-filled-hover': alpha('var(--mantine-color-cyan-5)', 0.9), + '--mantine-color-sky-filled': 'var(--mantine-color-sky-5)', + '--mantine-color-sky-filled-hover': alpha('var(--mantine-color-sky-5)', 0.9), + '--mantine-color-blue-filled': 'var(--mantine-color-blue-6)', + '--mantine-color-blue-filled-hover': alpha('var(--mantine-color-blue-6)', 0.9), + '--mantine-color-indigo-filled': 'var(--mantine-color-indigo-5)', + '--mantine-color-indigo-filled-hover': alpha('var(--mantine-color-indigo-5)', 0.9), + '--mantine-color-violet-filled': 'var(--mantine-color-violet-5)', + '--mantine-color-violet-filled-hover': alpha('var(--mantine-color-violet-5)', 0.9), + '--mantine-color-purple-filled': 'var(--mantine-color-purple-5)', + '--mantine-color-purple-filled-hover': alpha('var(--mantine-color-purple-5)', 0.9), + '--mantine-color-fuchsia-filled': 'var(--mantine-color-fuchsia-5)', + '--mantine-color-fuchsia-filled-hover': alpha('var(--mantine-color-fuchsia-5)', 0.9), + '--mantine-color-pink-filled': 'var(--mantine-color-pink-5)', + '--mantine-color-pink-filled-hover': alpha('var(--mantine-color-pink-5)', 0.9), + + // all light colors + '--mantine-color-zinc-light': alpha('var(--mantine-color-zinc-4)', 0.1), + '--mantine-color-zinc-light-hover': alpha('var(--mantine-color-zinc-light)', 0.8), + '--mantine-color-zinc-light-color': 'var(--mantine-color-zinc-6)', + '--mantine-color-slate-light': alpha('var(--mantine-color-slate-4)', 0.1), + '--mantine-color-slate-light-hover': alpha('var(--mantine-color-slate-light)', 0.8), + '--mantine-color-slate-light-color': 'var(--mantine-color-slate-6)', + '--mantine-color-gray-light': alpha('var(--mantine-color-gray-4)', 0.1), + '--mantine-color-gray-light-hover': alpha('var(--mantine-color-gray-light)', 0.8), + '--mantine-color-gray-light-color': 'var(--mantine-color-gray-6)', + '--mantine-color-neutral-light': alpha('var(--mantine-color-neutral-4)', 0.1), + '--mantine-color-neutral-light-hover': alpha('var(--mantine-color-neutral-light)', 0.8), + '--mantine-color-neutral-light-color': 'var(--mantine-color-neutral-6)', + '--mantine-color-stone-light': alpha('var(--mantine-color-stone-4)', 0.1), + '--mantine-color-stone-light-hover': alpha('var(--mantine-color-stone-light)', 0.8), + '--mantine-color-stone-light-color': 'var(--mantine-color-stone-6)', + '--mantine-color-red-light': alpha('var(--mantine-color-red-4)', 0.1), + '--mantine-color-red-light-hover': alpha('var(--mantine-color-red-light)', 0.8), + '--mantine-color-red-light-color': 'var(--mantine-color-red-6)', + '--mantine-color-rose-light': alpha('var(--mantine-color-rose-4)', 0.1), + '--mantine-color-rose-light-hover': alpha('var(--mantine-color-rose-light)', 0.8), + '--mantine-color-rose-light-color': 'var(--mantine-color-rose-6)', + '--mantine-color-orange-light': alpha('var(--mantine-color-orange-4)', 0.1), + '--mantine-color-orange-light-hover': alpha('var(--mantine-color-orange-light)', 0.8), + '--mantine-color-orange-light-color': 'var(--mantine-color-orange-6)', + '--mantine-color-amber-light': alpha('var(--mantine-color-amber-4)', 0.1), + '--mantine-color-amber-light-hover': alpha('var(--mantine-color-amber-light)', 0.8), + '--mantine-color-amber-light-color': 'var(--mantine-color-amber-6)', + '--mantine-color-yellow-light': alpha('var(--mantine-color-yellow-4)', 0.1), + '--mantine-color-yellow-light-hover': alpha('var(--mantine-color-yellow-light)', 0.8), + '--mantine-color-yellow-light-color': 'var(--mantine-color-yellow-6)', + '--mantine-color-lime-light': alpha('var(--mantine-color-lime-4)', 0.1), + '--mantine-color-lime-light-hover': alpha('var(--mantine-color-lime-light)', 0.8), + '--mantine-color-lime-light-color': 'var(--mantine-color-lime-6)', + '--mantine-color-green-light': alpha('var(--mantine-color-green-4)', 0.1), + '--mantine-color-green-light-hover': alpha('var(--mantine-color-green-light)', 0.8), + '--mantine-color-green-light-color': 'var(--mantine-color-green-6)', + '--mantine-color-emerald-light': alpha('var(--mantine-color-emerald-4)', 0.1), + '--mantine-color-emerald-light-hover': alpha('var(--mantine-color-emerald-light)', 0.8), + '--mantine-color-emerald-light-color': 'var(--mantine-color-emerald-6)', + '--mantine-color-teal-light': alpha('var(--mantine-color-teal-4)', 0.1), + '--mantine-color-teal-light-hover': alpha('var(--mantine-color-teal-light)', 0.8), + '--mantine-color-teal-light-color': 'var(--mantine-color-teal-6)', + '--mantine-color-cyan-light': alpha('var(--mantine-color-cyan-4)', 0.1), + '--mantine-color-cyan-light-hover': alpha('var(--mantine-color-cyan-light)', 0.8), + '--mantine-color-cyan-light-color': 'var(--mantine-color-cyan-6)', + '--mantine-color-sky-light': alpha('var(--mantine-color-sky-4)', 0.1), + '--mantine-color-sky-light-hover': alpha('var(--mantine-color-sky-light)', 0.8), + '--mantine-color-sky-light-color': 'var(--mantine-color-sky-6)', + '--mantine-color-blue-light': alpha('var(--mantine-color-blue-4)', 0.1), + '--mantine-color-blue-light-hover': alpha('var(--mantine-color-blue-light)', 0.8), + '--mantine-color-blue-light-color': 'var(--mantine-color-blue-6)', + '--mantine-color-indigo-light': alpha('var(--mantine-color-indigo-4)', 0.1), + '--mantine-color-indigo-light-hover': alpha('var(--mantine-color-indigo-light)', 0.8), + '--mantine-color-indigo-light-color': 'var(--mantine-color-indigo-6)', + '--mantine-color-violet-light': alpha('var(--mantine-color-violet-4)', 0.1), + '--mantine-color-violet-light-hover': alpha('var(--mantine-color-violet-light)', 0.8), + '--mantine-color-violet-light-color': 'var(--mantine-color-violet-6)', + '--mantine-color-purple-light': alpha('var(--mantine-color-purple-4)', 0.1), + '--mantine-color-purple-light-hover': alpha('var(--mantine-color-purple-light)', 0.8), + '--mantine-color-purple-light-color': 'var(--mantine-color-purple-6)', + '--mantine-color-fuchsia-light': alpha('var(--mantine-color-fuchsia-4)', 0.1), + '--mantine-color-fuchsia-light-hover': alpha('var(--mantine-color-fuchsia-light)', 0.8), + '--mantine-color-fuchsia-light-color': 'var(--mantine-color-fuchsia-6)', + '--mantine-color-pink-light': alpha('var(--mantine-color-pink-4)', 0.1), + '--mantine-color-pink-light-hover': alpha('var(--mantine-color-pink-light)', 0.8), + '--mantine-color-pink-light-color': 'var(--mantine-color-pink-6)', + + // all outline colors + '--mantine-color-zinc-outline': 'var(--mantine-color-zinc-8)', + '--mantine-color-zinc-outline-hover': alpha('var(--mantine-color-zinc-4)', 0.1), + '--mantine-color-slate-outline': 'var(--mantine-color-slate-8)', + '--mantine-color-slate-outline-hover': alpha('var(--mantine-color-slate-4)', 0.1), + '--mantine-color-gray-outline': 'var(--mantine-color-gray-8)', + '--mantine-color-gray-outline-hover': alpha('var(--mantine-color-gray-4)', 0.1), + '--mantine-color-neutral-outline': 'var(--mantine-color-neutral-8)', + '--mantine-color-neutral-outline-hover': alpha('var(--mantine-color-neutral-4)', 0.1), + '--mantine-color-stone-outline': 'var(--mantine-color-stone-8)', + '--mantine-color-stone-outline-hover': alpha('var(--mantine-color-stone-4)', 0.1), + '--mantine-color-red-outline': 'var(--mantine-color-red-5)', + '--mantine-color-red-outline-hover': alpha('var(--mantine-color-red-4)', 0.1), + '--mantine-color-rose-outline': 'var(--mantine-color-rose-5)', + '--mantine-color-rose-outline-hover': alpha('var(--mantine-color-rose-4)', 0.1), + '--mantine-color-orange-outline': 'var(--mantine-color-orange-5)', + '--mantine-color-orange-outline-hover': alpha('var(--mantine-color-orange-4)', 0.1), + '--mantine-color-amber-outline': 'var(--mantine-color-amber-5)', + '--mantine-color-amber-outline-hover': alpha('var(--mantine-color-amber-4)', 0.1), + '--mantine-color-yellow-outline': 'var(--mantine-color-yellow-4)', + '--mantine-color-yellow-outline-hover': alpha('var(--mantine-color-yellow-4)', 0.1), + '--mantine-color-lime-outline': 'var(--mantine-color-lime-5)', + '--mantine-color-lime-outline-hover': alpha('var(--mantine-color-lime-4)', 0.1), + '--mantine-color-green-outline': 'var(--mantine-color-green-6)', + '--mantine-color-green-outline-hover': alpha('var(--mantine-color-green-4)', 0.1), + '--mantine-color-emerald-outline': 'var(--mantine-color-emerald-5)', + '--mantine-color-emerald-outline-hover': alpha('var(--mantine-color-emerald-4)', 0.1), + '--mantine-color-teal-outline': 'var(--mantine-color-teal-5)', + '--mantine-color-teal-outline-hover': alpha('var(--mantine-color-teal-4)', 0.1), + '--mantine-color-cyan-outline': 'var(--mantine-color-cyan-5)', + '--mantine-color-cyan-outline-hover': alpha('var(--mantine-color-cyan-4)', 0.1), + '--mantine-color-sky-outline': 'var(--mantine-color-sky-5)', + '--mantine-color-sky-outline-hover': alpha('var(--mantine-color-sky-4)', 0.1), + '--mantine-color-blue-outline': 'var(--mantine-color-blue-6)', + '--mantine-color-blue-outline-hover': alpha('var(--mantine-color-blue-4)', 0.1), + '--mantine-color-indigo-outline': 'var(--mantine-color-indigo-5)', + '--mantine-color-indigo-outline-hover': alpha('var(--mantine-color-indigo-4)', 0.1), + '--mantine-color-violet-outline': 'var(--mantine-color-violet-5)', + '--mantine-color-violet-outline-hover': alpha('var(--mantine-color-violet-4)', 0.1), + '--mantine-color-purple-outline': 'var(--mantine-color-purple-5)', + '--mantine-color-purple-outline-hover': alpha('var(--mantine-color-purple-4)', 0.1), + '--mantine-color-fuchsia-outline': 'var(--mantine-color-fuchsia-5)', + '--mantine-color-fuchsia-outline-hover': alpha('var(--mantine-color-fuchsia-4)', 0.1), + '--mantine-color-pink-outline': 'var(--mantine-color-pink-5)', + '--mantine-color-pink-outline-hover': alpha('var(--mantine-color-pink-4)', 0.1), + + // all contrast colors + '--mantine-color-zinc-contrast': 'var(--mantine-color-zinc-0)', + '--mantine-color-slate-contrast': 'var(--mantine-color-slate-0)', + '--mantine-color-gray-contrast': 'var(--mantine-color-gray-0)', + '--mantine-color-neutral-contrast': 'var(--mantine-color-neutral-0)', + '--mantine-color-stone-contrast': 'var(--mantine-color-stone-0)', + '--mantine-color-red-contrast': 'var(--mantine-color-red-0)', + '--mantine-color-rose-contrast': 'var(--mantine-color-rose-0)', + '--mantine-color-orange-contrast': 'var(--mantine-color-stone-0)', + '--mantine-color-amber-contrast': 'var(--mantine-color-amber-0)', + '--mantine-color-yellow-contrast': '#422006', + '--mantine-color-lime-contrast': 'var(--mantine-color-lime-0)', + '--mantine-color-green-contrast': 'var(--mantine-color-rose-0)', + '--mantine-color-emerald-contrast': 'var(--mantine-color-emerald-0)', + '--mantine-color-teal-contrast': 'var(--mantine-color-teal-0)', + '--mantine-color-cyan-contrast': 'var(--mantine-color-cyan-0)', + '--mantine-color-sky-contrast': 'var(--mantine-color-sky-0)', + '--mantine-color-blue-contrast': 'var(--mantine-color-slate-0)', + '--mantine-color-indigo-contrast': 'var(--mantine-color-indigo-0)', + '--mantine-color-violet-contrast': 'var(--mantine-color-gray-0)', + '--mantine-color-purple-contrast': 'var(--mantine-color-purple-0)', + '--mantine-color-fuchsia-contrast': 'var(--mantine-color-fuchsia-0)', + '--mantine-color-pink-contrast': 'var(--mantine-color-pink-0)', + }, + dark: { + // all variables that depend on dark color scheme + '--mantine-primary-color-contrast': 'var(--mantine-color-zinc-8)', // used as primary color contrast + '--mantine-color-text': 'var(--mantine-color-secondary-0)', // used as text color + '--mantine-color-body': 'var(--mantine-color-secondary-9)', // used as body color + '--mantine-color-error': 'var(--mantine-color-error-10)', // used as error color + '--mantine-color-placeholder': 'var(--mantine-color-secondary-4)', // used as placeholder color + '--mantine-color-anchor': 'var(--mantine-color-secondary-4)', // used as anchor color + + '--mantine-color-default': 'var(--mantine-color-secondary-9)', // used as default surface color + '--mantine-color-default-hover': 'var(--mantine-color-secondary-7)', // used as default hover color + '--mantine-color-default-color': 'var(--mantine-color-secondary-1)', // used as default text color + '--mantine-color-default-border': 'var(--mantine-color-secondary-7)', // used as default border color + '--mantine-color-dimmed': 'var(--mantine-color-secondary-4)', // used as dimmed text color + + '--mantine-color-secondary-filled': 'var(--mantine-color-secondary-8)', // used as secondary surface color + '--mantine-color-secondary-filled-hover': alpha('var(--mantine-color-secondary-filled)', 0.9), // used as secondary hover color + + '--mantine-color-secondary-light': 'var(--mantine-color-secondary-7)', // used as primary light color + '--mantine-color-secondary-light-hover': alpha('var(--mantine-color-secondary-light)', 0.8), // used as primary light hover color + + '--mantine-color-secondary-text': 'var(--mantine-primary-color-contrast)', // can be used as secondary text color + '--mantine-color-secondary-light-color': 'var(--mantine-color-secondary-0)', // used as primary light text color + + '--mantine-color-secondary-outline': 'var(--mantine-color-secondary-7)', + '--mantine-color-secondary-outline-hover': 'var(--mantine-color-secondary-7)', + + // all filled colors + '--mantine-color-zinc-filled': 'var(--mantine-color-zinc-0)', + '--mantine-color-zinc-filled-hover': alpha('var(--mantine-color-zinc-0)', 0.9), + '--mantine-color-slate-filled': 'var(--mantine-color-slate-0)', + '--mantine-color-slate-filled-hover': alpha('var(--mantine-color-slate-0)', 0.9), + '--mantine-color-gray-filled': 'var(--mantine-color-gray-0)', + '--mantine-color-gray-filled-hover': alpha('var(--mantine-color-gray-0)', 0.9), + '--mantine-color-neutral-filled': 'var(--mantine-color-neutral-0)', + '--mantine-color-neutral-filled-hover': alpha('var(--mantine-color-neutral-0)', 0.9), + '--mantine-color-stone-filled': 'var(--mantine-color-stone-0)', + '--mantine-color-stone-filled-hover': alpha('var(--mantine-color-stone-0)', 0.9), + '--mantine-color-red-filled': 'var(--mantine-color-red-5)', + '--mantine-color-red-filled-hover': alpha('var(--mantine-color-red-5)', 0.9), + '--mantine-color-rose-filled': 'var(--mantine-color-rose-5)', + '--mantine-color-rose-filled-hover': alpha('var(--mantine-color-rose-5)', 0.9), + '--mantine-color-orange-filled': 'var(--mantine-color-orange-6)', + '--mantine-color-orange-filled-hover': alpha('var(--mantine-color-orange-6)', 0.9), + '--mantine-color-amber-filled': 'var(--mantine-color-amber-5)', + '--mantine-color-amber-filled-hover': alpha('var(--mantine-color-amber-5)', 0.9), + '--mantine-color-yellow-filled': 'var(--mantine-color-yellow-4)', + '--mantine-color-yellow-filled-hover': alpha('var(--mantine-color-yellow-4)', 0.9), + '--mantine-color-lime-filled': 'var(--mantine-color-lime-4)', + '--mantine-color-lime-filled-hover': alpha('var(--mantine-color-lime-4)', 0.9), + '--mantine-color-green-filled': 'var(--mantine-color-green-5)', + '--mantine-color-green-filled-hover': alpha('var(--mantine-color-green-5)', 0.9), + '--mantine-color-emerald-filled': 'var(--mantine-color-emerald-5)', + '--mantine-color-emerald-filled-hover': alpha('var(--mantine-color-emerald-5)', 0.9), + '--mantine-color-teal-filled': 'var(--mantine-color-teal-4)', + '--mantine-color-teal-filled-hover': alpha('var(--mantine-color-teal-4)', 0.9), + '--mantine-color-cyan-filled': 'var(--mantine-color-cyan-4)', + '--mantine-color-cyan-filled-hover': alpha('var(--mantine-color-cyan-4)', 0.9), + '--mantine-color-sky-filled': 'var(--mantine-color-sky-4)', + '--mantine-color-sky-filled-hover': alpha('var(--mantine-color-sky-4)', 0.9), + '--mantine-color-blue-filled': 'var(--mantine-color-blue-5)', + '--mantine-color-blue-filled-hover': alpha('var(--mantine-color-blue-5)', 0.9), + '--mantine-color-indigo-filled': 'var(--mantine-color-indigo-6)', + '--mantine-color-indigo-filled-hover': alpha('var(--mantine-color-indigo-6)', 0.9), + '--mantine-color-violet-filled': 'var(--mantine-color-violet-6)', + '--mantine-color-violet-filled-hover': alpha('var(--mantine-color-violet-6)', 0.9), + '--mantine-color-purple-filled': 'var(--mantine-color-purple-6)', + '--mantine-color-purple-filled-hover': alpha('var(--mantine-color-purple-6)', 0.9), + '--mantine-color-fuchsia-filled': 'var(--mantine-color-fuchsia-7)', + '--mantine-color-fuchsia-filled-hover': alpha('var(--mantine-color-fuchsia-7)', 0.9), + '--mantine-color-pink-filled': 'var(--mantine-color-pink-6)', + '--mantine-color-pink-filled-hover': alpha('var(--mantine-color-pink-6)', 0.9), + + // all light colors + '--mantine-color-zinc-light': alpha('var(--mantine-color-zinc-4)', 0.15), + '--mantine-color-zinc-light-hover': alpha('var(--mantine-color-zinc-light)', 0.8), + '--mantine-color-zinc-light-color': 'var(--mantine-color-zinc-3)', + '--mantine-color-slate-light': alpha('var(--mantine-color-slate-4)', 0.15), + '--mantine-color-slate-light-hover': alpha('var(--mantine-color-slate-light)', 0.8), + '--mantine-color-slate-light-color': 'var(--mantine-color-slate-3)', + '--mantine-color-gray-light': alpha('var(--mantine-color-gray-4)', 0.15), + '--mantine-color-gray-light-hover': alpha('var(--mantine-color-gray-light)', 0.8), + '--mantine-color-gray-light-color': 'var(--mantine-color-gray-3)', + '--mantine-color-neutral-light': alpha('var(--mantine-color-neutral-4)', 0.15), + '--mantine-color-neutral-light-hover': alpha('var(--mantine-color-neutral-light)', 0.8), + '--mantine-color-neutral-light-color': 'var(--mantine-color-neutral-3)', + '--mantine-color-stone-light': alpha('var(--mantine-color-stone-4)', 0.15), + '--mantine-color-stone-light-hover': alpha('var(--mantine-color-stone-light)', 0.8), + '--mantine-color-stone-light-color': 'var(--mantine-color-stone-3)', + '--mantine-color-red-light': alpha('var(--mantine-color-red-4)', 0.15), + '--mantine-color-red-light-hover': alpha('var(--mantine-color-red-light)', 0.8), + '--mantine-color-red-light-color': 'var(--mantine-color-red-3)', + '--mantine-color-rose-light': alpha('var(--mantine-color-rose-4)', 0.15), + '--mantine-color-rose-light-hover': alpha('var(--mantine-color-rose-light)', 0.8), + '--mantine-color-rose-light-color': 'var(--mantine-color-rose-3)', + '--mantine-color-orange-light': alpha('var(--mantine-color-orange-4)', 0.15), + '--mantine-color-orange-light-hover': alpha('var(--mantine-color-orange-light)', 0.8), + '--mantine-color-orange-light-color': 'var(--mantine-color-orange-3)', + '--mantine-color-amber-light': alpha('var(--mantine-color-amber-4)', 0.15), + '--mantine-color-amber-light-hover': alpha('var(--mantine-color-amber-light)', 0.8), + '--mantine-color-amber-light-color': 'var(--mantine-color-amber-3)', + '--mantine-color-yellow-light': alpha('var(--mantine-color-yellow-4)', 0.15), + '--mantine-color-yellow-light-hover': alpha('var(--mantine-color-yellow-light)', 0.8), + '--mantine-color-yellow-light-color': 'var(--mantine-color-yellow-3)', + '--mantine-color-lime-light': alpha('var(--mantine-color-lime-4)', 0.15), + '--mantine-color-lime-light-hover': alpha('var(--mantine-color-lime-light)', 0.8), + '--mantine-color-lime-light-color': 'var(--mantine-color-lime-3)', + '--mantine-color-green-light': alpha('var(--mantine-color-green-4)', 0.15), + '--mantine-color-green-light-hover': alpha('var(--mantine-color-green-light)', 0.8), + '--mantine-color-green-light-color': 'var(--mantine-color-green-3)', + '--mantine-color-emerald-light': alpha('var(--mantine-color-emerald-4)', 0.15), + '--mantine-color-emerald-light-hover': alpha('var(--mantine-color-emerald-light)', 0.8), + '--mantine-color-emerald-light-color': 'var(--mantine-color-emerald-3)', + '--mantine-color-teal-light': alpha('var(--mantine-color-teal-4)', 0.15), + '--mantine-color-teal-light-hover': alpha('var(--mantine-color-teal-light)', 0.8), + '--mantine-color-teal-light-color': 'var(--mantine-color-teal-3)', + '--mantine-color-cyan-light': alpha('var(--mantine-color-cyan-4)', 0.15), + '--mantine-color-cyan-light-hover': alpha('var(--mantine-color-cyan-light)', 0.8), + '--mantine-color-cyan-light-color': 'var(--mantine-color-cyan-3)', + '--mantine-color-sky-light': alpha('var(--mantine-color-sky-4)', 0.15), + '--mantine-color-sky-light-hover': alpha('var(--mantine-color-sky-light)', 0.8), + '--mantine-color-sky-light-color': 'var(--mantine-color-sky-3)', + '--mantine-color-blue-light': alpha('var(--mantine-color-blue-4)', 0.15), + '--mantine-color-blue-light-hover': alpha('var(--mantine-color-blue-light)', 0.8), + '--mantine-color-blue-light-color': 'var(--mantine-color-blue-3)', + '--mantine-color-indigo-light': alpha('var(--mantine-color-indigo-4)', 0.15), + '--mantine-color-indigo-light-hover': alpha('var(--mantine-color-indigo-light)', 0.8), + '--mantine-color-indigo-light-color': 'var(--mantine-color-indigo-3)', + '--mantine-color-violet-light': alpha('var(--mantine-color-violet-4)', 0.15), + '--mantine-color-violet-light-hover': alpha('var(--mantine-color-violet-light)', 0.8), + '--mantine-color-violet-light-color': 'var(--mantine-color-violet-3)', + '--mantine-color-purple-light': alpha('var(--mantine-color-purple-4)', 0.15), + '--mantine-color-purple-light-hover': alpha('var(--mantine-color-purple-light)', 0.8), + '--mantine-color-purple-light-color': 'var(--mantine-color-purple-3)', + '--mantine-color-fuchsia-light': alpha('var(--mantine-color-fuchsia-4)', 0.15), + '--mantine-color-fuchsia-light-hover': alpha('var(--mantine-color-fuchsia-light)', 0.8), + '--mantine-color-fuchsia-light-color': 'var(--mantine-color-fuchsia-3)', + '--mantine-color-pink-light': alpha('var(--mantine-color-pink-4)', 0.15), + '--mantine-color-pink-light-hover': alpha('var(--mantine-color-pink-light)', 0.8), + '--mantine-color-pink-light-color': 'var(--mantine-color-pink-3)', + + // all outline colors + '--mantine-color-zinc-outline': 'var(--mantine-color-zinc-0)', + '--mantine-color-zinc-outline-hover': alpha('var(--mantine-color-zinc-4)', 0.15), + '--mantine-color-slate-outline': 'var(--mantine-color-slate-0)', + '--mantine-color-slate-outline-hover': alpha('var(--mantine-color-slate-4)', 0.15), + '--mantine-color-gray-outline': 'var(--mantine-color-gray-0)', + '--mantine-color-gray-outline-hover': alpha('var(--mantine-color-gray-4)', 0.15), + '--mantine-color-neutral-outline': 'var(--mantine-color-neutral-0)', + '--mantine-color-neutral-outline-hover': alpha('var(--mantine-color-neutral-4)', 0.15), + '--mantine-color-stone-outline': 'var(--mantine-color-stone-0)', + '--mantine-color-stone-outline-hover': alpha('var(--mantine-color-stone-4)', 0.15), + '--mantine-color-red-outline': 'var(--mantine-color-red-5)', + '--mantine-color-red-outline-hover': alpha('var(--mantine-color-red-4)', 0.15), + '--mantine-color-rose-outline': 'var(--mantine-color-rose-5)', + '--mantine-color-rose-outline-hover': alpha('var(--mantine-color-rose-4)', 0.15), + '--mantine-color-orange-outline': 'var(--mantine-color-orange-6)', + '--mantine-color-orange-outline-hover': alpha('var(--mantine-color-orange-4)', 0.15), + '--mantine-color-amber-outline': 'var(--mantine-color-amber-5)', + '--mantine-color-amber-outline-hover': alpha('var(--mantine-color-amber-4)', 0.15), + '--mantine-color-yellow-outline': 'var(--mantine-color-yellow-4)', + '--mantine-color-yellow-outline-hover': alpha('var(--mantine-color-yellow-4)', 0.15), + '--mantine-color-lime-outline': 'var(--mantine-color-lime-4)', + '--mantine-color-lime-outline-hover': alpha('var(--mantine-color-lime-4)', 0.15), + '--mantine-color-green-outline': 'var(--mantine-color-green-5)', + '--mantine-color-green-outline-hover': alpha('var(--mantine-color-green-4)', 0.15), + '--mantine-color-emerald-outline': 'var(--mantine-color-emerald-5)', + '--mantine-color-emerald-outline-hover': alpha('var(--mantine-color-emerald-4)', 0.15), + '--mantine-color-teal-outline': 'var(--mantine-color-teal-4)', + '--mantine-color-teal-outline-hover': alpha('var(--mantine-color-teal-4)', 0.15), + '--mantine-color-cyan-outline': 'var(--mantine-color-cyan-4)', + '--mantine-color-cyan-outline-hover': alpha('var(--mantine-color-cyan-4)', 0.15), + '--mantine-color-sky-outline': 'var(--mantine-color-sky-4)', + '--mantine-color-sky-outline-hover': alpha('var(--mantine-color-sky-4)', 0.15), + '--mantine-color-blue-outline': 'var(--mantine-color-blue-5)', + '--mantine-color-blue-outline-hover': alpha('var(--mantine-color-blue-4)', 0.15), + '--mantine-color-indigo-outline': 'var(--mantine-color-indigo-6)', + '--mantine-color-indigo-outline-hover': alpha('var(--mantine-color-indigo-4)', 0.15), + '--mantine-color-violet-outline': 'var(--mantine-color-violet-6)', + '--mantine-color-violet-outline-hover': alpha('var(--mantine-color-violet-4)', 0.15), + '--mantine-color-purple-outline': 'var(--mantine-color-purple-6)', + '--mantine-color-purple-outline-hover': alpha('var(--mantine-color-purple-4)', 0.15), + '--mantine-color-fuchsia-outline': 'var(--mantine-color-fuchsia-7)', + '--mantine-color-fuchsia-outline-hover': alpha('var(--mantine-color-fuchsia-4)', 0.15), + '--mantine-color-pink-outline': 'var(--mantine-color-pink-6)', + '--mantine-color-pink-outline-hover': alpha('var(--mantine-color-pink-4)', 0.15), + + // all contrast colors + '--mantine-color-zinc-contrast': 'var(--mantine-color-zinc-8)', + '--mantine-color-slate-contrast': 'var(--mantine-color-slate-8)', + '--mantine-color-gray-contrast': 'var(--mantine-color-gray-8)', + '--mantine-color-neutral-contrast': 'var(--mantine-color-neutral-8)', + '--mantine-color-stone-contrast': 'var(--mantine-color-stone-8)', + '--mantine-color-red-contrast': 'var(--mantine-color-red-0)', + '--mantine-color-rose-contrast': 'var(--mantine-color-rose-0)', + '--mantine-color-orange-contrast': 'var(--mantine-color-stone-0)', + '--mantine-color-amber-contrast': 'var(--mantine-color-stone-8)', + '--mantine-color-yellow-contrast': '#422006', + '--mantine-color-lime-contrast': 'var(--mantine-color-stone-8)', + '--mantine-color-green-contrast': 'var(--mantine-color-green-9)', + '--mantine-color-emerald-contrast': 'var(--mantine-color-stone-0)', + '--mantine-color-teal-contrast': 'var(--mantine-color-slate-8)', + '--mantine-color-cyan-contrast': 'var(--mantine-color-slate-8)', + '--mantine-color-sky-contrast': 'var(--mantine-color-slate-8)', + '--mantine-color-blue-contrast': 'var(--mantine-color-slate-0)', + '--mantine-color-indigo-contrast': 'var(--mantine-color-gray-0)', + '--mantine-color-violet-contrast': 'var(--mantine-color-gray-0)', + '--mantine-color-purple-contrast': 'var(--mantine-color-gray-0)', + '--mantine-color-fuchsia-contrast': 'var(--mantine-color-gray-0)', + '--mantine-color-pink-contrast': 'var(--mantine-color-gray-0)', + }, +}); diff --git a/dashboard/src/favicon.svg b/dashboard/src/favicon.svg new file mode 100644 index 00000000..767b7138 --- /dev/null +++ b/dashboard/src/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dashboard/src/features/config/index.ts b/dashboard/src/features/config/index.ts new file mode 100644 index 00000000..feb92064 --- /dev/null +++ b/dashboard/src/features/config/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './slice'; +export * from './selectors'; diff --git a/dashboard/src/features/config/selectors.ts b/dashboard/src/features/config/selectors.ts new file mode 100644 index 00000000..15f21ac1 --- /dev/null +++ b/dashboard/src/features/config/selectors.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { RootState } from '../../store'; + +export const selectConfig = (state: RootState) => state.config; +export const selectAutoRefreshMs = (state: RootState) => state.config.autoRefreshMs; +export const selectBaseUrl = (state: RootState) => state.config.baseUrl; +export const selectThemePreference = (state: RootState) => state.config.theme; diff --git a/dashboard/src/features/config/slice.ts b/dashboard/src/features/config/slice.ts new file mode 100644 index 00000000..3bccb300 --- /dev/null +++ b/dashboard/src/features/config/slice.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { ConfigState, ThemePreference } from '@/types'; + +export const initialConfigState: ConfigState = { + baseUrl: typeof window !== 'undefined' ? window.location.origin : '', + autoRefreshMs: 0, + theme: 'system', +}; + +const configSlice = createSlice({ + name: 'config', + initialState: initialConfigState, + reducers: { + setBaseUrl(state, action: PayloadAction) { + state.baseUrl = action.payload; + }, + setAutoRefreshMs(state, action: PayloadAction) { + state.autoRefreshMs = action.payload; + }, + setTheme(state, action: PayloadAction) { + state.theme = action.payload; + }, + }, +}); + +export const { setAutoRefreshMs, setBaseUrl, setTheme } = configSlice.actions; + +export const configReducer = configSlice.reducer; diff --git a/dashboard/src/features/resources/index.ts b/dashboard/src/features/resources/index.ts new file mode 100644 index 00000000..a763037e --- /dev/null +++ b/dashboard/src/features/resources/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './slice'; +export * from './selectors'; +export { useGetResourcesQuery } from '../rollouts'; diff --git a/dashboard/src/features/resources/resources.test.ts b/dashboard/src/features/resources/resources.test.ts new file mode 100644 index 00000000..f98dec17 --- /dev/null +++ b/dashboard/src/features/resources/resources.test.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createServerBackedStore } from '@test-utils'; +import { describe, expect, it } from 'vitest'; +import { rolloutsApi } from '@/features/rollouts'; +import type { Resources } from '@/types'; +import { selectResourcesQueryArgs } from './selectors'; +import { + resetResourcesFilters, + setResourcesPage, + setResourcesRecordsPerPage, + setResourcesSearchTerm, + setResourcesSort, +} from './slice'; + +const extractResourceIds = (resources: Resources[]): string[] => resources.map((resource) => resource.resourcesId); + +describe('resources feature integration', () => { + it('builds default query arguments from the UI state', () => { + const store = createServerBackedStore(); + const queryArgs = selectResourcesQueryArgs(store.getState()); + + expect(queryArgs).toMatchObject({ + limit: 50, + offset: 0, + sortBy: 'update_time', + sortOrder: 'desc', + resourcesIdContains: undefined, + }); + }); + + it('fetches resources from the Python LightningStore server', async () => { + const store = createServerBackedStore(); + const queryArgs = selectResourcesQueryArgs(store.getState()); + + const subscription = store.dispatch(rolloutsApi.endpoints.getResources.initiate(queryArgs)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.total).toBe(5); + expect(data.items).toHaveLength(5); + + const resourceIds = extractResourceIds(data.items); + expect(resourceIds).toEqual(expect.arrayContaining(['rs-story-001', 'rs-story-005'])); + + const updateTimes = data.items.map((resource) => resource.updateTime); + const sortedUpdateTimes = [...updateTimes].sort((a, b) => b - a); + expect(updateTimes).toEqual(sortedUpdateTimes); + + expect(data.items[0].resources).toBeDefined(); + expect(Object.keys(data.items[0].resources)).not.toHaveLength(0); + }); + + it('paginates resource results based on UI state', async () => { + const store = createServerBackedStore(); + store.dispatch(setResourcesRecordsPerPage(2)); + store.dispatch(setResourcesPage(2)); + + const queryArgs = selectResourcesQueryArgs(store.getState()); + expect(queryArgs).toMatchObject({ limit: 2, offset: 2 }); + + const subscription = store.dispatch(rolloutsApi.endpoints.getResources.initiate(queryArgs)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.items).toHaveLength(2); + expect(data.total).toBe(5); + expect(data.items.map((resource) => resource.resourcesId)).toEqual(['rs-story-005', 'rs-story-002']); + }); + + it('applies search and sorting preferences', async () => { + const store = createServerBackedStore(); + store.dispatch(resetResourcesFilters()); + store.dispatch(setResourcesSearchTerm('rs-story-003')); + store.dispatch(setResourcesSort({ column: 'version', direction: 'asc' })); + + const queryArgs = selectResourcesQueryArgs(store.getState()); + expect(queryArgs).toMatchObject({ + limit: 50, + offset: 0, + sortBy: 'version', + sortOrder: 'asc', + resourcesIdContains: 'rs-story-003', + }); + + const subscription = store.dispatch(rolloutsApi.endpoints.getResources.initiate(queryArgs)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.items).toHaveLength(1); + expect(data.items[0].resourcesId).toBe('rs-story-003'); + expect(data.items[0].version).toBeGreaterThan(0); + }); +}); diff --git a/dashboard/src/features/resources/selectors.ts b/dashboard/src/features/resources/selectors.ts new file mode 100644 index 00000000..3313a616 --- /dev/null +++ b/dashboard/src/features/resources/selectors.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSelector } from '@reduxjs/toolkit'; +import type { GetResourcesQueryArgs } from '@/features/rollouts'; +import type { RootState } from '@/store'; +import type { ResourcesSortState } from './slice'; + +const RESOURCES_SORT_FIELD_MAP: Record = { + resourcesId: 'resources_id', + version: 'version', + createTime: 'create_time', + updateTime: 'update_time', +}; + +const resolveResourcesSortField = (sort: ResourcesSortState): string => + RESOURCES_SORT_FIELD_MAP[sort.column] ?? 'update_time'; + +export const selectResourcesUiState = (state: RootState) => state.resources; + +export const selectResourcesSearchTerm = (state: RootState) => selectResourcesUiState(state).searchTerm; +export const selectResourcesPage = (state: RootState) => selectResourcesUiState(state).page; +export const selectResourcesRecordsPerPage = (state: RootState) => selectResourcesUiState(state).recordsPerPage; +export const selectResourcesSort = (state: RootState) => selectResourcesUiState(state).sort; + +export const selectResourcesQueryArgs = createSelector( + [selectResourcesSearchTerm, selectResourcesPage, selectResourcesRecordsPerPage, selectResourcesSort], + (searchTerm, page, recordsPerPage, sort): GetResourcesQueryArgs => { + const normalizedSearch = searchTerm.trim(); + const limit = Math.max(1, recordsPerPage); + const offset = Math.max(0, (page - 1) * limit); + const sortBy = resolveResourcesSortField(sort); + + return { + limit, + offset, + sortBy, + sortOrder: sort.direction, + resourcesIdContains: normalizedSearch.length > 0 ? normalizedSearch : undefined, + }; + }, +); diff --git a/dashboard/src/features/resources/slice.ts b/dashboard/src/features/resources/slice.ts new file mode 100644 index 00000000..88f90676 --- /dev/null +++ b/dashboard/src/features/resources/slice.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; + +export type SortDirection = 'asc' | 'desc'; + +export type ResourcesSortState = { + column: string; + direction: SortDirection; +}; + +export type ResourcesUiState = { + searchTerm: string; + page: number; + recordsPerPage: number; + sort: ResourcesSortState; +}; + +export const initialResourcesUiState: ResourcesUiState = { + searchTerm: '', + page: 1, + recordsPerPage: 50, + sort: { + column: 'updateTime', + direction: 'desc', + }, +}; + +const resourcesSlice = createSlice({ + name: 'resources', + initialState: initialResourcesUiState, + reducers: { + setResourcesSearchTerm(state, action: PayloadAction) { + state.searchTerm = action.payload; + state.page = 1; + }, + setResourcesPage(state, action: PayloadAction) { + state.page = action.payload; + }, + setResourcesRecordsPerPage(state, action: PayloadAction) { + state.recordsPerPage = action.payload; + state.page = 1; + }, + setResourcesSort(state, action: PayloadAction) { + state.sort = action.payload; + }, + resetResourcesFilters(state) { + state.searchTerm = initialResourcesUiState.searchTerm; + state.page = initialResourcesUiState.page; + state.recordsPerPage = initialResourcesUiState.recordsPerPage; + state.sort = initialResourcesUiState.sort; + }, + }, +}); + +export const { + setResourcesSearchTerm, + setResourcesPage, + setResourcesRecordsPerPage, + setResourcesSort, + resetResourcesFilters, +} = resourcesSlice.actions; + +export const resourcesReducer = resourcesSlice.reducer; diff --git a/dashboard/src/features/rollouts/api.ts b/dashboard/src/features/rollouts/api.ts new file mode 100644 index 00000000..0e0fde6c --- /dev/null +++ b/dashboard/src/features/rollouts/api.ts @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { BaseQueryFn } from '@reduxjs/toolkit/query'; +import { createApi, fetchBaseQuery, type FetchArgs, type FetchBaseQueryError } from '@reduxjs/toolkit/query/react'; +import type { RootState } from '@/store'; +import { camelCaseKeys } from '@/utils/format'; +import type { + Attempt, + PaginatedResponse, + Resources, + Rollout, + RolloutMode, + RolloutStatus, + Span, + Timestamp, +} from '../../types'; + +const rawBaseQuery = fetchBaseQuery({ + baseUrl: '/', +}); + +const buildAbsoluteUrl = (baseUrl: string, path: string) => { + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + + const normalizedBase = baseUrl.replace(/\/+$/, ''); + const normalizedPath = path.replace(/^\/+/, ''); + if (!normalizedBase) { + return `/${normalizedPath}`; + } + return `${normalizedBase}/${normalizedPath}`; +}; + +const normalizeHeartbeat = ( + attempt: Partial & { lastHeartbeatTime?: Timestamp | null; lastHeartBeatTime?: Timestamp | null }, +): Timestamp | null => { + if (typeof attempt.lastHeartbeatTime === 'number') { + return attempt.lastHeartbeatTime; + } + + if (typeof attempt.lastHeartBeatTime === 'number') { + return attempt.lastHeartBeatTime; + } + + if (typeof attempt.startTime === 'number') { + return attempt.startTime; + } + + return null; +}; + +const normalizeAttempt = (value: unknown): Attempt | null => { + if (value === null || typeof value === 'undefined') { + return null; + } + + const camelized = camelCaseKeys(value) as Attempt & { + lastHeartbeatTime?: Timestamp | null; + lastHeartBeatTime?: Timestamp | null; + }; + const { lastHeartbeatTime, lastHeartBeatTime, ...rest } = camelized; + + return { + ...rest, + lastHeartbeatTime: normalizeHeartbeat({ ...rest, lastHeartbeatTime, lastHeartBeatTime }), + }; +}; + +const normalizeAttemptStrict = (value: unknown): Attempt => { + const normalized = normalizeAttempt(value); + if (!normalized) { + throw new Error('Expected attempt payload'); + } + return normalized; +}; + +const normalizeRollout = (value: unknown): Rollout => { + const camelized = camelCaseKeys(value) as Rollout & { attempt?: unknown }; + const { attempt, ...rest } = camelized; + + return { + ...rest, + attempt: normalizeAttempt(attempt), + }; +}; + +const normalizeSpan = (value: unknown): Span => { + const camelized = camelCaseKeys(value) as Span & { + status?: { + status_code?: Span['status']['status_code']; + statusCode?: Span['status']['status_code']; + description?: string | null; + }; + }; + const rawStatus = camelized.status ?? { status_code: 'UNSET', description: null }; + return { + ...camelized, + parentId: camelized.parentId ?? null, + attributes: camelized.attributes ?? {}, + status: { + status_code: rawStatus.status_code ?? rawStatus.statusCode ?? 'UNSET', + description: rawStatus.description ?? null, + }, + }; +}; + +const normalizeResources = (value: unknown): Resources => { + const camelized = camelCaseKeys(value) as Resources; + return { + resourcesId: camelized.resourcesId, + version: camelized.version, + createTime: camelized.createTime, + updateTime: camelized.updateTime, + resources: camelized.resources ?? {}, + }; +}; + +const normalizePaginatedResponse = (value: unknown, normalizer: (item: unknown) => T): PaginatedResponse => { + if (!value || typeof value !== 'object') { + throw new Error('Expected paginated response payload'); + } + + const camelized = camelCaseKeys(value) as { + items?: unknown; + limit?: number; + offset?: number; + total?: number; + }; + + const itemsSource = Array.isArray(camelized.items) ? camelized.items : []; + + return { + items: itemsSource.map((item) => normalizer(item)), + limit: typeof camelized.limit === 'number' ? camelized.limit : itemsSource.length, + offset: typeof camelized.offset === 'number' ? camelized.offset : 0, + total: typeof camelized.total === 'number' ? camelized.total : itemsSource.length, + }; +}; + +const dynamicBaseQuery: BaseQueryFn = async ( + args, + api, + extraOptions, +) => { + const state = api.getState() as RootState; + const stateBaseUrl = state.config?.baseUrl; + const fallbackBaseUrl = typeof window !== 'undefined' ? window.location.origin : ''; + const baseUrl = stateBaseUrl && stateBaseUrl.trim().length > 0 ? stateBaseUrl : fallbackBaseUrl; + const preparedArgs: FetchArgs = + typeof args === 'string' + ? { url: args } + : { + ...args, + url: args.url ?? '', + }; + + const absoluteUrl = buildAbsoluteUrl(baseUrl, preparedArgs.url ?? ''); + return rawBaseQuery({ ...preparedArgs, url: absoluteUrl }, api, extraOptions); +}; + +export type GetRolloutsQueryArgs = { + limit: number; + offset: number; + sortBy?: string | null; + sortOrder?: 'asc' | 'desc'; + statusIn?: RolloutStatus[]; + rolloutIdContains?: string | null; + modeIn?: RolloutMode[]; +}; + +export type GetResourcesQueryArgs = { + limit: number; + offset: number; + sortBy?: string | null; + sortOrder?: 'asc' | 'desc'; + resourcesIdContains?: string | null; +}; + +export type GetRolloutAttemptsQueryArgs = { + rolloutId: string; + limit?: number; + offset?: number; + sortBy?: string | null; + sortOrder?: 'asc' | 'desc'; +}; + +export type GetSpansQueryArgs = { + rolloutId: string; + attemptId?: string | null; + limit?: number; + offset?: number; + sortBy?: string | null; + sortOrder?: 'asc' | 'desc'; + traceIdContains?: string | null; + spanIdContains?: string | null; + parentIdContains?: string | null; + nameContains?: string | null; + filterLogic?: 'and' | 'or' | null; +}; + +export const rolloutsApi = createApi({ + reducerPath: 'rolloutsApi', + baseQuery: dynamicBaseQuery, + tagTypes: ['Rollout', 'Span', 'Resources'], + endpoints: (builder) => ({ + getResources: builder.query, GetResourcesQueryArgs>({ + query: ({ limit, offset, sortBy, sortOrder, resourcesIdContains }) => { + const searchParams = new URLSearchParams(); + searchParams.set('limit', String(typeof limit === 'number' ? limit : -1)); + searchParams.set('offset', String(typeof offset === 'number' ? offset : 0)); + if (sortBy) { + searchParams.set('sort_by', sortBy); + } + if (sortOrder) { + searchParams.set('sort_order', sortOrder); + } + if (resourcesIdContains && resourcesIdContains.trim().length > 0) { + searchParams.set('resources_id_contains', resourcesIdContains.trim()); + } + + const queryString = searchParams.toString(); + const url = queryString.length > 0 ? `v1/agl/resources?${queryString}` : 'v1/agl/resources'; + return { url, method: 'GET' }; + }, + transformResponse: (response: unknown) => normalizePaginatedResponse(response, normalizeResources), + providesTags: (result) => + result + ? [ + { type: 'Resources' as const, id: 'LIST' }, + ...result.items.map((item) => ({ type: 'Resources' as const, id: item.resourcesId })), + ] + : [{ type: 'Resources' as const, id: 'LIST' }], + }), + getRollouts: builder.query, GetRolloutsQueryArgs>({ + query: ({ limit, offset, sortBy, sortOrder, statusIn, rolloutIdContains, modeIn }) => { + const searchParams = new URLSearchParams(); + searchParams.set('limit', String(typeof limit === 'number' ? limit : -1)); + searchParams.set('offset', String(typeof offset === 'number' ? offset : 0)); + if (sortBy) { + searchParams.set('sort_by', sortBy); + } + if (sortOrder) { + searchParams.set('sort_order', sortOrder); + } + if (statusIn && statusIn.length > 0) { + statusIn.forEach((status) => searchParams.append('status_in', status)); + } + if (modeIn && modeIn.length > 0) { + modeIn.forEach((mode) => searchParams.append('mode_in', mode)); + } + if (rolloutIdContains && rolloutIdContains.trim().length > 0) { + searchParams.set('rollout_id_contains', rolloutIdContains.trim()); + } + + const queryString = searchParams.toString(); + const url = queryString.length > 0 ? `v1/agl/rollouts?${queryString}` : 'v1/agl/rollouts'; + return { url, method: 'GET' }; + }, + transformResponse: (response: unknown) => normalizePaginatedResponse(response, normalizeRollout), + providesTags: (result) => + result + ? [ + { type: 'Rollout' as const, id: 'LIST' }, + ...result.items.map((rollout) => ({ type: 'Rollout' as const, id: rollout.rolloutId })), + ] + : [{ type: 'Rollout' as const, id: 'LIST' }], + }), + getRolloutAttempts: builder.query, GetRolloutAttemptsQueryArgs>({ + query: ({ rolloutId, limit = -1, offset = 0, sortBy, sortOrder }) => { + const searchParams = new URLSearchParams(); + searchParams.set('limit', String(typeof limit === 'number' ? limit : -1)); + searchParams.set('offset', String(typeof offset === 'number' ? offset : 0)); + if (sortBy) { + searchParams.set('sort_by', sortBy); + } + if (sortOrder) { + searchParams.set('sort_order', sortOrder); + } + const queryString = searchParams.toString(); + const url = + queryString.length > 0 + ? `v1/agl/rollouts/${rolloutId}/attempts?${queryString}` + : `v1/agl/rollouts/${rolloutId}/attempts`; + return { url, method: 'GET' }; + }, + transformResponse: (response: unknown) => normalizePaginatedResponse(response, normalizeAttemptStrict), + providesTags: (_result, _error, queryArgs) => [{ type: 'Rollout', id: queryArgs.rolloutId }], + }), + getSpans: builder.query, GetSpansQueryArgs>({ + query: (args) => { + if (!args.rolloutId) { + throw new Error('rolloutId is required to fetch spans'); + } + const searchParams = new URLSearchParams({ rollout_id: args.rolloutId }); + if (args.attemptId) { + searchParams.set('attempt_id', args.attemptId); + } + if (typeof args.limit === 'number') { + searchParams.set('limit', String(args.limit)); + } + if (typeof args.offset === 'number') { + searchParams.set('offset', String(args.offset)); + } + if (args.sortBy) { + searchParams.set('sort_by', args.sortBy); + } + if (args.sortOrder) { + searchParams.set('sort_order', args.sortOrder); + } + if (args.traceIdContains) { + searchParams.set('trace_id_contains', args.traceIdContains); + } + if (args.spanIdContains) { + searchParams.set('span_id_contains', args.spanIdContains); + } + if (args.parentIdContains) { + searchParams.set('parent_id_contains', args.parentIdContains); + } + if (args.nameContains) { + searchParams.set('name_contains', args.nameContains); + } + if (args.filterLogic) { + searchParams.set('filter_logic', args.filterLogic); + } + return { url: `v1/agl/spans?${searchParams.toString()}`, method: 'GET' }; + }, + transformResponse: (response: unknown) => normalizePaginatedResponse(response, normalizeSpan), + providesTags: (_result, _error, args) => + args + ? [ + { type: 'Span' as const, id: `${args.rolloutId}:${args.attemptId ?? 'latest'}` }, + { type: 'Span' as const, id: 'LIST' }, + ] + : [{ type: 'Span' as const, id: 'LIST' }], + }), + }), +}); + +export const { useGetResourcesQuery, useGetRolloutsQuery, useGetRolloutAttemptsQuery, useGetSpansQuery } = rolloutsApi; diff --git a/dashboard/src/features/rollouts/index.ts b/dashboard/src/features/rollouts/index.ts new file mode 100644 index 00000000..8038d785 --- /dev/null +++ b/dashboard/src/features/rollouts/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './api'; +export * from './slice'; +export * from './selectors'; +export * from '../../types'; diff --git a/dashboard/src/features/rollouts/rollouts.test.ts b/dashboard/src/features/rollouts/rollouts.test.ts new file mode 100644 index 00000000..255a2417 --- /dev/null +++ b/dashboard/src/features/rollouts/rollouts.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createServerBackedStore } from '@test-utils'; +import { describe, expect, it } from 'vitest'; +import { rolloutsApi } from './api'; +import { selectRolloutsQueryArgs } from './selectors'; +import { + resetRolloutsFilters, + setRolloutsModeFilters, + setRolloutsPage, + setRolloutsRecordsPerPage, + setRolloutsSearchTerm, + setRolloutsSort, + setRolloutsStatusFilters, +} from './slice'; + +describe('rollouts feature integration', () => { + it('builds default query arguments from the UI state', () => { + const store = createServerBackedStore(); + const queryArgs = selectRolloutsQueryArgs(store.getState()); + + expect(queryArgs).toMatchObject({ + limit: 100, + offset: 0, + sortBy: 'start_time', + sortOrder: 'desc', + rolloutIdContains: undefined, + statusIn: undefined, + modeIn: undefined, + }); + }); + + it('retrieves rollouts from the Python LightningStore server', async () => { + const store = createServerBackedStore(); + const queryArgs = selectRolloutsQueryArgs(store.getState()); + + const subscription = store.dispatch(rolloutsApi.endpoints.getRollouts.initiate(queryArgs)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.total).toBe(6); + expect(data.items).toHaveLength(6); + + const rolloutIds = data.items.map((rollout) => rollout.rolloutId); + expect(rolloutIds).toEqual( + expect.arrayContaining(['ro-story-001', 'ro-story-002', 'ro-story-003', 'ro-story-004', 'ro-story-005']), + ); + + const startTimes = data.items.map((rollout) => rollout.startTime); + const sortedStartTimes = [...startTimes].sort((a, b) => b - a); + expect(startTimes).toEqual(sortedStartTimes); + expect(data.items[0].rolloutId).toBe('ro-story-005'); + expect(data.items[0].status).toBeDefined(); + }); + + it('retrieves attempts for a rollout from the Python server', async () => { + const store = createServerBackedStore(); + const subscription = store.dispatch( + rolloutsApi.endpoints.getRolloutAttempts.initiate({ rolloutId: 'ro-story-002' }), + ); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.total).toBe(2); + expect(data.items.map((attempt) => attempt.attemptId)).toEqual(['at-story-021', 'at-story-022']); + }); + + it('paginates rollouts with custom UI state', async () => { + const store = createServerBackedStore(); + store.dispatch(setRolloutsRecordsPerPage(2)); + store.dispatch(setRolloutsPage(2)); + + const queryArgs = selectRolloutsQueryArgs(store.getState()); + expect(queryArgs).toMatchObject({ limit: 2, offset: 2 }); + + const subscription = store.dispatch(rolloutsApi.endpoints.getRollouts.initiate(queryArgs)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.items).toHaveLength(2); + expect(data.items.map((rollout) => rollout.rolloutId)).toEqual(['ro-story-004', 'ro-story-002']); + }); + + it('filters and sorts rollouts based on UI selections', async () => { + const store = createServerBackedStore(); + store.dispatch(resetRolloutsFilters()); + store.dispatch(setRolloutsStatusFilters(['succeeded'])); + store.dispatch(setRolloutsModeFilters(['val'])); + store.dispatch(setRolloutsSearchTerm('ro-story-002')); + store.dispatch(setRolloutsSort({ column: 'rolloutId', direction: 'asc' })); + + const queryArgs = selectRolloutsQueryArgs(store.getState()); + expect(queryArgs).toMatchObject({ + statusIn: ['succeeded'], + modeIn: ['val'], + rolloutIdContains: 'ro-story-002', + sortBy: 'rollout_id', + sortOrder: 'asc', + }); + + const subscription = store.dispatch(rolloutsApi.endpoints.getRollouts.initiate(queryArgs)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.items).toHaveLength(1); + expect(data.items[0].rolloutId).toBe('ro-story-002'); + expect(data.items[0].status).toBe('succeeded'); + }); +}); diff --git a/dashboard/src/features/rollouts/selectors.ts b/dashboard/src/features/rollouts/selectors.ts new file mode 100644 index 00000000..f7c70b5e --- /dev/null +++ b/dashboard/src/features/rollouts/selectors.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSelector } from '@reduxjs/toolkit'; +import type { RootState } from '@/store'; +import type { RolloutMode, RolloutStatus } from '@/types'; +import type { RolloutsSortState } from './slice'; + +const ROLLOUTS_SORT_FIELD_MAP: Record = { + rolloutId: 'rollout_id', + attemptId: 'attempt_id', + statusValue: 'status', + resourcesId: 'resources_id', + mode: 'mode', + startTimestamp: 'start_time', + durationSeconds: 'duration', + lastHeartbeatTimestamp: 'last_heartbeat_time', + workerId: 'worker_id', +}; + +const resolveRolloutsSortField = (sort: RolloutsSortState): string => + ROLLOUTS_SORT_FIELD_MAP[sort.column] ?? 'start_time'; + +export const selectRolloutsUiState = (state: RootState) => state.rollouts; +export const selectRolloutsSearchTerm = (state: RootState) => state.rollouts.searchTerm; +export const selectRolloutsStatusFilters = (state: RootState) => state.rollouts.statusFilters; +export const selectRolloutsModeFilters = (state: RootState) => state.rollouts.modeFilters; +export const selectRolloutsPage = (state: RootState) => state.rollouts.page; +export const selectRolloutsRecordsPerPage = (state: RootState) => state.rollouts.recordsPerPage; +export const selectRolloutsSort = (state: RootState) => state.rollouts.sort; + +export const selectRolloutsQueryArgs = createSelector( + [ + selectRolloutsSearchTerm, + selectRolloutsStatusFilters, + selectRolloutsModeFilters, + selectRolloutsPage, + selectRolloutsRecordsPerPage, + selectRolloutsSort, + ], + ( + searchTerm: string, + statusFilters: RolloutStatus[], + modeFilters: RolloutMode[], + page: number, + recordsPerPage: number, + sort: RolloutsSortState, + ) => { + const normalizedSearch = searchTerm.trim(); + const limit = Math.max(1, recordsPerPage); + const offset = Math.max(0, (page - 1) * limit); + const sortBy = resolveRolloutsSortField(sort); + + return { + limit, + offset, + sortBy, + sortOrder: sort.direction, + statusIn: statusFilters.length > 0 ? statusFilters : undefined, + rolloutIdContains: normalizedSearch.length > 0 ? normalizedSearch : undefined, + modeIn: modeFilters.length > 0 ? modeFilters : undefined, + }; + }, +); diff --git a/dashboard/src/features/rollouts/slice.ts b/dashboard/src/features/rollouts/slice.ts new file mode 100644 index 00000000..4cccbed2 --- /dev/null +++ b/dashboard/src/features/rollouts/slice.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RolloutMode, RolloutStatus } from '../../types'; + +export type SortDirection = 'asc' | 'desc'; + +export type RolloutsSortState = { + column: string; + direction: SortDirection; +}; + +export type RolloutsUiState = { + searchTerm: string; + statusFilters: RolloutStatus[]; + modeFilters: RolloutMode[]; + page: number; + recordsPerPage: number; + sort: RolloutsSortState; +}; + +export const initialRolloutsUiState: RolloutsUiState = { + searchTerm: '', + statusFilters: [], + modeFilters: [], + page: 1, + recordsPerPage: 100, + sort: { + column: 'startTimestamp', + direction: 'desc', + }, +}; + +const rolloutsSlice = createSlice({ + name: 'rollouts', + initialState: initialRolloutsUiState, + reducers: { + setRolloutsSearchTerm(state, action: PayloadAction) { + state.searchTerm = action.payload; + state.page = 1; + }, + setRolloutsStatusFilters(state, action: PayloadAction) { + state.statusFilters = action.payload; + state.page = 1; + }, + setRolloutsModeFilters(state, action: PayloadAction) { + state.modeFilters = action.payload; + state.page = 1; + }, + setRolloutsPage(state, action: PayloadAction) { + state.page = action.payload; + }, + setRolloutsRecordsPerPage(state, action: PayloadAction) { + state.recordsPerPage = action.payload; + state.page = 1; + }, + setRolloutsSort(state, action: PayloadAction) { + state.sort = action.payload; + }, + resetRolloutsFilters(state) { + state.statusFilters = initialRolloutsUiState.statusFilters; + state.modeFilters = initialRolloutsUiState.modeFilters; + state.searchTerm = initialRolloutsUiState.searchTerm; + state.page = initialRolloutsUiState.page; + state.sort = initialRolloutsUiState.sort; + }, + }, +}); + +export const { + setRolloutsSearchTerm, + setRolloutsStatusFilters, + setRolloutsModeFilters, + setRolloutsPage, + setRolloutsRecordsPerPage, + setRolloutsSort, + resetRolloutsFilters, +} = rolloutsSlice.actions; + +export const rolloutsReducer = rolloutsSlice.reducer; diff --git a/dashboard/src/features/traces/index.ts b/dashboard/src/features/traces/index.ts new file mode 100644 index 00000000..feb92064 --- /dev/null +++ b/dashboard/src/features/traces/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './slice'; +export * from './selectors'; diff --git a/dashboard/src/features/traces/selectors.ts b/dashboard/src/features/traces/selectors.ts new file mode 100644 index 00000000..f9e7e368 --- /dev/null +++ b/dashboard/src/features/traces/selectors.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSelector } from '@reduxjs/toolkit'; +import type { GetSpansQueryArgs } from '@/features/rollouts'; +import type { RootState } from '@/store'; +import type { TracesSortState } from './slice'; + +export const selectTracesState = (state: RootState) => state.traces; + +export const selectTracesRolloutId = (state: RootState) => selectTracesState(state).rolloutId; + +export const selectTracesAttemptId = (state: RootState) => selectTracesState(state).attemptId; + +export const selectTracesSearchTerm = (state: RootState) => selectTracesState(state).searchTerm; + +export const selectTracesPage = (state: RootState) => selectTracesState(state).page; + +export const selectTracesRecordsPerPage = (state: RootState) => selectTracesState(state).recordsPerPage; + +export const selectTracesSort = (state: RootState) => selectTracesState(state).sort; + +export const selectTracesViewMode = (state: RootState) => selectTracesState(state).viewMode; + +const TRACES_SORT_FIELD_MAP: Record = { + name: 'name', + traceId: 'trace_id', + spanId: 'span_id', + parentId: 'parent_id', + statusCode: 'status_code', + startTime: 'start_time', + duration: 'duration', +}; + +const resolveTracesSortField = (sort: TracesSortState): string => TRACES_SORT_FIELD_MAP[sort.column] ?? 'start_time'; + +export const selectTracesQueryArgs = createSelector( + [ + selectTracesRolloutId, + selectTracesAttemptId, + selectTracesSearchTerm, + selectTracesPage, + selectTracesRecordsPerPage, + selectTracesSort, + ], + (rolloutId, attemptId, searchTerm, page, recordsPerPage, sort): GetSpansQueryArgs | undefined => { + if (!rolloutId) { + return undefined; + } + const limit = Math.max(1, recordsPerPage); + const offset = Math.max(0, (page - 1) * limit); + const normalizedSearch = searchTerm.trim(); + const containsValue = normalizedSearch.length > 0 ? normalizedSearch : undefined; + + const sortBy = resolveTracesSortField(sort); + + return { + rolloutId, + attemptId: attemptId ?? undefined, + limit, + offset, + sortBy, + sortOrder: sort.direction, + traceIdContains: containsValue, + spanIdContains: containsValue, + nameContains: containsValue, + filterLogic: containsValue ? 'or' : undefined, + }; + }, +); diff --git a/dashboard/src/features/traces/slice.ts b/dashboard/src/features/traces/slice.ts new file mode 100644 index 00000000..fbbfd372 --- /dev/null +++ b/dashboard/src/features/traces/slice.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; + +export type SortDirection = 'asc' | 'desc'; + +export type TracesSortState = { + column: string; + direction: SortDirection; +}; + +export type TracesViewMode = 'table' | 'waterfall' | 'tree'; + +export type TracesUiState = { + rolloutId: string | null; + attemptId: string | null; + searchTerm: string; + page: number; + recordsPerPage: number; + sort: TracesSortState; + viewMode: TracesViewMode; +}; + +export const initialTracesUiState: TracesUiState = { + rolloutId: null, + attemptId: null, + searchTerm: '', + page: 1, + recordsPerPage: 100, + sort: { + column: 'startTime', + direction: 'desc', + }, + viewMode: 'table', +}; + +const tracesSlice = createSlice({ + name: 'traces', + initialState: initialTracesUiState, + reducers: { + setTracesRolloutId(state, action: PayloadAction) { + state.rolloutId = action.payload; + state.page = 1; + state.attemptId = null; + }, + setTracesAttemptId(state, action: PayloadAction) { + state.attemptId = action.payload; + state.page = 1; + }, + setTracesSearchTerm(state, action: PayloadAction) { + state.searchTerm = action.payload; + state.page = 1; + }, + setTracesPage(state, action: PayloadAction) { + state.page = action.payload; + }, + setTracesRecordsPerPage(state, action: PayloadAction) { + state.recordsPerPage = action.payload; + state.page = 1; + }, + setTracesSort(state, action: PayloadAction) { + state.sort = action.payload; + }, + setTracesViewMode(state, action: PayloadAction) { + state.viewMode = action.payload; + }, + resetTracesFilters(state) { + state.searchTerm = initialTracesUiState.searchTerm; + state.page = initialTracesUiState.page; + state.sort = initialTracesUiState.sort; + }, + }, +}); + +export const { + setTracesRolloutId, + setTracesAttemptId, + setTracesSearchTerm, + setTracesPage, + setTracesRecordsPerPage, + setTracesSort, + setTracesViewMode, + resetTracesFilters, +} = tracesSlice.actions; + +export const tracesReducer = tracesSlice.reducer; diff --git a/dashboard/src/features/traces/traces.test.ts b/dashboard/src/features/traces/traces.test.ts new file mode 100644 index 00000000..1bba62a6 --- /dev/null +++ b/dashboard/src/features/traces/traces.test.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createServerBackedStore } from '@test-utils'; +import { describe, expect, it } from 'vitest'; +import { rolloutsApi } from '../rollouts'; +import { selectTracesQueryArgs } from './selectors'; +import { setTracesPage, setTracesRecordsPerPage, setTracesRolloutId, setTracesSearchTerm } from './slice'; + +describe('traces feature integration', () => { + it('requires a rollout id before building query arguments', () => { + const store = createServerBackedStore(); + expect(selectTracesQueryArgs(store.getState())).toBeUndefined(); + }); + + it('builds query arguments when targeting a rollout', () => { + const store = createServerBackedStore(); + store.dispatch(setTracesRolloutId('ro-story-001')); + + const queryArgs = selectTracesQueryArgs(store.getState()); + expect(queryArgs).toBeDefined(); + expect(queryArgs).toMatchObject({ + rolloutId: 'ro-story-001', + limit: 100, + offset: 0, + sortBy: 'start_time', + sortOrder: 'desc', + filterLogic: undefined, + }); + + store.dispatch(setTracesSearchTerm('span-00')); + const filteredArgs = selectTracesQueryArgs(store.getState()); + expect(filteredArgs?.filterLogic).toBe('or'); + }); + + it('fetches spans from the Python LightningStore server', async () => { + const store = createServerBackedStore(); + store.dispatch(setTracesRolloutId('ro-story-001')); + + const queryArgs = selectTracesQueryArgs(store.getState()); + expect(queryArgs).toBeDefined(); + + const subscription = store.dispatch(rolloutsApi.endpoints.getSpans.initiate(queryArgs!)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.total).toBe(3); + const spanIds = data.items.map((span) => span.spanId).sort(); + expect(spanIds).toEqual(['span-001-root', 'span-002-llm', 'span-003-tool']); + expect(new Set(data.items.map((span) => span.status.status_code))).toEqual(new Set(['OK'])); + }); + + it('paginates spans based on UI state', async () => { + const store = createServerBackedStore(); + store.dispatch(setTracesRolloutId('ro-story-001')); + store.dispatch(setTracesRecordsPerPage(2)); + + const firstPageArgs = selectTracesQueryArgs(store.getState()); + expect(firstPageArgs).toMatchObject({ limit: 2, offset: 0 }); + + const firstPageSub = store.dispatch(rolloutsApi.endpoints.getSpans.initiate(firstPageArgs!)); + const firstPage = await firstPageSub.unwrap(); + firstPageSub.unsubscribe(); + + expect(firstPage.items.map((span) => span.spanId)).toEqual(['span-003-tool', 'span-002-llm']); + + store.dispatch(setTracesPage(2)); + const secondPageArgs = selectTracesQueryArgs(store.getState()); + expect(secondPageArgs).toMatchObject({ limit: 2, offset: 2 }); + + const secondPageSub = store.dispatch(rolloutsApi.endpoints.getSpans.initiate(secondPageArgs!)); + const secondPage = await secondPageSub.unwrap(); + secondPageSub.unsubscribe(); + + expect(secondPage.items.map((span) => span.spanId)).toEqual(['span-001-root']); + expect(secondPage.total).toBe(3); + }); + + it('filters spans using the search term', async () => { + const store = createServerBackedStore(); + store.dispatch(setTracesRolloutId('ro-story-001')); + store.dispatch(setTracesSearchTerm('span-003')); + + const queryArgs = selectTracesQueryArgs(store.getState()); + expect(queryArgs).toMatchObject({ + traceIdContains: 'span-003', + spanIdContains: 'span-003', + nameContains: 'span-003', + filterLogic: 'or', + }); + + const subscription = store.dispatch(rolloutsApi.endpoints.getSpans.initiate(queryArgs!)); + const data = await subscription.unwrap(); + subscription.unsubscribe(); + + expect(data.items).toHaveLength(1); + expect(data.items[0].spanId).toBe('span-003-tool'); + }); +}); diff --git a/dashboard/src/features/ui/alert/index.ts b/dashboard/src/features/ui/alert/index.ts new file mode 100644 index 00000000..feb92064 --- /dev/null +++ b/dashboard/src/features/ui/alert/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './slice'; +export * from './selectors'; diff --git a/dashboard/src/features/ui/alert/selectors.ts b/dashboard/src/features/ui/alert/selectors.ts new file mode 100644 index 00000000..c59cb682 --- /dev/null +++ b/dashboard/src/features/ui/alert/selectors.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSelector } from '@reduxjs/toolkit'; +import type { RootState } from '@/store'; +import type { AlertsState, AlertTone } from './slice'; + +const ALERT_PRIORITY: Record = { + error: 3, + warning: 2, + info: 1, +}; + +const selectAlertState = (state: RootState): AlertsState => state.alert; + +export const selectVisibleAlerts = createSelector(selectAlertState, (state) => + state.alerts.filter((alert) => alert.isVisible), +); + +export const selectHighestPriorityAlert = createSelector(selectVisibleAlerts, (alerts) => { + if (alerts.length === 0) { + return null; + } + + return alerts + .slice() + .sort((a, b) => { + const priorityDiff = ALERT_PRIORITY[a.tone] - ALERT_PRIORITY[b.tone]; + if (priorityDiff !== 0) { + return priorityDiff; + } + return a.createdAt - b.createdAt; + }) + .at(-1)!; +}); diff --git a/dashboard/src/features/ui/alert/slice.ts b/dashboard/src/features/ui/alert/slice.ts new file mode 100644 index 00000000..d1a49f1d --- /dev/null +++ b/dashboard/src/features/ui/alert/slice.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; + +export type AlertTone = 'info' | 'warning' | 'error'; + +export type AppAlert = { + id: string; + message: string; + tone: AlertTone; + isVisible: boolean; + createdAt: number; +}; + +export type AlertsState = { + alerts: AppAlert[]; +}; + +const initialState: AlertsState = { + alerts: [], +}; + +type ShowAlertPayload = { + id: string; + message: string; + tone?: AlertTone; +}; + +type HideAlertPayload = { + id: string; +}; + +const alertsSlice = createSlice({ + name: 'alert', + initialState, + reducers: { + showAlert(state, action: PayloadAction) { + const { id, message, tone = 'info' } = action.payload; + const existing = state.alerts.find((alert) => alert.id === id); + + if (existing) { + existing.message = message; + existing.tone = tone; + existing.isVisible = true; + existing.createdAt = Date.now(); + } else { + state.alerts.push({ + id, + message, + tone, + isVisible: true, + createdAt: Date.now(), + }); + } + }, + hideAlert(state, action: PayloadAction) { + state.alerts = state.alerts.filter((alert) => alert.id !== action.payload.id); + }, + dismissAlert(state, action: PayloadAction) { + const entry = state.alerts.find((alert) => alert.id === action.payload.id); + if (entry) { + entry.isVisible = false; + } + }, + clearAlerts(state) { + state.alerts = []; + }, + }, +}); + +export const { showAlert, hideAlert, dismissAlert, clearAlerts } = alertsSlice.actions; +export const alertReducer = alertsSlice.reducer; diff --git a/dashboard/src/features/ui/drawer/index.ts b/dashboard/src/features/ui/drawer/index.ts new file mode 100644 index 00000000..feb92064 --- /dev/null +++ b/dashboard/src/features/ui/drawer/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +export * from './slice'; +export * from './selectors'; diff --git a/dashboard/src/features/ui/drawer/selectors.ts b/dashboard/src/features/ui/drawer/selectors.ts new file mode 100644 index 00000000..74eca215 --- /dev/null +++ b/dashboard/src/features/ui/drawer/selectors.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { RootState } from '@/store'; + +export const selectDrawerState = (state: RootState) => state.drawer; + +export const selectDrawerIsOpen = (state: RootState) => selectDrawerState(state).isOpen; + +export const selectDrawerContent = (state: RootState) => selectDrawerState(state).content; diff --git a/dashboard/src/features/ui/drawer/slice.ts b/dashboard/src/features/ui/drawer/slice.ts new file mode 100644 index 00000000..eecae7bc --- /dev/null +++ b/dashboard/src/features/ui/drawer/slice.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { Attempt, Rollout, Span } from '@/types'; + +export type DrawerType = 'rollout-json' | 'rollout-traces' | 'trace-detail'; + +export type DrawerContent = + | { + type: 'rollout-json' | 'rollout-traces'; + rollout: Rollout; + attempt: Attempt | null; + isNested: boolean; + } + | { + type: 'trace-detail'; + span: Span; + rollout: Rollout | null; + attempt: Attempt | null; + }; + +export type DrawerState = { + isOpen: boolean; + content: DrawerContent | null; +}; + +export const initialDrawerState: DrawerState = { + isOpen: false, + content: null, +}; + +const drawerSlice = createSlice({ + name: 'drawer', + initialState: initialDrawerState, + reducers: { + openDrawer(state, action: PayloadAction) { + state.isOpen = true; + state.content = action.payload; + }, + closeDrawer(state) { + state.isOpen = false; + state.content = null; + }, + }, +}); + +export const { openDrawer, closeDrawer } = drawerSlice.actions; + +export const drawerReducer = drawerSlice.reducer; diff --git a/dashboard/src/layouts/AppLayout.story.tsx b/dashboard/src/layouts/AppLayout.story.tsx new file mode 100644 index 00000000..cc041db8 --- /dev/null +++ b/dashboard/src/layouts/AppLayout.story.tsx @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { http, HttpResponse } from 'msw'; +import { Provider } from 'react-redux'; +import { createMemoryRouter, RouterProvider } from 'react-router-dom'; +import { Stack, Text } from '@mantine/core'; +import { initialConfigState } from '@/features/config/slice'; +import type { AlertsState, AlertTone } from '@/features/ui/alert'; +import { createAppStore } from '@/store'; +import { STORY_BASE_URL, STORY_DATE_NOW_MS } from '../../.storybook/constants'; +import { AppLayout, AppLayoutProps } from './AppLayout'; + +const Placeholder = ({ title, description }: { title: string; description: string }) => ( + + + {title} + + + {description} + + +); + +const ROUTES = [ + { + path: '/', + element: , + children: [ + { + path: 'rollouts', + element: , + }, + { + path: 'resources', + element: , + }, + { + path: 'traces', + element: , + }, + { + path: 'settings', + element: ( + + ), + }, + ], + }, +]; + +function createAlertState(message: string, tone: AlertTone): AlertsState { + return { + alerts: [ + { + id: 'storybook-alert', + message, + tone, + isVisible: true, + createdAt: STORY_DATE_NOW_MS, + }, + ], + }; +} + +function renderAppLayout(args: AppLayoutProps, initialEntry = '/rollouts', alertState?: AlertsState) { + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + }, + alert: alertState ?? { alerts: [] }, + }); + const router = createMemoryRouter( + ROUTES.map((route) => ({ + ...route, + element: , + })), + { initialEntries: [initialEntry] }, + ); + + return ( + + + + ); +} + +const meta: Meta = { + title: 'Layouts/AppLayout', + component: AppLayout, + parameters: { + layout: 'fullscreen', + }, + render: (args) => renderAppLayout(args, '/rollouts'), + args: { + config: { + baseUrl: STORY_BASE_URL, + autoRefreshMs: 0, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const NoServerConfigured: Story = { + args: { + config: { + baseUrl: '', + autoRefreshMs: 0, + }, + }, +}; + +export const ServerOnline: Story = { + parameters: { + msw: { + handlers: [ + http.get(`${STORY_BASE_URL}/v1/agl/health`, () => HttpResponse.json({ status: 'ok' }, { status: 200 })), + ], + }, + }, +}; + +export const ServerOffline: Story = { + parameters: { + msw: { + handlers: [ + http.get(`${STORY_BASE_URL}/v1/agl/health`, () => + HttpResponse.json({ message: 'unavailable' }, { status: 503 }), + ), + ], + }, + }, +}; + +export const ResourcesNavActive: Story = { + render: (args) => renderAppLayout(args, '/resources'), +}; + +export const TracesNavActive: Story = { + render: (args) => renderAppLayout(args, '/traces'), +}; + +export const SettingsNavActive: Story = { + render: (args) => renderAppLayout(args, '/settings'), +}; + +export const PollingEveryFiveSeconds: Story = { + args: { + config: { + baseUrl: STORY_BASE_URL, + autoRefreshMs: 5000, + }, + }, + parameters: { + msw: { + handlers: [ + http.get(`${STORY_BASE_URL}/v1/agl/health`, () => HttpResponse.json({ status: 'ok' }, { status: 200 })), + ], + }, + }, +}; + +export const InfoAlertActive: Story = { + render: (args) => + renderAppLayout(args, '/rollouts', createAlertState('Background synchronization completed successfully.', 'info')), +}; + +export const WarningAlertActive: Story = { + render: (args) => + renderAppLayout( + args, + '/rollouts', + createAlertState('Rollout data may be stale. Check connectivity before proceeding.', 'warning'), + ), +}; + +export const ErrorAlertActive: Story = { + render: (args) => + renderAppLayout( + args, + '/rollouts', + createAlertState('Unable to reach Agent-lightning API. Retry or adjust server settings.', 'error'), + ), +}; diff --git a/dashboard/src/layouts/AppLayout.tsx b/dashboard/src/layouts/AppLayout.tsx new file mode 100644 index 00000000..7867bf70 --- /dev/null +++ b/dashboard/src/layouts/AppLayout.tsx @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useEffect, useMemo, useState, type ReactNode } from 'react'; +import { IconCpu, IconRouteSquare, IconSettings, IconTimeline } from '@tabler/icons-react'; +import { Outlet, NavLink as RouterNavLink, useLocation, useNavigate } from 'react-router-dom'; +import { AppShell, Badge, Group, Image, NavLink as MantineNavLink, Stack, Text, UnstyledButton } from '@mantine/core'; +import { AppAlertBanner } from '@/components/AppAlertBanner'; +import { AppDrawerContainer } from '@/components/AppDrawer.component'; +import faviconUrl from '../favicon.svg'; +import { selectConfig } from '../features/config'; +import { useAppSelector } from '../store/hooks'; + +type ConnectionStatus = 'online' | 'offline' | 'unknown'; + +type NavItem = { + label: string; + to: string; + icon: ReactNode; +}; + +const NAV_ITEMS: NavItem[] = [ + { label: 'Rollouts', to: '/rollouts', icon: }, + { label: 'Resources', to: '/resources', icon: }, + { label: 'Traces', to: '/traces', icon: }, + { label: 'Settings', to: '/settings', icon: }, +]; + +const CONNECTION_STATUS_META: Record = { + offline: { color: 'red', label: 'Offline' }, + online: { color: 'teal', label: 'Online' }, + unknown: { color: 'gray', label: 'Unknown' }, +}; + +const DEFAULT_AUTO_REFRESH_MS = 30_000; + +type ConnectionOptions = { + baseUrl?: string; + autoRefreshMs?: number; +}; + +function getSameOriginUrl() { + return window.location.origin; +} + +function buildHealthUrl(baseUrl: string) { + if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) { + const normalized = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + return new URL('v1/agl/health', normalized).toString(); + } + + const normalizedBase = baseUrl.startsWith('/') ? baseUrl : `/${baseUrl}`; + const trimmed = normalizedBase.replace(/\/+$/, ''); + return `${trimmed}/v1/agl/health`; +} + +function useServerConnection({ baseUrl, autoRefreshMs }: ConnectionOptions) { + const [status, setStatus] = useState('unknown'); + const [isRefreshing, setIsRefreshing] = useState(false); + + useEffect(() => { + if (!baseUrl) { + setStatus('unknown'); + setIsRefreshing(false); + return; + } + + let disposed = false; + let intervalId: number | undefined; + let activeController: AbortController | undefined; + const healthUrl = buildHealthUrl(baseUrl); + + const check = async () => { + activeController?.abort(); + const controller = new AbortController(); + activeController = controller; + if (!disposed) { + setIsRefreshing(true); + } + + try { + const response = await fetch(healthUrl, { signal: controller.signal }); + if (!disposed && activeController === controller) { + setStatus(response.ok ? 'online' : 'offline'); + } + } catch (error) { + if (disposed || (error instanceof DOMException && error.name === 'AbortError')) { + return; + } + + if (!disposed && activeController === controller) { + setStatus('offline'); + } + } finally { + if (!disposed && activeController === controller) { + setIsRefreshing(false); + } + } + }; + + check(); + + if (autoRefreshMs && autoRefreshMs > 0) { + intervalId = window.setInterval(check, autoRefreshMs); + } + + return () => { + disposed = true; + activeController?.abort(); + if (intervalId) { + window.clearInterval(intervalId); + } + }; + }, [autoRefreshMs, baseUrl]); + + return { status, isRefreshing }; +} + +function ConnectionIndicator({ + baseUrl, + status = 'unknown', + isRefreshing = false, + onClick, +}: { + baseUrl?: string; + status?: ConnectionStatus; + isRefreshing?: boolean; + onClick?: () => void; +}) { + const { color, label } = CONNECTION_STATUS_META[status ?? 'unknown']; + const connectionTarget = baseUrl && baseUrl.length > 0 ? baseUrl : 'No server configured'; + + const content = ( + + + Server connection + + + {isRefreshing ? ( + + Refreshing + + ) : ( + + {label} + + )} + + {connectionTarget} + + + + ); + + if (!onClick) { + return content; + } + + return ( + + {content} + + ); +} + +export type AppLayoutProps = { + config?: ConnectionOptions; +}; + +export function AppLayout({ config }: AppLayoutProps = {}) { + const location = useLocation(); + const navigate = useNavigate(); + const resolvedBaseUrl = config?.baseUrl ?? getSameOriginUrl() ?? ''; + const autoRefreshMs = config?.autoRefreshMs !== undefined ? config.autoRefreshMs : DEFAULT_AUTO_REFRESH_MS; + const connectionState = useServerConnection({ + baseUrl: resolvedBaseUrl || undefined, + autoRefreshMs, + }); + const navItems = useMemo( + () => + NAV_ITEMS.map((item) => ({ + ...item, + active: location.pathname === item.to || location.pathname.startsWith(`${item.to}/`), + })), + [location.pathname], + ); + + return ( + + + + + Agent-lightning logo + + Agent-lightning Dashboard + + + + + + {navItems.map((item) => ( + + ))} + + + + navigate('/settings')} + /> + + + + + + + + ); +} + +export function AppLayoutWithState() { + const config = useAppSelector(selectConfig); + + return ( + <> + + + + ); +} diff --git a/dashboard/src/main.tsx b/dashboard/src/main.tsx new file mode 100644 index 00000000..326d1986 --- /dev/null +++ b/dashboard/src/main.tsx @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; +import App from './App'; +import { store } from './store'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/dashboard/src/pages/Resources.page.story.tsx b/dashboard/src/pages/Resources.page.story.tsx new file mode 100644 index 00000000..5de18cfe --- /dev/null +++ b/dashboard/src/pages/Resources.page.story.tsx @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { delay, http, HttpResponse } from 'msw'; +import { Provider } from 'react-redux'; +import { AppAlertBanner } from '@/components/AppAlertBanner'; +import { initialConfigState } from '@/features/config/slice'; +import { initialResourcesUiState } from '@/features/resources/slice'; +import { initialRolloutsUiState } from '@/features/rollouts/slice'; +import { createAppStore } from '@/store'; +import type { Resources } from '@/types'; +import { createResourcesHandlers } from '@/utils/mock'; +import { STORY_BASE_URL, STORY_DATE_NOW_SECONDS } from '../../.storybook/constants'; +import { allModes } from '../../.storybook/modes'; +import { ResourcesPage } from './Resources.page'; + +const meta: Meta = { + title: 'Pages/ResourcesPage', + component: ResourcesPage, + parameters: { + layout: 'fullscreen', + chromatic: { + modes: allModes, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const now = STORY_DATE_NOW_SECONDS; + +const sampleResources: Resources[] = [ + { + resourcesId: 'rs-a1b2c3d4e5f6', + version: 3, + createTime: now - 7 * 24 * 3600, + updateTime: now - 3600, + resources: { + main_llm: { + resource_type: 'llm', + endpoint: 'https://api.openai.com/v1', + model: 'gpt-4', + sampling_parameters: { + temperature: 0.7, + max_tokens: 2048, + }, + }, + backup_llm: { + resource_type: 'llm', + endpoint: 'https://api.anthropic.com/v1', + model: 'claude-3-opus-20240229', + sampling_parameters: { + temperature: 0.5, + max_tokens: 4096, + }, + }, + greeting_template: { + resource_type: 'prompt_template', + template: 'Hello {name}! Welcome to {service}.', + engine: 'f-string', + }, + }, + }, + { + resourcesId: 'rs-f6e5d4c3b2a1', + version: 2, + createTime: now - 14 * 24 * 3600, + updateTime: now - 2 * 24 * 3600, + resources: { + production_llm: { + resource_type: 'llm', + endpoint: 'https://api.openai.com/v1', + model: 'gpt-3.5-turbo', + sampling_parameters: { + temperature: 0.8, + max_tokens: 1024, + }, + }, + system_prompt: { + resource_type: 'prompt_template', + template: 'You are a helpful assistant. {context}', + engine: 'f-string', + }, + }, + }, + { + resourcesId: 'rs-1234567890ab', + version: 1, + createTime: now - 30 * 24 * 3600, + updateTime: now - 15 * 24 * 3600, + resources: { + legacy_llm: { + resource_type: 'llm', + endpoint: 'https://api.openai.com/v1', + model: 'gpt-3.5-turbo-0301', + sampling_parameters: { + temperature: 0.9, + }, + }, + }, + }, + { + resourcesId: 'rs-abcdef123456', + version: 5, + createTime: now - 60 * 24 * 3600, + updateTime: now - 12 * 3600, + resources: { + eval_llm: { + resource_type: 'llm', + endpoint: 'https://api.anthropic.com/v1', + model: 'claude-3-sonnet-20240229', + sampling_parameters: { + temperature: 0.3, + max_tokens: 2048, + }, + }, + judge_template: { + resource_type: 'prompt_template', + template: 'Evaluate the following response: {response}\n\nCriteria: {criteria}', + engine: 'jinja2', + }, + dataset: { + resource_type: 'dataset', + path: 's3://my-bucket/eval-data/v1/', + format: 'jsonl', + }, + }, + }, + { + resourcesId: 'rs-999888777666', + version: 1, + createTime: now - 2 * 24 * 3600, + updateTime: now - 2 * 24 * 3600, + resources: { + test_llm: { + resource_type: 'llm', + endpoint: 'http://localhost:8080/v1', + model: 'local-model', + sampling_parameters: { + temperature: 1.0, + }, + }, + }, + }, +]; + +const defaultHandlers = createResourcesHandlers(sampleResources); + +function renderWithStore(configOverrides?: Partial) { + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + autoRefreshMs: 0, + ...configOverrides, + }, + rollouts: initialRolloutsUiState, + resources: initialResourcesUiState, + }); + + return ( + + + + + ); +} + +export const Default: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: defaultHandlers, + }, + }, +}; + +export const EmptyState: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: [http.get('*/v1/agl/resources', () => HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }))], + }, + }, +}; + +export const ServerError: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/resources', () => HttpResponse.json({ detail: 'Internal server error' }, { status: 500 })), + ], + }, + }, +}; + +export const RequestTimeout: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/resources', async () => { + await delay(1200); + return HttpResponse.json({ detail: 'Request timed out' }, { status: 504, statusText: 'Timeout' }); + }), + ], + }, + }, +}; + +export const SingleResource: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createResourcesHandlers([sampleResources[0]]), + }, + }, +}; + +export const ParseFailure: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/resources', () => + HttpResponse.text('{ malformed json', { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }), + ), + ], + }, + }, +}; + +export const ManyResources: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createResourcesHandlers( + Array.from({ length: 50 }, (_, index) => ({ + resourcesId: `rs-generated-${index + 1}`.padEnd(17, '0'), + version: (index % 5) + 1, + createTime: now - (50 - index) * 24 * 3600, + updateTime: now - (50 - index) * 3600, + resources: { + llm: { + resource_type: 'llm', + endpoint: `https://api.example.com/v${(index % 3) + 1}`, + model: index % 2 === 0 ? 'gpt-4' : 'claude-3-opus', + sampling_parameters: { + temperature: 0.5 + (index % 5) * 0.1, + }, + }, + }, + })), + ), + }, + }, +}; + +export const ComplexResources: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createResourcesHandlers([ + { + resourcesId: 'rs-complex-123456', + version: 10, + createTime: now - 90 * 24 * 3600, + updateTime: now - 600, + resources: { + primary_llm: { + resource_type: 'llm', + endpoint: 'https://api.openai.com/v1', + model: 'gpt-4-turbo-preview', + sampling_parameters: { + temperature: 0.7, + top_p: 0.95, + frequency_penalty: 0.1, + presence_penalty: 0.1, + max_tokens: 4096, + }, + }, + fallback_llm: { + resource_type: 'llm', + endpoint: 'https://api.anthropic.com/v1', + model: 'claude-3-opus-20240229', + sampling_parameters: { + temperature: 0.7, + max_tokens: 4096, + top_k: 40, + }, + }, + embedding_model: { + resource_type: 'embedding', + endpoint: 'https://api.openai.com/v1', + model: 'text-embedding-3-large', + dimensions: 1536, + }, + system_prompt: { + resource_type: 'prompt_template', + template: 'You are an AI assistant. Context: {context}\nUser: {user_input}\nAssistant:', + engine: 'jinja2', + variables: ['context', 'user_input'], + }, + training_dataset: { + resource_type: 'dataset', + path: 's3://ml-datasets/training/v2/data.jsonl', + format: 'jsonl', + size_bytes: 1024000000, + num_examples: 50000, + }, + validation_dataset: { + resource_type: 'dataset', + path: 's3://ml-datasets/validation/v2/data.jsonl', + format: 'jsonl', + size_bytes: 102400000, + num_examples: 5000, + }, + }, + }, + ]), + }, + }, +}; + +export const DarkTheme: Story = { + render: () => renderWithStore({ theme: 'dark' }), + parameters: { + theme: 'dark', + msw: { + handlers: defaultHandlers, + }, + }, +}; diff --git a/dashboard/src/pages/Resources.page.tsx b/dashboard/src/pages/Resources.page.tsx new file mode 100644 index 00000000..fb2979ce --- /dev/null +++ b/dashboard/src/pages/Resources.page.tsx @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useCallback, useEffect } from 'react'; +import { IconSearch } from '@tabler/icons-react'; +import type { DataTableSortStatus } from 'mantine-datatable'; +import { Skeleton, Stack, TextInput, Title } from '@mantine/core'; +import { ResourcesTable, type ResourcesTableRecord } from '@/components/ResourcesTable.component'; +import { ResourcesTree } from '@/components/ResourcesTree.component'; +import { selectAutoRefreshMs } from '@/features/config'; +import { + resetResourcesFilters, + selectResourcesPage, + selectResourcesQueryArgs, + selectResourcesRecordsPerPage, + selectResourcesSearchTerm, + selectResourcesSort, + setResourcesPage, + setResourcesRecordsPerPage, + setResourcesSearchTerm, + setResourcesSort, +} from '@/features/resources'; +import { useGetResourcesQuery } from '@/features/rollouts'; +import { hideAlert, showAlert } from '@/features/ui/alert'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import type { PaginatedResponse, Resources } from '@/types'; +import { getErrorDescriptor } from '@/utils/error'; + +export function ResourcesPage() { + const dispatch = useAppDispatch(); + const autoRefreshMs = useAppSelector(selectAutoRefreshMs); + const searchTerm = useAppSelector(selectResourcesSearchTerm); + const page = useAppSelector(selectResourcesPage); + const recordsPerPage = useAppSelector(selectResourcesRecordsPerPage); + const sort = useAppSelector(selectResourcesSort); + const queryArgs = useAppSelector(selectResourcesQueryArgs); + + const resourcesQueryResult = useGetResourcesQuery(queryArgs, { + pollingInterval: autoRefreshMs > 0 ? autoRefreshMs : undefined, + }); + + const resourcesData = resourcesQueryResult.data as PaginatedResponse | undefined; + const { isLoading, isFetching, isError, error, refetch } = resourcesQueryResult; + + const handleSearchTermChange = useCallback( + (value: string) => { + dispatch(setResourcesSearchTerm(value)); + }, + [dispatch], + ); + + const handleSortStatusChange = useCallback( + (status: DataTableSortStatus) => { + dispatch( + setResourcesSort({ + column: status.columnAccessor, + direction: status.direction, + }), + ); + }, + [dispatch], + ); + + const handlePageChange = useCallback( + (nextPage: number) => { + dispatch(setResourcesPage(nextPage)); + }, + [dispatch], + ); + + const handleRecordsPerPageChange = useCallback( + (value: number) => { + dispatch(setResourcesRecordsPerPage(value)); + }, + [dispatch], + ); + + const handleResetFilters = useCallback(() => { + dispatch(resetResourcesFilters()); + }, [dispatch]); + + const showSkeleton = isLoading && !((resourcesData?.items?.length ?? 0) > 0); + + useEffect(() => { + const descriptor = isError ? getErrorDescriptor(error) : null; + if (isError) { + const detailSuffix = descriptor ? ` (${descriptor})` : ''; + dispatch( + showAlert({ + id: 'resources-fetch', + message: `Unable to refresh resources${detailSuffix}. The table may be out of date until the connection recovers.`, + tone: 'error', + }), + ); + return; + } + + if (!isLoading && !isFetching) { + dispatch(hideAlert({ id: 'resources-fetch' })); + } + }, [dispatch, error, isError, isFetching, isLoading]); + + useEffect( + () => () => { + dispatch(hideAlert({ id: 'resources-fetch' })); + }, + [dispatch], + ); + + return ( + + Resources + + handleSearchTermChange(event.currentTarget.value)} + leftSection={} + data-testid='resources-search-input' + w='100%' + style={{ maxWidth: 360 }} + /> + + {showSkeleton ? ( + + ) : ( + } + /> + )} + + ); +} diff --git a/dashboard/src/pages/Rollouts.page.story.tsx b/dashboard/src/pages/Rollouts.page.story.tsx new file mode 100644 index 00000000..44d15460 --- /dev/null +++ b/dashboard/src/pages/Rollouts.page.story.tsx @@ -0,0 +1,776 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { waitFor, within } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import { delay, http, HttpResponse } from 'msw'; +import { Provider } from 'react-redux'; +import { AppAlertBanner } from '@/components/AppAlertBanner'; +import { AppDrawerContainer } from '@/components/AppDrawer.component'; +import { createMockHandlers } from '@/utils/mock'; +import { STORY_BASE_URL, STORY_DATE_NOW_SECONDS } from '../../.storybook/constants'; +import { allModes } from '../../.storybook/modes'; +import { initialConfigState } from '../features/config/slice'; +import { initialResourcesUiState } from '../features/resources/slice'; +import type { Attempt, Rollout, Span } from '../features/rollouts'; +import { initialRolloutsUiState, type RolloutsUiState } from '../features/rollouts/slice'; +import { createAppStore } from '../store'; +import { RolloutsPage } from './Rollouts.page'; + +const meta: Meta = { + title: 'Pages/RolloutsPage', + component: RolloutsPage, + parameters: { + layout: 'fullscreen', + chromatic: { + modes: allModes, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const now = STORY_DATE_NOW_SECONDS; + +const sampleRollouts: Rollout[] = [ + { + rolloutId: 'ro-7fa3b6e2', + input: { task: 'Summarize report' }, + status: 'running', + mode: 'train', + resourcesId: 'rs-100', + startTime: now - 1200, + endTime: null, + attempt: { + rolloutId: 'ro-7fa3b6e2', + attemptId: 'at-9001', + sequenceId: 1, + status: 'running', + startTime: now - 1200, + endTime: null, + workerId: 'worker-alpha', + lastHeartbeatTime: now - 30, + metadata: { lastHeartbeatAt: now - 30 }, + }, + config: { retries: 0 }, + metadata: { owner: 'alice' }, + }, + { + rolloutId: 'ro-116eab45', + input: { task: 'Classify dataset' }, + status: 'succeeded', + mode: 'val', + resourcesId: 'rs-101', + startTime: now - 5400, + endTime: now - 3600, + attempt: { + rolloutId: 'ro-116eab45', + attemptId: 'at-9002', + sequenceId: 2, + status: 'succeeded', + startTime: now - 4000, + endTime: now - 3600, + workerId: 'worker-beta', + lastHeartbeatTime: now - 3600, + metadata: { lastHeartbeatAt: now - 3600 }, + }, + config: { retries: 1 }, + metadata: { owner: 'bob' }, + }, + { + rolloutId: 'ro-9ae77c11', + input: { task: 'Evaluate prompt variations' }, + status: 'failed', + mode: 'test', + resourcesId: 'rs-102', + startTime: now - 9600, + endTime: now - 8400, + attempt: { + rolloutId: 'ro-9ae77c11', + attemptId: 'at-9005', + sequenceId: 3, + status: 'failed', + startTime: now - 8800, + endTime: now - 8400, + workerId: 'worker-gamma', + lastHeartbeatTime: now - 8400, + metadata: { lastHeartbeatAt: now - 8400 }, + }, + config: { retries: 2 }, + metadata: { owner: 'carol' }, + }, +]; + +const attemptsByRollout: Record = { + 'ro-7fa3b6e2': [ + { + rolloutId: 'ro-7fa3b6e2', + attemptId: 'at-9001', + sequenceId: 1, + status: 'running', + startTime: now - 1200, + endTime: null, + workerId: 'worker-alpha', + lastHeartbeatTime: now - 30, + metadata: { lastHeartbeatAt: now - 30 }, + }, + ], + 'ro-116eab45': [ + { + rolloutId: 'ro-116eab45', + attemptId: 'at-9000', + sequenceId: 1, + status: 'failed', + startTime: now - 5400, + endTime: now - 5000, + workerId: 'worker-beta', + lastHeartbeatTime: now - 5000, + metadata: { lastHeartbeatAt: now - 5000 }, + }, + { + rolloutId: 'ro-116eab45', + attemptId: 'at-9002', + sequenceId: 2, + status: 'succeeded', + startTime: now - 4000, + endTime: now - 3600, + workerId: 'worker-beta', + lastHeartbeatTime: now - 3600, + metadata: { lastHeartbeatAt: now - 3600 }, + }, + ], + 'ro-9ae77c11': [ + { + rolloutId: 'ro-9ae77c11', + attemptId: 'at-9003', + sequenceId: 1, + status: 'preparing', + startTime: now - 9600, + endTime: now - 9300, + workerId: 'worker-gamma', + lastHeartbeatTime: now - 9300, + metadata: { lastHeartbeatAt: now - 9300 }, + }, + { + rolloutId: 'ro-9ae77c11', + attemptId: 'at-9004', + sequenceId: 2, + status: 'running', + startTime: now - 9200, + endTime: now - 8800, + workerId: 'worker-delta', + lastHeartbeatTime: now - 8800, + metadata: { lastHeartbeatAt: now - 8800 }, + }, + { + rolloutId: 'ro-9ae77c11', + attemptId: 'at-9005', + sequenceId: 3, + status: 'failed', + startTime: now - 8800, + endTime: now - 8400, + workerId: 'worker-gamma', + lastHeartbeatTime: now - 8400, + metadata: { lastHeartbeatAt: now - 8400 }, + }, + ], +}; + +const sampleSpansByAttempt: Record = { + 'ro-7fa3b6e2:at-9001': [ + { + rolloutId: 'ro-7fa3b6e2', + attemptId: 'at-9001', + sequenceId: 1, + traceId: 'tr-7fa3b6e2-1', + spanId: 'sp-7fa3b6e2-setup', + parentId: null, + name: 'Initialize rollout', + status: { status_code: 'OK', description: null }, + attributes: { step: 'init', duration_ms: 120 }, + startTime: now - 1100, + endTime: now - 1000, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-7fa3b6e2', + attemptId: 'at-9001', + sequenceId: 2, + traceId: 'tr-7fa3b6e2-1', + spanId: 'sp-7fa3b6e2-run', + parentId: 'sp-7fa3b6e2-setup', + name: 'Execute task', + status: { status_code: 'OK', description: null }, + attributes: { step: 'run', duration_ms: 450 }, + startTime: now - 950, + endTime: now - 500, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + ], + 'ro-116eab45:at-9002': [ + { + rolloutId: 'ro-116eab45', + attemptId: 'at-9002', + sequenceId: 1, + traceId: 'tr-116eab45-1', + spanId: 'sp-116eab45-validate', + parentId: null, + name: 'Validate input', + status: { status_code: 'OK', description: null }, + attributes: { step: 'validate', duration_ms: 80 }, + startTime: now - 3800, + endTime: now - 3720, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-116eab45', + attemptId: 'at-9002', + sequenceId: 2, + traceId: 'tr-116eab45-1', + spanId: 'sp-116eab45-execute', + parentId: 'sp-116eab45-validate', + name: 'Execute workflow', + status: { status_code: 'OK', description: null }, + attributes: { step: 'execute', duration_ms: 260 }, + startTime: now - 3700, + endTime: now - 3440, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + ], + 'ro-9ae77c11:at-9005': [ + { + rolloutId: 'ro-9ae77c11', + attemptId: 'at-9005', + sequenceId: 1, + traceId: 'tr-9ae77c11-1', + spanId: 'sp-9ae77c11-fetch', + parentId: null, + name: 'Fetch resources', + status: { status_code: 'OK', description: null }, + attributes: { step: 'fetch', duration_ms: 200 }, + startTime: now - 8700, + endTime: now - 8500, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-9ae77c11', + attemptId: 'at-9005', + sequenceId: 2, + traceId: 'tr-9ae77c11-1', + spanId: 'sp-9ae77c11-run', + parentId: 'sp-9ae77c11-fetch', + name: 'Run evaluation', + status: { status_code: 'ERROR', description: 'Worker timeout' }, + attributes: { step: 'evaluate', duration_ms: 600 }, + startTime: now - 8450, + endTime: now - 7850, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + ], +}; + +const longDurationRollouts: Rollout[] = [ + { + rolloutId: 'ro-long-duration', + input: { task: 'Long running training' }, + status: 'running', + mode: 'train', + resourcesId: 'rs-200', + startTime: now - 7 * 24 * 3600, + endTime: null, + attempt: { + rolloutId: 'ro-long-duration', + attemptId: 'at-long-001', + sequenceId: 1, + status: 'running', + startTime: now - 7 * 24 * 3600, + endTime: null, + workerId: 'worker-long', + lastHeartbeatTime: now - 45, + metadata: null, + }, + config: { retries: 0 }, + metadata: { owner: 'delta' }, + }, +]; + +const longDurationAttempts: Record = { + 'ro-long-duration': [ + { + rolloutId: 'ro-long-duration', + attemptId: 'at-long-001', + sequenceId: 1, + status: 'running', + startTime: now - 7 * 24 * 3600, + endTime: null, + workerId: 'worker-long', + lastHeartbeatTime: now - 45, + metadata: null, + }, + ], +}; + +const staleHeartbeatRollouts: Rollout[] = [ + { + rolloutId: 'ro-stale-heartbeat', + input: { task: 'Investigate stale worker' }, + status: 'running', + mode: 'test', + resourcesId: 'rs-201', + startTime: now - 6 * 3600, + endTime: null, + attempt: { + rolloutId: 'ro-stale-heartbeat', + attemptId: 'at-stale-001', + sequenceId: 1, + status: 'running', + startTime: now - 6 * 3600, + endTime: null, + workerId: 'worker-stale', + lastHeartbeatTime: now - 3 * 24 * 3600, + metadata: null, + }, + config: { retries: 0 }, + metadata: { owner: 'echo' }, + }, +]; + +const staleHeartbeatAttempts: Record = { + 'ro-stale-heartbeat': [ + { + rolloutId: 'ro-stale-heartbeat', + attemptId: 'at-stale-001', + sequenceId: 1, + status: 'running', + startTime: now - 6 * 3600, + endTime: null, + workerId: 'worker-stale', + lastHeartbeatTime: now - 3 * 24 * 3600, + metadata: null, + }, + ], +}; + +const statusMismatchRollouts: Rollout[] = [ + { + rolloutId: 'ro-status-mismatch', + input: { task: 'Edge case validation' }, + status: 'running', + mode: 'val', + resourcesId: null, + startTime: now - 3600, + endTime: now - 1800, + attempt: { + rolloutId: 'ro-status-mismatch', + attemptId: 'at-mismatch-003', + sequenceId: 3, + status: 'failed', + startTime: now - 4200, + endTime: now - 1800, + workerId: 'worker-mismatch', + lastHeartbeatTime: now - 1700, + metadata: null, + }, + config: { retries: 3 }, + metadata: { owner: 'foxtrot' }, + }, +]; + +const statusMismatchAttempts: Record = { + 'ro-status-mismatch': [ + { + rolloutId: 'ro-status-mismatch', + attemptId: 'at-mismatch-001', + sequenceId: 1, + status: 'preparing', + startTime: now - 5400, + endTime: now - 5000, + workerId: 'worker-mismatch', + lastHeartbeatTime: now - 5000, + metadata: null, + }, + { + rolloutId: 'ro-status-mismatch', + attemptId: 'at-mismatch-002', + sequenceId: 2, + status: 'running', + startTime: now - 5000, + endTime: now - 4200, + workerId: 'worker-mismatch', + lastHeartbeatTime: now - 4000, + metadata: null, + }, + { + rolloutId: 'ro-status-mismatch', + attemptId: 'at-mismatch-003', + sequenceId: 3, + status: 'failed', + startTime: now - 4200, + endTime: now - 1800, + workerId: 'worker-mismatch', + lastHeartbeatTime: now - 1700, + metadata: null, + }, + ], +}; + +const veryLongInput = `{"prompt":"${'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(12)}"}`; + +const longInputRollouts: Rollout[] = [ + { + rolloutId: 'ro-long-input', + input: veryLongInput, + status: 'queuing', + mode: 'test', + resourcesId: 'rs-300', + startTime: now - 120, + endTime: null, + attempt: null, + config: { retries: 0 }, + metadata: { owner: 'golf' }, + }, +]; + +const longInputAttempts: Record = { + 'ro-long-input': [], +}; + +const paginationRollouts: Rollout[] = Array.from({ length: 120 }, (_item, index) => { + const startOffset = index * 90; + const rolloutId = `ro-page-${index + 1}`; + + return { + rolloutId, + input: { item: index + 1 }, + status: index % 3 === 0 ? 'running' : index % 3 === 1 ? 'failed' : 'succeeded', + mode: index % 2 === 0 ? 'train' : 'test', + resourcesId: index % 5 === 0 ? `rs-${100 + index}` : null, + startTime: now - startOffset - 300, + endTime: index % 3 === 0 ? null : now - startOffset, + attempt: { + rolloutId, + attemptId: `at-page-${index + 1}`, + sequenceId: 1, + status: index % 3 === 0 ? 'running' : index % 3 === 1 ? 'failed' : 'succeeded', + startTime: now - startOffset - 300, + endTime: index % 3 === 0 ? null : now - startOffset, + workerId: `worker-${(index % 7) + 1}`, + lastHeartbeatTime: index % 3 === 0 ? now - startOffset - 60 : now - startOffset, + metadata: null, + }, + config: {}, + metadata: null, + }; +}); + +const paginationAttempts: Record = Object.fromEntries( + paginationRollouts.map((rollout) => [rollout.rolloutId, rollout.attempt ? [rollout.attempt] : []]), +); + +const autoExpandRollouts: Rollout[] = [ + { + rolloutId: 'ro-auto-expand', + input: { task: 'Auto expand test' }, + status: 'running', + mode: 'train', + resourcesId: 'rs-400', + startTime: now - 3600, + endTime: null, + attempt: { + rolloutId: 'ro-auto-expand', + attemptId: 'at-expand-003', + sequenceId: 3, + status: 'running', + startTime: now - 3600, + endTime: null, + workerId: 'worker-auto', + lastHeartbeatTime: now - 30, + metadata: null, + }, + config: {}, + metadata: null, + }, +]; + +const autoExpandAttempts: Record = { + 'ro-auto-expand': [ + { + rolloutId: 'ro-auto-expand', + attemptId: 'at-expand-001', + sequenceId: 1, + status: 'preparing', + startTime: now - 5400, + endTime: now - 5000, + workerId: 'worker-auto', + lastHeartbeatTime: now - 5000, + metadata: null, + }, + { + rolloutId: 'ro-auto-expand', + attemptId: 'at-expand-002', + sequenceId: 2, + status: 'failed', + startTime: now - 5000, + endTime: now - 4200, + workerId: 'worker-auto', + lastHeartbeatTime: now - 4200, + metadata: null, + }, + { + rolloutId: 'ro-auto-expand', + attemptId: 'at-expand-003', + sequenceId: 3, + status: 'running', + startTime: now - 3600, + endTime: null, + workerId: 'worker-auto', + lastHeartbeatTime: now - 30, + metadata: null, + }, + ], +}; +function renderWithStore(uiOverrides?: Partial, configOverrides?: Partial) { + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + autoRefreshMs: 0, + ...configOverrides, + }, + rollouts: { + ...initialRolloutsUiState, + ...uiOverrides, + }, + resources: initialResourcesUiState, + }); + + return ( + + <> + + + + + + ); +} + +const defaultHandlers = createMockHandlers(sampleRollouts, attemptsByRollout, sampleSpansByAttempt); + +export const Default: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: defaultHandlers, + }, + }, +}; + +export const DarkTheme: Story = { + render: () => renderWithStore(undefined, { theme: 'dark' }), + parameters: { + theme: 'dark', + msw: { + handlers: defaultHandlers, + }, + }, +}; + +export const EmptyState: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/rollouts', () => HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 })), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', () => + HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }), + ), + ], + }, + }, +}; + +export const ServerError: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/rollouts', () => HttpResponse.json({ detail: 'Internal error' }, { status: 500 })), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', () => + HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }, { status: 200 }), + ), + ], + }, + }, +}; + +export const Loading: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/rollouts', async () => { + await delay('infinite'); + return HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }); + }), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', async () => { + await delay('infinite'); + return HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }); + }), + ], + }, + }, +}; + +export const LongDuration: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createMockHandlers(longDurationRollouts, longDurationAttempts, {}), + }, + }, +}; + +export const StaleHeartbeat: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createMockHandlers(staleHeartbeatRollouts, staleHeartbeatAttempts, {}), + }, + }, +}; + +export const StatusMismatch: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createMockHandlers(statusMismatchRollouts, statusMismatchAttempts, {}), + }, + }, +}; + +export const LongInput: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createMockHandlers(longInputRollouts, longInputAttempts, {}), + }, + }, +}; + +export const Pagination: Story = { + render: () => renderWithStore({ recordsPerPage: 20 }), + parameters: { + msw: { + handlers: createMockHandlers(paginationRollouts, paginationAttempts, {}), + }, + }, +}; + +export const AutoExpandedAttempt: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: createMockHandlers(autoExpandRollouts, autoExpandAttempts, {}), + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByText('ro-auto-expand'); + + const rolloutCell = canvas.getByText('ro-auto-expand'); + const rolloutRow = rolloutCell.closest('tr'); + + if (!rolloutRow) { + throw new Error('Unable to locate the rollout row for expansion'); + } + + await userEvent.click(rolloutRow); + await waitFor( + async () => { + await canvas.findByText('at-expand-001'); + }, + { timeout: 3_000 }, + ); + }, +}; + +export const RawJsonDrawer: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: defaultHandlers, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByText('ro-7fa3b6e2'); + const rolloutCell = canvas.getByText('ro-7fa3b6e2'); + const rolloutRow = rolloutCell.closest('tr'); + + if (!rolloutRow) { + throw new Error('Unable to locate rollout row for raw JSON drawer'); + } + + const rowScope = within(rolloutRow); + const rawButtons = rowScope.getAllByRole('button', { name: 'View raw JSON' }); + const rawButton = rawButtons[0]; + await userEvent.click(rawButton); + + const drawer = await within(document.body).findByRole('dialog'); + await within(drawer).findByText('Attempt'); + await within(drawer).findByText(/worker-alpha/); + }, +}; + +export const TracesDrawer: Story = { + render: () => renderWithStore(), + parameters: { + msw: { + handlers: defaultHandlers, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByText('ro-7fa3b6e2'); + const rolloutCell = canvas.getByText('ro-7fa3b6e2'); + const rolloutRow = rolloutCell.closest('tr'); + + if (!rolloutRow) { + throw new Error('Unable to locate rollout row for traces drawer'); + } + + const rowScope = within(rolloutRow); + const traceButtons = rowScope.getAllByRole('button', { name: 'View traces' }); + const tracesButton = traceButtons[0]; + await userEvent.click(tracesButton); + + await within(document.body).findByRole('dialog'); + }, +}; diff --git a/dashboard/src/pages/Rollouts.page.tsx b/dashboard/src/pages/Rollouts.page.tsx new file mode 100644 index 00000000..1a056bae --- /dev/null +++ b/dashboard/src/pages/Rollouts.page.tsx @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useCallback, useEffect } from 'react'; +import { IconSearch } from '@tabler/icons-react'; +import type { DataTableColumn, DataTableSortStatus } from 'mantine-datatable'; +import { Skeleton, Stack, TextInput, Title } from '@mantine/core'; +import { RolloutAttemptsTable, RolloutTable, type RolloutTableRecord } from '@/components/RolloutTable.component'; +import { selectAutoRefreshMs } from '@/features/config'; +import { + resetRolloutsFilters, + selectRolloutsModeFilters, + selectRolloutsPage, + selectRolloutsQueryArgs, + selectRolloutsRecordsPerPage, + selectRolloutsSearchTerm, + selectRolloutsSort, + selectRolloutsStatusFilters, + setRolloutsModeFilters, + setRolloutsPage, + setRolloutsRecordsPerPage, + setRolloutsSearchTerm, + setRolloutsSort, + setRolloutsStatusFilters, + useGetRolloutAttemptsQuery, + useGetRolloutsQuery, + type Rollout, + type RolloutMode, + type RolloutStatus, +} from '@/features/rollouts'; +import { hideAlert, showAlert } from '@/features/ui/alert'; +import { openDrawer } from '@/features/ui/drawer'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; + +function RolloutAttemptsContent({ + rollout, + columns, +}: { + rollout: Rollout; + columns: DataTableColumn[]; +}) { + const { data, isFetching, isError, refetch } = useGetRolloutAttemptsQuery({ + rolloutId: rollout.rolloutId, + limit: 100, + sortBy: 'sequence_id', + sortOrder: 'desc', + }); + + return ( + + ); +} + +function toRolloutFromRecord(record: RolloutTableRecord): Rollout { + const { rolloutId, input, startTime, endTime, mode, resourcesId, status, config, metadata, attempt } = record; + + return { + rolloutId, + input, + startTime, + endTime, + mode, + resourcesId, + status, + config, + metadata, + attempt: attempt ?? null, + }; +} + +export function RolloutsPage() { + const dispatch = useAppDispatch(); + const autoRefreshMs = useAppSelector(selectAutoRefreshMs); + const searchTerm = useAppSelector(selectRolloutsSearchTerm); + const statusFilters = useAppSelector(selectRolloutsStatusFilters); + const modeFilters = useAppSelector(selectRolloutsModeFilters); + const page = useAppSelector(selectRolloutsPage); + const recordsPerPage = useAppSelector(selectRolloutsRecordsPerPage); + const sort = useAppSelector(selectRolloutsSort); + const rolloutsQueryArgs = useAppSelector(selectRolloutsQueryArgs); + + const { + data: rolloutsData, + isLoading, + isFetching, + isError, + error, + refetch, + } = useGetRolloutsQuery(rolloutsQueryArgs, { + pollingInterval: autoRefreshMs > 0 ? autoRefreshMs : undefined, + }); + + const handleSearchTermChange = useCallback( + (value: string) => { + dispatch(setRolloutsSearchTerm(value)); + }, + [dispatch], + ); + + const handleStatusFilterChange = useCallback( + (values: RolloutStatus[]) => { + dispatch(setRolloutsStatusFilters(values)); + }, + [dispatch], + ); + + const handleStatusFilterReset = useCallback(() => { + dispatch(setRolloutsStatusFilters([])); + }, [dispatch]); + + const handleModeFilterChange = useCallback( + (values: RolloutMode[]) => { + dispatch(setRolloutsModeFilters(values)); + }, + [dispatch], + ); + + const handleModeFilterReset = useCallback(() => { + dispatch(setRolloutsModeFilters([])); + }, [dispatch]); + + const handleSortStatusChange = useCallback( + (status: DataTableSortStatus) => { + dispatch( + setRolloutsSort({ + column: status.columnAccessor as string, + direction: status.direction, + }), + ); + }, + [dispatch], + ); + + const handlePageChange = useCallback( + (nextPage: number) => { + dispatch(setRolloutsPage(nextPage)); + }, + [dispatch], + ); + + const handleRecordsPerPageChange = useCallback( + (value: number) => { + dispatch(setRolloutsRecordsPerPage(value)); + }, + [dispatch], + ); + + const handleResetFilters = useCallback(() => { + dispatch(resetRolloutsFilters()); + }, [dispatch]); + + const handleViewRawJson = useCallback( + (record: RolloutTableRecord) => { + dispatch( + openDrawer({ + type: 'rollout-json', + rollout: toRolloutFromRecord(record), + attempt: record.attempt ?? null, + isNested: record.isNested, + }), + ); + }, + [dispatch], + ); + + const handleViewTraces = useCallback( + (record: RolloutTableRecord) => { + dispatch( + openDrawer({ + type: 'rollout-traces', + rollout: toRolloutFromRecord(record), + attempt: record.attempt ?? null, + isNested: record.isNested, + }), + ); + }, + [dispatch], + ); + + const hasRollouts = Array.isArray(rolloutsData?.items) && rolloutsData.items.length > 0; + const showSkeleton = isLoading && !hasRollouts; + + useEffect(() => { + if (isError) { + dispatch( + showAlert({ + id: 'rollouts-fetch', + message: 'Unable to refresh rollouts. The list below may be out of date until the connection recovers.', + tone: 'error', + }), + ); + return; + } + + if (!isLoading && !isFetching) { + dispatch(hideAlert({ id: 'rollouts-fetch' })); + } + }, [dispatch, isError, isFetching, isLoading]); + + useEffect( + () => () => { + dispatch(hideAlert({ id: 'rollouts-fetch' })); + }, + [dispatch], + ); + + return ( + + Rollouts + + handleSearchTermChange(event.currentTarget.value)} + leftSection={} + data-testid='rollouts-search-input' + w='100%' + style={{ maxWidth: 360 }} + /> + + {showSkeleton ? ( + + ) : ( + } + /> + )} + + ); +} diff --git a/dashboard/src/pages/Settings.page.story.tsx b/dashboard/src/pages/Settings.page.story.tsx new file mode 100644 index 00000000..96fc0ae0 --- /dev/null +++ b/dashboard/src/pages/Settings.page.story.tsx @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { Provider } from 'react-redux'; +import { initialConfigState } from '@/features/config/slice'; +import { createAppStore } from '@/store'; +import type { ConfigState } from '@/types'; +import { STORY_BASE_URL } from '../../.storybook/constants'; +import { SettingsPage } from './Settings.page'; + +const meta: Meta = { + title: 'Pages/SettingsPage', + component: SettingsPage, + parameters: { + layout: 'centered', + }, +}; + +export default meta; + +type Story = StoryObj; + +function renderWithConfig(partial?: Partial) { + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + ...partial, + }, + }); + + return ( + + + + ); +} + +export const Default: Story = { + render: () => renderWithConfig(), +}; + +export const AutoRefreshEveryMinute: Story = { + render: () => renderWithConfig({ autoRefreshMs: 60_000 }), +}; + +export const DarkThemeSelected: Story = { + render: () => renderWithConfig({ theme: 'dark' }), + parameters: { + theme: 'dark', + }, +}; + +export const CustomBackendUrl: Story = { + render: () => renderWithConfig({ baseUrl: 'https://api.agent-lightning.dev' }), +}; diff --git a/dashboard/src/pages/Settings.page.tsx b/dashboard/src/pages/Settings.page.tsx new file mode 100644 index 00000000..a8c76ab7 --- /dev/null +++ b/dashboard/src/pages/Settings.page.tsx @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useEffect, useState } from 'react'; +import { Divider, Select, Stack, Text, TextInput, Title } from '@mantine/core'; +import { selectConfig } from '../features/config'; +import { setAutoRefreshMs, setBaseUrl, setTheme } from '../features/config/slice'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; + +const AUTO_REFRESH_OPTIONS = [ + { label: 'Off', value: '0' }, + { label: 'Every 5 seconds', value: '5000' }, + { label: 'Every 15 seconds', value: '15000' }, + { label: 'Every 60 seconds', value: '60000' }, + { label: 'Every 5 minutes', value: '300000' }, +]; + +const THEME_OPTIONS = [ + { label: 'System default', value: 'system' }, + { label: 'Light', value: 'light' }, + { label: 'Dark', value: 'dark' }, +]; + +export function SettingsPage() { + const dispatch = useAppDispatch(); + const config = useAppSelector(selectConfig); + const [baseUrlInput, setBaseUrlInput] = useState(config.baseUrl); + + useEffect(() => { + setBaseUrlInput(config.baseUrl); + }, [config.baseUrl]); + + return ( + + Settings + + + + Backend base URL + + + The dashboard uses this address for health checks and API calls. + + setBaseUrlInput(event.currentTarget.value)} + onBlur={() => dispatch(setBaseUrl(baseUrlInput.trim()))} + data-testid='settings-base-url' + /> + + + + + Auto-refresh interval + + + Controls how often the dashboard polls the server. + + { + if (!value) { + return; + } + dispatch(setTheme(value as typeof config.theme)); + }} + data-testid='settings-theme' + /> + + + + ); +} diff --git a/dashboard/src/pages/Traces.page.story.tsx b/dashboard/src/pages/Traces.page.story.tsx new file mode 100644 index 00000000..9c57d4f8 --- /dev/null +++ b/dashboard/src/pages/Traces.page.story.tsx @@ -0,0 +1,485 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { Meta, StoryObj } from '@storybook/react'; +import { delay, http, HttpResponse } from 'msw'; +import { Provider } from 'react-redux'; +import { AppAlertBanner } from '@/components/AppAlertBanner'; +import { AppDrawerContainer } from '@/components/AppDrawer.component'; +import { createMockHandlers } from '@/utils/mock'; +import { STORY_BASE_URL, STORY_DATE_NOW_SECONDS } from '../../.storybook/constants'; +import { allModes } from '../../.storybook/modes'; +import { initialConfigState } from '../features/config/slice'; +import { initialResourcesUiState } from '../features/resources/slice'; +import { initialRolloutsUiState } from '../features/rollouts/slice'; +import { initialTracesUiState, type TracesUiState } from '../features/traces/slice'; +import { createAppStore } from '../store'; +import type { Attempt, Rollout, Span } from '../types'; +import { TracesPage } from './Traces.page'; + +const meta: Meta = { + title: 'Pages/TracesPage', + component: TracesPage, + parameters: { + layout: 'fullscreen', + chromatic: { + modes: allModes, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const now = STORY_DATE_NOW_SECONDS; + +const sampleRollouts: Rollout[] = [ + { + rolloutId: 'ro-traces-001', + input: { task: 'Generate onboarding flow' }, + status: 'running', + mode: 'train', + resourcesId: 'rs-traces-001', + startTime: now - 1800, + endTime: null, + attempt: { + rolloutId: 'ro-traces-001', + attemptId: 'at-traces-001', + sequenceId: 1, + status: 'running', + startTime: now - 1800, + endTime: null, + workerId: 'worker-delta', + lastHeartbeatTime: now - 30, + metadata: { region: 'us-east-1' }, + }, + config: { retries: 0 }, + metadata: { owner: 'ava' }, + }, + { + rolloutId: 'ro-traces-002', + input: { task: 'Classify support emails' }, + status: 'succeeded', + mode: 'val', + resourcesId: 'rs-traces-002', + startTime: now - 5400, + endTime: now - 3600, + attempt: { + rolloutId: 'ro-traces-002', + attemptId: 'at-traces-004', + sequenceId: 4, + status: 'succeeded', + startTime: now - 4000, + endTime: now - 3600, + workerId: 'worker-epsilon', + lastHeartbeatTime: now - 3600, + metadata: { region: 'us-west-2' }, + }, + config: { retries: 2 }, + metadata: { owner: 'ben' }, + }, +]; + +const attemptsByRollout: Record = { + 'ro-traces-001': [ + { + rolloutId: 'ro-traces-001', + attemptId: 'at-traces-001', + sequenceId: 1, + status: 'running', + startTime: now - 1800, + endTime: null, + workerId: 'worker-delta', + lastHeartbeatTime: now - 30, + metadata: { region: 'us-east-1' }, + }, + { + rolloutId: 'ro-traces-001', + attemptId: 'at-traces-002', + sequenceId: 2, + status: 'failed', + startTime: now - 5400, + endTime: now - 4800, + workerId: 'worker-theta', + lastHeartbeatTime: now - 4800, + metadata: { error: 'Network timeout' }, + }, + ], + 'ro-traces-002': [ + { + rolloutId: 'ro-traces-002', + attemptId: 'at-traces-004', + sequenceId: 4, + status: 'succeeded', + startTime: now - 4000, + endTime: now - 3600, + workerId: 'worker-epsilon', + lastHeartbeatTime: now - 3600, + metadata: { region: 'us-west-2' }, + }, + ], +}; + +const spansByAttempt: Record = { + 'ro-traces-001:at-traces-001': [ + { + rolloutId: 'ro-traces-001', + attemptId: 'at-traces-001', + sequenceId: 1, + traceId: 'tr-001', + spanId: 'sp-001', + parentId: null, + name: 'Initialize rollout', + status: { status_code: 'OK', description: null }, + attributes: { stage: 'init', duration_ms: 120 }, + startTime: now - 1600, + endTime: now - 1580, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-traces-001', + attemptId: 'at-traces-001', + sequenceId: 2, + traceId: 'tr-001', + spanId: 'sp-002', + parentId: 'sp-001', + name: 'Fetch resources', + status: { status_code: 'OK', description: null }, + attributes: { endpoint: '/resources/latest', duration_ms: 240 }, + startTime: now - 1580, + endTime: now - 1540, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + ], + 'ro-traces-001:at-traces-002': [ + { + rolloutId: 'ro-traces-001', + attemptId: 'at-traces-002', + sequenceId: 1, + traceId: 'tr-002', + spanId: 'sp-101', + parentId: null, + name: 'Initialize rollout', + status: { status_code: 'ERROR', description: 'Timeout' }, + attributes: { stage: 'init', duration_ms: 600 }, + startTime: now - 5300, + endTime: now - 4700, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + ], + 'ro-traces-002:at-traces-004': [ + { + rolloutId: 'ro-traces-002', + attemptId: 'at-traces-004', + sequenceId: 1, + traceId: 'tr-200', + spanId: 'sp-201', + parentId: null, + name: 'Load dataset', + status: { status_code: 'OK', description: null }, + attributes: { records: 1200, duration_ms: 420 }, + startTime: now - 3800, + endTime: now - 3720, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-traces-002', + attemptId: 'at-traces-004', + sequenceId: 2, + traceId: 'tr-200', + spanId: 'sp-202', + parentId: 'sp-201', + name: 'Classify batch', + status: { status_code: 'OK', description: null }, + attributes: { batch: 1, duration_ms: 320 }, + startTime: now - 3720, + endTime: now - 3660, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + ], +}; + +const emptyAttemptsByRollout: Record = {}; +const emptySpansByAttempt: Record = {}; + +const singleRollout = sampleRollouts[1]; +const singleRollouts: Rollout[] = [singleRollout]; +const singleAttemptsByRollout: Record = { + [singleRollout.rolloutId]: attemptsByRollout[singleRollout.rolloutId] ?? [], +}; +const singleSpansByAttempt = Object.fromEntries( + Object.entries(spansByAttempt).filter(([key]) => key.startsWith(`${singleRollout.rolloutId}:`)), +) as Record; + +const owners = ['ava', 'ben', 'carla', 'diego'] as const; + +const manyAttemptsByRollout: Record = {}; +const manySpansByAttempt: Record = {}; + +const manyRollouts: Rollout[] = Array.from({ length: 24 }, (_, index) => { + const rolloutId = `ro-many-${String(index + 1).padStart(3, '0')}`; + const statusOptions = ['running', 'succeeded', 'failed'] as const; + const modeOptions = ['train', 'val', 'test'] as const; + const status = statusOptions[index % statusOptions.length]; + const mode = modeOptions[index % modeOptions.length]; + const startTime = now - (index + 1) * 420; + const endTime = status === 'running' ? null : startTime + 240; + const attemptId = `${rolloutId}-attempt`; + const attemptStatus: Attempt['status'] = + status === 'failed' ? 'failed' : status === 'succeeded' ? 'succeeded' : 'running'; + const attempt: Attempt = { + rolloutId, + attemptId, + sequenceId: 1, + status: attemptStatus, + startTime, + endTime, + workerId: `worker-${String.fromCharCode(97 + (index % 26))}`, + lastHeartbeatTime: endTime ?? startTime + 180, + metadata: { region: index % 2 === 0 ? 'us-east-1' : 'eu-west-1' }, + }; + manyAttemptsByRollout[rolloutId] = [attempt]; + manySpansByAttempt[`${rolloutId}:${attemptId}`] = [ + { + rolloutId, + attemptId, + sequenceId: 1, + traceId: `tr-many-${index + 1}`, + spanId: `sp-many-${index + 1}-root`, + parentId: null, + name: 'Synthetic root span', + status: { + status_code: status === 'failed' ? 'ERROR' : 'OK', + description: status === 'failed' ? 'Synthetic failure' : null, + }, + attributes: { + 'trace.sample': index + 1, + 'duration_ms': 240, + }, + startTime, + endTime: endTime ?? startTime + 240, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + ]; + return { + rolloutId, + input: { task: `Synthetic trace ${index + 1}` }, + status, + mode, + resourcesId: `rs-many-${(index % 7) + 1}`, + startTime, + endTime, + attempt, + config: { retries: index % 3 }, + metadata: { owner: owners[index % owners.length] }, + }; +}); + +function createHandlers(delayMs?: number) { + return createMockHandlers(sampleRollouts, attemptsByRollout, spansByAttempt, delayMs); +} + +function createRequestTimeoutHandlers() { + return [ + http.get('*/v1/agl/rollouts', async () => { + await delay(1200); + return HttpResponse.json({ detail: 'Request timed out' }, { status: 504, statusText: 'Timeout' }); + }), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', async ({ params }) => { + await delay(1200); + return HttpResponse.json( + { detail: 'Request timed out', rolloutId: params.rolloutId }, + { status: 504, statusText: 'Timeout' }, + ); + }), + http.get('*/v1/agl/spans', async () => { + await delay(1200); + return HttpResponse.json({ detail: 'Request timed out' }, { status: 504, statusText: 'Timeout' }); + }), + ]; +} + +const rolloutsAndAttemptsHandlers = createMockHandlers(sampleRollouts, attemptsByRollout); + +function renderTracesPage( + preloadedTracesState?: Partial, + configOverrides?: Partial, +) { + const store = createAppStore({ + config: { + ...initialConfigState, + baseUrl: STORY_BASE_URL, + ...configOverrides, + }, + rollouts: initialRolloutsUiState, + resources: initialResourcesUiState, + traces: { ...initialTracesUiState, ...preloadedTracesState }, + }); + + return ( + + + + + + ); +} + +export const DefaultView: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: createHandlers(), + }, + }, +}; + +export const DarkTheme: Story = { + render: () => renderTracesPage(undefined, { theme: 'dark' }), + parameters: { + theme: 'dark', + msw: { + handlers: createHandlers(), + }, + }, +}; + +export const EmptyState: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: createMockHandlers([], emptyAttemptsByRollout, emptySpansByAttempt), + }, + }, +}; + +export const SingleResult: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: createMockHandlers(singleRollouts, singleAttemptsByRollout, singleSpansByAttempt), + }, + }, +}; + +export const ManyResults: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: createMockHandlers(manyRollouts, manyAttemptsByRollout, manySpansByAttempt), + }, + }, +}; + +export const LoadingState: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: createHandlers(800), + }, + }, +}; + +export const RequestTimeout: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: createRequestTimeoutHandlers(), + }, + }, +}; + +export const AttemptScoped: Story = { + render: () => + renderTracesPage({ + attemptId: 'at-traces-002', + rolloutId: 'ro-traces-001', + }), + parameters: { + msw: { + handlers: createHandlers(), + }, + }, +}; + +export const ServerError: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/rollouts', () => + HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }, { status: 200 }), + ), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', () => + HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }, { status: 200 }), + ), + http.get('*/v1/agl/spans', () => HttpResponse.json({ detail: 'server error' }, { status: 500 })), + ], + }, + }, +}; + +export const RolloutParseFailure: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: [ + http.get('*/v1/agl/rollouts', () => + HttpResponse.text('not valid json', { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }), + ), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', () => + HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 }), + ), + http.get('*/v1/agl/spans', () => HttpResponse.json({ items: [], limit: 0, offset: 0, total: 0 })), + ], + }, + }, +}; + +export const ParseFailure: Story = { + render: () => renderTracesPage(), + parameters: { + msw: { + handlers: [ + ...rolloutsAndAttemptsHandlers, + http.get('*/v1/agl/spans', () => + HttpResponse.text('not valid json', { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }), + ), + ], + }, + }, +}; diff --git a/dashboard/src/pages/Traces.page.tsx b/dashboard/src/pages/Traces.page.tsx new file mode 100644 index 00000000..8a2e1352 --- /dev/null +++ b/dashboard/src/pages/Traces.page.tsx @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { useCallback, useEffect, useMemo } from 'react'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { IconCheck, IconChevronDown, IconSearch } from '@tabler/icons-react'; +import type { DataTableSortStatus } from 'mantine-datatable'; +import { Button, Group, Menu, Select, Skeleton, Stack, TextInput, Title } from '@mantine/core'; +import { TracesTable, type TracesTableRecord } from '@/components/TracesTable.component'; +import { selectAutoRefreshMs } from '@/features/config'; +import { + useGetRolloutAttemptsQuery, + useGetRolloutsQuery, + useGetSpansQuery, + type GetRolloutsQueryArgs, +} from '@/features/rollouts'; +import { + resetTracesFilters, + selectTracesAttemptId, + selectTracesPage, + selectTracesQueryArgs, + selectTracesRecordsPerPage, + selectTracesRolloutId, + selectTracesSearchTerm, + selectTracesSort, + selectTracesViewMode, + setTracesAttemptId, + setTracesPage, + setTracesRecordsPerPage, + setTracesRolloutId, + setTracesSearchTerm, + setTracesSort, + setTracesViewMode, +} from '@/features/traces'; +import { hideAlert, showAlert } from '@/features/ui/alert'; +import { openDrawer } from '@/features/ui/drawer'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import type { Attempt, Rollout, Span } from '@/types'; +import { getErrorDescriptor } from '@/utils/error'; +import { formatStatusLabel } from '@/utils/format'; + +const VIEW_OPTIONS = [ + { value: 'table', label: 'Table View', disabled: false }, + { value: 'waterfall', label: 'Waterfall View (Coming Soon)', disabled: true }, + { value: 'tree', label: 'Tree View (Coming Soon)', disabled: true }, +] as const; + +type ViewOptionValue = (typeof VIEW_OPTIONS)[number]['value']; + +function getLatestAttempt(attempts: Attempt[]): Attempt | null { + if (!attempts.length) { + return null; + } + return [...attempts].sort((a, b) => a.sequenceId - b.sequenceId).at(-1) ?? null; +} + +function findRollout(rollouts: Rollout[] | undefined, rolloutId: string | null): Rollout | null { + if (!rollouts || !rolloutId) { + return null; + } + return rollouts.find((rollout) => rollout.rolloutId === rolloutId) ?? null; +} + +export function TracesPage() { + const dispatch = useAppDispatch(); + const autoRefreshMs = useAppSelector(selectAutoRefreshMs); + const rolloutId = useAppSelector(selectTracesRolloutId); + const attemptId = useAppSelector(selectTracesAttemptId); + const searchTerm = useAppSelector(selectTracesSearchTerm); + const page = useAppSelector(selectTracesPage); + const recordsPerPage = useAppSelector(selectTracesRecordsPerPage); + const sort = useAppSelector(selectTracesSort); + const viewMode = useAppSelector(selectTracesViewMode); + const spansQueryArgs = useAppSelector(selectTracesQueryArgs); + + const rolloutsQueryArgs = useMemo( + () => ({ + limit: 100, + offset: 0, + sortBy: 'start_time', + sortOrder: 'desc', + }), + [], + ); + + const { + data: rolloutsData, + isLoading: rolloutsLoading, + isFetching: rolloutsFetching, + isError: rolloutsIsError, + error: rolloutsError, + } = useGetRolloutsQuery(rolloutsQueryArgs, { + pollingInterval: autoRefreshMs > 0 ? autoRefreshMs : undefined, + }); + + const rolloutItems = rolloutsData?.items ?? []; + + const selectedRollout = useMemo(() => findRollout(rolloutItems, rolloutId), [rolloutItems, rolloutId]); + + const attemptsQueryArgs = + rolloutId !== null + ? { + rolloutId, + limit: 200, + sortBy: 'sequence_id', + sortOrder: 'desc' as const, + } + : skipToken; + + const { + data: attemptsData, + isFetching: attemptsFetching, + isError: attemptsIsError, + error: attemptsError, + } = useGetRolloutAttemptsQuery(attemptsQueryArgs, { + pollingInterval: autoRefreshMs > 0 ? autoRefreshMs : undefined, + }); + + const { + data: spansData, + isFetching: spansFetching, + isError: spansIsError, + error: spansError, + refetch: refetchSpans, + } = useGetSpansQuery(spansQueryArgs ?? skipToken, { + pollingInterval: autoRefreshMs > 0 ? autoRefreshMs : undefined, + }); + + useEffect(() => { + if (!rolloutsData) { + return; + } + if (rolloutItems.length === 0) { + if (rolloutId !== null) { + dispatch(setTracesRolloutId(null)); + } + return; + } + const rolloutExists = rolloutId ? rolloutItems.some((rollout) => rollout.rolloutId === rolloutId) : false; + if (!rolloutExists) { + dispatch(setTracesRolloutId(rolloutItems[0].rolloutId)); + } + }, [dispatch, rolloutsData, rolloutId, rolloutItems]); + + useEffect(() => { + if (!rolloutId) { + if (attemptId !== null) { + dispatch(setTracesAttemptId(null)); + } + return; + } + + if (attemptsData && attemptsData.items.length > 0) { + const hasSelected = attemptId ? attemptsData.items.some((attempt) => attempt.attemptId === attemptId) : false; + if (!hasSelected) { + const latest = getLatestAttempt(attemptsData.items); + if (latest && latest.attemptId !== attemptId) { + dispatch(setTracesAttemptId(latest.attemptId)); + } + } + return; + } + + const fallbackAttemptId = selectedRollout?.attempt?.attemptId ?? null; + if (fallbackAttemptId !== attemptId) { + dispatch(setTracesAttemptId(fallbackAttemptId)); + } + }, [attemptsData, attemptId, dispatch, rolloutId, selectedRollout]); + + const rolloutOptions = useMemo( + () => + rolloutItems.map((rollout) => ({ + value: rollout.rolloutId, + label: rollout.rolloutId, + })), + [rolloutItems], + ); + + const attemptOptions = useMemo(() => { + if (attemptsData && attemptsData.items.length > 0) { + return [...attemptsData.items] + .sort((a, b) => b.sequenceId - a.sequenceId) + .map((attempt) => ({ + value: attempt.attemptId, + label: `Attempt ${attempt.sequenceId} (${attempt.attemptId}) - ${formatStatusLabel(attempt.status)}`, + })); + } + if (selectedRollout?.attempt) { + const attempt = selectedRollout.attempt; + return [ + { + value: attempt.attemptId, + label: `Attempt ${attempt.sequenceId} (${attempt.attemptId}) - ${formatStatusLabel(attempt.status)}`, + }, + ]; + } + return []; + }, [attemptsData, selectedRollout]); + + const rawSpansData = spansData as any as { items?: Span[]; total?: number } | undefined; + const spans = rawSpansData?.items ?? []; + const spansTotal = rawSpansData?.total ?? 0; + const recordsPerPageOptions = [50, 100, 200, 500]; + const isInitialLoading = rolloutsLoading && rolloutItems.length === 0; + const isFetching = spansFetching || rolloutsFetching || attemptsFetching; + + const selectionMessage = useMemo(() => { + if (!rolloutId && !attemptId) { + return 'Select a rollout and attempt to view traces.'; + } + if (!rolloutId) { + return 'Select a rollout to view traces.'; + } + if (!attemptId) { + return 'Select an attempt to view traces.'; + } + return undefined; + }, [attemptId, rolloutId]); + + const tableSpans = selectionMessage ? [] : spans; + const tableIsError = selectionMessage ? false : spansIsError; + const tableError = selectionMessage ? undefined : spansError; + const tableIsFetching = selectionMessage ? false : isFetching; + + useEffect(() => { + const anyError = rolloutsIsError || attemptsIsError || spansIsError; + if (anyError) { + const descriptor = + (rolloutsIsError && getErrorDescriptor(rolloutsError)) || + (attemptsIsError && getErrorDescriptor(attemptsError)) || + (spansIsError && getErrorDescriptor(spansError)) || + null; + const detailSuffix = descriptor ? ` (${descriptor})` : ''; + dispatch( + showAlert({ + id: 'traces-fetch', + message: `Unable to refresh traces${detailSuffix}. The table may be out of date until the connection recovers.`, + tone: 'error', + }), + ); + return; + } + + if (!rolloutsLoading && !rolloutsFetching && !spansFetching && !attemptsFetching) { + dispatch(hideAlert({ id: 'traces-fetch' })); + } + }, [ + attemptsError, + attemptsFetching, + attemptsIsError, + dispatch, + rolloutsError, + rolloutsFetching, + rolloutsIsError, + rolloutsLoading, + spansError, + spansFetching, + spansIsError, + ]); + + useEffect( + () => () => { + dispatch(hideAlert({ id: 'traces-fetch' })); + }, + [dispatch], + ); + + const handleSearchTermChange = useCallback( + (value: string) => { + dispatch(setTracesSearchTerm(value)); + }, + [dispatch], + ); + + const handleSortStatusChange = useCallback( + (status: DataTableSortStatus) => { + dispatch( + setTracesSort({ + column: status.columnAccessor as string, + direction: status.direction, + }), + ); + }, + [dispatch], + ); + + const handlePageChange = useCallback( + (nextPage: number) => { + dispatch(setTracesPage(nextPage)); + }, + [dispatch], + ); + + const handleRecordsPerPageChange = useCallback( + (value: number) => { + dispatch(setTracesRecordsPerPage(value)); + }, + [dispatch], + ); + + const handleResetFilters = useCallback(() => { + dispatch(resetTracesFilters()); + }, [dispatch]); + + const handleRefetch = useCallback(() => { + if (rolloutId) { + void refetchSpans(); + } + }, [refetchSpans, rolloutId]); + + const handleShowRollout = useCallback( + (record: TracesTableRecord) => { + if (rolloutItems.length === 0) { + return; + } + const rollout = rolloutItems.find((item) => item.rolloutId === record.rolloutId); + if (!rollout) { + return; + } + + const attempts = attemptsData?.items ?? []; + const attemptForRecord = + attempts.find((attempt) => attempt.attemptId === record.attemptId) ?? rollout.attempt ?? null; + + dispatch( + openDrawer({ + type: 'rollout-json', + rollout, + attempt: attemptForRecord, + isNested: false, + }), + ); + }, + [attemptsData, dispatch, rolloutItems], + ); + + const handleShowSpanDetail = useCallback( + (record: TracesTableRecord) => { + const rolloutForSpan = + rolloutItems.length > 0 ? (rolloutItems.find((item) => item.rolloutId === record.rolloutId) ?? null) : null; + const attempts = attemptsData?.items ?? []; + const attemptForSpan = + attempts.find((attempt) => attempt.attemptId === record.attemptId) ?? rolloutForSpan?.attempt ?? null; + + dispatch( + openDrawer({ + type: 'trace-detail', + span: record, + rollout: rolloutForSpan, + attempt: attemptForSpan, + }), + ); + }, + [attemptsData, dispatch, rolloutItems], + ); + + const handleParentIdClick = useCallback( + (parentId: string) => { + dispatch(setTracesSearchTerm(parentId)); + }, + [dispatch], + ); + + const handleViewChange = useCallback( + (value: ViewOptionValue) => { + dispatch(setTracesViewMode(value)); + }, + [dispatch], + ); + + const activeViewLabel = VIEW_OPTIONS.find((option) => option.value === viewMode)?.label ?? 'Table View'; + + return ( + + + + Traces + + { + if (value !== attemptId) { + dispatch(setTracesAttemptId(value)); + } + }} + searchable + placeholder='Latest attempt' + aria-label='Select attempt' + nothingFoundMessage={attemptsFetching ? 'Loading...' : 'No attempts'} + comboboxProps={{ withinPortal: true }} + w={280} + disabled={!rolloutId || attemptOptions.length === 0} + /> + handleSearchTermChange(event.currentTarget.value)} + placeholder='Search spans' + aria-label='Search spans' + leftSection={} + w={280} + /> + + + + + + + + {VIEW_OPTIONS.map((option) => ( + : null} + onClick={() => { + if (!option.disabled) { + handleViewChange(option.value); + } + }} + > + {option.label} + + ))} + + + + + + + + + ); +} diff --git a/dashboard/src/store/hooks.ts b/dashboard/src/store/hooks.ts new file mode 100644 index 00000000..0cc6786b --- /dev/null +++ b/dashboard/src/store/hooks.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { AppDispatch, RootState } from './index'; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/dashboard/src/store/index.ts b/dashboard/src/store/index.ts new file mode 100644 index 00000000..a7461977 --- /dev/null +++ b/dashboard/src/store/index.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { configReducer } from '../features/config'; +import { resourcesReducer } from '../features/resources'; +import { rolloutsApi, rolloutsReducer } from '../features/rollouts'; +import { tracesReducer } from '../features/traces'; +import { alertReducer } from '../features/ui/alert'; +import { drawerReducer } from '../features/ui/drawer'; + +const rootReducer = combineReducers({ + config: configReducer, + drawer: drawerReducer, + alert: alertReducer, + rollouts: rolloutsReducer, + resources: resourcesReducer, + traces: tracesReducer, + [rolloutsApi.reducerPath]: rolloutsApi.reducer, +}); + +export type RootState = ReturnType; + +export const createAppStore = (preloadedState?: Partial) => + configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(rolloutsApi.middleware), + preloadedState, + }); + +export const store = createAppStore(); + +export type AppStore = ReturnType; +export type AppDispatch = AppStore['dispatch']; diff --git a/dashboard/src/styles/app.css b/dashboard/src/styles/app.css new file mode 100644 index 00000000..502df6ac --- /dev/null +++ b/dashboard/src/styles/app.css @@ -0,0 +1,99 @@ +@keyframes connection-refreshing-blink { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.3; + } +} + +.connection-refreshing { + animation: connection-refreshing-blink 1s linear infinite; +} + +.connection-indicator-button { + display: block; + width: 100%; + text-align: left; + padding: var(--mantine-spacing-xs); + border-radius: var(--mantine-radius-sm); + transition: background-color 150ms ease; + cursor: pointer; +} + +.connection-indicator-button:hover, +.connection-indicator-button:focus-visible { + background-color: var(--mantine-color-gray-0); +} + +[data-mantine-color-scheme='dark'] .connection-indicator-button:hover, +[data-mantine-color-scheme='dark'] .connection-indicator-button:focus-visible { + background-color: var(--mantine-color-dark-8); +} + +.connection-indicator-button:focus-visible { + outline: 2px solid var(--mantine-color-blue-filled); + outline-offset: 2px; +} + +[data-mantine-color-scheme='dark'] .mantine-AppShell-root .mantine-AppShell-navbar { + --app-shell-border-color: var(--mantine-color-dark-6); +} + +.mantine-datatable { + --mantine-datatable-background-color: var(--mantine-color-body); +} + +.mantine-datatable .mantine-datatable-empty-state { + pointer-events: none; +} + +.mantine-datatable .mantine-datatable-empty-state[data-active] { + pointer-events: auto; +} + +[data-mantine-color-scheme='dark'] .mantine-datatable { + --mantine-datatable-border-color-dark: var(--mantine-color-dark-6); + --mantine-datatable-highlight-on-hover-color: var(--mantine-color-dark-7); +} + +/* Rollout Nested Table */ +.rollouts-table .mantine-datatable-row-expansion-cell { + padding: 0; +} + +.rollouts-table .mantine-datatable-row-expansion-cell-content { + border: 0; + padding: 0; + background-color: transparent; +} + +.rollouts-table--nested { + --mantine-datatable-background-color: var(--mantine-color-gray-1); +} + +.rollouts-table--nested .mantine-datatable-table, +.rollouts-table--nested .mantine-datatable-table td, +.rollouts-table--nested .mantine-datatable-table th { + background-color: var(--mantine-datatable-background-color); +} + +.rollouts-table--nested .mantine-datatable-table tbody tr:first-of-type td { + border-top: 1px solid var(--mantine-color-gray-3); +} + +[data-mantine-color-scheme='dark'] .rollouts-table--nested { + --mantine-datatable-background-color: var(--mantine-color-dark-6); +} + +[data-mantine-color-scheme='dark'] .rollouts-table--nested .mantine-datatable-table tbody tr:first-of-type td { + border-top: 1px solid var(--mantine-color-dark-4); +} + +/* Fix Mantine Conflicts with Mantine-datatable */ +.mantine-datatable-page-size-selector-active { + color: var(--mantine-color-text); + background: transparent; +} diff --git a/dashboard/src/styles/theme.css b/dashboard/src/styles/theme.css new file mode 100644 index 00000000..1ae31892 --- /dev/null +++ b/dashboard/src/styles/theme.css @@ -0,0 +1,796 @@ +/* Checkbox */ +.mantine-Checkbox-root { + --checkbox-color: var(--mantine-primary-color-filled); + --checkbox-icon-color: var(--mantine-primary-color-contrast); + + .mantine-Checkbox-input { + background-color: transparent; + border-color: var(--checkbox-color); + + &[data-indeterminate], + &:checked { + background-color: var(--checkbox-color); + } + + &:disabled { + opacity: 0.5; + + & + .mantine-Checkbox-icon { + opacity: 0.5; + color: var(--checkbox-icon-color); + } + } + } + + .mantine-Checkbox-label { + &[data-disabled] { + color: inherit; + opacity: 0.7; + } + } + + &[data-variant='outline'] { + .mantine-Checkbox-input { + background-color: transparent; + + & + .mantine-Checkbox-icon { + color: var(--checkbox-color); + } + } + } +} + +/* Checkbox Group */ +.mantine-CheckboxGroup-root { + .mantine-CheckboxGroup-error { + margin-top: 8px; + } +} + +/* Chip */ +.mantine-Chip-root { + --chip-color: var(--mantine-primary-color-contrast); + + &[data-variant='outline']:not([data-disabled]) { + .mantine-Chip-label { + background-color: transparent; + --chip-bd: 1px solid var(--mantine-color-default-border); + + &:hover { + background-color: var(--mantine-color-default-hover); + } + + &:where([data-disabled]) { + --chip-icon-color: var(--mantine-color-text); + border-color: var(--mantine-color-default-border); + color: var(--mantine-color-text); + opacity: 0.5; + + &:hover { + background-color: transparent; + color: var(--mantine-color-text); + --chip-bd: 1px solid var(--mantine-color-default-border); + --chip-icon-color: var(--mantine-color-text); + } + } + } + } +} + +/* Input */ +[data-mantine-color-scheme='light'], +[data-mantine-color-scheme='dark'] { + .mantine-Input-wrapper { + --input-disabled-bg: alpha(var(--mantine-color-default-hover), 0.5); + --input-disabled-color: alpha(var(--mantine-color-text), 0.5); + + &[data-variant='default'] { + --input-bd: var(--mantine-color-default-border); + --input-bg: transparent; + --input-bd-focus: var(--mantine-primary-color-filled); + } + + &[data-variant='filled'] { + --input-bd: transparent; + --input-bg: var(--mantine-color-default-hover); + --input-bd-focus: var(--mantine-primary-color-filled); + } + + &[data-variant='unstyled'] { + --input-bd: transparent; + --input-bg: transparent; + --input-bd-focus: transparent; + } + } +} + +/* Color Input */ +.mantine-ColorInput-dropdown { + background-color: var(--mantine-color-default); + border-color: var(--mantine-color-default-border); +} + +/* Fieldset */ +.mantine-Fieldset-root { + border-color: var(--mantine-color-default-border); + background-color: transparent; + + &[data-variant='filled'] { + border-color: var(--mantine-color-default-border); + background-color: alpha(var(--mantine-color-default-hover), 0.4); + } + + &[data-variant='unstyled'] { + border-color: transparent; + background-color: transparent; + } +} + +/* Radio */ +.mantine-Radio-root { + .mantine-Radio-radio { + background-color: transparent; + border-color: var(--radio-color); + + &:checked { + background-color: var(--radio-color); + border-color: var(--radio-color); + } + + & + .mantine-Radio-icon { + color: var(--radio-icon-color); + } + + &:disabled { + opacity: 0.5; + } + } + + &[data-variant='outline'] { + .mantine-Radio-radio { + background-color: transparent; + border-color: var(--radio-color); + + &:checked:not(:disabled) { + background-color: transparent; + border-color: var(--radio-color); + } + + &:disabled { + opacity: 0.5; + + & + .mantine-Radio-icon { + color: var(--radio-color); + --radio-icon-opacity: 0.5; + } + } + } + } +} + +/* Segmented Control */ +.mantine-SegmentedControl-root { + background-color: var(--mantine-color-secondary-outline-hover); + + .mantine-SegmentedControl-label { + color: var(--mantine-color-dimmed); + + &[data-active] { + color: var(--mantine-color-text); + } + } +} + +[data-mantine-color-scheme='light'] { + .mantine-SegmentedControl-root { + .mantine-SegmentedControl-label { + &:where(:not([data-disabled], [data-active], [data-read-only])) { + &:hover { + color: var(--mantine-color-black); + } + } + } + } +} + +[data-mantine-color-scheme='dark'] { + .mantine-SegmentedControl-root { + .mantine-SegmentedControl-label { + &:where(:not([data-disabled], [data-active], [data-read-only])) { + &:hover { + color: var(--mantine-color-white); + } + } + } + } +} + +[data-mantine-color-scheme='light'] { + .mantine-SegmentedControl-root { + .mantine-SegmentedControl-label { + &:where(:not([data-disabled], [data-active], [data-read-only])) { + &:hover { + color: var(--mantine-color-black); + } + } + } + } +} + +[data-mantine-color-scheme='dark'] { + .mantine-SegmentedControl-root { + .mantine-SegmentedControl-label { + &:where(:not([data-disabled], [data-active], [data-read-only])) { + &:hover { + color: var(--mantine-color-white); + } + } + } + } +} + +/* Switch */ + +/* Slider */ +[data-mantine-color-scheme='light'], +[data-mantine-color-scheme='dark'] { + .mantine-Slider-root { + --slider-track-bg: var(--mantine-color-secondary-outline-hover); + } +} + +.mantine-Slider-root { + .mantine-Slider-trackContainer { + &[data-disabled] { + opacity: 0.5; + } + } + + .mantine-Slider-label { + background-color: var(--mantine-color-default) !important; + color: var(--mantine-color-text); + border: 1px solid var(--mantine-color-default-border) !important; + top: -45px; + } +} + +/* Pills Input */ +.mantine-PillsInput-wrapper { + .mantine-PillsInput-input { + &[data-variant='filled'] { + .mantine-Pill-root { + background-color: var(--mantine-color-body); + } + } + } +} + +/* Tags Input */ +.mantine-TagsInput-wrapper { + .mantine-TagsInput-input { + &[data-variant='filled'] { + .mantine-Pill-root { + background-color: var(--mantine-color-body); + } + } + } +} + +/* ActionIcon */ +.mantine-ActionIcon-root { + &:where(:disabled:not([data-loading]), [data-disabled]:not([data-loading])) { + border: var(--ai-bd); + opacity: 0.5; + + &:active { + transform: none; + } + } +} + +/* Button */ +.mantine-Button-root { + &:where(:disabled:not([data-loading]), [data-disabled]:not([data-loading])) { + border: var(--button-bd); + opacity: 0.5; + } +} + +/* Close Button */ +.mantine-CloseButton-root { + &[data-variant='subtle']:where(:not([data-disabled], :disabled)) { + &:hover { + background-color: var(--mantine-color-default-hover); + } + } +} + +/* NavLink */ +.mantine-NavLink-root { + &:hover { + background-color: var(--mantine-color-default-hover); + } + + &:where([data-active], [aria-current='page']) { + background-color: var(--nl-bg); + color: var(--nl-color); + + &:hover { + background-color: var(--nl-hover); + } + + .description { + --description-opacity: 0.9; + --description-color: var(--nl-color); + } + } +} + +/* Pagination */ +.mantine-Pagination-root { + .mantine-Pagination-control { + border-color: var(--mantine-color-default-border); + background-color: var(--mantine-color-body); + + &:hover { + &:where(:not(:disabled, [data-disabled])) { + background-color: var(--mantine-color-default-hover); + } + } + + &:where([data-active]) { + background-color: var(--pagination-active-bg); + border-color: var(--pagination-active-bg); + color: var(--pagination-active-color, var(--mantine-primary-color-contrast)); + + &:hover { + background-color: rgb(from var(--pagination-active-bg) r g b / 0.9); + } + } + } +} + +/* Stepper */ +.mantine-Stepper-root { + .mantine-Stepper-stepIcon { + background-color: var(--mantine-color-body); + border-color: var(--mantine-color-default-border); + color: var(--mantine-color-text); + + &:where([data-progress]) { + border-color: var(--step-color); + } + + &:where([data-completed]) { + color: var(--stepper-icon-color, var(--mantine-color-white)); + background-color: var(--step-color); + border-color: var(--step-color); + } + } +} + +/* Tabs */ +[data-mantine-color-scheme='light'], +[data-mantine-color-scheme='dark'] { + .mantine-Tabs-root { + --tab-border-color: var(--mantine-color-default-border); + + &[data-variant='default'] { + --tabs-list-border-width: 2px; + --tab-hover-color: var(--mantine-color-default-hover); + } + + &[data-variant='pills'] { + --tabs-list-gap: calc(var(--mantine-spacing-sm) / 2); + --tabs-text-color: var(--mantine-color-body) !important; + --tab-hover-color: var(--mantine-color-default-hover); + } + + .mantine-Tabs-tab { + color: var(--mantine-color-dimmed); + + &[data-variant='default'], + &[data-variant='outline'] { + &[data-active='true'] { + color: var(--mantine-color-text); + } + + &:hover { + background-color: transparent; + color: var(--mantine-color-text); + } + } + + &[data-variant='pills'] { + &[data-active='true'] { + color: var(--mantine-primary-color-contrast); + + &:hover { + color: var(--mantine-primary-color-contrast); + } + } + + &:hover { + color: var(--mantine-color-text); + } + } + } + } +} + +/* Notification */ +.mantine-Notification-root { + background-color: var(--mantine-color-secondary-filled); + + &:where([data-with-border]) { + border: 1px solid var(--mantine-color-default-border); + } +} + +/* Progress */ +.mantine-Progress-root { + background-color: var(--mantine-color-default-hover); +} + +/* Ring Progress */ +.mantine-RingProgress-root { + .mantine-RingProgress-curve { + --rp-curve-root-color: var(--mantine-color-default-hover); + } +} + +/* SemiCircle Progress */ +.mantine-SemiCircleProgress-root { + --scp-empty-segment-color: var(--mantine-color-default-hover); +} + +/* Skeleton */ +.mantine-Skeleton-root { + &:where([data-visible]) { + &::after { + background-color: var(--mantine-color-default-hover); + } + } +} + +/* Drawer */ +.mantine-Drawer-root { + .mantine-Drawer-content { + border-right: 1px solid var(--mantine-color-default-border); + border-left: 1px solid var(--mantine-color-default-border); + } +} + +/* Hover Card */ +.mantine-HoverCard-dropdown { + background-color: var(--mantine-color-body); + border-color: var(--mantine-color-default-border); + border-radius: var(--mantine-radius-default); +} + +/* Menu Dropdown */ +.mantine-Menu-dropdown { + background-color: var(--mantine-color-body); + border-color: var(--mantine-color-default-border); + + .mantine-Menu-item { + background-color: transparent; + + &:where([data-hovered]) { + background-color: var(--menu-item-hover, var(--mantine-color-default-hover)); + } + } + + .mantine-Menu-divider { + border-color: var(--mantine-color-default-border); + } +} + +/* Modal */ +.mantine-Modal-root { + .mantine-Modal-content { + border: 1px solid var(--mantine-color-default-border); + border-radius: var(--mantine-radius-default); + } +} + +/* Popover Start */ +.mantine-Popover-dropdown { + --popover-border-color: var(--mantine-color-default-border); + background-color: var(--mantine-color-body); +} + +.mantine-Popover-arrow { + --popover-border-color: var(--mantine-color-default-border); +} +/* Popover End */ + +/* Tooltip */ +.mantine-Tooltip-tooltip { + font-size: var(--mantine-font-size-xs); + padding-top: 0; + padding-bottom: 0; +} + +/* Accordion */ +.mantine-Accordion-root { + .mantine-Accordion-control { + color: var(--mantine-color-text); + } + + .mantine-Accordion-item { + --item-border-color: var(--mantine-color-default-border); + --item-filled-color: var(--mantine-color-default-hover); + } + + &[data-variant='default'] { + .mantine-Accordion-control { + &:where(:not(:disabled, [data-disabled])) { + &:hover { + background-color: transparent; + } + } + } + + .mantine-Accordion-label { + &:where(:not(:disabled, [data-disabled])) { + &:hover { + text-decoration: underline; + } + } + } + } + + &[data-variant='contained'], + &[data-variant='filled'] { + .mantine-Accordion-control { + &:where(:not(:disabled, [data-disabled])) { + &:hover { + background-color: var(--mantine-color-default-hover); + } + } + } + } +} + +/* Avatar */ +.mantine-Avatar-root { + --avatar-bg: var(--mantine-primary-color-light); + --avatar-bd: 1px solid transparent; + --avatar-color: var(--mantine-primary-color-light-color); +} + +/* Badge */ +.mantine-Badge-root { + --badge-color: var(--mantine-primary-color-contrast); + + &[data-variant='dot'] { + background-color: var(--mantine-color-secondary-light); + border-color: var(--mantine-color-secondary-light); + color: var(--mantine-color-text); + } +} + +/* Card */ +.mantine-Card-root { + background-color: var(--mantine-color-default); + + .mantine-Card-section { + border-color: var(--mantine-color-default-border); + } +} + +/* Timeline */ +.mantine-Timeline-root { + --item-border-color: var(--mantine-color-default-border); + + .mantine-Timeline-itemBullet { + border-color: var(--mantine-color-default-border); + + &:where([data-with-child]) { + background-color: var(--mantine-color-secondary-light); + } + + &:where([data-active]) { + border-color: var(--tli-color, var(--tl-color)); + background-color: var(--mantine-color-white); + color: var(--tl-icon-color, var(--mantine-color-white)); + + &:where([data-with-child]) { + background-color: var(--tli-color, var(--tl-color)); + color: var(--tl-icon-color, var(--mantine-color-white)); + } + } + } +} + +/* Code */ +.mantine-Code-root { + background-color: var(--code-bg, var(--mantine-color-secondary-filled)); + color: var(--mantine-color-text); +} + +/* Table */ +.mantine-Table-table { + --table-hover-color: var(--mantine-color-default-hover); + --table-striped-color: var(--mantine-color-default-hover); + --table-border-color: var(--mantine-color-default-border); + + &:where([data-with-table-border]) { + border: rem(1px) solid var(--table-border-color); + } +} + +/* Title */ +.mantine-Title-root, +.mantine-Modal-title, +.mantine-Drawer-title { + letter-spacing: -0.025em; +} + +/* Divider */ +.mantine-Divider-root { + --divider-color: var(--mantine-color-default-border); +} + +/* Paper */ +.mantine-Paper-root { + &:where([data-with-border]) { + border: rem(1px) solid var(--mantine-color-default-border); + } +} + +/* Date Picker */ +.mantine-DatePicker-levelsGroup { + .mantine-DatePicker-calendarHeader { + --dch-control-size-xs: calc(1.25rem * var(--mantine-scale)); + --dch-control-size-sm: calc(1.75rem * var(--mantine-scale)); + --dch-control-size-md: calc(2.25rem * var(--mantine-scale)); + --dch-control-size-lg: calc(2.5rem * var(--mantine-scale)); + --dch-control-size-xl: calc(2.75rem * var(--mantine-scale)); + + max-width: none; + gap: var(--mantine-spacing-xs); + + .mantine-DatePicker-calendarHeaderControl { + border: 1px solid var(--mantine-color-default-border); + border-radius: var(--mantine-radius-default); + + &:hover { + background-color: var(--mantine-color-default-hover) !important; + } + } + + .mantine-DatePicker-calendarHeaderLevel { + &:hover { + background-color: var(--mantine-color-default-hover) !important; + } + + font-size: var(--mantine-font-size-sm); + } + } + + .mantine-DatePicker-month { + border-collapse: separate; + border-spacing: 0 5px; + + .mantine-DatePicker-weekdaysRow { + .mantine-DatePicker-weekday { + font-size: var(--mantine-font-size-xs); + } + } + + .mantine-DatePicker-monthRow { + margin-top: var(--mantine-spacing-xs); + margin-bottom: var(--mantine-spacing-xs); + + .mantine-DatePicker-day { + --day-size-xs: calc(1.5rem * var(--mantine-scale)); + --day-size-sm: calc(2rem * var(--mantine-scale)); + --day-size-md: calc(2.5rem * var(--mantine-scale)); + --day-size-lg: calc(3rem * var(--mantine-scale)); + --day-size-xl: calc(3.5rem * var(--mantine-scale)); + + font-size: calc(var(--day-size) / 2.28); + + &:hover:where(:not([data-static], [data-disabled], [data-selected], [data-in-range])) { + background-color: var(--mantine-color-default-hover) !important; + border-radius: var(--mantine-radius-default); + + &[data-in-range] { + border-radius: 0; + } + } + + &:where([data-selected]):hover:where(:not([data-disabled], [data-static])) { + background-color: var(--mantine-primary-color-filled-hover); + border-radius: var(--mantine-radius-default); + } + + &[data-weekend] { + color: var(--mantine-color-text); + } + + &[data-last-in-range], + &[data-first-in-range] { + border-radius: var(--mantine-radius-default); + } + } + } + } + + .mantine-DatePicker-monthsList { + .mantine-DatePicker-monthsListControl { + --dpc-size-xs: calc(1.5rem * var(--mantine-scale)); + --dpc-size-sm: calc(1.96rem * var(--mantine-scale)); + --dpc-size-md: calc(2.5rem * var(--mantine-scale)); + --dpc-size-lg: calc(3rem * var(--mantine-scale)); + --dpc-size-xl: calc(3.5rem * var(--mantine-scale)); + + &:hover:where(:not([data-disabled], :disabled)) { + background-color: var(--mantine-color-default-hover) !important; + } + } + } + + .mantine-DatePicker-yearsList { + .mantine-DatePicker-yearsListControl { + --dpc-size-xs: calc(1.5rem * var(--mantine-scale)); + --dpc-size-sm: calc(1.96rem * var(--mantine-scale)); + --dpc-size-md: calc(2.5rem * var(--mantine-scale)); + --dpc-size-lg: calc(3rem * var(--mantine-scale)); + --dpc-size-xl: calc(3.5rem * var(--mantine-scale)); + + &:hover:where(:not([data-disabled], :disabled)) { + background-color: var(--mantine-color-default-hover) !important; + } + } + } +} + +/* Spotlight */ +.mantine-Spotlight-root { + .mantine-Spotlight-content { + border: 1px solid var(--mantine-color-default-border); + } + + .mantine-Spotlight-actionsList { + border-color: var(--mantine-color-default-border); + } + + .mantine-Spotlight-action { + &:where(:not([data-selected])) { + &:hover { + background-color: var(--mantine-color-default-hover); + } + } + } + + .mantine-Spotlight-footer { + border-color: var(--mantine-color-default-border); + } +} + +/* Code Highlight */ +.mantine-CodeHighlightTabs-root { + .mantine-CodeHighlightTabs-file { + color: var(--mantine-color-text); + border-color: var(--mantine-color-default-border); + padding: 7px 12px; + font-size: 12px; + + &:where([data-active]) { + background-color: var(--mantine-color-default-hover); + color: var(--mantine-color-text); + } + } + + .mantine-CodeHighlightTabs-control { + color: var(--mantine-primary-color-contrast); + } +} + +/* Chart Tooltip */ +.mantine-ChartTooltip-tooltip { + border: 1px solid var(--mantine-color-default-border); +} diff --git a/dashboard/src/theme.ts b/dashboard/src/theme.ts new file mode 100644 index 00000000..4b4c49ac --- /dev/null +++ b/dashboard/src/theme.ts @@ -0,0 +1,863 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { + ActionIcon, + Alert, + Anchor, + Avatar, + Badge, + Blockquote, + Button, + Card, + Checkbox, + Chip, + Container, + createTheme, + Dialog, + Indicator, + MantineColorsTuple, + MantineThemeOverride, + Mark, + NavLink, + Pagination, + Paper, + Radio, + rem, + SegmentedControl, + Select, + Stepper, + Switch, + ThemeIcon, + Timeline, + Tooltip, +} from '@mantine/core'; + +const CONTAINER_SIZES: Record = { + xxs: rem('200px'), + xs: rem('300px'), + sm: rem('400px'), + md: rem('500px'), + lg: rem('600px'), + xl: rem('1400px'), + xxl: rem('1600px'), +}; + +export const FONT_FAMILY = + 'ui-sans-serif, -apple-system, system-ui, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"'; + +const zincColors: MantineColorsTuple = [ + '#fafafa', + '#f4f4f5', + '#e4e4e7', + '#d4d4d8', + '#a1a1aa', + '#52525b', + '#3f3f46', + '#27272a', + '#18181b', + '#09090b', + '#71717A', +]; +const slateColors: MantineColorsTuple = [ + '#f8fafc', + '#f1f5f9', + '#e2e8f0', + '#cbd5e1', + '#94a3b8', + '#475569', + '#334155', + '#1e293b', + '#0f172a', + '#020817', + '#64748B', +]; +const grayColors: MantineColorsTuple = [ + '#f9fafb', + '#f3f4f6', + '#e5e7eb', + '#d1d5db', + '#9ca3af', + '#4b5563', + '#374151', + '#1f2937', + '#111827', + '#030712', + '#6B7280', +]; +const neutralColors: MantineColorsTuple = [ + '#fafafa', + '#f5f5f5', + '#e5e5e5', + '#d4d4d4', + '#a3a3a3', + '#525252', + '#404040', + '#262626', + '#171717', + '#0a0a0a', + '#737373', +]; +const stoneColors: MantineColorsTuple = [ + '#fafaf9', + '#f5f5f4', + '#e7e5e4', + '#d6d3d1', + '#a8a29e', + '#57534e', + '#44403c', + '#292524', + '#1c1917', + '#0c0a09', + '#78716C', +]; +const redColors: MantineColorsTuple = [ + '#FEF2F2', + '#FEE2E2', + '#FECACA', + '#FCA5A5', + '#F87171', + '#DC2626', + '#B91C1C', + '#991B1B', + '#7F1D1D', + '#450A0A', + '#EF4444', +]; +const roseColors: MantineColorsTuple = [ + '#fff1f2', + '#ffe4e6', + '#fecdd3', + '#fda4af', + '#fb7185', + '#e11d48', + '#be123c', + '#9f1239', + '#881337', + '#4c0519', + '#F43F5E', +]; +const orangeColors: MantineColorsTuple = [ + '#fff7ed', + '#ffedd5', + '#fed7aa', + '#fdba74', + '#fb923c', + '#f97316', + '#ea580c', + '#9a3412', + '#7c2d12', + '#431407', + '#F97316', +]; +const amberColors: MantineColorsTuple = [ + '#FFFBEB', + '#FEF3C7', + '#FDE68A', + '#FCD34D', + '#FBBF24', + '#f59e0b', + '#D97706', + '#92400E', + '#78350F', + '#451A03', + '#F59E0B', +]; +const yellowColors: MantineColorsTuple = [ + '#fefce8', + '#fef9c3', + '#fef08a', + '#fde047', + '#facc15', + '#ca8a04', + '#a16207', + '#854d0e', + '#713f12', + '#3f2c06', + '#F59E0B', +]; +const limeColors: MantineColorsTuple = [ + '#f7fee7', + '#ecfccb', + '#d9f99d', + '#bef264', + '#a3e635', + '#4d7c0f', + '#3f6212', + '#365314', + '#1a2e05', + '#0f1903', + '#84CC16', +]; +const greenColors: MantineColorsTuple = [ + '#F0FDF4', + '#DCFCE7', + '#BBF7D0', + '#86EFAC', + '#4ADE80', + '#22c55e', + '#16A34A', + '#166534', + '#14532D', + '#052E16', + '#10B981', +]; +const emeraldColors: MantineColorsTuple = [ + '#ecfdf5', + '#d1fae5', + '#a7f3d0', + '#6ee7b7', + '#34d399', + '#059669', + '#047857', + '#065f46', + '#064e3b', + '#022c22', + '#10B981', +]; +const tealColors: MantineColorsTuple = [ + '#f0fdfa', + '#ccfbf1', + '#99f6e4', + '#5eead4', + '#2dd4bf', + '#0d9488', + '#0f766e', + '#115e59', + '#134e4a', + '#042f2e', + '#14B8A6', +]; +const cyanColors: MantineColorsTuple = [ + '#ecfeff', + '#cffafe', + '#a5f3fc', + '#67e8f9', + '#22d3ee', + '#0891b2', + '#0e7490', + '#155e75', + '#164e63', + '#083344', + '#06B6D4', +]; +const skyColors: MantineColorsTuple = [ + '#f0f9ff', + '#e0f2fe', + '#bae6fd', + '#7dd3fc', + '#38bdf8', + '#0284c7', + '#0369a1', + '#075985', + '#0c4a6e', + '#082f49', + '#0EA5E9', +]; +const blueColors: MantineColorsTuple = [ + '#eff6ff', + '#dbeafe', + '#bfdbfe', + '#93c5fd', + '#60a5fa', + '#3b82f6', + '#2563eb', + '#1e40af', + '#1e3a8a', + '#172554', + '#3B82F6', +]; +const indigoColors: MantineColorsTuple = [ + '#eef2ff', + '#e0e7ff', + '#c7d2fe', + '#a5b4fc', + '#818cf8', + '#4f46e5', + '#4338ca', + '#3730a3', + '#312e81', + '#1e1b4b', + '#6366F1', +]; +const violetColors: MantineColorsTuple = [ + '#f5f3ff', + '#ede9fe', + '#ddd6fe', + '#c4b5fd', + '#a78bfa', + '#7c3aed', + '#6d28d9', + '#5b21b6', + '#4c1d95', + '#1e1b4b', + '#8B5CF6', +]; +const purpleColors: MantineColorsTuple = [ + '#faf5ff', + '#f3e8ff', + '#e9d5ff', + '#d8b4fe', + '#c084fc', + '#9333ea', + '#7e22ce', + '#6b21a8', + '#581c87', + '#2e1065', + '#A855F7', +]; +const fuchsiaColors: MantineColorsTuple = [ + '#fdf4ff', + '#fae8ff', + '#f5d0fe', + '#f0abfc', + '#e879f9', + '#c026d3', + '#a21caf', + '#86198f', + '#701a75', + '#4a044e', + '#D946EF', +]; +const pinkColors: MantineColorsTuple = [ + '#fdf2f8', + '#fce7f3', + '#fbcfe8', + '#f9a8d4', + '#f472b6', + '#db2777', + '#be185d', + '#9d174d', + '#831843', + '#500724', + '#EC4899', +]; + +export const shadcnTheme: MantineThemeOverride = createTheme({ + colors: { + slate: slateColors, + gray: grayColors, + zinc: zincColors, + neutral: neutralColors, + stone: stoneColors, + + red: redColors, + rose: roseColors, + orange: orangeColors, + amber: amberColors, + yellow: yellowColors, + + lime: limeColors, + green: greenColors, + emerald: emeraldColors, + + teal: tealColors, + cyan: cyanColors, + sky: skyColors, + blue: blueColors, + + indigo: indigoColors, + violet: violetColors, + purple: purpleColors, + fuchsia: fuchsiaColors, + pink: pinkColors, + + primary: zincColors, + secondary: zincColors, + dark: zincColors, + + error: redColors as MantineColorsTuple, + success: greenColors, + info: blueColors, + warning: amberColors, + }, + focusRing: 'never', + scale: 1, + primaryColor: 'primary', + primaryShade: { light: 8, dark: 0 }, + autoContrast: true, + luminanceThreshold: 0.3, + fontFamily: FONT_FAMILY, + radius: { + xs: rem('6px'), + sm: rem('8px'), + md: rem('12px'), + lg: rem('16px'), + xl: rem('24px'), + }, + defaultRadius: 'sm', + spacing: { + '4xs': rem('2px'), + '3xs': rem('4px'), + '2xs': rem('8px'), + 'xs': rem('10px'), + 'sm': rem('12px'), + 'md': rem('16px'), + 'lg': rem('20px'), + 'xl': rem('24px'), + '2xl': rem('28px'), + '3xl': rem('32px'), + '4xl': rem('40px'), + }, + fontSizes: { + 'xs': rem('12px'), + 'sm': rem('14px'), + 'md': rem('16px'), + 'lg': rem('18px'), + 'xl': rem('20px'), + '2xl': rem('24px'), + '3xl': rem('30px'), + '4xl': rem('36px'), + '5xl': rem('48px'), + }, + lineHeights: { + xs: rem('18px'), + sm: rem('20px'), + md: rem('24px'), + lg: rem('28px'), + }, + + headings: { + fontFamily: FONT_FAMILY, + sizes: { + h1: { + fontSize: rem('36px'), + lineHeight: rem('44px'), + fontWeight: '600', + }, + h2: { + fontSize: rem('30px'), + lineHeight: rem('38px'), + fontWeight: '600', + }, + h3: { + fontSize: rem('24px'), + lineHeight: rem('32px'), + fontWeight: '600', + }, + h4: { + fontSize: rem('20px'), + lineHeight: rem('30px'), + fontWeight: '600', + }, + }, + }, + shadows: { + xs: '0 1px 2px rgba(0, 0, 0, 0.05)', + sm: '0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06)', + md: '0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)', + lg: '0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05)', + xl: '0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04)', + xxl: '0 25px 50px rgba(0, 0, 0, 0.25)', + }, + + cursorType: 'pointer', + other: { + style: 'shadcn', + }, + components: { + Container: Container.extend({ + vars: (_, { size, fluid }) => ({ + root: { + '--container-size': fluid + ? '100%' + : size !== undefined && size in CONTAINER_SIZES + ? CONTAINER_SIZES[size] + : rem(size), + }, + }), + }), + Checkbox: Checkbox.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + return { + root: { + '--checkbox-color': colorKey + ? `var(--mantine-color-${colorKey}-filled)` + : 'var(--mantine-primary-color-filled)', + + '--checkbox-icon-color': colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)', + }, + }; + }, + }), + Chip: Chip.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const variant = props.variant ?? 'filled'; + return { + root: { + '--chip-bg': + variant !== 'light' + ? colorKey + ? `var(--mantine-color-${colorKey}-filled)` + : 'var(--mantine-primary-color-filled)' + : undefined, + '--chip-color': + variant === 'filled' + ? colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)' + : undefined, + }, + }; + }, + }), + Radio: Radio.extend({ + vars: (theme, props) => ({ + root: { + '--radio-color': props.color + ? Object.keys(theme.colors).includes(props.color) + ? `var(--mantine-color-${props.color}-filled)` + : props.color + : 'var(--mantine-primary-color-filled)', + + '--radio-icon-color': props.color + ? Object.keys(theme.colors).includes(props.color) + ? `var(--mantine-color-${props.color}-contrast)` + : props.color + : 'var(--mantine-primary-color-contrast)', + }, + }), + }), + SegmentedControl: SegmentedControl.extend({ + vars: (theme, props) => ({ + root: { + '--sc-color': props.color + ? Object.keys(theme.colors).includes(props.color) + ? ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(props.color) + ? 'var(--mantine-color-body)' + : `var(--mantine-color-${props.color}-filled)` + : props.color + : 'var(--mantine-color-default)', + }, + }), + }), + Switch: Switch.extend({ + styles: () => ({ + thumb: { + backgroundColor: 'var(--mantine-color-default)', + borderColor: 'var(--mantine-color-default-border)', + }, + track: { + borderColor: 'var(--mantine-color-default-border)', + }, + }), + }), + Select: Select.extend({ + defaultProps: { + checkIconPosition: 'right', + }, + }), + ActionIcon: ActionIcon.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const isNeutralColor = colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(colorKey); + const isNeutralPrimaryColor = + !colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(theme.primaryColor); + const variant = props.variant ?? 'filled'; + + return { + root: { + '--ai-color': (() => { + if (variant === 'filled') { + if (colorKey) { + return `var(--mantine-color-${colorKey}-contrast)`; + } + return 'var(--mantine-primary-color-contrast)'; + } + if (variant === 'white') { + if (isNeutralColor || isNeutralPrimaryColor) { + return 'var(--mantine-color-black)'; + } + return undefined; + } + return undefined; + })(), + }, + }; + }, + }), + Button: Button.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const isNeutralColor = colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(colorKey); + const isNeutralPrimaryColor = + !colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(theme.primaryColor); + const variant = props.variant ?? 'filled'; + return { + root: { + '--button-color': (() => { + if (variant === 'filled') { + if (colorKey) { + return `var(--mantine-color-${colorKey}-contrast)`; + } + return 'var(--mantine-primary-color-contrast)'; + } + if (variant === 'white') { + if (isNeutralColor || isNeutralPrimaryColor) { + return 'var(--mantine-color-black)'; + } + return undefined; + } + return undefined; + })(), + }, + }; + }, + }), + Anchor: Anchor.extend({ + defaultProps: { + underline: 'always', + }, + }), + NavLink: NavLink.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const variant = props.variant ?? 'light'; + return { + root: { + '--nl-color': + variant === 'filled' + ? colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)' + : undefined, + }, + children: {}, + }; + }, + }), + Pagination: Pagination.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + return { + root: { + '--pagination-active-color': colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)', + }, + }; + }, + }), + Stepper: Stepper.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + return { + root: { + '--stepper-icon-color': colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)', + }, + }; + }, + }), + Alert: Alert.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const isNeutralColor = colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(colorKey); + const isNeutralPrimaryColor = + !colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(theme.primaryColor); + const variant = props.variant ?? 'light'; + return { + root: { + '--alert-color': + variant === 'filled' + ? colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)' + : variant === 'white' + ? isNeutralColor || isNeutralPrimaryColor + ? 'var(--mantine-color-black)' + : undefined + : undefined, + }, + }; + }, + }), + Dialog: Dialog.extend({ + defaultProps: { + withBorder: true, + }, + }), + Tooltip: Tooltip.extend({ + vars: () => ({ + tooltip: { + '--tooltip-bg': 'var(--mantine-color-primary-color-filled)', + '--tooltip-color': 'var(--mantine-color-primary-color-contrast)', + }, + }), + }), + Avatar: Avatar.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const isNeutralColor = colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(colorKey); + const isNeutralPrimaryColor = + !colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(theme.primaryColor); + const variant = props.variant ?? 'light'; + return { + root: { + '--avatar-bg': + variant === 'filled' + ? colorKey + ? `var(--mantine-color-${colorKey}-filled)` + : 'var(--mantine-primary-color-filled)' + : variant === 'light' + ? colorKey + ? `var(--mantine-color-${colorKey}-light)` + : 'var(--mantine-primary-color-light)' + : undefined, + + '--avatar-color': + variant === 'filled' + ? colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)' + : variant === 'light' + ? colorKey + ? `var(--mantine-color-${colorKey}-light-color)` + : 'var(--mantine-primary-color-light-color)' + : variant === 'white' + ? isNeutralColor || isNeutralPrimaryColor + ? 'var(--mantine-color-black)' + : colorKey + ? `var(--mantine-color-${colorKey}-outline)` + : 'var(--mantine-primary-color-filled)' + : variant === 'outline' || variant === 'transparent' + ? colorKey + ? `var(--mantine-color-${colorKey}-outline)` + : 'var(--mantine-primary-color-filled)' + : undefined, + + '--avatar-bd': + variant === 'outline' + ? colorKey + ? `1px solid var(--mantine-color-${colorKey}-outline)` + : '1px solid var(--mantine-primary-color-filled)' + : undefined, + }, + }; + }, + }), + Badge: Badge.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const isNeutralColor = colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(colorKey); + const isNeutralPrimaryColor = + !colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(theme.primaryColor); + const variant = props.variant ?? 'filled'; + return { + root: { + '--badge-bg': variant === 'filled' && colorKey ? `var(--mantine-color-${colorKey}-filled)` : undefined, + '--badge-color': + variant === 'filled' + ? colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)' + : variant === 'white' + ? isNeutralColor || isNeutralPrimaryColor + ? 'var(--mantine-color-black)' + : undefined + : undefined, + }, + }; + }, + }), + Card: Card.extend({ + defaultProps: { + p: 'xl', + shadow: 'xl', + withBorder: true, + }, + styles: (theme) => { + return { + root: { + backgroundColor: + theme.primaryColor === 'rose' || theme.primaryColor === 'green' + ? 'var(--mantine-color-secondary-filled)' + : undefined, + }, + }; + }, + }), + Indicator: Indicator.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + return { + root: { + '--indicator-text-color': colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)', + }, + }; + }, + }), + ThemeIcon: ThemeIcon.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + const isNeutralColor = colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(colorKey); + const isNeutralPrimaryColor = + !colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(theme.primaryColor); + + const variant = props.variant ?? 'filled'; + return { + root: { + '--ti-color': + variant === 'filled' + ? colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)' + : variant === 'white' + ? isNeutralColor || isNeutralPrimaryColor + ? 'var(--mantine-color-black)' + : undefined + : undefined, + }, + }; + }, + }), + Timeline: Timeline.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + return { + root: { + '--tl-icon-color': colorKey + ? `var(--mantine-color-${colorKey}-contrast)` + : 'var(--mantine-primary-color-contrast)', + }, + }; + }, + }), + Blockquote: Blockquote.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : undefined; + return { + root: { + '--bq-bg-dark': colorKey ? `var(--mantine-color-${colorKey}-light)` : 'var(--mantine-primary-color-light)', + '--bq-bg-light': colorKey ? `var(--mantine-color-${colorKey}-light)` : 'var(--mantine-primary-color-light)', + }, + }; + }, + }), + Mark: Mark.extend({ + vars: (theme, props) => { + const colorKey = props.color && Object.keys(theme.colors).includes(props.color) ? props.color : 'yellow'; + const isNeutralColor = colorKey && ['zinc', 'slate', 'gray', 'neutral', 'stone'].includes(colorKey); + return { + root: { + '--mark-bg-light': `var(--mantine-color-${colorKey}-${isNeutralColor ? '3' : 'filled-hover'})`, + '--mark-bg-dark': `var(--mantine-color-${colorKey}-filled)`, + }, + }; + }, + }), + Paper: Paper.extend({ + defaultProps: { + shadow: 'xl', + }, + }), + }, +}); + +export const theme = shadcnTheme; diff --git a/dashboard/src/types.ts b/dashboard/src/types.ts new file mode 100644 index 00000000..171cf3ea --- /dev/null +++ b/dashboard/src/types.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This file should sync with agentlightning/types/core.py + +export type RolloutStatus = 'queuing' | 'preparing' | 'running' | 'failed' | 'succeeded' | 'cancelled' | 'requeuing'; + +export type AttemptStatus = 'preparing' | 'running' | 'failed' | 'succeeded' | 'unresponsive' | 'timeout'; + +export type RolloutMode = 'train' | 'val' | 'test'; + +export type TaskInput = any; + +export type Timestamp = number; + +/** + * Synced with agentlightning.types.core.Attempt + * with camel case and snake case conversions + */ +export type Attempt = { + rolloutId: string; + attemptId: string; + sequenceId: number; + startTime: Timestamp; + endTime: Timestamp | null; + status: AttemptStatus; + workerId: string | null; + lastHeartbeatTime: Timestamp | null; + metadata: Record | null; +}; + +/** + * Synced with agentlightning.types.core.Rollout + * with camel case and snake case conversions + * + * The `attempt` field is from `AttemptedRollout` class, + * which is the latest attempt of the rollout, if any. + */ +export type Rollout = { + rolloutId: string; + input: TaskInput; + startTime: Timestamp; + endTime: Timestamp | null; + mode: RolloutMode | null; + resourcesId: string | null; + status: RolloutStatus; + config: Record; + metadata: Record | null; + + attempt: Attempt | null; +}; + +export type Resource = Record; + +/** + * Synced with agentlightning.types.resources.Resources + * with camel case and snake case conversions + */ +export type Resources = { + resourcesId: string; + version: number; + createTime: Timestamp; + updateTime: Timestamp; + resources: Record; +}; + +/** + * Paginated response structure matching the Python server's PaginatedResponse + */ +export type PaginatedResponse = { + items: T[]; + limit: number; + offset: number; + total: number; +}; + +/** + * Synced with agentlightning.types.traces.Span + * with camel case and snake case conversions + */ +export type Span = { + rolloutId: string; + attemptId: string; + sequenceId: number; + traceId: string; + spanId: string; + parentId: string | null; + name: string; + status: { status_code: 'UNSET' | 'OK' | 'ERROR'; description: string | null }; + attributes: Record; + startTime: Timestamp; + endTime: Timestamp; + + // The fields below are less frequently used + events: any; + links: any; + context: any; + parent: any; + resource: any; +}; + +// Configs like server connection +export type Config = { + baseUrl: string; // e.g. http://localhost:8000 + autoRefreshMs: number; // polling/refresh interval + theme: 'light' | 'dark'; +}; + +export type ThemePreference = 'light' | 'dark' | 'system'; + +export type ConfigState = { + baseUrl: string; + autoRefreshMs: number; + theme: ThemePreference; +}; diff --git a/dashboard/src/utils/error.ts b/dashboard/src/utils/error.ts new file mode 100644 index 00000000..6adace38 --- /dev/null +++ b/dashboard/src/utils/error.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +/** + * Derive a human readable descriptor for an RTK Query-style error object. + * Normalises common timeout representations to "Timeout" so the UI can surface a friendly label. + */ +export function getErrorDescriptor(error: unknown): string | null { + if (!error || typeof error !== 'object') { + return null; + } + + const record = error as Record; + const statusValue = record.status; + const statusString = typeof statusValue === 'number' || typeof statusValue === 'string' ? String(statusValue) : null; + + const data = record.data; + let detail: string | null = null; + if (data && typeof data === 'object') { + const dataRecord = data as Record; + const rawDetail = dataRecord.detail ?? dataRecord.message; + if (typeof rawDetail === 'string') { + detail = rawDetail; + } + } + + const messageValue = record.message; + const errorValue = record.error; + const nestedErrorMessage = + errorValue && typeof errorValue === 'object' && 'message' in (errorValue as Record) + ? (errorValue as Record).message + : null; + + const directMessage = + typeof messageValue === 'string' + ? messageValue + : typeof nestedErrorMessage === 'string' + ? nestedErrorMessage + : typeof errorValue === 'string' + ? errorValue + : null; + + const looksLikeTimeout = (value: string | null) => + typeof value === 'string' && value.toLowerCase().includes('timeout'); + + if (statusString === '408' || statusString === '504') { + return 'Timeout'; + } + if (looksLikeTimeout(detail)) { + return 'Timeout'; + } + if (looksLikeTimeout(directMessage)) { + return 'Timeout'; + } + if (detail && detail.trim().length > 0) { + return detail; + } + if (directMessage && directMessage.trim().length > 0) { + return directMessage; + } + if (statusString) { + return `status: ${statusString}`; + } + return null; +} diff --git a/dashboard/src/utils/format.ts b/dashboard/src/utils/format.ts new file mode 100644 index 00000000..2f7c8d56 --- /dev/null +++ b/dashboard/src/utils/format.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. + +import dayjs from 'dayjs'; + +export function toTimestamp(value: unknown): number | null { + if (typeof value !== 'number' || Number.isNaN(value)) { + return null; + } + return value; +} + +export function clampToNow(start: number | null, end: number | null): number | null { + if (start == null) { + return null; + } + + const effectiveEnd = end ?? Date.now() / 1000; + const diff = effectiveEnd - start; + return diff > 0 ? diff : 0; +} + +export function formatDateTime(timestamp: number | null): string { + if (timestamp == null) { + return '—'; + } + + return dayjs(timestamp * 1000).format('YYYY-MM-DD HH:mm:ss'); +} + +export function formatDuration(seconds: number | null): string { + if (seconds == null) { + return '—'; + } + + const total = Math.floor(seconds); + if (total <= 0) { + return '—'; + } + + const hours = Math.floor(total / 3600); + const minutes = Math.floor((total % 3600) / 60); + const secs = total % 60; + const parts: string[] = []; + + if (hours > 0) { + parts.push(`${hours}h`); + } + if (minutes > 0) { + parts.push(`${minutes}m`); + } + if (secs > 0 || parts.length === 0) { + parts.push(`${secs}s`); + } + + return parts.join(' '); +} + +export function formatRelativeTime(timestamp: number | null): string { + if (timestamp == null) { + return '—'; + } + + const now = Date.now() / 1000; + const diff = Math.floor(now - timestamp); + + if (diff <= 5) { + return 'Just now'; + } + + return `${formatDuration(diff)} ago`; +} + +export function formatStatusLabel(status: string): string { + return status + .replace(/[_-]/g, ' ') + .toLowerCase() + .replace(/^\w|\s\w/g, (match) => match.toUpperCase()); +} + +export function formatInputPreview(input: unknown, maxLength = 35): { preview: string; full: string } { + if (input === null || input === undefined) { + return { preview: '—', full: '—' }; + } + + const serialized = typeof input === 'string' ? input : safeStringify(input); + if (serialized.length <= maxLength) { + return { preview: serialized, full: serialized }; + } + + return { preview: `${serialized.slice(0, maxLength - 3)}...`, full: serialized }; +} + +export function safeStringify(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +const isPlainObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const toCamelCaseKey = (key: string) => key.replace(/_([a-z])/g, (_match, letter: string) => letter.toUpperCase()); + +const toSnakeCaseKey = (key: string) => + key + .replace(/([A-Z])/g, '_$1') + .replace(/[-\s]+/g, '_') + .toLowerCase(); + +export const camelCaseKeys = (value: T): T => { + if (Array.isArray(value)) { + return value.map((item) => camelCaseKeys(item)) as unknown as T; + } + + if (isPlainObject(value)) { + const entries = Object.entries(value).map(([key, item]) => [toCamelCaseKey(key), camelCaseKeys(item)]); + return Object.fromEntries(entries) as T; + } + + return value; +}; + +export const snakeCaseKeys = (value: T): T => { + if (Array.isArray(value)) { + return value.map((item) => snakeCaseKeys(item)) as unknown as T; + } + + if (isPlainObject(value)) { + const entries = Object.entries(value).map(([key, item]) => [toSnakeCaseKey(key), snakeCaseKeys(item)]); + return Object.fromEntries(entries) as T; + } + + return value; +}; diff --git a/dashboard/src/utils/mock.test.ts b/dashboard/src/utils/mock.test.ts new file mode 100644 index 00000000..65ffbfb0 --- /dev/null +++ b/dashboard/src/utils/mock.test.ts @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft. All rights reserved. + +/** + * Tests for mock API handlers. + * + * These tests verify that the mock implementation behaves consistently with + * the Python server in agentlightning/store/client_server.py. + */ + +import { describe, expect, it } from 'vitest'; +import type { Attempt, Resources, Rollout, Span } from '@/types'; +import { + buildAttemptsResponse, + buildResourcesResponse, + buildRolloutsResponse, + buildSpansResponse, + createMockHandlers, + createResourcesHandlers, + createRolloutsHandlers, + createSpansHandlers, + filterResourcesForParams, + filterRolloutsForParams, + filterSpansForParams, + getResourcesSortValue, + getRolloutSortValue, + getSpanSortValue, + parseNumberParam, + sortAttemptsForParams, + sortResourcesForParams, + sortRolloutsForParams, + sortSpansForParams, +} from './mock'; + +const now = Math.floor(Date.now() / 1000); + +const sampleRollouts: Rollout[] = [ + { + rolloutId: 'ro-001', + input: { task: 'Test 1' }, + status: 'running', + mode: 'train', + resourcesId: 'rs-100', + startTime: now - 1000, + endTime: null, + attempt: { + rolloutId: 'ro-001', + attemptId: 'at-001', + sequenceId: 1, + status: 'running', + startTime: now - 1000, + endTime: null, + workerId: 'worker-1', + lastHeartbeatTime: now - 10, + metadata: null, + }, + config: {}, + metadata: null, + }, + { + rolloutId: 'ro-002', + input: { task: 'Test 2' }, + status: 'succeeded', + mode: 'val', + resourcesId: 'rs-101', + startTime: now - 2000, + endTime: now - 1500, + attempt: { + rolloutId: 'ro-002', + attemptId: 'at-002', + sequenceId: 1, + status: 'succeeded', + startTime: now - 2000, + endTime: now - 1500, + workerId: 'worker-2', + lastHeartbeatTime: now - 1500, + metadata: null, + }, + config: {}, + metadata: null, + }, + { + rolloutId: 'ro-003', + input: { task: 'Test 3' }, + status: 'failed', + mode: 'test', + resourcesId: null, + startTime: now - 3000, + endTime: now - 2500, + attempt: { + rolloutId: 'ro-003', + attemptId: 'at-003', + sequenceId: 1, + status: 'failed', + startTime: now - 3000, + endTime: now - 2500, + workerId: 'worker-3', + lastHeartbeatTime: now - 2500, + metadata: null, + }, + config: {}, + metadata: null, + }, +]; + +const sampleAttempts: Attempt[] = [ + { + rolloutId: 'ro-001', + attemptId: 'at-001', + sequenceId: 1, + status: 'running', + startTime: now - 1000, + endTime: null, + workerId: 'worker-1', + lastHeartbeatTime: now - 10, + metadata: null, + }, + { + rolloutId: 'ro-001', + attemptId: 'at-002', + sequenceId: 2, + status: 'failed', + startTime: now - 900, + endTime: now - 800, + workerId: 'worker-1', + lastHeartbeatTime: now - 800, + metadata: null, + }, + { + rolloutId: 'ro-001', + attemptId: 'at-003', + sequenceId: 3, + status: 'running', + startTime: now - 700, + endTime: null, + workerId: 'worker-2', + lastHeartbeatTime: now - 5, + metadata: null, + }, +]; + +const sampleSpans: Span[] = [ + { + rolloutId: 'ro-001', + attemptId: 'at-001', + sequenceId: 1, + traceId: 'tr-001', + spanId: 'sp-001', + parentId: null, + name: 'Initialize', + status: { status_code: 'OK', description: null }, + attributes: {}, + startTime: now - 1000, + endTime: now - 950, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-001', + attemptId: 'at-001', + sequenceId: 2, + traceId: 'tr-001', + spanId: 'sp-002', + parentId: 'sp-001', + name: 'Process', + status: { status_code: 'OK', description: null }, + attributes: {}, + startTime: now - 950, + endTime: now - 900, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, + { + rolloutId: 'ro-001', + attemptId: 'at-001', + sequenceId: 3, + traceId: 'tr-002', + spanId: 'sp-003', + parentId: null, + name: 'Finalize', + status: { status_code: 'ERROR', description: 'Failed' }, + attributes: {}, + startTime: now - 850, + endTime: now - 800, + events: [], + links: [], + context: {}, + parent: null, + resource: {}, + }, +]; + +const sampleResources: Resources[] = [ + { + resourcesId: 'rs-001', + version: 2, + createTime: now - 400, + updateTime: now - 200, + resources: { config: { learning_rate: 0.01 } }, + }, + { + resourcesId: 'rs-002', + version: 3, + createTime: now - 300, + updateTime: now - 100, + resources: { config: { learning_rate: 0.001 } }, + }, + { + resourcesId: 'rs-003', + version: 1, + createTime: now - 200, + updateTime: now - 50, + resources: { config: { learning_rate: 0.1 } }, + }, +]; + +describe('parseNumberParam', () => { + it('returns default value when param is missing', () => { + const params = new URLSearchParams(); + expect(parseNumberParam(params, 'limit', 10)).toBe(10); + }); + + it('parses valid number', () => { + const params = new URLSearchParams('limit=25'); + expect(parseNumberParam(params, 'limit', 10)).toBe(25); + }); + + it('returns default for invalid number', () => { + const params = new URLSearchParams('limit=invalid'); + expect(parseNumberParam(params, 'limit', 10)).toBe(10); + }); + + it('returns default for NaN', () => { + const params = new URLSearchParams('limit=NaN'); + expect(parseNumberParam(params, 'limit', 10)).toBe(10); + }); + + it('handles negative numbers', () => { + const params = new URLSearchParams('offset=-1'); + expect(parseNumberParam(params, 'offset', 0)).toBe(-1); + }); +}); + +describe('filterRolloutsForParams', () => { + it('returns all rollouts when no filters are provided', () => { + const params = new URLSearchParams(); + const result = filterRolloutsForParams(sampleRollouts, params); + expect(result).toHaveLength(3); + }); + + it('filters by single status', () => { + const params = new URLSearchParams('status_in=running'); + const result = filterRolloutsForParams(sampleRollouts, params); + expect(result).toHaveLength(1); + expect(result[0].status).toBe('running'); + }); + + it('filters by multiple statuses', () => { + const params = new URLSearchParams(); + params.append('status_in', 'running'); + params.append('status_in', 'succeeded'); + const result = filterRolloutsForParams(sampleRollouts, params); + expect(result).toHaveLength(2); + }); + + it('filters by mode', () => { + const params = new URLSearchParams(); + params.append('mode_in', 'train'); + const result = filterRolloutsForParams(sampleRollouts, params); + expect(result).toHaveLength(1); + expect(result[0].mode).toBe('train'); + }); + + it('filters by rollout_id_contains', () => { + const params = new URLSearchParams('rollout_id_contains=002'); + const result = filterRolloutsForParams(sampleRollouts, params); + expect(result).toHaveLength(1); + expect(result[0].rolloutId).toBe('ro-002'); + }); + + it('applies multiple filters together', () => { + const params = new URLSearchParams(); + params.append('status_in', 'running'); + params.append('mode_in', 'train'); + const result = filterRolloutsForParams(sampleRollouts, params); + expect(result).toHaveLength(1); + expect(result[0].rolloutId).toBe('ro-001'); + }); + + it('supports filter_logic=or across filters', () => { + const params = new URLSearchParams(); + params.append('status_in', 'failed'); + params.append('mode_in', 'train'); + params.set('filter_logic', 'or'); + const result = filterRolloutsForParams(sampleRollouts, params); + expect(result.map((r) => r.rolloutId).sort()).toEqual(['ro-001', 'ro-003']); + }); +}); + +describe('getRolloutSortValue', () => { + const rollout = sampleRollouts[0]; + + it('returns rollout_id for rollout_id sort', () => { + expect(getRolloutSortValue(rollout, 'rollout_id')).toBe('ro-001'); + }); + + it('returns status for status sort', () => { + expect(getRolloutSortValue(rollout, 'status')).toBe('running'); + }); + + it('returns mode for mode sort', () => { + expect(getRolloutSortValue(rollout, 'mode')).toBe('train'); + }); + + it('returns empty string for null mode', () => { + const rolloutWithoutMode = { ...rollout, mode: null }; + expect(getRolloutSortValue(rolloutWithoutMode, 'mode')).toBe(''); + }); + + it('returns start_time as default', () => { + expect(getRolloutSortValue(rollout, 'unknown_field')).toBe(now - 1000); + }); + + it('falls back to rollout.startTime when attempt is null', () => { + const rolloutWithoutAttempt = { ...rollout, attempt: null }; + expect(getRolloutSortValue(rolloutWithoutAttempt, 'start_time')).toBe(now - 1000); + }); +}); + +describe('sortRolloutsForParams', () => { + it('sorts by start_time ascending by default', () => { + const result = sortRolloutsForParams(sampleRollouts, null, 'asc'); + expect(result[0].rolloutId).toBe('ro-003'); + expect(result[2].rolloutId).toBe('ro-001'); + }); + + it('sorts by start_time descending', () => { + const result = sortRolloutsForParams(sampleRollouts, 'start_time', 'desc'); + expect(result[0].rolloutId).toBe('ro-001'); + expect(result[2].rolloutId).toBe('ro-003'); + }); + + it('sorts by rollout_id ascending', () => { + const result = sortRolloutsForParams(sampleRollouts, 'rollout_id', 'asc'); + expect(result[0].rolloutId).toBe('ro-001'); + expect(result[2].rolloutId).toBe('ro-003'); + }); + + it('sorts by status', () => { + const result = sortRolloutsForParams(sampleRollouts, 'status', 'asc'); + expect(result[0].status).toBe('failed'); + expect(result[1].status).toBe('running'); + expect(result[2].status).toBe('succeeded'); + }); + + it('handles null values in sorting', () => { + const rolloutsWithNull: Rollout[] = [{ ...sampleRollouts[0], attempt: null, startTime: 0 }, sampleRollouts[1]]; + const result = sortRolloutsForParams(rolloutsWithNull, 'start_time', 'asc'); + expect(result[0].attempt).toBeNull(); + }); +}); + +describe('buildRolloutsResponse', () => { + it('returns all rollouts with default pagination', () => { + const request = new Request('http://localhost/v1/agl/rollouts'); + const response = buildRolloutsResponse(sampleRollouts, request); + expect(response.items).toHaveLength(3); + expect(response.total).toBe(3); + expect(response.limit).toBe(3); + expect(response.offset).toBe(0); + }); + + it('applies limit pagination', () => { + const request = new Request('http://localhost/v1/agl/rollouts?limit=2'); + const response = buildRolloutsResponse(sampleRollouts, request); + expect(response.items).toHaveLength(2); + expect(response.limit).toBe(2); + expect(response.total).toBe(3); + }); + + it('applies offset pagination', () => { + const request = new Request('http://localhost/v1/agl/rollouts?offset=1'); + const response = buildRolloutsResponse(sampleRollouts, request); + expect(response.items).toHaveLength(2); + expect(response.offset).toBe(1); + }); + + it('applies limit and offset together', () => { + const request = new Request('http://localhost/v1/agl/rollouts?limit=1&offset=1'); + const response = buildRolloutsResponse(sampleRollouts, request); + expect(response.items).toHaveLength(1); + expect(response.limit).toBe(1); + expect(response.offset).toBe(1); + }); + + it('handles negative limit (returns all)', () => { + const request = new Request('http://localhost/v1/agl/rollouts?limit=-1'); + const response = buildRolloutsResponse(sampleRollouts, request); + expect(response.items).toHaveLength(3); + expect(response.limit).toBe(3); + }); + + it('applies filtering before pagination', () => { + const request = new Request('http://localhost/v1/agl/rollouts?status_in=running&limit=10'); + const response = buildRolloutsResponse(sampleRollouts, request); + expect(response.items).toHaveLength(1); + expect(response.total).toBe(1); + }); + + it('applies sorting before pagination', () => { + const request = new Request('http://localhost/v1/agl/rollouts?sort_by=rollout_id&sort_order=desc&limit=1'); + const response = buildRolloutsResponse(sampleRollouts, request); + const items = response.items as Array>; + expect(items[0].rollout_id).toBe('ro-003'); + }); + + it('uses snake_case keys in response', () => { + const request = new Request('http://localhost/v1/agl/rollouts'); + const response = buildRolloutsResponse(sampleRollouts, request); + const items = response.items as Array>; + expect(items[0]).toHaveProperty('rollout_id'); + expect(items[0]).toHaveProperty('start_time'); + expect(items[0]).not.toHaveProperty('rolloutId'); + }); +}); + +describe('sortAttemptsForParams', () => { + it('sorts by sequence_id ascending by default', () => { + const result = sortAttemptsForParams(sampleAttempts, null, 'asc'); + expect(result[0].sequenceId).toBe(1); + expect(result[2].sequenceId).toBe(3); + }); + + it('sorts by sequence_id descending', () => { + const result = sortAttemptsForParams(sampleAttempts, 'sequence_id', 'desc'); + expect(result[0].sequenceId).toBe(3); + expect(result[2].sequenceId).toBe(1); + }); + + it('sorts by start_time', () => { + const result = sortAttemptsForParams(sampleAttempts, 'start_time', 'asc'); + expect(result[0].startTime).toBe(now - 1000); + expect(result[2].startTime).toBe(now - 700); + }); +}); + +describe('buildAttemptsResponse', () => { + it('returns all attempts with default pagination', () => { + const request = new Request('http://localhost/v1/agl/rollouts/ro-001/attempts'); + const response = buildAttemptsResponse(sampleAttempts, request); + expect(response.items).toHaveLength(3); + expect(response.total).toBe(3); + }); + + it('applies pagination correctly', () => { + const request = new Request('http://localhost/v1/agl/rollouts/ro-001/attempts?limit=2&offset=1'); + const response = buildAttemptsResponse(sampleAttempts, request); + expect(response.items).toHaveLength(2); + expect(response.offset).toBe(1); + }); + + it('sorts before paginating', () => { + const request = new Request( + 'http://localhost/v1/agl/rollouts/ro-001/attempts?sort_by=sequence_id&sort_order=desc&limit=1', + ); + const response = buildAttemptsResponse(sampleAttempts, request); + const items = response.items as Array>; + expect(items[0].sequence_id).toBe(3); + }); +}); + +describe('filterSpansForParams', () => { + it('returns all spans when no filters are provided', () => { + const params = new URLSearchParams(); + const result = filterSpansForParams(sampleSpans, params); + expect(result).toHaveLength(3); + }); + + it('filters by trace_id_contains', () => { + const params = new URLSearchParams('trace_id_contains=tr-001'); + const result = filterSpansForParams(sampleSpans, params); + expect(result).toHaveLength(2); + }); + + it('filters by span_id_contains', () => { + const params = new URLSearchParams('span_id_contains=sp-003'); + const result = filterSpansForParams(sampleSpans, params); + expect(result).toHaveLength(1); + expect(result[0].spanId).toBe('sp-003'); + }); + + it('filters by name_contains (case insensitive)', () => { + const params = new URLSearchParams('name_contains=INIT'); + const result = filterSpansForParams(sampleSpans, params); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Initialize'); + }); + + it('applies multiple filters together', () => { + const params = new URLSearchParams(); + params.set('trace_id_contains', 'tr-001'); + params.set('name_contains', 'Process'); + const result = filterSpansForParams(sampleSpans, params); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Process'); + }); + + it('supports filter_logic=or for spans', () => { + const params = new URLSearchParams(); + params.set('trace_id_contains', 'tr-001'); + params.set('name_contains', 'Finalize'); + params.set('filter_logic', 'or'); + const result = filterSpansForParams(sampleSpans, params); + expect(result).toHaveLength(3); + }); +}); + +describe('getSpanSortValue', () => { + const span = sampleSpans[0]; + + it('returns trace_id for trace_id sort', () => { + expect(getSpanSortValue(span, 'trace_id')).toBe('tr-001'); + }); + + it('returns span_id for span_id sort', () => { + expect(getSpanSortValue(span, 'span_id')).toBe('sp-001'); + }); + + it('returns parent_id for parent_id sort', () => { + expect(getSpanSortValue(sampleSpans[1], 'parent_id')).toBe('sp-001'); + }); + + it('returns empty string for null parent_id', () => { + expect(getSpanSortValue(span, 'parent_id')).toBe(''); + }); + + it('returns name for name sort', () => { + expect(getSpanSortValue(span, 'name')).toBe('Initialize'); + }); + + it('returns status_code for status_code sort', () => { + expect(getSpanSortValue(span, 'status_code')).toBe('OK'); + }); + + it('calculates duration', () => { + const duration = getSpanSortValue(span, 'duration'); + expect(duration).toBe(50); + }); + + it('returns null duration when times are missing', () => { + const spanWithoutEnd: Span = { ...span, endTime: now - 950 }; + expect(getSpanSortValue(spanWithoutEnd, 'duration')).toBe(50); + }); + + it('returns start_time as default', () => { + expect(getSpanSortValue(span, 'unknown_field')).toBe(now - 1000); + }); +}); + +describe('sortSpansForParams', () => { + it('sorts by start_time ascending by default', () => { + const result = sortSpansForParams(sampleSpans, null, 'asc'); + expect(result[0].spanId).toBe('sp-001'); + expect(result[2].spanId).toBe('sp-003'); + }); + + it('sorts by name', () => { + const result = sortSpansForParams(sampleSpans, 'name', 'asc'); + expect(result[0].name).toBe('Finalize'); + expect(result[2].name).toBe('Process'); + }); + + it('sorts by duration', () => { + const result = sortSpansForParams(sampleSpans, 'duration', 'desc'); + // All spans have duration of 50, so we just check that sorting works + expect(result[0].endTime! - result[0].startTime!).toBe(50); + }); +}); + +describe('buildSpansResponse', () => { + const spansByAttempt = { + 'ro-001:at-001': sampleSpans, + }; + + it('returns empty when rollout_id is missing', () => { + const request = new Request('http://localhost/v1/agl/spans'); + const response = buildSpansResponse(spansByAttempt, request); + expect(response.items).toHaveLength(0); + expect(response.total).toBe(0); + }); + + it('returns spans for rollout and attempt', () => { + const request = new Request('http://localhost/v1/agl/spans?rollout_id=ro-001&attempt_id=at-001'); + const response = buildSpansResponse(spansByAttempt, request); + expect(response.items).toHaveLength(3); + expect(response.total).toBe(3); + }); + + it('handles missing attempt_id', () => { + const request = new Request('http://localhost/v1/agl/spans?rollout_id=ro-001'); + const response = buildSpansResponse(spansByAttempt, request); + expect(response.items).toHaveLength(0); + }); + + it('applies filtering', () => { + const request = new Request( + 'http://localhost/v1/agl/spans?rollout_id=ro-001&attempt_id=at-001&trace_id_contains=tr-001', + ); + const response = buildSpansResponse(spansByAttempt, request); + expect(response.items).toHaveLength(2); + }); + + it('applies sorting and pagination', () => { + const request = new Request( + 'http://localhost/v1/agl/spans?rollout_id=ro-001&attempt_id=at-001&sort_by=name&limit=2', + ); + const response = buildSpansResponse(spansByAttempt, request); + expect(response.items).toHaveLength(2); + const items = response.items as Span[]; + expect(items[0].name).toBe('Finalize'); + }); + + it('supports filter_logic=or in responses', () => { + const request = new Request( + 'http://localhost/v1/agl/spans?rollout_id=ro-001&attempt_id=at-001&trace_id_contains=tr-001&name_contains=Finalize&filter_logic=or', + ); + const response = buildSpansResponse(spansByAttempt, request); + expect(response.items).toHaveLength(3); + }); +}); + +describe('filterResourcesForParams', () => { + it('returns all resources when no filter is applied', () => { + const params = new URLSearchParams(); + const result = filterResourcesForParams(sampleResources, params); + expect(result).toHaveLength(3); + }); + + it('filters resources by resources_id_contains', () => { + const params = new URLSearchParams('resources_id_contains=002'); + const result = filterResourcesForParams(sampleResources, params); + expect(result).toHaveLength(1); + expect(result[0].resourcesId).toBe('rs-002'); + }); +}); + +describe('getResourcesSortValue', () => { + const resource = sampleResources[0]; + + it('returns resources_id value', () => { + expect(getResourcesSortValue(resource, 'resources_id')).toBe('rs-001'); + }); + + it('returns version value', () => { + expect(getResourcesSortValue(resource, 'version')).toBe(2); + }); + + it('returns create_time value', () => { + expect(getResourcesSortValue(resource, 'create_time')).toBe(now - 400); + }); + + it('returns update_time for unknown sort keys', () => { + expect(getResourcesSortValue(resource, 'unknown')).toBe(now - 200); + }); +}); + +describe('sortResourcesForParams', () => { + it('sorts by update_time ascending by default', () => { + const result = sortResourcesForParams(sampleResources, null, 'asc'); + expect(result[0].resourcesId).toBe('rs-001'); + expect(result[2].resourcesId).toBe('rs-003'); + }); + + it('sorts by update_time descending', () => { + const result = sortResourcesForParams(sampleResources, 'update_time', 'desc'); + expect(result[0].resourcesId).toBe('rs-003'); + expect(result[2].resourcesId).toBe('rs-001'); + }); + + it('sorts by version', () => { + const result = sortResourcesForParams(sampleResources, 'version', 'asc'); + expect(result[0].version).toBe(1); + expect(result[2].version).toBe(3); + }); +}); + +describe('buildResourcesResponse', () => { + it('returns paginated resources with defaults', () => { + const request = new Request('http://localhost/v1/agl/resources'); + const response = buildResourcesResponse(sampleResources, request); + expect(response.items).toHaveLength(3); + expect(response.offset).toBe(0); + expect(response.limit).toBe(3); + expect(response.total).toBe(3); + }); + + it('applies filter before pagination', () => { + const request = new Request('http://localhost/v1/agl/resources?resources_id_contains=003'); + const response = buildResourcesResponse(sampleResources, request); + expect(response.items).toHaveLength(1); + const items = response.items as Array>; + expect(items[0].resources_id).toBe('rs-003'); + }); + + it('applies sort order and pagination parameters', () => { + const request = new Request('http://localhost/v1/agl/resources?sort_by=version&sort_order=desc&limit=1&offset=1'); + const response = buildResourcesResponse(sampleResources, request); + expect(response.items).toHaveLength(1); + expect(response.offset).toBe(1); + const items = response.items as Array>; + expect(items[0].version).toBe(2); + }); +}); + +describe('createResourcesHandlers', () => { + it('creates handler for resources endpoint', () => { + const handlers = createResourcesHandlers(sampleResources); + expect(handlers).toHaveLength(1); + expect(handlers[0].info.header).toContain('GET'); + }); +}); + +describe('createRolloutsHandlers', () => { + it('creates handlers that return correct rollout data', async () => { + const attemptsByRollout = { 'ro-001': sampleAttempts }; + const handlers = createRolloutsHandlers(sampleRollouts, attemptsByRollout); + + expect(handlers).toHaveLength(2); + expect(handlers[0].info.header).toContain('GET'); + expect(handlers[1].info.header).toContain('GET'); + }); +}); + +describe('createSpansHandlers', () => { + it('creates handler for spans endpoint', () => { + const spansByAttempt = { 'ro-001:at-001': sampleSpans }; + const handlers = createSpansHandlers(spansByAttempt); + + expect(handlers).toHaveLength(1); + expect(handlers[0].info.header).toContain('GET'); + }); +}); + +describe('createMockHandlers', () => { + it('creates combined handlers for rollouts and attempts', () => { + const attemptsByRollout = { 'ro-001': sampleAttempts }; + const handlers = createMockHandlers(sampleRollouts, attemptsByRollout); + + expect(handlers).toHaveLength(2); + }); + + it('includes spans handlers when provided', () => { + const attemptsByRollout = { 'ro-001': sampleAttempts }; + const spansByAttempt = { 'ro-001:at-001': sampleSpans }; + const handlers = createMockHandlers(sampleRollouts, attemptsByRollout, spansByAttempt); + + expect(handlers).toHaveLength(3); + }); + + it('works without spans handlers', () => { + const attemptsByRollout = { 'ro-001': sampleAttempts }; + const handlers = createMockHandlers(sampleRollouts, attemptsByRollout); + + expect(handlers).toHaveLength(2); + }); +}); diff --git a/dashboard/src/utils/mock.ts b/dashboard/src/utils/mock.ts new file mode 100644 index 00000000..9dfdc171 --- /dev/null +++ b/dashboard/src/utils/mock.ts @@ -0,0 +1,511 @@ +// Copyright (c) Microsoft. All rights reserved. + +/** + * Mock API handlers for testing and Storybook. + * + * This module provides utilities for creating MSW (Mock Service Worker) handlers + * that simulate the Agent Lightning Store API. The implementation is based on the + * actual Python server in agentlightning/store/client_server.py. + * The mock covers a useful subset of the server's behavior; when functionality + * is missing compared to the Python implementation it is an intentional tradeoff + * aimed at keeping the mocks light for UI tests. + * + * @module mock + */ + +import { delay, http, HttpResponse } from 'msw'; +import type { Attempt, Resources, Rollout, Span } from '@/types'; +import { snakeCaseKeys } from './format'; + +/** + * Parse a numeric query parameter with a default value. + * Returns the parsed number or the default if parsing fails. + */ +export function parseNumberParam(params: URLSearchParams, key: string, defaultValue: number): number { + const raw = params.get(key); + if (raw == null) { + return defaultValue; + } + const value = Number(raw); + if (!Number.isFinite(value)) { + return defaultValue; + } + return value; +} + +/** + * Filter rollouts based on query parameters. + * Supports: status_in, mode_in, rollout_id_contains + */ +export function filterRolloutsForParams(rollouts: Rollout[], params: URLSearchParams): Rollout[] { + const statusFilters = params.getAll('status_in'); + const modeFilters = params.getAll('mode_in'); + const rolloutIdContains = params.get('rollout_id_contains'); + const filterLogic = params.get('filter_logic') === 'or' ? 'or' : 'and'; + + return rollouts.filter((rollout) => { + const checks: boolean[] = []; + if (statusFilters.length > 0) { + checks.push(statusFilters.includes(rollout.status)); + } + if (modeFilters.length > 0) { + checks.push(rollout.mode != null && modeFilters.includes(rollout.mode)); + } + if (rolloutIdContains) { + checks.push(rollout.rolloutId.includes(rolloutIdContains)); + } + if (checks.length === 0) { + return true; + } + return filterLogic === 'or' ? checks.some(Boolean) : checks.every(Boolean); + }); +} + +/** + * Get the sort value for a rollout based on the sort_by field. + */ +export function getRolloutSortValue(rollout: Rollout, sortBy: string): string | number | null { + switch (sortBy) { + case 'rollout_id': + return rollout.rolloutId; + case 'status': + return rollout.status; + case 'mode': + return rollout.mode ?? ''; + case 'start_time': + default: + return rollout.attempt?.startTime ?? rollout.startTime ?? null; + } +} + +/** + * Sort rollouts based on query parameters. + * Default sort_by is 'start_time', default sort_order is 'asc'. + */ +export function sortRolloutsForParams( + rollouts: Rollout[], + sortBy: string | null, + sortOrder: 'asc' | 'desc', +): Rollout[] { + const resolvedSortBy = sortBy ?? 'start_time'; + const sorted = [...rollouts].sort((a, b) => { + const aValue = getRolloutSortValue(a, resolvedSortBy); + const bValue = getRolloutSortValue(b, resolvedSortBy); + if (aValue === bValue) { + return 0; + } + if (aValue == null) { + return -1; + } + if (bValue == null) { + return 1; + } + if (typeof aValue === 'number' && typeof bValue === 'number') { + return aValue - bValue; + } + return String(aValue).localeCompare(String(bValue)); + }); + + if (sortOrder === 'desc') { + sorted.reverse(); + } + + return sorted; +} + +/** + * Build a paginated rollouts response matching the Python server's format. + * Applies filtering, sorting, and pagination based on query parameters. + */ +export function buildRolloutsResponse(rollouts: Rollout[], request: Request): Record { + const url = new URL(request.url); + const params = url.searchParams; + const filtered = filterRolloutsForParams(rollouts, params); + const sortBy = params.get('sort_by'); + const sortOrder = params.get('sort_order') === 'desc' ? 'desc' : 'asc'; + const sorted = sortRolloutsForParams(filtered, sortBy, sortOrder); + const limitParam = parseNumberParam(params, 'limit', sorted.length); + const offsetParam = parseNumberParam(params, 'offset', 0); + const effectiveLimit = limitParam < 0 ? sorted.length : limitParam; + const offset = offsetParam < 0 ? 0 : offsetParam; + const paginated = effectiveLimit >= 0 ? sorted.slice(offset, offset + effectiveLimit) : [...sorted]; + + return snakeCaseKeys({ + items: paginated, + limit: effectiveLimit, + offset, + total: filtered.length, + }); +} + +/** + * Sort attempts based on query parameters. + * Default sort_by is 'sequence_id', default sort_order is 'asc'. + */ +export function sortAttemptsForParams( + attempts: Attempt[], + sortBy: string | null, + sortOrder: 'asc' | 'desc', +): Attempt[] { + const sorted = [...attempts]; + const resolvedSortBy = sortBy ?? 'sequence_id'; + sorted.sort((a, b) => { + if (resolvedSortBy === 'start_time') { + return a.startTime - b.startTime; + } + return a.sequenceId - b.sequenceId; + }); + if (sortOrder === 'desc') { + sorted.reverse(); + } + return sorted; +} + +/** + * Build a paginated attempts response matching the Python server's format. + * Applies sorting and pagination based on query parameters. + */ +export function buildAttemptsResponse(attempts: Attempt[], request: Request): Record { + const url = new URL(request.url); + const params = url.searchParams; + const sortBy = params.get('sort_by'); + const sortOrder = params.get('sort_order') === 'desc' ? 'desc' : 'asc'; + const sorted = sortAttemptsForParams(attempts, sortBy, sortOrder); + const limitParam = parseNumberParam(params, 'limit', sorted.length); + const offsetParam = parseNumberParam(params, 'offset', 0); + const effectiveLimit = limitParam < 0 ? sorted.length : limitParam; + const offset = offsetParam < 0 ? 0 : offsetParam; + const paginated = effectiveLimit >= 0 ? sorted.slice(offset, offset + effectiveLimit) : [...sorted]; + + return snakeCaseKeys({ + items: paginated, + limit: effectiveLimit, + offset, + total: attempts.length, + }); +} + +/** + * Filter spans based on query parameters. + * Supports: trace_id_contains, span_id_contains, name_contains + */ +export function filterSpansForParams(spans: Span[], params: URLSearchParams): Span[] { + const traceContains = params.get('trace_id_contains'); + const spanContains = params.get('span_id_contains'); + const nameContains = params.get('name_contains'); + const filterLogic = params.get('filter_logic') === 'or' ? 'or' : 'and'; + + return spans.filter((span) => { + const checks: boolean[] = []; + if (traceContains) { + checks.push(span.traceId.includes(traceContains)); + } + if (spanContains) { + checks.push(span.spanId.includes(spanContains)); + } + if (nameContains) { + checks.push(span.name.toLowerCase().includes(nameContains.toLowerCase())); + } + if (checks.length === 0) { + return true; + } + return filterLogic === 'or' ? checks.some(Boolean) : checks.every(Boolean); + }); +} + +/** + * Get the sort value for a span based on the sort_by field. + */ +export function getSpanSortValue(span: Span, sortBy: string): string | number | null { + switch (sortBy) { + case 'trace_id': + return span.traceId; + case 'span_id': + return span.spanId; + case 'parent_id': + return span.parentId ?? ''; + case 'name': + return span.name; + case 'status_code': + return span.status?.status_code ?? ''; + case 'duration': { + if (span.startTime != null && span.endTime != null) { + return span.endTime - span.startTime; + } + return null; + } + case 'start_time': + default: + return span.startTime ?? null; + } +} + +/** + * Sort spans based on query parameters. + * Default sort_by is 'start_time', default sort_order is 'asc'. + */ +export function sortSpansForParams(spans: Span[], sortBy: string | null, sortOrder: 'asc' | 'desc'): Span[] { + const resolvedSortBy = sortBy ?? 'start_time'; + const sorted = [...spans].sort((a, b) => { + const aValue = getSpanSortValue(a, resolvedSortBy); + const bValue = getSpanSortValue(b, resolvedSortBy); + if (aValue === bValue) { + return 0; + } + if (aValue == null) { + return -1; + } + if (bValue == null) { + return 1; + } + if (typeof aValue === 'number' && typeof bValue === 'number') { + return aValue - bValue; + } + return String(aValue).localeCompare(String(bValue)); + }); + if (sortOrder === 'desc') { + sorted.reverse(); + } + return sorted; +} + +/** + * Build a paginated spans response matching the Python server's format. + * Applies filtering, sorting, and pagination based on query parameters. + */ +export function buildSpansResponse(spansByAttempt: Record, request: Request): Record { + const url = new URL(request.url); + const params = url.searchParams; + const rolloutId = params.get('rollout_id'); + + if (!rolloutId) { + return snakeCaseKeys({ items: [], limit: 0, offset: 0, total: 0 }); + } + + const attemptId = params.get('attempt_id'); + const key = attemptId ? `${rolloutId}:${attemptId}` : rolloutId; + const spans = spansByAttempt[key] ?? []; + + const filtered = filterSpansForParams(spans, params); + const sortBy = params.get('sort_by'); + const sortOrder = params.get('sort_order') === 'desc' ? 'desc' : 'asc'; + const sorted = sortSpansForParams(filtered, sortBy, sortOrder); + const limitParam = parseNumberParam(params, 'limit', sorted.length); + const offsetParam = parseNumberParam(params, 'offset', 0); + const effectiveLimit = limitParam < 0 ? sorted.length : limitParam; + const offset = offsetParam < 0 ? 0 : offsetParam; + const paginated = effectiveLimit >= 0 ? sorted.slice(offset, offset + effectiveLimit) : [...sorted]; + + return snakeCaseKeys({ + items: paginated, + limit: effectiveLimit, + offset, + total: filtered.length, + }); +} + +/** + * Create MSW handlers for rollouts and attempts endpoints. + * + * @param rollouts - Array of rollout objects to serve + * @param attemptsByRollout - Map of rollout IDs to their attempts + * @returns Array of MSW request handlers + * + * @example + * ```ts + * const handlers = createRolloutsHandlers(sampleRollouts, attemptsByRollout); + * ``` + */ +export function createRolloutsHandlers(rollouts: Rollout[], attemptsByRollout: Record) { + return [ + http.get('*/v1/agl/rollouts', ({ request }) => HttpResponse.json(buildRolloutsResponse(rollouts, request))), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', ({ params, request }) => { + const rolloutId = params.rolloutId as string; + const attempts = attemptsByRollout[rolloutId] ?? []; + return HttpResponse.json(buildAttemptsResponse(attempts, request)); + }), + ]; +} + +/** + * Create MSW handlers for spans endpoints. + * + * @param spansByAttempt - Map of "rolloutId:attemptId" to their spans + * @returns Array of MSW request handlers + * + * @example + * ```ts + * const handlers = createSpansHandlers({ 'ro-1:at-1': [span1, span2] }); + * ``` + */ +export function createSpansHandlers(spansByAttempt: Record) { + return [http.get('*/v1/agl/spans', ({ request }) => HttpResponse.json(buildSpansResponse(spansByAttempt, request)))]; +} + +/** + * Filter resources based on query parameters. + * Supports: resources_id_contains + */ +export function filterResourcesForParams(resources: Resources[], params: URLSearchParams): Resources[] { + const resourcesIdContains = params.get('resources_id_contains'); + + return resources.filter((resource) => { + if (resourcesIdContains && !resource.resourcesId.includes(resourcesIdContains)) { + return false; + } + return true; + }); +} + +/** + * Get the sort value for resources based on the sort_by field. + */ +export function getResourcesSortValue(resource: Resources, sortBy: string): string | number | null { + switch (sortBy) { + case 'resources_id': + return resource.resourcesId; + case 'version': + return resource.version; + case 'create_time': + return resource.createTime; + case 'update_time': + default: + return resource.updateTime; + } +} + +/** + * Sort resources based on query parameters. + * Default sort_by is 'update_time', default sort_order is 'desc'. + */ +export function sortResourcesForParams( + resources: Resources[], + sortBy: string | null, + sortOrder: 'asc' | 'desc', +): Resources[] { + const resolvedSortBy = sortBy ?? 'update_time'; + const sorted = [...resources].sort((a, b) => { + const aValue = getResourcesSortValue(a, resolvedSortBy); + const bValue = getResourcesSortValue(b, resolvedSortBy); + if (aValue === bValue) { + return 0; + } + if (aValue == null) { + return -1; + } + if (bValue == null) { + return 1; + } + if (typeof aValue === 'number' && typeof bValue === 'number') { + return aValue - bValue; + } + return String(aValue).localeCompare(String(bValue)); + }); + + if (sortOrder === 'desc') { + sorted.reverse(); + } + + return sorted; +} + +/** + * Build a paginated resources response matching the Python server's format. + * Applies filtering, sorting, and pagination based on query parameters. + */ +export function buildResourcesResponse(resources: Resources[], request: Request): Record { + const url = new URL(request.url); + const params = url.searchParams; + const filtered = filterResourcesForParams(resources, params); + const sortBy = params.get('sort_by'); + const sortOrder = params.get('sort_order') === 'desc' ? 'desc' : 'asc'; + const sorted = sortResourcesForParams(filtered, sortBy, sortOrder); + const limitParam = parseNumberParam(params, 'limit', sorted.length); + const offsetParam = parseNumberParam(params, 'offset', 0); + const effectiveLimit = limitParam < 0 ? sorted.length : limitParam; + const offset = offsetParam < 0 ? 0 : offsetParam; + const paginated = effectiveLimit >= 0 ? sorted.slice(offset, offset + effectiveLimit) : [...sorted]; + + return snakeCaseKeys({ + items: paginated, + limit: effectiveLimit, + offset, + total: filtered.length, + }); +} + +/** + * Create MSW handlers for resources endpoints. + * + * @param resources - Array of resources objects to serve + * @returns Array of MSW request handlers + * + * @example + * ```ts + * const handlers = createResourcesHandlers(sampleResources); + * ``` + */ +export function createResourcesHandlers(resources: Resources[]) { + return [ + http.get('*/v1/agl/resources', ({ request }) => HttpResponse.json(buildResourcesResponse(resources, request))), + ]; +} + +/** + * Create complete MSW handlers for rollouts, attempts, and spans. + * + * This is a convenience function that combines createRolloutsHandlers and + * createSpansHandlers for stories that need both. + * + * @param rollouts - Array of rollout objects to serve + * @param attemptsByRollout - Map of rollout IDs to their attempts + * @param spansByAttempt - Map of "rolloutId:attemptId" to their spans + * @param delayMs - Optional delay in milliseconds to add before each response (for testing loading states) + * @returns Array of MSW request handlers + * + * @example + * ```ts + * // Create handlers without delay + * const handlers = createMockHandlers(rollouts, attempts, spans); + * + * // Create handlers with 800ms delay to test loading states + * const loadingHandlers = createMockHandlers(rollouts, attempts, spans, 800); + * ``` + */ +export function createMockHandlers( + rollouts: Rollout[], + attemptsByRollout: Record, + spansByAttempt?: Record, + delayMs?: number, +) { + // If no delay, use synchronous handlers + if (!delayMs) { + const handlers = createRolloutsHandlers(rollouts, attemptsByRollout); + if (spansByAttempt) { + handlers.push(...createSpansHandlers(spansByAttempt)); + } + return handlers; + } + + // Create delayed handlers + return [ + http.get('*/v1/agl/rollouts', async ({ request }) => { + await delay(delayMs); + return HttpResponse.json(buildRolloutsResponse(rollouts, request)); + }), + http.get('*/v1/agl/rollouts/:rolloutId/attempts', async ({ params, request }) => { + await delay(delayMs); + const rolloutId = params.rolloutId as string; + const attempts = attemptsByRollout[rolloutId] ?? []; + return HttpResponse.json(buildAttemptsResponse(attempts, request)); + }), + ...(spansByAttempt + ? [ + http.get('*/v1/agl/spans', async ({ request }) => { + await delay(delayMs); + return HttpResponse.json(buildSpansResponse(spansByAttempt, request)); + }), + ] + : []), + ]; +} diff --git a/dashboard/src/utils/table.ts b/dashboard/src/utils/table.ts new file mode 100644 index 00000000..62ecfe77 --- /dev/null +++ b/dashboard/src/utils/table.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. + +import type { DataTableColumn } from 'mantine-datatable'; + +/** + * Configuration for column visibility based on screen width + */ +export type ColumnVisibilityConfig = + | { + minWidth: number; + priority: number; + fixedWidth?: never; + } + | { + fixedWidth: number; + priority: number; + minWidth?: never; + }; + +/** + * Compare two records for sorting purposes + * Handles null/undefined values and both string and number comparisons + */ +export function compareRecords(a: T, b: T, key: K): number { + const valueA = a[key]; + const valueB = b[key]; + + if (valueA === valueB) { + return 0; + } + + if (valueA === null || valueA === undefined) { + return 1; + } + + if (valueB === null || valueB === undefined) { + return -1; + } + + if (typeof valueA === 'number' && typeof valueB === 'number') { + return valueA - valueB; + } + + return String(valueA).localeCompare(String(valueB)); +} + +/** + * Create responsive columns based on container width + * Columns with priority 0 are always shown, others are shown based on available space + */ +const EM_IN_PIXELS = 16; + +function resolveWidth(config: ColumnVisibilityConfig): { widthEm: number; fixed: boolean } { + if ('fixedWidth' in config && typeof config.fixedWidth === 'number') { + return { widthEm: config.fixedWidth, fixed: true }; + } + return { widthEm: config.minWidth, fixed: false }; +} + +export function createResponsiveColumns( + columns: DataTableColumn[], + containerWidth: number, + columnVisibilityConfig: Record, +): DataTableColumn[] { + const measuredWidth = containerWidth ? Math.max(containerWidth - 48, 0) : Number.POSITIVE_INFINITY; + + const columnEntries = columns.map((column, index) => { + const accessorKey = String(column.accessor); + const config = + columnVisibilityConfig[accessorKey] ?? + ({ + minWidth: 10, + priority: 3, + } satisfies ColumnVisibilityConfig); + const { widthEm, fixed } = resolveWidth(config); + return { + column, + index, + accessorKey, + ...config, + widthEm, + widthPx: widthEm * EM_IN_PIXELS, + fixed, + }; + }); + + const sortedByPriority = columnEntries + .slice() + .sort((a, b) => (a.priority === b.priority ? a.index - b.index : a.priority - b.priority)); + + const visibleColumnIndices = new Set(); + let usedWidth = 0; + + // First pass: add all priority 0 columns + sortedByPriority.forEach((entry) => { + if (entry.priority === 0) { + visibleColumnIndices.add(entry.index); + usedWidth += entry.widthPx; + } + }); + + // Second pass: add remaining columns that fit + sortedByPriority.forEach((entry) => { + if (visibleColumnIndices.has(entry.index)) { + return; + } + if (usedWidth + entry.widthPx <= measuredWidth) { + visibleColumnIndices.add(entry.index); + usedWidth += entry.widthPx; + } + }); + + return columnEntries.map(({ column, index, fixed, widthEm }) => { + const columnWidth = `${widthEm}em`; + const existingStyle = (column as any).style ?? {}; + return { + ...column, + width: fixed ? columnWidth : column.width, + style: { + ...existingStyle, + minWidth: columnWidth, + }, + hidden: !visibleColumnIndices.has(index), + }; + }); +} diff --git a/dashboard/src/vite-env.d.ts b/dashboard/src/vite-env.d.ts new file mode 100644 index 00000000..948b8a3e --- /dev/null +++ b/dashboard/src/vite-env.d.ts @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft. All rights reserved. + +/// diff --git a/dashboard/static/mockServiceWorker.js b/dashboard/static/mockServiceWorker.js new file mode 100644 index 00000000..b8931a1a --- /dev/null +++ b/dashboard/static/mockServiceWorker.js @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.11.6'; +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'; +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse'); +const activeClientIds = new Set(); + +addEventListener('install', function () { + self.skipWaiting(); +}); + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()); +}); + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id'); + + if (!clientId || !self.clients) { + return; + } + + const client = await self.clients.get(clientId); + + if (!client) { + return; + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }); + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }); + break; + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }); + break; + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId); + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }); + break; + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId); + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId; + }); + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister(); + } + + break; + } + } +}); + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now(); + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return; + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { + return; + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return; + } + + const requestId = crypto.randomUUID(); + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)); +}); + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event); + const requestCloneForEvents = event.request.clone(); + const response = await getResponse(event, client, requestId, requestInterceptedAt); + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents); + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone(); + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ); + } + + return response; +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId); + + if (activeClientIds.has(event.clientId)) { + return client; + } + + if (client?.frameType === 'top-level') { + return client; + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }); + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible'; + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id); + }); +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone(); + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers); + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept'); + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()); + const filteredValues = values.filter((value) => value !== 'msw/passthrough'); + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')); + } else { + headers.delete('accept'); + } + } + + return fetch(requestClone, { headers }); + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough(); + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough(); + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request); + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ); + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data); + } + + case 'PASSTHROUGH': { + return passthrough(); + } + } + + return passthrough(); +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error); + } + + resolve(event.data); + }; + + client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]); + }); +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error(); + } + + const mockedResponse = new Response(response.body, response); + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }); + + return mockedResponse; +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + }; +} diff --git a/dashboard/test-utils/index.ts b/dashboard/test-utils/index.ts new file mode 100644 index 00000000..9a175703 --- /dev/null +++ b/dashboard/test-utils/index.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. + +import userEvent from '@testing-library/user-event'; + +export * from '@testing-library/react'; +export { render } from './render'; +export { userEvent }; +export { createServerBackedStore, getPythonServerBaseUrl } from './server'; diff --git a/dashboard/test-utils/python-server.py b/dashboard/test-utils/python-server.py new file mode 100644 index 00000000..0c13afa2 --- /dev/null +++ b/dashboard/test-utils/python-server.py @@ -0,0 +1,662 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +Script to inject mock data directly into an InMemoryLightningStore. + +This script creates sample rollouts, attempts, spans, and resources that match +the TypeScript mock data (from dashboard/src/components/RolloutTable.story.tsx) +and injects them directly into the store's private variables for testing. + +Usage: + from dashboard.test_utils.inject_mock_data import inject_mock_data + + store = InMemoryLightningStore() + inject_mock_data(store) +""" + +# pyright: reportPrivateUsage=false + +import argparse +import asyncio +import time +from typing import List + +from agentlightning.store.client_server import LightningStoreServer +from agentlightning.store.memory import InMemoryLightningStore +from agentlightning.types import ( + LLM, + Attempt, + OtelResource, + PromptTemplate, + ResourcesUpdate, + Rollout, + RolloutConfig, + Span, + TraceStatus, +) + + +def inject_mock_data(store: InMemoryLightningStore, now: float | None = None) -> None: + """ + Inject mock data directly into the InMemoryLightningStore. + + Args: + store: The InMemoryLightningStore instance to inject data into + now: The current timestamp (defaults to current time) + """ + if now is None: + now = time.time() + + # Create rollouts matching the TypeScript mock data + # Based on sampleRollouts from dashboard/src/components/RolloutTable.story.tsx + + # Rollout 1: Running + rollout1 = Rollout( + rollout_id="ro-story-001", + input=dict(task="Generate onboarding summary"), + start_time=now - 3200, + end_time=None, + mode="train", + resources_id="rs-story-001", + status="running", + config=RolloutConfig(max_attempts=1), + metadata={"owner": "alice"}, + ) + attempt1 = Attempt( + rollout_id="ro-story-001", + attempt_id="at-story-010", + sequence_id=1, + start_time=now - 3200, + end_time=None, + status="running", + worker_id="worker-east", + last_heartbeat_time=now - 45, + metadata={"info": "Worker is processing"}, + ) + + # Rollout 2: Succeeded + rollout2 = Rollout( + rollout_id="ro-story-002", + # NOTE: input is a string here, not a dict + input="Classify feedback tickets", + start_time=now - 7200, + end_time=now - 5400, + mode="val", + resources_id="rs-story-002", + status="succeeded", + config=RolloutConfig(max_attempts=2, timeout_seconds=86400, unresponsive_seconds=86400 * 2), + metadata={"owner": "bob"}, + ) + attempt2_1 = Attempt( + rollout_id="ro-story-002", + attempt_id="at-story-021", + sequence_id=1, + start_time=now - 7200, + end_time=now - 5400, + status="timeout", + last_heartbeat_time=None, + worker_id=None, + metadata=None, + ) + attempt2_2 = Attempt( + rollout_id="ro-story-002", + attempt_id="at-story-022", + sequence_id=2, + start_time=now - 6200, + end_time=now - 5400, + status="succeeded", + worker_id="worker-north", + last_heartbeat_time=now - 5400, + metadata={"previousAttempt": "at-story-010"}, + ) + + # Rollout 3: Failed + rollout3 = Rollout( + rollout_id="ro-story-003", + input=dict(task="Analyze experiment results"), + start_time=now - 10800, + end_time=now - 9600, + mode="test", + resources_id="rs-story-003", + status="failed", + config=RolloutConfig(max_attempts=3), + metadata={"owner": "carol"}, + ) + attempt3_1 = Attempt( + rollout_id="ro-story-003", + attempt_id="at-story-031", + sequence_id=3, + start_time=now - 10200, + end_time=now - 9600, + status="failed", + worker_id="worker-west", + last_heartbeat_time=now - 9600, + metadata={"reason": "Timeout"}, + ) + attempt3_2 = Attempt( + rollout_id="ro-story-003", + attempt_id="at-story-032", + sequence_id=2, + start_time=now - 9600, + end_time=None, + status="unresponsive", + worker_id="worker-west", + last_heartbeat_time=now - 3600, + metadata={"reason": "Unresponsive"}, + ) + attempt3_3 = Attempt( + rollout_id="ro-story-003", + attempt_id="at-story-033", + sequence_id=3, + start_time=now - 9000, + end_time=now - 3600, + status="failed", + worker_id="worker-west", + ) + + # Rollout 4: Queuing (no attempt yet) + rollout4 = Rollout( + rollout_id="ro-story-004", + input=dict(task="Evaluate prompt variants"), + start_time=now - 3600, + end_time=None, + mode="train", + resources_id=None, + status="requeuing", + config=RolloutConfig(max_attempts=5, retry_condition=["timeout"]), + metadata={"owner": "dave"}, + ) + + # Rollout 5: Running + rollout5 = Rollout( + rollout_id="ro-story-005", + input=dict(task="Generate quick answers"), + start_time=now - 1800, + end_time=None, + mode="val", + resources_id="rs-story-004", + status="running", + config=RolloutConfig(max_attempts=1), + metadata={"owner": "eva"}, + ) + attempt5 = Attempt( + rollout_id="ro-story-005", + attempt_id="at-story-013", + sequence_id=1, + start_time=now - 1800, + end_time=None, + status="running", + worker_id=None, + last_heartbeat_time=now - 75, + metadata=None, + ) + + # Rollout 6: Cancelled + rollout6 = Rollout( + rollout_id="ro-story-006", + input=dict(task="Compile release notes"), + start_time=now - 9600, + end_time=now - 9000, + mode=None, + resources_id="rs-story-005", + status="cancelled", + config=RolloutConfig(max_attempts=3), + metadata=None, + ) + attempt6 = Attempt( + rollout_id="ro-story-006", + attempt_id="at-story-014", + sequence_id=1, + start_time=now - 9600, + end_time=now - 9000, + status="timeout", + worker_id="worker-south", + last_heartbeat_time=now - 9000, + metadata={"info": "Cancelled by operator"}, + ) + + # Inject rollouts directly into store + store._rollouts["ro-story-001"] = rollout1 + store._rollouts["ro-story-002"] = rollout2 + store._rollouts["ro-story-003"] = rollout3 + store._rollouts["ro-story-004"] = rollout4 + store._rollouts["ro-story-005"] = rollout5 + store._rollouts["ro-story-006"] = rollout6 + + # Inject attempts directly into store + store._attempts["ro-story-001"] = [attempt1] + store._attempts["ro-story-002"] = [attempt2_1, attempt2_2] + store._attempts["ro-story-003"] = [attempt3_1, attempt3_2, attempt3_3] + store._attempts["ro-story-004"] = [] # No attempt for preparing rollout + store._attempts["ro-story-005"] = [attempt5] + store._attempts["ro-story-006"] = [attempt6] + + # Create and inject spans with diverse data + # Spans for ro-story-001 (Running) - Multiple nested spans with ongoing execution + spans_ro1: List[Span] = [ + Span( + rollout_id="ro-story-001", + attempt_id="at-story-010", + sequence_id=1, + trace_id="trace-001-main", + span_id="span-001-root", + parent_id=None, + name="agent_execution", + status=TraceStatus(status_code="OK", description=None), + attributes={"component": "agent", "framework": "autogen", "version": "0.2.0"}, + events=[], + links=[], + start_time=now - 3200, + end_time=now - 3100, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "agent-service"}, schema_url=""), + ), + Span( + rollout_id="ro-story-001", + attempt_id="at-story-010", + sequence_id=2, + trace_id="trace-001-main", + span_id="span-002-llm", + parent_id="span-001-root", + name="llm_call", + status=TraceStatus(status_code="OK", description=None), + attributes={"model": "gpt-4", "temperature": 0.7, "tokens": 150}, + events=[], + links=[], + start_time=now - 3180, + end_time=now - 3120, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "llm-service"}, schema_url=""), + ), + Span( + rollout_id="ro-story-001", + attempt_id="at-story-010", + sequence_id=3, + trace_id="trace-001-main", + span_id="span-003-tool", + parent_id="span-001-root", + name="tool_execution", + status=TraceStatus(status_code="OK", description=None), + attributes={"tool": "web_search", "query": "onboarding summary"}, + events=[], + links=[], + start_time=now - 3140, + end_time=now - 3100, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "tool-service"}, schema_url=""), + ), + ] + + # Spans for ro-story-002 attempt 1 (timeout) - Failed workflow + spans_ro2_a1: List[Span] = [ + Span( + rollout_id="ro-story-002", + attempt_id="at-story-021", + sequence_id=1, + trace_id="trace-002-attempt1", + span_id="span-004-root", + parent_id=None, + name="classification_pipeline", + status=TraceStatus(status_code="ERROR", description="Request timeout"), + attributes={"type": "classification", "model": "bert-classifier", "timeout": True}, + events=[], + links=[], + start_time=now - 7200, + end_time=now - 6800, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "classifier"}, schema_url=""), + ), + ] + + # Spans for ro-story-002 attempt 2 (succeeded) - Successful workflow + spans_ro2_a2: List[Span] = [ + Span( + rollout_id="ro-story-002", + attempt_id="at-story-022", + sequence_id=1, + trace_id="trace-002-attempt2", + span_id="span-005-root", + parent_id=None, + name="classification_pipeline", + status=TraceStatus(status_code="OK", description=None), + attributes={"type": "classification", "model": "bert-classifier", "retry": True}, + events=[], + links=[], + start_time=now - 6200, + end_time=now - 5400, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "classifier"}, schema_url=""), + ), + Span( + rollout_id="ro-story-002", + attempt_id="at-story-022", + sequence_id=2, + trace_id="trace-002-attempt2", + span_id="span-006-preprocess", + parent_id="span-005-root", + name="preprocess_tickets", + status=TraceStatus(status_code="OK", description=None), + attributes={"tickets_count": 50, "duration_ms": 120}, + events=[], + links=[], + start_time=now - 6200, + end_time=now - 6100, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + Span( + rollout_id="ro-story-002", + attempt_id="at-story-022", + sequence_id=3, + trace_id="trace-002-attempt2", + span_id="span-007-classify", + parent_id="span-005-root", + name="run_classifier", + status=TraceStatus(status_code="OK", description=None), + attributes={"batch_size": 10, "accuracy": 0.95}, + events=[], + links=[], + start_time=now - 6000, + end_time=now - 5400, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + ] + + # Spans for ro-story-003 attempt 3 (failed) - Error scenario with nested failures + spans_ro3_a3: List[Span] = [ + Span( + rollout_id="ro-story-003", + attempt_id="at-story-033", + sequence_id=1, + trace_id="trace-003-experiment", + span_id="span-008-root", + parent_id=None, + name="experiment_analysis", + status=TraceStatus(status_code="ERROR", description="Analysis failed"), + attributes={"experiment_id": "exp-123", "timeout_seconds": 300}, + events=[], + links=[], + start_time=now - 9000, + end_time=now - 3600, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "experiment-runner"}, schema_url=""), + ), + Span( + rollout_id="ro-story-003", + attempt_id="at-story-033", + sequence_id=2, + trace_id="trace-003-experiment", + span_id="span-009-fetch", + parent_id="span-008-root", + name="fetch_experiment_data", + status=TraceStatus(status_code="ERROR", description="Connection timeout"), + attributes={"data_source": "database", "retry_count": 3}, + events=[], + links=[], + start_time=now - 9000, + end_time=now - 7000, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + Span( + rollout_id="ro-story-003", + attempt_id="at-story-033", + sequence_id=3, + trace_id="trace-003-experiment", + span_id="span-010-process", + parent_id="span-008-root", + name="process_results", + status=TraceStatus(status_code="UNSET", description=None), + attributes={"stage": "aggregation", "partial_results": True}, + events=[], + links=[], + start_time=now - 6000, + end_time=now - 3600, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + ] + + # Spans for ro-story-005 (Running) - In-progress with mixed statuses + spans_ro5: List[Span] = [ + Span( + rollout_id="ro-story-005", + attempt_id="at-story-013", + sequence_id=1, + trace_id="trace-005-answers", + span_id="span-011-root", + parent_id=None, + name="quick_answer_generation", + status=TraceStatus(status_code="OK", description=None), + attributes={"batch_size": 10, "model": "llama-3"}, + events=[], + links=[], + start_time=now - 1800, + end_time=now - 1500, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "qa-service"}, schema_url=""), + ), + Span( + rollout_id="ro-story-005", + attempt_id="at-story-013", + sequence_id=2, + trace_id="trace-005-answers", + span_id="span-012-llm", + parent_id="span-011-root", + name="llm_inference", + status=TraceStatus(status_code="UNSET", description=None), + attributes={"model": "llama-3-70b", "batch_id": "batch-001"}, + events=[], + links=[], + start_time=now - 1700, + end_time=None, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + Span( + rollout_id="ro-story-005", + attempt_id="at-story-013", + sequence_id=3, + trace_id="trace-005-answers", + span_id="span-013-retrieve", + parent_id="span-011-root", + name="retrieve_context", + status=TraceStatus(status_code="OK", description=None), + attributes={"documents_count": 5, "vector_db": "chroma"}, + events=[], + links=[], + start_time=now - 1780, + end_time=now - 1700, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + ] + + # Spans for ro-story-006 (Cancelled) - Cancelled workflow + spans_ro6: List[Span] = [ + Span( + rollout_id="ro-story-006", + attempt_id="at-story-014", + sequence_id=1, + trace_id="trace-006-release", + span_id="span-014-root", + parent_id=None, + name="compile_release_notes", + status=TraceStatus(status_code="ERROR", description="Operation cancelled by user"), + attributes={"release_version": "v2.0.0", "cancelled": True}, + events=[], + links=[], + start_time=now - 9600, + end_time=now - 9000, + context=None, + parent=None, + resource=OtelResource(attributes={"service.name": "release-automation"}, schema_url=""), + ), + Span( + rollout_id="ro-story-006", + attempt_id="at-story-014", + sequence_id=2, + trace_id="trace-006-release", + span_id="span-015-git", + parent_id="span-014-root", + name="fetch_git_commits", + status=TraceStatus(status_code="OK", description=None), + attributes={"repo": "main", "commits_count": 45}, + events=[], + links=[], + start_time=now - 9600, + end_time=now - 9400, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + Span( + rollout_id="ro-story-006", + attempt_id="at-story-014", + sequence_id=3, + trace_id="trace-006-release", + span_id="span-016-format", + parent_id="span-014-root", + name="format_changelog", + status=TraceStatus(status_code="ERROR", description="Cancelled during formatting"), + attributes={"format": "markdown", "cancelled_at": "50%"}, + events=[], + links=[], + start_time=now - 9300, + end_time=now - 9000, + context=None, + parent=None, + resource=OtelResource(attributes={}, schema_url=""), + ), + ] + + store._spans["ro-story-001"] = spans_ro1 + store._spans["ro-story-002"] = spans_ro2_a1 + spans_ro2_a2 + store._spans["ro-story-003"] = spans_ro3_a3 + store._spans["ro-story-005"] = spans_ro5 + store._spans["ro-story-006"] = spans_ro6 + + # Create and inject resources with diverse types + resource1 = ResourcesUpdate( + resources_id="rs-story-001", + version=1, + create_time=now - 86400, + update_time=now - 86400, + resources={ + "model": LLM( + endpoint="https://api.openai.com/v1", + model="gpt-4", + sampling_parameters={"temperature": 0.7, "max_tokens": 1000}, + ), + }, + ) + + resource2 = ResourcesUpdate( + resources_id="rs-story-002", + version=1, + create_time=now - 86400, + update_time=now - 43200, + resources={ + "prompt_main": PromptTemplate( + template="Classify the following ticket: {ticket}\nCategory:", + engine="f-string", + ), + }, + ) + + resource3 = ResourcesUpdate( + resources_id="rs-story-003", + version=2, + create_time=now - 86400, + update_time=now - 3600, + resources={ + "model": LLM( + endpoint="https://api.anthropic.com/v1", + model="claude-3-sonnet", + sampling_parameters={"temperature": 0.8, "max_tokens": 2048}, + ), + "prompt": PromptTemplate( + template="Analyze experiment results:\n{% for result in results %}\n- {{ result }}\n{% endfor %}", + engine="jinja", + ), + }, + ) + + resource4 = ResourcesUpdate( + resources_id="rs-story-004", + version=1, + create_time=now - 7200, + update_time=now - 7200, + resources={ + "system_prompt": PromptTemplate( + template="You are a helpful assistant that generates quick, concise answers.", + engine="f-string", + ), + "model": LLM( + endpoint="http://localhost:8000/v1", + model="llama-3-70b", + sampling_parameters={"temperature": 0.6, "top_p": 0.95}, + ), + }, + ) + + resource5 = ResourcesUpdate( + resources_id="rs-story-005", + version=1, + create_time=now - 14400, + update_time=now - 14400, + resources={ + "prompt": PromptTemplate( + template="## Release Notes v{{ version }}\n\n### Changes\n{{ changes }}", + engine="jinja", + ), + }, + ) + + store._resources["rs-story-001"] = resource1 + store._resources["rs-story-002"] = resource2 + store._resources["rs-story-003"] = resource3 + store._resources["rs-story-004"] = resource4 + store._resources["rs-story-005"] = resource5 + store._latest_resources_id = "rs-story-005" + + +async def main(): + parser = argparse.ArgumentParser(description="Run a Python server for the LightningStore") + parser.add_argument("--now", type=float, default=time.time(), help="The current timestamp") + args = parser.parse_args() + + store = InMemoryLightningStore() + inject_mock_data(store, now=args.now) + + # Start server + server = LightningStoreServer(store, "127.0.0.1", 8765, "*") + await server.start() + + print(f"Server started at {server.endpoint}") + print("Press Ctrl+C to stop...") + + try: + # Keep server running + await asyncio.Event().wait() + except KeyboardInterrupt: + print("\nStopping server...") + await server.stop() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/dashboard/test-utils/render.tsx b/dashboard/test-utils/render.tsx new file mode 100644 index 00000000..1e82cff1 --- /dev/null +++ b/dashboard/test-utils/render.tsx @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { render as testingLibraryRender } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import { theme } from '../src/theme'; + +export function render(ui: React.ReactNode) { + return testingLibraryRender(ui, { + wrapper: ({ children }: { children: React.ReactNode }) => ( + + {children} + + ), + }); +} diff --git a/dashboard/test-utils/server.ts b/dashboard/test-utils/server.ts new file mode 100644 index 00000000..b4bc6715 --- /dev/null +++ b/dashboard/test-utils/server.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { setBaseUrl } from '@/features/config'; +import { createAppStore, type AppStore } from '@/store'; + +const resolvePythonServerBaseUrl = (): string => { + const host = process.env.VITEST_PYTHON_HOST ?? '127.0.0.1'; + const port = process.env.VITEST_PYTHON_PORT ?? '8765'; + const configured = process.env.VITEST_PYTHON_URL; + const base = configured && configured.trim().length > 0 ? configured : `http://${host}:${port}`; + return base.replace(/\/$/, ''); +}; + +export const getPythonServerBaseUrl = (): string => resolvePythonServerBaseUrl(); + +export const createServerBackedStore = (preloadedState?: Parameters[0]): AppStore => { + const store = createAppStore(preloadedState); + store.dispatch(setBaseUrl(getPythonServerBaseUrl())); + return store; +}; diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 00000000..cc1f82f2 --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "types": ["node", "@testing-library/jest-dom", "vitest/globals"], + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "paths": { + "@/*": ["./src/*"], + "@test-utils": ["./test-utils"] + } + }, + "include": ["src", "public", "test-utils", ".storybook/main.ts", ".storybook/preview.tsx", ".storybook/modes.ts", ".storybook/constants.ts"] +} diff --git a/dashboard/vite.config.mjs b/dashboard/vite.config.mjs new file mode 100644 index 00000000..c954ef06 --- /dev/null +++ b/dashboard/vite.config.mjs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = __dirname; +const appRoot = path.resolve(projectRoot, 'public'); + +export default defineConfig({ + root: appRoot, + plugins: [react(), tsconfigPaths()], + publicDir: path.resolve(projectRoot, 'static'), + server: { + fs: { + allow: [projectRoot], + }, + }, + build: { + outDir: path.resolve(projectRoot, 'dist'), + emptyOutDir: true, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './vitest.setup.mjs', + globalSetup: './vitest.global-setup.mjs', + root: projectRoot, + }, +}); diff --git a/dashboard/vitest.global-setup.mjs b/dashboard/vitest.global-setup.mjs new file mode 100644 index 00000000..d482d7fc --- /dev/null +++ b/dashboard/vitest.global-setup.mjs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { spawn, spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { fileURLToPath } from 'node:url'; + +const serverHost = process.env.VITEST_PYTHON_HOST ?? '127.0.0.1'; +const serverPort = Number.parseInt(process.env.VITEST_PYTHON_PORT ?? '8765', 10); +const serverUrl = + (process.env.VITEST_PYTHON_URL && process.env.VITEST_PYTHON_URL.trim().replace(/\/$/, '')) ?? + `http://${serverHost}:${Number.isFinite(serverPort) ? serverPort : 8765}`; +const serverHealthUrl = `${serverUrl}/v1/agl/health`; + +let pythonServerProcess = null; +let startedByGlobalSetup = false; + +const locatePythonServerPaths = () => { + const candidates = []; + const cwd = process.cwd(); + candidates.push({ workspace: cwd, repo: path.resolve(cwd, '..') }); + + if ( + typeof import.meta !== 'undefined' && + typeof import.meta.url === 'string' && + import.meta.url.startsWith('file:') + ) { + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + candidates.push({ workspace: moduleDir, repo: path.resolve(moduleDir, '..') }); + } + + for (const candidate of candidates) { + const serverScript = path.resolve(candidate.workspace, 'test-utils/python-server.py'); + if (fs.existsSync(serverScript)) { + return { repoRoot: candidate.repo, serverScript }; + } + } + + throw new Error('Unable to locate dashboard/test-utils/python-server.py from Vitest global setup.'); +}; + +const waitForHealth = async (timeoutMs) => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + const controller = new AbortController(); + const abortTimer = setTimeout(() => controller.abort(), 1000); + const response = await fetch(serverHealthUrl, { signal: controller.signal }); + clearTimeout(abortTimer); + if (response.ok) { + return true; + } + } catch { + // Retry until timeout. + } + await sleep(200); + } + return false; +}; + +const resolvePythonRunners = (repoRoot) => { + const runners = []; + const seenCommands = new Set(); + + const pushRunner = (command, args, description, extraEnv = {}) => { + if (seenCommands.has(command)) { + return; + } + seenCommands.add(command); + runners.push({ command, args, description, env: extraEnv }); + }; + + const explicit = process.env.VITEST_PYTHON_BIN?.trim(); + if (explicit) { + pushRunner(explicit, [], explicit); + } + + const hasUv = (() => { + try { + const result = spawnSync('uv', ['--version'], { stdio: 'ignore' }); + return result.status === 0; + } catch { + return false; + } + })(); + + if (hasUv) { + const uvCacheDir = path.resolve(repoRoot, '.uv-cache'); + pushRunner('uv', ['run', '--no-sync'], 'uv run --no-sync', { UV_CACHE_DIR: uvCacheDir }); + } + + const candidates = [process.env.PYTHON, process.env.PYTHON3, 'python3', 'python'].filter( + (candidate) => typeof candidate === 'string' && candidate.length > 0, + ); + + for (const candidate of candidates) { + if (seenCommands.has(candidate)) { + continue; + } + try { + const result = spawnSync(candidate, ['--version'], { stdio: 'ignore' }); + if (result.status === 0) { + pushRunner(candidate, [], candidate); + } + } catch { + // try next candidate + } + } + + if (runners.length === 0) { + throw new Error('Unable to locate a Python interpreter or uv CLI for launching the LightningStore server.'); + } + + return runners; +}; + +const ensurePythonServer = async () => { + if (await waitForHealth(1000)) { + startedByGlobalSetup = false; + return; + } + + const { repoRoot, serverScript } = locatePythonServerPaths(); + const runners = resolvePythonRunners(repoRoot); + const attemptErrors = []; + + const collectOutput = (stream, buffer) => { + if (!stream) { + return; + } + stream.on('data', (chunk) => { + buffer.push(chunk.toString()); + if (process.env.VITEST_DEBUG_PYTHON_SERVER) { + process.stderr.write(chunk); + } + }); + }; + + for (const runner of runners) { + const child = spawn(runner.command, [...runner.args, serverScript], { + cwd: repoRoot, + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + PYTHONUNBUFFERED: '1', + ...runner.env, + }, + }); + + pythonServerProcess = child; + + const recordedStdout = []; + const recordedStderr = []; + collectOutput(child.stdout, recordedStdout); + collectOutput(child.stderr, recordedStderr); + + let exitPayload = null; + const exitPromise = new Promise((resolve) => { + child.on('exit', (code, signal) => { + exitPayload = { code, signal }; + resolve(); + }); + }); + + const ready = await waitForHealth(10000); + if (ready) { + startedByGlobalSetup = true; + return; + } + + const reused = await waitForHealth(3000); + if (reused) { + startedByGlobalSetup = false; + if (child.exitCode == null) { + child.kill('SIGTERM'); + await exitPromise; + } + pythonServerProcess = null; + return; + } + + if (child.exitCode == null) { + child.kill('SIGTERM'); + await exitPromise; + } else { + await exitPromise; + } + + const extra = exitPayload ? ` (code: ${exitPayload.code ?? 'null'}, signal: ${exitPayload.signal ?? 'null'})` : ''; + const stderrLog = recordedStderr.join('').trim(); + const stdoutLog = recordedStdout.join('').trim(); + const message = [ + `[${runner.description}] Failed to start LightningStore server at ${serverUrl}${extra}.`, + stderrLog.length > 0 ? `stderr:\n${stderrLog}` : '', + stdoutLog.length > 0 ? `stdout:\n${stdoutLog}` : '', + ] + .filter(Boolean) + .join('\n\n'); + + attemptErrors.push(message); + } + + throw new Error(attemptErrors.join('\n\n')); +}; + +export default async function setup() { + await ensurePythonServer(); + + return async () => { + if (!startedByGlobalSetup || !pythonServerProcess) { + return; + } + if (pythonServerProcess.exitCode == null) { + pythonServerProcess.kill('SIGTERM'); + await new Promise((resolve) => { + pythonServerProcess?.once('exit', resolve); + }); + } + }; +} diff --git a/dashboard/vitest.setup.mjs b/dashboard/vitest.setup.mjs new file mode 100644 index 00000000..0b86aaed --- /dev/null +++ b/dashboard/vitest.setup.mjs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +import '@testing-library/jest-dom/vitest'; + +import { vi } from 'vitest'; + +const { getComputedStyle } = window; +window.getComputedStyle = (elt) => getComputedStyle(elt); +window.HTMLElement.prototype.scrollIntoView = () => {}; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +window.ResizeObserver = ResizeObserver; diff --git a/scripts/check_python_headers.py b/scripts/check_headers.py similarity index 52% rename from scripts/check_python_headers.py rename to scripts/check_headers.py index a29c6481..ba4b3387 100644 --- a/scripts/check_python_headers.py +++ b/scripts/check_headers.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -"""Utility script to ensure Python files include the required copyright header.""" +"""Ensure tracked source files include the required copyright header.""" from __future__ import annotations @@ -8,12 +8,34 @@ import subprocess import sys from pathlib import Path -REQUIRED_HEADER = "# Copyright (c) Microsoft. All rights reserved." +HEADER_TEXT = "Copyright (c) Microsoft. All rights reserved." REPO_ROOT = Path(__file__).resolve().parent.parent +COMMENT_PREFIX_BY_SUFFIX: dict[str, str] = { + ".py": "#", + ".pyi": "#", + ".pyw": "#", + ".js": "//", + ".jsx": "//", + ".ts": "//", + ".tsx": "//", + ".mjs": "//", + ".mts": "//", + ".cjs": "//", + ".cts": "//", +} +REQUIRED_HEADER_BY_SUFFIX = { + suffix: f"{prefix} {HEADER_TEXT}" if not prefix.endswith(" ") else f"{prefix}{HEADER_TEXT}" + for suffix, prefix in COMMENT_PREFIX_BY_SUFFIX.items() +} -def iter_python_files() -> list[Path]: - """Return a list of tracked Python files respecting .gitignore rules.""" + +def iter_source_files() -> list[Path]: + """Return tracked source files matching supported extensions.""" + if not REQUIRED_HEADER_BY_SUFFIX: + return [] + + pathspecs = [f"*{suffix}" for suffix in sorted(REQUIRED_HEADER_BY_SUFFIX)] result = subprocess.run( [ "git", @@ -21,7 +43,8 @@ def iter_python_files() -> list[Path]: "--cached", "--others", "--exclude-standard", - "*.py", + "--", + *pathspecs, ], capture_output=True, text=True, @@ -35,7 +58,14 @@ def main() -> int: missing_header: list[str] = [] missing_blank_line: list[str] = [] - for file_path in iter_python_files(): + for file_path in iter_source_files(): + expected_header = REQUIRED_HEADER_BY_SUFFIX.get(file_path.suffix.lower()) + if expected_header is None: + continue + + if not file_path.exists(): + continue + try: with file_path.open("r", encoding="utf-8") as file: first_line = file.readline().rstrip("\r\n") @@ -44,7 +74,7 @@ def main() -> int: print(f"Failed to read {file_path}: {exc}", file=sys.stderr) return 1 - if first_line != REQUIRED_HEADER: + if first_line != expected_header: missing_header.append(str(file_path.relative_to(REPO_ROOT))) continue @@ -53,16 +83,14 @@ def main() -> int: missing_blank_line.append(str(file_path.relative_to(REPO_ROOT))) if missing_header: - print("The following Python files are missing the required copyright header:") + print("The following files are missing the required copyright header:") for path in missing_header: print(f" - {path}") - print( - "Run the appropriate script or add the header manually:", - f"\n{REQUIRED_HEADER}", - ) + header_examples = "\n".join(sorted(set(REQUIRED_HEADER_BY_SUFFIX.values()))) + print(f"Run the appropriate script or add the header manually:\n{header_examples}") if missing_blank_line: - print("The following Python files are missing a blank line after the copyright header:") + print("The following files are missing a blank line after the copyright header:") for path in missing_blank_line: print(f" - {path}") print("Ensure there is an empty line separating the header from the rest of the file.")