5 Commits
v0.1.5 ... main

Author SHA1 Message Date
2ec7705b4b update thresholds
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 12:10:45 +01:00
5877d90f07 fix eslint ver
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 11:23:55 +01:00
2b96277bec fix yml
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 11:21:37 +01:00
063d1073de add code analysis step
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 11:19:44 +01:00
d7e144620e add unit tests
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 11:14:24 +01:00
23 changed files with 2788 additions and 20 deletions

View File

@@ -27,6 +27,33 @@ steps:
commands: commands:
- yarn build - yarn build
- name: unit-tests
image: node:25
environment:
NODE_OPTIONS: --no-webstorage
commands:
- yarn test:coverage
- test -f coverage/lcov.info
- name: code-analysis
when:
event:
- push
image: sonarsource/sonar-scanner-cli:latest
commands:
- |
test -f coverage/lcov.info
SONAR_ARGS="-Dsonar.projectKey=$SONAR_PROJECT_KEY -Dsonar.host.url=$SONAR_INSTANCE_URL -Dsonar.token=$SONAR_LOGIN_KEY -Dsonar.sources=src -Dsonar.tests=tests -Dsonar.test.inclusions=tests/**/*.{test,spec}.{ts,tsx,js,jsx} -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info -Dsonar.working.directory=/tmp/.scannerwork"
sonar-scanner $SONAR_ARGS
environment:
SONAR_USER_HOME: /tmp/.sonar
SONAR_PROJECT_KEY:
from_secret: sonar_project_key
SONAR_INSTANCE_URL:
from_secret: sonar_instance_url
SONAR_LOGIN_KEY:
from_secret: sonar_login_key
--- ---
kind: pipeline kind: pipeline
type: docker type: docker

View File

@@ -48,4 +48,14 @@ export default tseslint.config(
'react-refresh/only-export-components': 'off', 'react-refresh/only-export-components': 'off',
}, },
}, },
{
files: ['tests/**/*.{ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.vitest,
},
},
},
); );

View File

@@ -19,6 +19,9 @@
"scripts": { "scripts": {
"clean": "rm -rf dist", "clean": "rm -rf dist",
"build": "yarn clean && vite build && tsc -p tsconfig.build.json", "build": "yarn clean && vite build && tsc -p tsconfig.build.json",
"test": "vitest run",
"test:coverage": "vitest run --coverage --coverage.reporter=lcov --coverage.reporter=text-summary",
"test:watch": "vitest",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"format": "prettier . --write", "format": "prettier . --write",
@@ -35,17 +38,24 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10", "@eslint/js": "^10",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^10", "eslint": "^10",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.1.0-canary-ab18f33d-20260220",
"eslint-plugin-react-refresh": "^0.5.1", "eslint-plugin-react-refresh": "^0.5.1",
"globals": "^17.3.0", "globals": "^17.3.0",
"jsdom": "^28.1.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"typescript-eslint": "^8.56.0", "typescript-eslint": "^8.56.0",
"vite": "^7.0.0" "vite": "^7.0.0",
"vitest": "^4.0.18"
} }
} }

View File

@@ -0,0 +1,154 @@
import { describe, expect, it, vi } from 'vitest';
import { ApiError, createApiClient } from '../../src/api/createApiClient';
describe('createApiClient', () => {
it('sends json requests and returns parsed payload', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({ ok: true }),
} as unknown as Response);
const client = createApiClient({
baseUrl: 'https://api.example.com',
fetchImpl: fetchMock as typeof fetch,
});
const result = await client.request<{ ok: boolean }>('/users', {
method: 'POST',
token: 'token-123',
body: { hello: 'world' },
});
expect(result).toEqual({ ok: true });
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer token-123',
},
body: JSON.stringify({ hello: 'world' }),
});
});
it('maps api error payload through custom resolver', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 401,
json: vi.fn().mockResolvedValue({
code: 'AUTH_UNAUTHORIZED',
error: 'unauthorized',
requestId: 'req-1',
details: { reason: 'expired' },
}),
} as unknown as Response);
const resolveError = vi.fn(() => 'Unauthorized access. Please sign in again.');
const client = createApiClient({
baseUrl: 'https://api.example.com',
fetchImpl: fetchMock as typeof fetch,
resolveError,
});
await expect(client.request('/users')).rejects.toMatchObject({
name: 'ApiError',
status: 401,
code: 'AUTH_UNAUTHORIZED',
requestId: 'req-1',
details: { reason: 'expired' },
rawMessage: 'unauthorized',
message: 'Unauthorized access. Please sign in again.',
});
expect(resolveError).toHaveBeenCalledWith({
code: 'AUTH_UNAUTHORIZED',
status: 401,
fallbackMessage: 'unauthorized',
});
});
it('infers an error code from status when payload has no code', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 404,
json: vi.fn().mockResolvedValue({ error: 'missing resource' }),
} as unknown as Response);
const inferErrorCodeFromStatus = vi.fn((status?: number | null) =>
status === 404 ? 'USER_NOT_FOUND' : undefined,
);
const client = createApiClient({
baseUrl: 'https://api.example.com',
fetchImpl: fetchMock as typeof fetch,
inferErrorCodeFromStatus,
resolveError: ({ code }) => (code === 'USER_NOT_FOUND' ? 'User not found.' : 'Unknown'),
});
await expect(client.request('/users/missing')).rejects.toMatchObject({
code: 'USER_NOT_FOUND',
message: 'User not found.',
});
});
it('falls back to default messages when response is not valid json', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 500,
json: vi.fn().mockRejectedValue(new Error('invalid json')),
} as unknown as Response);
const client = createApiClient({
baseUrl: 'https://api.example.com',
fetchImpl: fetchMock as typeof fetch,
});
await expect(client.request('/users')).rejects.toMatchObject({
code: undefined,
rawMessage: undefined,
message: 'Request failed (500).',
});
});
it('uses generic default message when status is unavailable', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: undefined,
json: vi.fn().mockResolvedValue(null),
} as unknown as Response);
const client = createApiClient({
baseUrl: 'https://api.example.com',
fetchImpl: fetchMock as typeof fetch,
});
let thrown: unknown;
try {
await client.request('/users');
} catch (err) {
thrown = err;
}
expect(thrown).toBeInstanceOf(ApiError);
expect((thrown as ApiError).message).toBe('Request failed. Please try again.');
});
it('uses raw fallback error text with the default resolver when present', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 400,
json: vi.fn().mockResolvedValue({
error: 'Validation failed in backend.',
}),
} as unknown as Response);
const client = createApiClient({
baseUrl: 'https://api.example.com',
fetchImpl: fetchMock as typeof fetch,
});
await expect(client.request('/users')).rejects.toMatchObject({
message: 'Validation failed in backend.',
rawMessage: 'Validation failed in backend.',
});
});
});

25
tests/api/query.test.ts Normal file
View File

@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { buildListQuery } from '../../src/api/query';
describe('buildListQuery', () => {
it('builds query with trimmed search and explicit sort', () => {
const result = buildListQuery({
q: ' jane ',
page: 3,
pageSize: 20,
sort: ' createdAt ',
defaultSort: '-createdAt',
});
expect(result).toBe('q=jane&page=3&pageSize=20&sort=createdAt');
});
it('falls back to defaults when query is blank and sort missing', () => {
const result = buildListQuery({
q: ' ',
defaultSort: '-createdAt',
});
expect(result).toBe('page=1&pageSize=10&sort=-createdAt');
});
});

View File

@@ -0,0 +1,143 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { createAuthContext } from '../../src/auth/createAuthContext';
type User = {
id: string;
username: string;
};
const defaultAuth = createAuthContext<User>();
function createUser(username: string): User {
return {
id: `id-${username}`,
username,
};
}
function AuthHarness() {
const { authToken, refreshToken, currentUser, setSession, setCurrentUser, clearSession } =
defaultAuth.useAuth();
return (
<div>
<span data-testid="auth-token">{authToken ?? 'none'}</span>
<span data-testid="refresh-token">{refreshToken ?? 'none'}</span>
<span data-testid="username">{currentUser?.username ?? 'none'}</span>
<button
type="button"
onClick={() => setSession('auth-next', 'refresh-next', createUser('after-set'))}
>
set-session
</button>
<button type="button" onClick={() => setCurrentUser(createUser('patched'))}>
set-user
</button>
<button type="button" onClick={clearSession}>
clear
</button>
</div>
);
}
describe('createAuthContext', () => {
it('throws when hook is used outside the provider', () => {
function Invalid() {
defaultAuth.useAuth();
return null;
}
expect(() => render(<Invalid />)).toThrow('useAuth must be used within AuthProvider');
});
it('reads persisted tokens and cleans default legacy keys', () => {
localStorage.setItem('authToken', 'auth-1');
localStorage.setItem('refreshToken', 'refresh-1');
localStorage.setItem('auth_token', 'legacy');
localStorage.setItem('auth_user', 'legacy');
localStorage.setItem('token', 'legacy');
render(
<defaultAuth.AuthProvider>
<AuthHarness />
</defaultAuth.AuthProvider>,
);
expect(screen.getByTestId('auth-token')).toHaveTextContent('auth-1');
expect(screen.getByTestId('refresh-token')).toHaveTextContent('refresh-1');
expect(localStorage.getItem('auth_token')).toBeNull();
expect(localStorage.getItem('auth_user')).toBeNull();
expect(localStorage.getItem('token')).toBeNull();
});
it('supports session lifecycle updates', () => {
render(
<defaultAuth.AuthProvider>
<AuthHarness />
</defaultAuth.AuthProvider>,
);
fireEvent.click(screen.getByRole('button', { name: 'set-session' }));
expect(screen.getByTestId('auth-token')).toHaveTextContent('auth-next');
expect(screen.getByTestId('refresh-token')).toHaveTextContent('refresh-next');
expect(screen.getByTestId('username')).toHaveTextContent('after-set');
expect(localStorage.getItem('authToken')).toBe('auth-next');
expect(localStorage.getItem('refreshToken')).toBe('refresh-next');
fireEvent.click(screen.getByRole('button', { name: 'set-user' }));
expect(screen.getByTestId('username')).toHaveTextContent('patched');
fireEvent.click(screen.getByRole('button', { name: 'clear' }));
expect(screen.getByTestId('auth-token')).toHaveTextContent('none');
expect(screen.getByTestId('refresh-token')).toHaveTextContent('none');
expect(screen.getByTestId('username')).toHaveTextContent('none');
expect(localStorage.getItem('authToken')).toBeNull();
expect(localStorage.getItem('refreshToken')).toBeNull();
});
it('supports custom token keys and legacy cleanup keys', () => {
const customAuth = createAuthContext<User>({
authTokenKey: 'custom-auth',
refreshTokenKey: 'custom-refresh',
legacyKeys: ['legacy-a', 'legacy-b'],
});
function CustomHarness() {
const { authToken, refreshToken, setSession } = customAuth.useAuth();
return (
<div>
<span data-testid="custom-auth-token">{authToken ?? 'none'}</span>
<span data-testid="custom-refresh-token">{refreshToken ?? 'none'}</span>
<button
type="button"
onClick={() => setSession('next-auth', 'next-refresh', createUser('custom'))}
>
set-custom-session
</button>
</div>
);
}
localStorage.setItem('custom-auth', 'auth-1');
localStorage.setItem('custom-refresh', 'refresh-1');
localStorage.setItem('legacy-a', 'legacy');
localStorage.setItem('legacy-b', 'legacy');
render(
<customAuth.AuthProvider>
<CustomHarness />
</customAuth.AuthProvider>,
);
expect(screen.getByTestId('custom-auth-token')).toHaveTextContent('auth-1');
expect(screen.getByTestId('custom-refresh-token')).toHaveTextContent('refresh-1');
expect(localStorage.getItem('legacy-a')).toBeNull();
expect(localStorage.getItem('legacy-b')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'set-custom-session' }));
expect(localStorage.getItem('custom-auth')).toBe('next-auth');
expect(localStorage.getItem('custom-refresh')).toBe('next-refresh');
});
});

60
tests/auth/jwt.test.ts Normal file
View File

@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { decodeJwtPayload, isJwtExpired } from '../../src/auth/jwt';
function createJwt(payload: Record<string, unknown>) {
const header = { alg: 'HS256', typ: 'JWT' };
const encode = (value: object) =>
btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
return `${encode(header)}.${encode(payload)}.signature`;
}
describe('decodeJwtPayload', () => {
it('returns null for malformed tokens or invalid payloads', () => {
expect(decodeJwtPayload('not-a-jwt')).toBeNull();
expect(decodeJwtPayload('a.b')).toBeNull();
expect(decodeJwtPayload('a.b.c.d')).toBeNull();
const nonJson = 'header.' + btoa('nope') + '.sig';
expect(decodeJwtPayload(nonJson)).toBeNull();
const nonObjectPayload = createJwt({ value: 1 }).replace(
/\.[^.]+\./,
`.${btoa('1').replace(/=+$/g, '')}.`,
);
expect(decodeJwtPayload(nonObjectPayload)).toBeNull();
});
it('decodes valid base64url payloads', () => {
const payload = { sub: 'user-1', role: 'ADMIN', exp: 1735689600 };
expect(decodeJwtPayload(createJwt(payload))).toEqual(payload);
});
});
describe('isJwtExpired', () => {
beforeEach(() => {
vi.useFakeTimers();
});
it('returns false for malformed tokens and invalid exp claim', () => {
expect(isJwtExpired('not-a-jwt')).toBe(false);
expect(isJwtExpired(createJwt({}))).toBe(false);
expect(isJwtExpired(createJwt({ exp: 'nope' }))).toBe(false);
expect(isJwtExpired(createJwt({ exp: Number.POSITIVE_INFINITY }))).toBe(false);
});
it('returns true when token is expired', () => {
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
const exp = Math.floor(new Date('2025-12-31T23:59:00Z').getTime() / 1000);
expect(isJwtExpired(createJwt({ exp }))).toBe(true);
});
it('applies skew seconds when evaluating expiration', () => {
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
const exp = Math.floor(new Date('2026-01-01T00:00:10Z').getTime() / 1000);
expect(isJwtExpired(createJwt({ exp }), 15)).toBe(true);
expect(isJwtExpired(createJwt({ exp }), 5)).toBe(false);
});
});

View File

@@ -0,0 +1,246 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { act } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
LeftMenuProvider,
type LeftMenuContent,
useLeftMenu,
} from '../../src/contexts/LeftMenuContext';
function buildDefaultContent(label = 'Default menu'): LeftMenuContent {
return {
ariaLabel: label,
render: ({ collapsed }) => <div>{collapsed ? `${label} (collapsed)` : `${label} (expanded)`}</div>,
};
}
function setViewportWidth(width: number) {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: width,
});
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: undefined,
});
}
function renderLeftMenuHarness(initialPathname = '/users') {
let currentPathname = initialPathname;
let currentValue: ReturnType<typeof useLeftMenu> | null = null;
const defaultContent = buildDefaultContent();
function Probe() {
currentValue = useLeftMenu();
const renderState = {
collapsed: currentValue.collapsed,
mobileOpen: currentValue.mobileOpen,
isDesktop: window.innerWidth >= 1024,
closeMenu: currentValue.closeMenu,
};
return <div data-testid="left-menu-content">{currentValue.content.render(renderState)}</div>;
}
function Wrapper({ pathname }: Readonly<{ pathname: string }>) {
return (
<LeftMenuProvider defaultContent={defaultContent} closeOnPathname={pathname}>
<Probe />
</LeftMenuProvider>
);
}
const rendered = render(<Wrapper pathname={currentPathname} />);
return {
getCurrent() {
if (!currentValue) {
throw new Error('Left menu context value not initialized');
}
return currentValue;
},
reroute(nextPathname: string) {
currentPathname = nextPathname;
rendered.rerender(<Wrapper pathname={currentPathname} />);
},
};
}
describe('LeftMenuContext', () => {
beforeEach(() => {
localStorage.removeItem('authSidebarWidth');
localStorage.removeItem('authSidebarCollapsed');
setViewportWidth(1024);
});
it('throws when useLeftMenu is used outside provider', () => {
function Invalid() {
useLeftMenu();
return null;
}
expect(() => render(<Invalid />)).toThrow('useLeftMenu must be used within LeftMenuProvider');
});
it('supports desktop collapse/expand/toggle semantics', () => {
const harness = renderLeftMenuHarness('/users');
expect(harness.getCurrent().collapsed).toBe(false);
act(() => {
harness.getCurrent().closeMenu();
});
expect(harness.getCurrent().collapsed).toBe(true);
act(() => {
harness.getCurrent().openMenu();
});
expect(harness.getCurrent().collapsed).toBe(false);
act(() => {
harness.getCurrent().toggleMenu();
});
expect(harness.getCurrent().collapsed).toBe(true);
});
it('restores collapsed state from storage and clamps persisted width', () => {
localStorage.setItem('authSidebarCollapsed', '1');
localStorage.setItem('authSidebarWidth', '999');
const harness = renderLeftMenuHarness('/users');
expect(harness.getCurrent().collapsed).toBe(true);
expect(harness.getCurrent().desktopMenuStyle['--auth-sidebar-width']).toBe('56px');
});
it('locks body scroll on mobile open and unlocks on close', () => {
setViewportWidth(768);
const harness = renderLeftMenuHarness('/users');
act(() => {
harness.getCurrent().openMenu();
});
expect(harness.getCurrent().mobileOpen).toBe(true);
expect(document.body.style.overflow).toBe('hidden');
act(() => {
harness.getCurrent().closeMenu();
});
expect(harness.getCurrent().mobileOpen).toBe(false);
expect(document.body.style.overflow).toBe('');
});
it('reads, clamps, and persists sidebar width through pointer resize', async () => {
localStorage.setItem('authSidebarWidth', '999');
const harness = renderLeftMenuHarness('/users');
expect(harness.getCurrent().desktopMenuStyle['--auth-sidebar-width']).toBe('420px');
const preventDefault = vi.fn();
act(() => {
harness.getCurrent().startResize({
clientX: 420,
preventDefault,
} as never);
});
act(() => {
fireEvent.pointerMove(window, { clientX: -1000 });
fireEvent.pointerUp(window);
});
await act(async () => {
await Promise.resolve();
});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(localStorage.getItem('authSidebarWidth')).toBe('220');
expect(harness.getCurrent().desktopMenuStyle['--auth-sidebar-width']).toBe('220px');
});
it('accepts custom content and resets to default on route changes', () => {
setViewportWidth(768);
const harness = renderLeftMenuHarness('/users');
act(() => {
harness.getCurrent().openMenu();
harness.getCurrent().setMenuContent({
ariaLabel: 'Custom menu',
render: () => <div>Custom menu content</div>,
});
});
expect(harness.getCurrent().mobileOpen).toBe(true);
expect(screen.getByText('Custom menu content')).toBeInTheDocument();
harness.reroute('/posts');
expect(harness.getCurrent().mobileOpen).toBe(false);
expect(screen.queryByText('Custom menu content')).not.toBeInTheDocument();
expect(screen.getByText('Default menu (expanded)')).toBeInTheDocument();
});
it('applies provided content in openMenu/toggleMenu and toggles mobile open state', () => {
setViewportWidth(768);
const harness = renderLeftMenuHarness('/users');
act(() => {
harness.getCurrent().openMenu({
render: () => <div>Open payload</div>,
});
});
expect(screen.getByText('Open payload')).toBeInTheDocument();
expect(harness.getCurrent().mobileOpen).toBe(true);
act(() => {
harness.getCurrent().toggleMenu({
render: () => <div>Toggle payload</div>,
});
});
expect(screen.getByText('Toggle payload')).toBeInTheDocument();
expect(harness.getCurrent().mobileOpen).toBe(false);
});
it('updates default content only when current content is still default', () => {
function Harness() {
const menu = useLeftMenu();
const state = {
collapsed: menu.collapsed,
mobileOpen: menu.mobileOpen,
isDesktop: true,
closeMenu: menu.closeMenu,
};
return (
<div>
<button type="button" onClick={() => menu.setMenuContent({ render: () => <div>Custom</div> })}>
custom
</button>
<div data-testid="content">{menu.content.render(state)}</div>
</div>
);
}
function Wrapper({ label }: Readonly<{ label: string }>) {
return (
<LeftMenuProvider defaultContent={buildDefaultContent(label)}>
<Harness />
</LeftMenuProvider>
);
}
const rendered = render(<Wrapper label="Menu A" />);
expect(screen.getByTestId('content')).toHaveTextContent('Menu A (expanded)');
rendered.rerender(<Wrapper label="Menu B" />);
expect(screen.getByTestId('content')).toHaveTextContent('Menu B (expanded)');
fireEvent.click(screen.getByRole('button', { name: 'custom' }));
expect(screen.getByTestId('content')).toHaveTextContent('Custom');
rendered.rerender(<Wrapper label="Menu C" />);
expect(screen.getByTestId('content')).toHaveTextContent('Custom');
});
});

View File

@@ -0,0 +1,249 @@
import { fireEvent, render } from '@testing-library/react';
import { act } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
RightSidebarProvider,
useRightSidebar,
} from '../../src/contexts/RightSidebarContext';
type RightSidebarHarnessOptions = {
pathname?: string;
onMobileOpenRequest?: () => void;
};
function setViewportWidth(width: number) {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: width,
});
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: undefined,
});
}
function renderRightSidebarHarness(options: RightSidebarHarnessOptions = {}) {
const { pathname = '/users', onMobileOpenRequest } = options;
let currentPathname = pathname;
let currentValue: ReturnType<typeof useRightSidebar> | null = null;
function Probe() {
currentValue = useRightSidebar();
return null;
}
function Wrapper({ pathname }: Readonly<{ pathname: string }>) {
return (
<RightSidebarProvider
closeOnPathname={pathname}
onMobileOpenRequest={onMobileOpenRequest}
>
<Probe />
</RightSidebarProvider>
);
}
const rendered = render(<Wrapper pathname={currentPathname} />);
return {
getCurrent() {
if (!currentValue) {
throw new Error('Right sidebar context value not initialized');
}
return currentValue;
},
reroute(nextPathname: string) {
currentPathname = nextPathname;
rendered.rerender(<Wrapper pathname={currentPathname} />);
},
unmount: rendered.unmount,
};
}
describe('RightSidebarContext', () => {
beforeEach(() => {
localStorage.removeItem('authRightSidebarWidth');
setViewportWidth(1024);
});
it('throws when useRightSidebar is used outside provider', () => {
function Invalid() {
useRightSidebar();
return null;
}
expect(() => render(<Invalid />)).toThrow('useRightSidebar must be used within RightSidebarProvider');
});
it('opens and closes with content', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(harness.getCurrent().content?.title).toBe('Meta');
act(() => {
harness.getCurrent().closeSidebar();
});
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('does not open without current or next content', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar();
});
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('updates content live and supports toggle semantics', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
act(() => {
harness.getCurrent().setSidebarContent({
title: 'Meta Updated',
content: <div>Updated</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(harness.getCurrent().content?.title).toBe('Meta Updated');
act(() => {
harness.getCurrent().toggleSidebar();
});
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('toggleSidebar opens with provided content when closed', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().toggleSidebar({
title: 'Toggle open',
content: <div>Toggle body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(harness.getCurrent().content?.title).toBe('Toggle open');
});
it('closes on pathname changes', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
harness.reroute('/posts');
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('reads/clamps persisted width, persists resized widths, and reacts to escape', async () => {
localStorage.setItem('authRightSidebarWidth', '999');
const harness = renderRightSidebarHarness({ pathname: '/users' });
expect(harness.getCurrent().desktopSidebarStyle['--auth-right-sidebar-width']).toBe(
'480px',
);
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
const preventDefault = vi.fn();
act(() => {
harness.getCurrent().startResize({
clientX: 480,
preventDefault,
} as never);
});
act(() => {
fireEvent.pointerMove(window, { clientX: -400 });
fireEvent.pointerUp(window);
});
await act(async () => {
await Promise.resolve();
});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(localStorage.getItem('authRightSidebarWidth')).toBe('480');
act(() => {
fireEvent.keyDown(window, { key: 'Escape' });
});
expect(harness.getCurrent().isOpen).toBe(false);
});
it('notifies mobile open requests when opening on mobile', () => {
setViewportWidth(768);
const onMobileOpenRequest = vi.fn();
const harness = renderRightSidebarHarness({
pathname: '/users',
onMobileOpenRequest,
});
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(onMobileOpenRequest).toHaveBeenCalledTimes(1);
});
it('normalizes custom sizing values', () => {
function Probe() {
const sidebar = useRightSidebar();
return <div data-testid="width">{sidebar.desktopSidebarStyle['--auth-right-sidebar-width']}</div>;
}
const { getByTestId } = render(
<RightSidebarProvider
sizing={{
defaultWidth: 1000,
minWidth: 500,
maxWidth: 300,
}}
>
<Probe />
</RightSidebarProvider>,
);
expect(getByTestId('width').textContent).toBe('500px');
});
});

View File

@@ -0,0 +1,143 @@
import { describe, expect, it } from 'vitest';
import { createErrorResolver } from '../../src/errors/createErrorResolver';
const CATALOG = {
AUTH_UNAUTHORIZED: 'Unauthorized access. Please sign in again.',
FORBIDDEN: 'You do not have permission to perform this action.',
USER_NOT_FOUND: 'User not found.',
INTERNAL_ERROR: 'Unexpected request error.',
REQUEST_FAILED: 'Request failed. Please try again.',
};
const resolver = createErrorResolver({
catalog: CATALOG,
fallbackCode: 'REQUEST_FAILED',
defaultContext: 'default',
contextOverrides: {
session: {
AUTH_UNAUTHORIZED: 'Session expired. Please sign in again.',
},
},
inferCodeFromStatus: (status?: number | null) => {
switch (status) {
case 401:
return 'AUTH_UNAUTHORIZED';
case 403:
return 'FORBIDDEN';
case 404:
return 'USER_NOT_FOUND';
default:
if (status != null && status >= 500) {
return 'INTERNAL_ERROR';
}
return undefined;
}
},
inferCodeFromLegacyMessage: (message?: string | null) => {
if (message?.toLowerCase() === 'unauthorized') {
return 'AUTH_UNAUTHORIZED';
}
return undefined;
},
});
describe('createErrorResolver', () => {
it('recognizes known error codes', () => {
expect(resolver.isKnownErrorCode('AUTH_UNAUTHORIZED')).toBe(true);
expect(resolver.isKnownErrorCode('NOPE')).toBe(false);
});
it('returns context override for known code', () => {
expect(
resolver.resolveErrorMessage({
code: 'AUTH_UNAUTHORIZED',
context: 'session',
}),
).toBe('Session expired. Please sign in again.');
});
it('falls back from unknown code to legacy message inference', () => {
expect(
resolver.resolveErrorMessage({
code: 'UNKNOWN_CODE',
fallbackMessage: 'unauthorized',
}),
).toBe('Unauthorized access. Please sign in again.');
});
it('falls back to status mapping when code is missing', () => {
expect(resolver.resolveErrorMessage({ status: 404 })).toBe('User not found.');
});
it('falls back to status mapping when legacy inference returns an unmapped code', () => {
const resolverWithUnmappedLegacyCode = createErrorResolver({
catalog: CATALOG,
inferCodeFromStatus: resolver.inferErrorCodeFromStatus,
inferCodeFromLegacyMessage: () => 'LEGACY_ONLY_CODE',
});
expect(
resolverWithUnmappedLegacyCode.resolveErrorMessage({
status: 403,
fallbackMessage: 'legacy message',
}),
).toBe('You do not have permission to perform this action.');
});
it('uses fallbackCode when no code/status resolve and fallback message is missing', () => {
expect(resolver.resolveErrorMessage({ status: 418 })).toBe('Request failed. Please try again.');
});
it('uses fallback message when no mapping exists and fallback code is unavailable', () => {
const noFallbackCodeResolver = createErrorResolver({
catalog: CATALOG,
inferCodeFromStatus: () => undefined,
});
expect(
noFallbackCodeResolver.resolveErrorMessage({
code: 'UNKNOWN',
fallbackMessage: 'raw backend message',
}),
).toBe('raw backend message');
});
it('returns default request failure message when no signal is available', () => {
const emptyResolver = createErrorResolver({
catalog: {},
});
expect(emptyResolver.resolveErrorMessage({})).toBe('Request failed. Please try again.');
});
it('resolveOptionalErrorMessage returns undefined for empty inputs', () => {
expect(resolver.resolveOptionalErrorMessage(undefined)).toBeUndefined();
expect(resolver.resolveOptionalErrorMessage(null)).toBeUndefined();
});
it('resolveOptionalErrorMessage resolves known codes', () => {
expect(resolver.resolveOptionalErrorMessage('FORBIDDEN')).toBe(
'You do not have permission to perform this action.',
);
});
it('toErrorMessage prefers rawMessage over message and handles unknown values', () => {
expect(
resolver.toErrorMessage({
status: 403,
rawMessage: 'unauthorized',
message: 'ignored message',
}),
).toBe('Unauthorized access. Please sign in again.');
expect(resolver.toErrorMessage('boom')).toBe('Request failed. Please try again.');
expect(resolver.toErrorMessage(null)).toBe('Request failed. Please try again.');
});
it('exposes inferErrorCodeFromStatus', () => {
expect(resolver.inferErrorCodeFromStatus(401)).toBe('AUTH_UNAUTHORIZED');
expect(resolver.inferErrorCodeFromStatus(500)).toBe('INTERNAL_ERROR');
expect(resolver.inferErrorCodeFromStatus(418)).toBeUndefined();
expect(resolver.inferErrorCodeFromStatus(null)).toBeUndefined();
});
});

View File

@@ -0,0 +1,36 @@
import { act } from 'react';
import { createRoot } from 'react-dom/client';
export function renderHook<T>(useHook: () => T) {
let currentValue: T;
function TestComponent() {
currentValue = useHook();
return null;
}
const container = document.createElement('div');
const root = createRoot(container);
act(() => {
root.render(<TestComponent />);
});
return {
result: {
get current() {
return currentValue;
},
},
rerender() {
act(() => {
root.render(<TestComponent />);
});
},
unmount() {
act(() => {
root.unmount();
});
},
};
}

View File

@@ -0,0 +1,64 @@
import { act } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useCooldownTimer } from '../../src/hooks/useCooldownTimer';
import { renderHook } from '../helpers/renderHook';
describe('useCooldownTimer', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('decrements cooldown every second while enabled', () => {
const { result } = renderHook(() => useCooldownTimer(2, true));
expect(result.current.cooldown).toBe(2);
act(() => {
vi.advanceTimersByTime(1000);
});
expect(result.current.cooldown).toBe(1);
act(() => {
vi.advanceTimersByTime(1000);
});
expect(result.current.cooldown).toBe(0);
});
it('does not decrement when disabled', () => {
const { result } = renderHook(() => useCooldownTimer(2, false));
act(() => {
vi.advanceTimersByTime(3000);
});
expect(result.current.cooldown).toBe(2);
});
it('startCooldown floors values and clamps negatives', () => {
const { result } = renderHook(() => useCooldownTimer(0, true));
act(() => {
result.current.startCooldown(2.8);
});
expect(result.current.cooldown).toBe(2);
act(() => {
result.current.startCooldown(-4);
});
expect(result.current.cooldown).toBe(0);
});
it('stays at zero when already expired', () => {
const { result } = renderHook(() => useCooldownTimer(0, true));
act(() => {
vi.advanceTimersByTime(2000);
});
expect(result.current.cooldown).toBe(0);
});
});

View File

@@ -0,0 +1,81 @@
import { act } from 'react';
import { describe, expect, it } from 'vitest';
import { useEditableForm } from '../../src/hooks/useEditableForm';
import { renderHook } from '../helpers/renderHook';
type FormValues = {
username: string;
email: string;
};
const INITIAL_VALUES: FormValues = {
username: '',
email: '',
};
function validate(values: FormValues) {
return {
username: values.username.trim().length < 3 ? 'Username too short' : undefined,
email: values.email.includes('@') ? undefined : 'Invalid email',
};
}
describe('useEditableForm', () => {
it('supports load/start/discard/commit edit lifecycle', () => {
const { result } = renderHook(() =>
useEditableForm({
initialValues: INITIAL_VALUES,
validate,
}),
);
expect(result.current.isEditing).toBe(false);
act(() => {
result.current.loadFromSource({ username: 'alice', email: 'alice@example.com' });
});
expect(result.current.values).toEqual({ username: 'alice', email: 'alice@example.com' });
expect(result.current.errors).toEqual({});
act(() => {
result.current.startEditing({ username: 'al', email: 'aliceexample.com' });
});
expect(result.current.isEditing).toBe(true);
act(() => {
result.current.setFieldValue('username', 'a');
});
expect(result.current.errors.username).toBe('Username too short');
act(() => {
result.current.discardChanges({ username: 'alice', email: 'alice@example.com' });
});
expect(result.current.isEditing).toBe(false);
expect(result.current.values).toEqual({ username: 'alice', email: 'alice@example.com' });
act(() => {
result.current.startEditing({ username: 'alice', email: 'alice@example.com' });
result.current.setFieldValue('username', 'alice_2');
result.current.commitSaved({ username: 'alice_2', email: 'alice@example.com' });
});
expect(result.current.isEditing).toBe(false);
expect(result.current.values.username).toBe('alice_2');
});
it('exposes direct editing toggles and field error injection', () => {
const { result } = renderHook(() =>
useEditableForm({
initialValues: INITIAL_VALUES,
validate,
}),
);
act(() => {
result.current.setIsEditing(true);
result.current.setFieldError('username', 'Username already taken');
});
expect(result.current.isEditing).toBe(true);
expect(result.current.errors.username).toBe('Username already taken');
});
});

View File

@@ -0,0 +1,170 @@
import { act } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { usePaginatedResource } from '../../src/hooks/usePaginatedResource';
import { renderHook } from '../helpers/renderHook';
type Item = {
id: string;
};
async function flushDebounce(delay = 250) {
await act(async () => {
vi.advanceTimersByTime(delay);
await Promise.resolve();
});
}
describe('usePaginatedResource', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('loads first page after debounce and stores response fields', async () => {
const loadMock = vi.fn(async () => ({
items: [{ id: '1' }] as Item[],
page: 1,
pageSize: 10,
total: 25,
totalPages: 3,
}));
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
sort: '-createdAt',
}),
);
await flushDebounce();
expect(loadMock).toHaveBeenCalledWith({
q: '',
page: 1,
pageSize: 10,
sort: '-createdAt',
});
expect(result.current.items).toEqual([{ id: '1' }]);
expect(result.current.total).toBe(25);
expect(result.current.totalPages).toBe(3);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
it('resets page to 1 when query or page size changes', async () => {
const loadMock = vi.fn(
async ({ q, page, pageSize }: { q: string; page: number; pageSize: number }) => ({
items: [{ id: `${q}:${page}:${pageSize}` }] as Item[],
page,
pageSize,
total: 1,
totalPages: 1,
}),
);
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
}),
);
await flushDebounce();
act(() => {
result.current.setPage(2);
});
await flushDebounce();
expect(loadMock).toHaveBeenLastCalledWith({
q: '',
page: 2,
pageSize: 10,
sort: undefined,
});
act(() => {
result.current.setQuery('search');
});
await flushDebounce();
expect(loadMock).toHaveBeenLastCalledWith({
q: 'search',
page: 1,
pageSize: 10,
sort: undefined,
});
act(() => {
result.current.setPage(3);
});
await flushDebounce();
act(() => {
result.current.setPageSize(20);
});
await flushDebounce();
expect(loadMock).toHaveBeenLastCalledWith({
q: 'search',
page: 1,
pageSize: 20,
sort: undefined,
});
});
it('cancels stale debounce timers when inputs change quickly', async () => {
const loadMock = vi.fn(async () => ({
items: [] as Item[],
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
}));
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
debounceMs: 100,
}),
);
act(() => {
result.current.setQuery('latest');
});
await flushDebounce(100);
expect(loadMock).toHaveBeenCalledTimes(1);
expect(loadMock).toHaveBeenCalledWith({
q: 'latest',
page: 1,
pageSize: 10,
sort: undefined,
});
});
it('maps thrown errors into string error state', async () => {
const loadMock = vi
.fn()
.mockRejectedValueOnce(new Error('Load failed'))
.mockRejectedValueOnce('unknown');
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
}),
);
await flushDebounce();
expect(result.current.error).toBe('Load failed');
expect(result.current.isLoading).toBe(false);
act(() => {
result.current.setPage(2);
});
await flushDebounce();
expect(result.current.error).toBe('Request failed. Please try again.');
expect(result.current.isLoading).toBe(false);
});
});

View File

@@ -0,0 +1,97 @@
import { act } from 'react';
import { describe, expect, it } from 'vitest';
import { formatSortParam, useSorting } from '../../src/hooks/useSorting';
import { renderHook } from '../helpers/renderHook';
describe('useSorting', () => {
it('starts from default sort and cycles asc/desc/default', () => {
const { result } = renderHook(() => useSorting({ field: 'createdAt', direction: 'asc' }));
expect(result.current.activeSort).toEqual({ field: 'createdAt', direction: 'asc' });
expect(result.current.sortParam).toBe('createdAt');
act(() => {
result.current.toggleSort('name');
});
expect(result.current.activeSort).toEqual({ field: 'name', direction: 'asc' });
expect(result.current.sortParam).toBe('name');
act(() => {
result.current.toggleSort('name');
});
expect(result.current.activeSort).toEqual({ field: 'name', direction: 'desc' });
expect(result.current.sortParam).toBe('-name');
act(() => {
result.current.toggleSort('name');
});
expect(result.current.activeSort).toEqual({ field: 'createdAt', direction: 'asc' });
expect(result.current.sortParam).toBe('createdAt');
});
it('cycles sort state without a baseline default', () => {
const { result } = renderHook(() => useSorting(null));
expect(result.current.activeSort).toBeNull();
expect(result.current.sortParam).toBeUndefined();
act(() => {
result.current.toggleSort('updatedAt');
});
expect(result.current.sortParam).toBe('updatedAt');
act(() => {
result.current.toggleSort('updatedAt');
});
expect(result.current.sortParam).toBe('-updatedAt');
act(() => {
result.current.toggleSort('updatedAt');
});
expect(result.current.sortParam).toBeUndefined();
});
it('supports manual setSort and resetSort', () => {
const { result } = renderHook(() => useSorting({ field: 'createdAt', direction: 'desc' }));
expect(result.current.sortParam).toBe('-createdAt');
act(() => {
result.current.setSort({ field: 'title', direction: 'asc' });
});
expect(result.current.sortParam).toBe('title');
act(() => {
result.current.resetSort();
});
expect(result.current.sortParam).toBe('-createdAt');
});
it('toggles baseline field directly between baseline and opposite direction', () => {
const { result } = renderHook(() => useSorting({ field: 'createdAt', direction: 'desc' }));
expect(result.current.sortParam).toBe('-createdAt');
act(() => {
result.current.toggleSort('createdAt');
});
expect(result.current.sortParam).toBe('createdAt');
act(() => {
result.current.toggleSort('createdAt');
});
expect(result.current.sortParam).toBe('-createdAt');
act(() => {
result.current.toggleSort('createdAt');
});
expect(result.current.sortParam).toBe('createdAt');
});
it('formats sort params safely', () => {
expect(formatSortParam(null)).toBeUndefined();
expect(formatSortParam(undefined)).toBeUndefined();
expect(formatSortParam({ field: 'updatedAt', direction: 'desc' })).toBe('-updatedAt');
expect(formatSortParam({ field: 'updatedAt', direction: 'asc' })).toBe('updatedAt');
});
});

View File

@@ -0,0 +1,44 @@
import { act } from 'react';
import { describe, expect, it } from 'vitest';
import { useSubmitState } from '../../src/hooks/useSubmitState';
import { renderHook } from '../helpers/renderHook';
describe('useSubmitState', () => {
it('tracks submit lifecycle and feedback state', () => {
const { result } = renderHook(() => useSubmitState<string | null>(null));
expect(result.current.isSubmitting).toBe(false);
expect(result.current.submitError).toBeNull();
expect(result.current.status).toBeNull();
act(() => {
result.current.startSubmitting();
result.current.setSubmitError('Oops');
result.current.setStatus('Done');
});
expect(result.current.isSubmitting).toBe(true);
expect(result.current.submitError).toBe('Oops');
expect(result.current.status).toBe('Done');
act(() => {
result.current.finishSubmitting();
result.current.clearFeedback();
});
expect(result.current.isSubmitting).toBe(false);
expect(result.current.submitError).toBeNull();
expect(result.current.status).toBeNull();
});
it('restores the typed initial status in clearFeedback', () => {
const { result } = renderHook(() => useSubmitState<'idle' | 'done'>('idle'));
act(() => {
result.current.setStatus('done');
result.current.clearFeedback();
});
expect(result.current.status).toBe('idle');
});
});

View File

@@ -0,0 +1,158 @@
import { act } from 'react';
import { describe, expect, it } from 'vitest';
import { useValidatedFields } from '../../src/hooks/useValidatedFields';
import { renderHook } from '../helpers/renderHook';
type FormValues = {
password: string;
confirmPassword: string;
};
function validate(values: FormValues) {
return {
password: values.password.length < 3 ? 'Password too short' : undefined,
confirmPassword:
values.confirmPassword !== values.password ? 'Passwords do not match' : undefined,
};
}
describe('useValidatedFields', () => {
it('initializes values and keeps errors hidden until fields are touched', () => {
const { result } = renderHook(() =>
useValidatedFields({
initialValues: { password: '', confirmPassword: '' },
validate,
}),
);
expect(result.current.values).toEqual({ password: '', confirmPassword: '' });
expect(result.current.errors).toEqual({});
expect(result.current.isValid).toBe(false);
});
it('setFieldValue touches and validates by default', () => {
const { result } = renderHook(() =>
useValidatedFields({
initialValues: { password: '', confirmPassword: '' },
validate,
}),
);
act(() => {
result.current.setFieldValue('password', 'ab');
});
expect(result.current.values.password).toBe('ab');
expect(result.current.errors.password).toBe('Password too short');
});
it('setFieldValue can skip touch and validation', () => {
const { result } = renderHook(() =>
useValidatedFields({
initialValues: { password: '', confirmPassword: '' },
validate,
}),
);
act(() => {
result.current.setFieldValue('password', 'ab', { touch: false, validate: false });
});
expect(result.current.values.password).toBe('ab');
expect(result.current.errors).toEqual({});
});
it('validateAll can avoid touching fields when requested', () => {
const { result } = renderHook(() =>
useValidatedFields({
initialValues: { password: 'abcd', confirmPassword: 'abce' },
validate,
}),
);
let validationErrors: ReturnType<typeof validate> | undefined;
act(() => {
validationErrors = result.current.validateAll({ touchAll: false });
});
expect(validationErrors).toEqual({
password: undefined,
confirmPassword: 'Passwords do not match',
});
expect(result.current.errors).toEqual({});
});
it('validateAll touches all fields by default', () => {
const { result } = renderHook(() =>
useValidatedFields({
initialValues: { password: '', confirmPassword: '' },
validate,
}),
);
act(() => {
result.current.validateAll();
});
expect(result.current.errors.password).toBe('Password too short');
expect(result.current.errors.confirmPassword).toBeUndefined();
});
it('supports setFieldError, setErrors and clearErrors', () => {
const { result } = renderHook(() =>
useValidatedFields({
initialValues: { password: 'abcd', confirmPassword: 'abcd' },
validate,
}),
);
act(() => {
result.current.setFieldError('password', 'Server-side password issue');
});
expect(result.current.errors.password).toBe('Server-side password issue');
act(() => {
result.current.setErrors({
password: undefined,
confirmPassword: 'Still invalid',
});
});
expect(result.current.errors.confirmPassword).toBe('Still invalid');
act(() => {
result.current.clearErrors();
});
expect(result.current.errors).toEqual({});
});
it('setValues with clearErrors resets touched state and revalidates', () => {
const { result } = renderHook(() =>
useValidatedFields({
initialValues: { password: '', confirmPassword: '' },
validate,
}),
);
act(() => {
result.current.setFieldValue('password', 'ab');
});
expect(result.current.errors.password).toBe('Password too short');
act(() => {
result.current.setValues(
{
password: 'abcd',
confirmPassword: 'abcd',
},
{ clearErrors: true },
);
});
expect(result.current.values).toEqual({
password: 'abcd',
confirmPassword: 'abcd',
});
expect(result.current.errors).toEqual({});
expect(result.current.isValid).toBe(true);
});
});

View File

@@ -0,0 +1,239 @@
import { fireEvent } from '@testing-library/react';
import { act } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { isDesktopViewport, useSidePanelMachine } from '../../src/panels/useSidePanelMachine';
import { renderHook } from '../helpers/renderHook';
function setViewportWidth(width: number) {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: width,
});
}
function resetMatchMedia() {
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: undefined,
});
}
describe('isDesktopViewport', () => {
it('uses matchMedia when available', () => {
const matchMedia = vi.fn(() => ({ matches: false }));
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: matchMedia,
});
expect(isDesktopViewport(1200)).toBe(false);
expect(matchMedia).toHaveBeenCalledWith('(min-width: 1200px)');
});
it('falls back to window.innerWidth when matchMedia is unavailable', () => {
resetMatchMedia();
setViewportWidth(1024);
expect(isDesktopViewport(1024)).toBe(true);
setViewportWidth(800);
expect(isDesktopViewport(1024)).toBe(false);
});
});
describe('useSidePanelMachine', () => {
it('reads stored width, clamps resize values, and persists final width', async () => {
resetMatchMedia();
setViewportWidth(1200);
localStorage.setItem('panel-width', '999');
const preventDefault = vi.fn();
const { result } = renderHook(() =>
useSidePanelMachine({
storageKey: 'panel-width',
defaultWidth: 300,
minWidth: 200,
maxWidth: 400,
resizeAxis: 'from-left',
resizingBodyClass: 'resizing',
isOpen: false,
canResize: true,
shouldPersistWidth: true,
closeOnPathname: undefined,
}),
);
expect(result.current.width).toBe(400);
expect(result.current.isDesktop).toBe(true);
expect(localStorage.getItem('panel-width')).toBe('400');
act(() => {
result.current.startResize({
clientX: 400,
preventDefault,
} as never);
});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(document.body.classList.contains('resizing')).toBe(true);
act(() => {
fireEvent.pointerMove(window, { clientX: 100 });
fireEvent.pointerUp(window);
});
await act(async () => {
await Promise.resolve();
});
expect(result.current.width).toBe(200);
expect(localStorage.getItem('panel-width')).toBe('200');
expect(document.body.classList.contains('resizing')).toBe(false);
});
it('falls back to default width for empty or invalid stored values', () => {
resetMatchMedia();
setViewportWidth(1200);
localStorage.setItem('panel-width', '');
const invalid = renderHook(() =>
useSidePanelMachine({
storageKey: 'panel-width',
defaultWidth: 310,
minWidth: 200,
maxWidth: 400,
resizeAxis: 'from-left',
resizingBodyClass: 'resizing',
isOpen: false,
canResize: true,
shouldPersistWidth: false,
closeOnPathname: undefined,
}),
);
expect(invalid.result.current.width).toBe(310);
invalid.unmount();
localStorage.setItem('panel-width', 'NaN-value');
const nonFinite = renderHook(() =>
useSidePanelMachine({
storageKey: 'panel-width',
defaultWidth: 315,
minWidth: 200,
maxWidth: 400,
resizeAxis: 'from-left',
resizingBodyClass: 'resizing',
isOpen: false,
canResize: true,
shouldPersistWidth: false,
closeOnPathname: undefined,
}),
);
expect(nonFinite.result.current.width).toBe(315);
});
it('does not start resizing when viewport is mobile or resizing is disabled', () => {
resetMatchMedia();
setViewportWidth(700);
const preventDefault = vi.fn();
const { result } = renderHook(() =>
useSidePanelMachine({
storageKey: 'panel-width',
defaultWidth: 300,
minWidth: 200,
maxWidth: 400,
resizeAxis: 'from-left',
resizingBodyClass: 'resizing',
isOpen: false,
canResize: false,
shouldPersistWidth: false,
closeOnPathname: undefined,
}),
);
act(() => {
result.current.startResize({
clientX: 300,
preventDefault,
} as never);
});
expect(preventDefault).not.toHaveBeenCalled();
expect(document.body.classList.contains('resizing')).toBe(false);
expect(localStorage.getItem('panel-width')).toBeNull();
});
it('ignores pointer events when not currently resizing', () => {
resetMatchMedia();
setViewportWidth(1200);
renderHook(() =>
useSidePanelMachine({
storageKey: 'panel-width',
defaultWidth: 300,
minWidth: 200,
maxWidth: 400,
resizeAxis: 'from-left',
resizingBodyClass: 'resizing',
isOpen: false,
canResize: true,
shouldPersistWidth: true,
closeOnPathname: undefined,
}),
);
act(() => {
fireEvent.pointerMove(window, { clientX: 1000 });
fireEvent.pointerUp(window);
});
expect(document.body.classList.contains('resizing')).toBe(false);
});
it('handles closeOnPathname, escape key callbacks and body overflow lock', () => {
resetMatchMedia();
setViewportWidth(700);
const onCloseOnPathname = vi.fn();
const onEscape = vi.fn();
let options = {
storageKey: 'panel-width',
defaultWidth: 300,
minWidth: 200,
maxWidth: 400,
resizeAxis: 'from-right' as const,
resizingBodyClass: 'resizing',
isOpen: true,
canResize: true,
shouldPersistWidth: false,
closeOnPathname: '/users',
onCloseOnPathname,
onEscape,
};
const { rerender } = renderHook(() => useSidePanelMachine(options));
expect(onCloseOnPathname).toHaveBeenCalledTimes(1);
expect(document.body.style.overflow).toBe('hidden');
act(() => {
fireEvent.keyDown(window, { key: 'Escape' });
});
expect(onEscape).toHaveBeenCalledTimes(1);
options = {
...options,
closeOnPathname: '/posts',
};
rerender();
expect(onCloseOnPathname).toHaveBeenCalledTimes(2);
options = {
...options,
isOpen: false,
};
rerender();
expect(document.body.style.overflow).toBe('');
});
});

15
tests/setup.ts Normal file
View File

@@ -0,0 +1,15 @@
// Required by React to silence act(...) warnings in jsdom tests.
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
afterEach(() => {
cleanup();
localStorage.clear();
document.body.className = '';
document.body.style.overflow = '';
vi.restoreAllMocks();
vi.useRealTimers();
});

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { capitalize, formatDate, splitAndCapitalize } from '../../src/utils/formatting';
describe('formatDate', () => {
it('formats date in it-IT locale and includes seconds when requested', () => {
const withoutSeconds = formatDate('2026-01-01T12:34:56.000Z');
const withSeconds = formatDate('2026-01-01T12:34:56.000Z', true);
expect(withoutSeconds).toContain('2026');
expect(withSeconds).toContain('56');
});
});
describe('capitalize', () => {
it('capitalizes every space-delimited word', () => {
expect(capitalize('hello WORLD')).toBe('Hello World');
expect(capitalize('mUlTi SPACES')).toBe('Multi Spaces');
});
});
describe('splitAndCapitalize', () => {
it('splits underscore and hyphen mode', () => {
expect(splitAndCapitalize('hello_world-test', 'underscore')).toBe('Hello World Test');
});
it('splits camel mode with acronym and number boundaries', () => {
expect(splitAndCapitalize('helloWorldXML2Http', 'camel')).toBe('Hello World Xml 2 Http');
});
it('auto mode prefers underscore splitting when underscore-like chars exist', () => {
expect(splitAndCapitalize('user_name')).toBe('User Name');
expect(splitAndCapitalize('kebab-case')).toBe('Kebab Case');
});
it('auto mode falls back to camel splitting when underscore-like chars are absent', () => {
expect(splitAndCapitalize('helloWorldTest')).toBe('Hello World Test');
});
it('returns empty string for empty or missing input', () => {
expect(splitAndCapitalize('')).toBe('');
expect(splitAndCapitalize(undefined)).toBe('');
});
});

View File

@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import { shouldShowVerifiedEmailBadge } from '../../src/utils/verifiedEmail';
describe('shouldShowVerifiedEmailBadge', () => {
it('returns false when email has not been verified', () => {
expect(
shouldShowVerifiedEmailBadge({
verifiedAt: null,
persistedEmail: 'a@example.com',
currentEmail: 'a@example.com',
isEditing: false,
}),
).toBe(false);
});
it('returns true when verified and not editing', () => {
expect(
shouldShowVerifiedEmailBadge({
verifiedAt: '2025-01-01T10:00:00Z',
persistedEmail: 'a@example.com',
currentEmail: 'other@example.com',
isEditing: false,
}),
).toBe(true);
});
it('while editing, returns true only when trimmed current email matches persisted email', () => {
expect(
shouldShowVerifiedEmailBadge({
verifiedAt: '2025-01-01T10:00:00Z',
persistedEmail: 'a@example.com',
currentEmail: ' a@example.com ',
isEditing: true,
}),
).toBe(true);
expect(
shouldShowVerifiedEmailBadge({
verifiedAt: '2025-01-01T10:00:00Z',
persistedEmail: 'a@example.com',
currentEmail: 'other@example.com',
isEditing: true,
}),
).toBe(false);
});
});

View File

@@ -1,4 +1,4 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
@@ -15,4 +15,18 @@ export default defineConfig({
external: ['react'], external: ['react'],
}, },
}, },
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/index.ts'],
thresholds: {
lines: 80,
functions: 75,
branches: 70,
},
},
},
}); });

728
yarn.lock
View File

@@ -2,7 +2,44 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": "@acemir/cssom@^0.9.31":
version "0.9.31"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@acemir/cssom/-/cssom-0.9.31.tgz#bd5337d290fb8be2ac18391f37386bc53778b0bc"
integrity sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==
"@adobe/css-tools@^4.4.0":
version "4.4.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9"
integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==
"@asamuzakjp/css-color@^5.0.0":
version "5.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@asamuzakjp/css-color/-/css-color-5.0.1.tgz#3b9462a9b52f3c6680a0945a3d0851881017550f"
integrity sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==
dependencies:
"@csstools/css-calc" "^3.1.1"
"@csstools/css-color-parser" "^4.0.2"
"@csstools/css-parser-algorithms" "^4.0.0"
"@csstools/css-tokenizer" "^4.0.0"
lru-cache "^11.2.6"
"@asamuzakjp/dom-selector@^6.8.1":
version "6.8.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz#39b20993672b106f7cd9a3a9a465212e87e0bfd1"
integrity sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==
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.6"
"@asamuzakjp/nwsapi@^2.3.9":
version "2.3.9"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz#ad5549322dfe9d153d4b4dd6f7ff2ae234b06e24"
integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==
"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0":
version "7.29.0" version "7.29.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c"
integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==
@@ -130,6 +167,11 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1"
"@babel/runtime@^7.12.5":
version "7.28.6"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b"
integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
"@babel/template@^7.28.6": "@babel/template@^7.28.6":
version "7.28.6" version "7.28.6"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57"
@@ -160,6 +202,51 @@
"@babel/helper-string-parser" "^7.27.1" "@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5" "@babel/helper-validator-identifier" "^7.28.5"
"@bcoe/v8-coverage@^1.0.2":
version "1.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa"
integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==
"@bramus/specificity@^2.4.2":
version "2.4.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@bramus/specificity/-/specificity-2.4.2.tgz#aa8db8eb173fdee7324f82284833106adeecc648"
integrity sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==
dependencies:
css-tree "^3.0.0"
"@csstools/color-helpers@^6.0.2":
version "6.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@csstools/color-helpers/-/color-helpers-6.0.2.tgz#82c59fd30649cf0b4d3c82160489748666e6550b"
integrity sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==
"@csstools/css-calc@^3.1.1":
version "3.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@csstools/css-calc/-/css-calc-3.1.1.tgz#78b494996dac41a02797dcca18ac3b46d25b3fd7"
integrity sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==
"@csstools/css-color-parser@^4.0.2":
version "4.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz#c27e03a3770d0352db92d668d6dde427a37859e5"
integrity sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==
dependencies:
"@csstools/color-helpers" "^6.0.2"
"@csstools/css-calc" "^3.1.1"
"@csstools/css-parser-algorithms@^4.0.0":
version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz#e1c65dc09378b42f26a111fca7f7075fc2c26164"
integrity sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==
"@csstools/css-syntax-patches-for-csstree@^1.0.28":
version "1.0.28"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz#cd239a16f95c0ed7c6d74315da4e38f2e93bbf19"
integrity sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==
"@csstools/css-tokenizer@^4.0.0":
version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz#798a33950d11226a0ebb6acafa60f5594424967f"
integrity sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==
"@esbuild/aix-ppc64@0.27.3": "@esbuild/aix-ppc64@0.27.3":
version "0.27.3" version "0.27.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2"
@@ -343,6 +430,11 @@
"@eslint/core" "^1.1.0" "@eslint/core" "^1.1.0"
levn "^0.4.1" levn "^0.4.1"
"@exodus/bytes@^1.11.0", "@exodus/bytes@^1.6.0":
version "1.14.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@exodus/bytes/-/bytes-1.14.1.tgz#9b5c29077162a35f1bd25613e0cd3c239f6e7ad8"
integrity sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==
"@humanfs/core@^0.19.1": "@humanfs/core@^0.19.1":
version "0.19.1" version "0.19.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" resolved "https://nexus.beatrice.wtf/repository/npm-group/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
@@ -387,12 +479,12 @@
resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5" version "1.5.5"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31":
version "0.3.31" version "0.3.31"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
@@ -530,6 +622,49 @@
resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c" resolved "https://nexus.beatrice.wtf/repository/npm-group/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c"
integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA== integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==
"@standard-schema/spec@^1.0.0":
version "1.1.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
"@testing-library/dom@^10.4.1":
version "10.4.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95"
integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==
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"
"@testing-library/jest-dom@^6.9.1":
version "6.9.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2"
integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==
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"
"@testing-library/react@^16.3.0":
version "16.3.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@testing-library/react/-/react-16.3.2.tgz#672883b7acb8e775fc0492d9e9d25e06e89786d0"
integrity sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==
dependencies:
"@babel/runtime" "^7.12.5"
"@types/aria-query@^5.0.1":
version "5.0.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
"@types/babel__core@^7.20.5": "@types/babel__core@^7.20.5":
version "7.20.5" version "7.20.5"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
@@ -563,12 +698,25 @@
dependencies: dependencies:
"@babel/types" "^7.28.2" "@babel/types" "^7.28.2"
"@types/chai@^5.2.2":
version "5.2.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a"
integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==
dependencies:
"@types/deep-eql" "*"
assertion-error "^2.0.1"
"@types/deep-eql@*":
version "4.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd"
integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==
"@types/esrecurse@^4.3.1": "@types/esrecurse@^4.3.1":
version "4.3.1" version "4.3.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec"
integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==
"@types/estree@1.0.8", "@types/estree@^1.0.6", "@types/estree@^1.0.8": "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.8" version "1.0.8"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
@@ -578,6 +726,11 @@
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/react-dom@^19.0.0":
version "19.2.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c"
integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==
"@types/react@^19.0.0": "@types/react@^19.0.0":
version "19.2.14" version "19.2.14"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad"
@@ -693,6 +846,80 @@
"@types/babel__core" "^7.20.5" "@types/babel__core" "^7.20.5"
react-refresh "^0.18.0" react-refresh "^0.18.0"
"@vitest/coverage-v8@^4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz#b9c4db7479acd51d5f0ced91b2853c29c3d0cda7"
integrity sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==
dependencies:
"@bcoe/v8-coverage" "^1.0.2"
"@vitest/utils" "4.0.18"
ast-v8-to-istanbul "^0.3.10"
istanbul-lib-coverage "^3.2.2"
istanbul-lib-report "^3.0.1"
istanbul-reports "^3.2.0"
magicast "^0.5.1"
obug "^2.1.1"
std-env "^3.10.0"
tinyrainbow "^3.0.3"
"@vitest/expect@4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/expect/-/expect-4.0.18.tgz#361510d99fbf20eb814222e4afcb8539d79dc94d"
integrity sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==
dependencies:
"@standard-schema/spec" "^1.0.0"
"@types/chai" "^5.2.2"
"@vitest/spy" "4.0.18"
"@vitest/utils" "4.0.18"
chai "^6.2.1"
tinyrainbow "^3.0.3"
"@vitest/mocker@4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/mocker/-/mocker-4.0.18.tgz#b9735da114ef65ea95652c5bdf13159c6fab4865"
integrity sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==
dependencies:
"@vitest/spy" "4.0.18"
estree-walker "^3.0.3"
magic-string "^0.30.21"
"@vitest/pretty-format@4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/pretty-format/-/pretty-format-4.0.18.tgz#fbccd4d910774072ec15463553edb8ca5ce53218"
integrity sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==
dependencies:
tinyrainbow "^3.0.3"
"@vitest/runner@4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/runner/-/runner-4.0.18.tgz#c2c0a3ed226ec85e9312f9cc8c43c5b3a893a8b1"
integrity sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==
dependencies:
"@vitest/utils" "4.0.18"
pathe "^2.0.3"
"@vitest/snapshot@4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/snapshot/-/snapshot-4.0.18.tgz#bcb40fd6d742679c2ac927ba295b66af1c6c34c5"
integrity sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==
dependencies:
"@vitest/pretty-format" "4.0.18"
magic-string "^0.30.21"
pathe "^2.0.3"
"@vitest/spy@4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/spy/-/spy-4.0.18.tgz#ba0f20503fb6d08baf3309d690b3efabdfa88762"
integrity sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==
"@vitest/utils@4.0.18":
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/utils/-/utils-4.0.18.tgz#9636b16d86a4152ec68a8d6859cff702896433d4"
integrity sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==
dependencies:
"@vitest/pretty-format" "4.0.18"
tinyrainbow "^3.0.3"
acorn-jsx@^5.3.2: acorn-jsx@^5.3.2:
version "5.3.2" version "5.3.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" resolved "https://nexus.beatrice.wtf/repository/npm-group/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -703,6 +930,11 @@ acorn@^8.16.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" resolved "https://nexus.beatrice.wtf/repository/npm-group/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
agent-base@^7.1.0, agent-base@^7.1.2:
version "7.1.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
ajv@^6.12.4: ajv@^6.12.4:
version "6.14.0" version "6.14.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" resolved "https://nexus.beatrice.wtf/repository/npm-group/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
@@ -713,6 +945,42 @@ ajv@^6.12.4:
json-schema-traverse "^0.4.1" json-schema-traverse "^0.4.1"
uri-js "^4.2.2" uri-js "^4.2.2"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^5.0.0:
version "5.2.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
aria-query@5.3.0:
version "5.3.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
dependencies:
dequal "^2.0.3"
aria-query@^5.0.0:
version "5.3.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
assertion-error@^2.0.1:
version "2.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
ast-v8-to-istanbul@^0.3.10:
version "0.3.11"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz#725b1f5e2ffdc8d71620cb5e78d6dc976d65e97a"
integrity sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==
dependencies:
"@jridgewell/trace-mapping" "^0.3.31"
estree-walker "^3.0.3"
js-tokens "^10.0.0"
balanced-match@^4.0.2: balanced-match@^4.0.2:
version "4.0.4" version "4.0.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" resolved "https://nexus.beatrice.wtf/repository/npm-group/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
@@ -723,6 +991,13 @@ baseline-browser-mapping@^2.9.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" resolved "https://nexus.beatrice.wtf/repository/npm-group/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9"
integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==
bidi-js@^1.0.3:
version "1.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2"
integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==
dependencies:
require-from-string "^2.0.2"
brace-expansion@^5.0.2: brace-expansion@^5.0.2:
version "5.0.3" version "5.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/brace-expansion/-/brace-expansion-5.0.3.tgz#6a9c6c268f85b53959ec527aeafe0f7300258eef" resolved "https://nexus.beatrice.wtf/repository/npm-group/brace-expansion/-/brace-expansion-5.0.3.tgz#6a9c6c268f85b53959ec527aeafe0f7300258eef"
@@ -742,9 +1017,14 @@ browserslist@^4.24.0:
update-browserslist-db "^1.2.0" update-browserslist-db "^1.2.0"
caniuse-lite@^1.0.30001759: caniuse-lite@^1.0.30001759:
version "1.0.30001772" version "1.0.30001774"
resolved "https://nexus.beatrice.wtf/repository/npm-group/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz#aa8a176eba0006e78c965a8215c7a1ceb030122d" resolved "https://nexus.beatrice.wtf/repository/npm-group/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz#0e576b6f374063abcd499d202b9ba1301be29b70"
integrity sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg== integrity sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==
chai@^6.2.1:
version "6.2.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
convert-source-map@^2.0.0: convert-source-map@^2.0.0:
version "2.0.0" version "2.0.0"
@@ -760,28 +1040,89 @@ cross-spawn@^7.0.6:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
css-tree@^3.0.0, css-tree@^3.1.0:
version "3.1.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/css-tree/-/css-tree-3.1.0.tgz#7aabc035f4e66b5c86f54570d55e05b1346eb0fd"
integrity sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==
dependencies:
mdn-data "2.12.2"
source-map-js "^1.0.1"
css.escape@^1.5.1:
version "1.5.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
cssstyle@^6.0.1:
version "6.1.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/cssstyle/-/cssstyle-6.1.0.tgz#07ae112dd2fbc590d11f6e11c73699bd50f27d51"
integrity sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==
dependencies:
"@asamuzakjp/css-color" "^5.0.0"
"@csstools/css-syntax-patches-for-csstree" "^1.0.28"
css-tree "^3.1.0"
lru-cache "^11.2.6"
csstype@^3.2.2: csstype@^3.2.2:
version "3.2.3" version "3.2.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" resolved "https://nexus.beatrice.wtf/repository/npm-group/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: data-urls@^7.0.0:
version "7.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/data-urls/-/data-urls-7.0.0.tgz#6dce8b63226a1ecfdd907ce18a8ccfb1eee506d3"
integrity sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==
dependencies:
whatwg-mimetype "^5.0.0"
whatwg-url "^16.0.0"
debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.3:
version "4.4.3" version "4.4.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" resolved "https://nexus.beatrice.wtf/repository/npm-group/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies: dependencies:
ms "^2.1.3" ms "^2.1.3"
decimal.js@^10.6.0:
version "10.6.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
deep-is@^0.1.3: deep-is@^0.1.3:
version "0.1.4" version "0.1.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" resolved "https://nexus.beatrice.wtf/repository/npm-group/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
dequal@^2.0.3:
version "2.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
dom-accessibility-api@^0.5.9:
version "0.5.16"
resolved "https://nexus.beatrice.wtf/repository/npm-group/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
dom-accessibility-api@^0.6.3:
version "0.6.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==
electron-to-chromium@^1.5.263: electron-to-chromium@^1.5.263:
version "1.5.302" version "1.5.302"
resolved "https://nexus.beatrice.wtf/repository/npm-group/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz#032a5802b31f7119269959c69fe2015d8dad5edb" resolved "https://nexus.beatrice.wtf/repository/npm-group/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz#032a5802b31f7119269959c69fe2015d8dad5edb"
integrity sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg== integrity sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==
entities@^6.0.0:
version "6.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694"
integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
es-module-lexer@^1.7.0:
version "1.7.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
esbuild@^0.27.0: esbuild@^0.27.0:
version "0.27.3" version "0.27.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" resolved "https://nexus.beatrice.wtf/repository/npm-group/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8"
@@ -824,10 +1165,10 @@ escape-string-regexp@^4.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" resolved "https://nexus.beatrice.wtf/repository/npm-group/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-plugin-react-hooks@^7.0.1: eslint-plugin-react-hooks@^7.1.0-canary-ab18f33d-20260220:
version "7.0.1" version "7.1.0-canary-fd524fe0-20251121"
resolved "https://nexus.beatrice.wtf/repository/npm-group/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169" resolved "https://nexus.beatrice.wtf/repository/npm-group/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.0-canary-fd524fe0-20251121.tgz#976c8feb747505e7eae45e15c5f0f16df4553ef9"
integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA== integrity sha512-G5we0+XjZTKpjkLL9AgdWxzmo4mqelVDIYzoR1dBlhhiN8Lf5PQ+l8frr+BmX02nU4g0AEez3eGSF/LNfHokEw==
dependencies: dependencies:
"@babel/core" "^7.24.4" "@babel/core" "^7.24.4"
"@babel/parser" "^7.24.4" "@babel/parser" "^7.24.4"
@@ -924,11 +1265,23 @@ estraverse@^5.1.0, estraverse@^5.2.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" resolved "https://nexus.beatrice.wtf/repository/npm-group/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^3.0.3:
version "3.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
dependencies:
"@types/estree" "^1.0.0"
esutils@^2.0.2: esutils@^2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://nexus.beatrice.wtf/repository/npm-group/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
expect-type@^1.2.2:
version "1.3.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68"
integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://nexus.beatrice.wtf/repository/npm-group/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -999,6 +1352,11 @@ globals@^17.3.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9" resolved "https://nexus.beatrice.wtf/repository/npm-group/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9"
integrity sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw== integrity sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
hermes-estree@0.25.1: hermes-estree@0.25.1:
version "0.25.1" version "0.25.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480" resolved "https://nexus.beatrice.wtf/repository/npm-group/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
@@ -1011,6 +1369,34 @@ hermes-parser@^0.25.1:
dependencies: dependencies:
hermes-estree "0.25.1" hermes-estree "0.25.1"
html-encoding-sniffer@^6.0.0:
version "6.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz#f8d9390b3b348b50d4f61c16dd2ef5c05980a882"
integrity sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==
dependencies:
"@exodus/bytes" "^1.6.0"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
http-proxy-agent@^7.0.2:
version "7.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
dependencies:
agent-base "^7.1.0"
debug "^4.3.4"
https-proxy-agent@^7.0.6:
version "7.0.6"
resolved "https://nexus.beatrice.wtf/repository/npm-group/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
dependencies:
agent-base "^7.1.2"
debug "4"
ignore@^5.2.0: ignore@^5.2.0:
version "5.3.2" version "5.3.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" resolved "https://nexus.beatrice.wtf/repository/npm-group/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@@ -1026,6 +1412,11 @@ imurmurhash@^0.1.4:
resolved "https://nexus.beatrice.wtf/repository/npm-group/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" resolved "https://nexus.beatrice.wtf/repository/npm-group/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
indent-string@^4.0.0:
version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
is-extglob@^2.1.1: is-extglob@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" resolved "https://nexus.beatrice.wtf/repository/npm-group/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1038,16 +1429,75 @@ is-glob@^4.0.0, is-glob@^4.0.3:
dependencies: dependencies:
is-extglob "^2.1.1" is-extglob "^2.1.1"
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
isexe@^2.0.0: isexe@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://nexus.beatrice.wtf/repository/npm-group/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2:
version "3.2.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1:
version "3.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
dependencies:
istanbul-lib-coverage "^3.0.0"
make-dir "^4.0.0"
supports-color "^7.1.0"
istanbul-reports@^3.2.0:
version "3.2.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93"
integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==
dependencies:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
js-tokens@^10.0.0:
version "10.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/js-tokens/-/js-tokens-10.0.0.tgz#dffe7599b4a8bb7fe30aff8d0235234dffb79831"
integrity sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==
js-tokens@^4.0.0: js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://nexus.beatrice.wtf/repository/npm-group/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
jsdom@^28.1.0:
version "28.1.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/jsdom/-/jsdom-28.1.0.tgz#ac4203e58fd24d7b0f34359ab00d6d9caebd4b62"
integrity sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==
dependencies:
"@acemir/cssom" "^0.9.31"
"@asamuzakjp/dom-selector" "^6.8.1"
"@bramus/specificity" "^2.4.2"
"@exodus/bytes" "^1.11.0"
cssstyle "^6.0.1"
data-urls "^7.0.0"
decimal.js "^10.6.0"
html-encoding-sniffer "^6.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"
saxes "^6.0.0"
symbol-tree "^3.2.4"
tough-cookie "^6.0.0"
undici "^7.21.0"
w3c-xmlserializer "^5.0.0"
webidl-conversions "^8.0.1"
whatwg-mimetype "^5.0.0"
whatwg-url "^16.0.0"
xml-name-validator "^5.0.0"
jsesc@^3.0.2: jsesc@^3.0.2:
version "3.1.0" version "3.1.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" resolved "https://nexus.beatrice.wtf/repository/npm-group/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
@@ -1095,6 +1545,11 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lru-cache@^11.2.6:
version "11.2.6"
resolved "https://nexus.beatrice.wtf/repository/npm-group/lru-cache/-/lru-cache-11.2.6.tgz#356bf8a29e88a7a2945507b31f6429a65a192c58"
integrity sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==
lru-cache@^5.1.1: lru-cache@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" resolved "https://nexus.beatrice.wtf/repository/npm-group/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -1102,6 +1557,44 @@ lru-cache@^5.1.1:
dependencies: dependencies:
yallist "^3.0.2" yallist "^3.0.2"
lz-string@^1.5.0:
version "1.5.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
magic-string@^0.30.21:
version "0.30.21"
resolved "https://nexus.beatrice.wtf/repository/npm-group/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
magicast@^0.5.1:
version "0.5.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/magicast/-/magicast-0.5.2.tgz#70cea9df729c164485049ea5df85a390281dfb9d"
integrity sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==
dependencies:
"@babel/parser" "^7.29.0"
"@babel/types" "^7.29.0"
source-map-js "^1.2.1"
make-dir@^4.0.0:
version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
dependencies:
semver "^7.5.3"
mdn-data@2.12.2:
version "2.12.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/mdn-data/-/mdn-data-2.12.2.tgz#9ae6c41a9e65adf61318b32bff7b64fbfb13f8cf"
integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==
min-indent@^1.0.0:
version "1.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
minimatch@^10.2.1: minimatch@^10.2.1:
version "10.2.2" version "10.2.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f" resolved "https://nexus.beatrice.wtf/repository/npm-group/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
@@ -1136,6 +1629,11 @@ node-releases@^2.0.27:
resolved "https://nexus.beatrice.wtf/repository/npm-group/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" resolved "https://nexus.beatrice.wtf/repository/npm-group/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
obug@^2.1.1:
version "2.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/obug/-/obug-2.1.1.tgz#2cba74ff241beb77d63055ddf4cd1e9f90b538be"
integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==
optionator@^0.9.3: optionator@^0.9.3:
version "0.9.4" version "0.9.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" resolved "https://nexus.beatrice.wtf/repository/npm-group/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
@@ -1162,6 +1660,13 @@ p-locate@^5.0.0:
dependencies: dependencies:
p-limit "^3.0.2" p-limit "^3.0.2"
parse5@^8.0.0:
version "8.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/parse5/-/parse5-8.0.0.tgz#aceb267f6b15f9b6e6ba9e35bfdd481fc2167b12"
integrity sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==
dependencies:
entities "^6.0.0"
path-exists@^4.0.0: path-exists@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" resolved "https://nexus.beatrice.wtf/repository/npm-group/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -1172,7 +1677,12 @@ path-key@^3.1.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" resolved "https://nexus.beatrice.wtf/repository/npm-group/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
picocolors@^1.1.1: pathe@^2.0.3:
version "2.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
picocolors@1.1.1, picocolors@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" resolved "https://nexus.beatrice.wtf/repository/npm-group/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -1201,7 +1711,16 @@ prettier@^3.8.1:
resolved "https://nexus.beatrice.wtf/repository/npm-group/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" resolved "https://nexus.beatrice.wtf/repository/npm-group/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==
punycode@^2.1.0: pretty-format@^27.0.2:
version "27.5.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
dependencies:
ansi-regex "^5.0.1"
ansi-styles "^5.0.0"
react-is "^17.0.1"
punycode@^2.1.0, punycode@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" resolved "https://nexus.beatrice.wtf/repository/npm-group/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@@ -1213,6 +1732,11 @@ react-dom@^19.0.0:
dependencies: dependencies:
scheduler "^0.27.0" scheduler "^0.27.0"
react-is@^17.0.1:
version "17.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-refresh@^0.18.0: react-refresh@^0.18.0:
version "0.18.0" version "0.18.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" resolved "https://nexus.beatrice.wtf/repository/npm-group/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062"
@@ -1223,6 +1747,19 @@ react@^19.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" resolved "https://nexus.beatrice.wtf/repository/npm-group/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a"
integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==
redent@^3.0.0:
version "3.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
dependencies:
indent-string "^4.0.0"
strip-indent "^3.0.0"
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
rollup@^4.43.0: rollup@^4.43.0:
version "4.59.0" version "4.59.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f" resolved "https://nexus.beatrice.wtf/repository/npm-group/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f"
@@ -1257,6 +1794,13 @@ rollup@^4.43.0:
"@rollup/rollup-win32-x64-msvc" "4.59.0" "@rollup/rollup-win32-x64-msvc" "4.59.0"
fsevents "~2.3.2" fsevents "~2.3.2"
saxes@^6.0.0:
version "6.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
dependencies:
xmlchars "^2.2.0"
scheduler@^0.27.0: scheduler@^0.27.0:
version "0.27.0" version "0.27.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" resolved "https://nexus.beatrice.wtf/repository/npm-group/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
@@ -1267,7 +1811,7 @@ semver@^6.3.1:
resolved "https://nexus.beatrice.wtf/repository/npm-group/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" resolved "https://nexus.beatrice.wtf/repository/npm-group/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.7.3: semver@^7.5.3, semver@^7.7.3:
version "7.7.4" version "7.7.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" resolved "https://nexus.beatrice.wtf/repository/npm-group/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
@@ -1284,11 +1828,55 @@ shebang-regex@^3.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" resolved "https://nexus.beatrice.wtf/repository/npm-group/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
source-map-js@^1.2.1: siginfo@^2.0.0:
version "2.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
source-map-js@^1.0.1, source-map-js@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" resolved "https://nexus.beatrice.wtf/repository/npm-group/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
stackback@0.0.2:
version "0.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
std-env@^3.10.0:
version "3.10.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b"
integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
strip-indent@^3.0.0:
version "3.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
dependencies:
min-indent "^1.0.0"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
tinybench@^2.9.0:
version "2.9.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
tinyexec@^1.0.2:
version "1.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyexec/-/tinyexec-1.0.2.tgz#bdd2737fe2ba40bd6f918ae26642f264b99ca251"
integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==
tinyglobby@^0.2.15: tinyglobby@^0.2.15:
version "0.2.15" version "0.2.15"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
@@ -1297,6 +1885,37 @@ tinyglobby@^0.2.15:
fdir "^6.5.0" fdir "^6.5.0"
picomatch "^4.0.3" picomatch "^4.0.3"
tinyrainbow@^3.0.3:
version "3.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyrainbow/-/tinyrainbow-3.0.3.tgz#984a5b1c1b25854a9b6bccbe77964d0593d1ea42"
integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==
tldts-core@^7.0.23:
version "7.0.23"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tldts-core/-/tldts-core-7.0.23.tgz#47bf18282a44641304a399d247703413b5d3e309"
integrity sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==
tldts@^7.0.5:
version "7.0.23"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tldts/-/tldts-7.0.23.tgz#444f0f0720fa777839a23ea665e04f61ee57217a"
integrity sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==
dependencies:
tldts-core "^7.0.23"
tough-cookie@^6.0.0:
version "6.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tough-cookie/-/tough-cookie-6.0.0.tgz#11e418b7864a2c0d874702bc8ce0f011261940e5"
integrity sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==
dependencies:
tldts "^7.0.5"
tr46@^6.0.0:
version "6.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tr46/-/tr46-6.0.0.tgz#f5a1ae546a0adb32a277a2278d0d17fa2f9093e6"
integrity sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==
dependencies:
punycode "^2.3.1"
ts-api-utils@^2.4.0: ts-api-utils@^2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" resolved "https://nexus.beatrice.wtf/repository/npm-group/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8"
@@ -1324,6 +1943,11 @@ typescript@^5.6.2:
resolved "https://nexus.beatrice.wtf/repository/npm-group/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" resolved "https://nexus.beatrice.wtf/repository/npm-group/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
undici@^7.21.0:
version "7.22.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/undici/-/undici-7.22.0.tgz#7a82590a5908e504a47d85c60b0f89ca14240e60"
integrity sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==
update-browserslist-db@^1.2.0: update-browserslist-db@^1.2.0:
version "1.2.3" version "1.2.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" resolved "https://nexus.beatrice.wtf/repository/npm-group/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
@@ -1339,7 +1963,7 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
vite@^7.0.0: "vite@^6.0.0 || ^7.0.0", vite@^7.0.0:
version "7.3.1" version "7.3.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" resolved "https://nexus.beatrice.wtf/repository/npm-group/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507"
integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==
@@ -1353,6 +1977,58 @@ vite@^7.0.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.3" fsevents "~2.3.3"
vitest@^4.0.18:
version "4.0.18"
resolved "https://nexus.beatrice.wtf/repository/npm-group/vitest/-/vitest-4.0.18.tgz#56f966353eca0b50f4df7540cd4350ca6d454a05"
integrity sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==
dependencies:
"@vitest/expect" "4.0.18"
"@vitest/mocker" "4.0.18"
"@vitest/pretty-format" "4.0.18"
"@vitest/runner" "4.0.18"
"@vitest/snapshot" "4.0.18"
"@vitest/spy" "4.0.18"
"@vitest/utils" "4.0.18"
es-module-lexer "^1.7.0"
expect-type "^1.2.2"
magic-string "^0.30.21"
obug "^2.1.1"
pathe "^2.0.3"
picomatch "^4.0.3"
std-env "^3.10.0"
tinybench "^2.9.0"
tinyexec "^1.0.2"
tinyglobby "^0.2.15"
tinyrainbow "^3.0.3"
vite "^6.0.0 || ^7.0.0"
why-is-node-running "^2.3.0"
w3c-xmlserializer@^5.0.0:
version "5.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
dependencies:
xml-name-validator "^5.0.0"
webidl-conversions@^8.0.1:
version "8.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/webidl-conversions/-/webidl-conversions-8.0.1.tgz#0657e571fe6f06fcb15ca50ed1fdbcb495cd1686"
integrity sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==
whatwg-mimetype@^5.0.0:
version "5.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz#d8232895dbd527ceaee74efd4162008fb8a8cf48"
integrity sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==
whatwg-url@^16.0.0:
version "16.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/whatwg-url/-/whatwg-url-16.0.1.tgz#047f7f4bd36ef76b7198c172d1b1cebc66f764dd"
integrity sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==
dependencies:
"@exodus/bytes" "^1.11.0"
tr46 "^6.0.0"
webidl-conversions "^8.0.1"
which@^2.0.1: which@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" resolved "https://nexus.beatrice.wtf/repository/npm-group/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -1360,11 +2036,29 @@ which@^2.0.1:
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
why-is-node-running@^2.3.0:
version "2.3.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"
integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==
dependencies:
siginfo "^2.0.0"
stackback "0.0.2"
word-wrap@^1.2.5: word-wrap@^1.2.5:
version "1.2.5" version "1.2.5"
resolved "https://nexus.beatrice.wtf/repository/npm-group/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" resolved "https://nexus.beatrice.wtf/repository/npm-group/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
xml-name-validator@^5.0.0:
version "5.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
yallist@^3.0.2: yallist@^3.0.2:
version "3.1.1" version "3.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" resolved "https://nexus.beatrice.wtf/repository/npm-group/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"