Compare commits

..

17 Commits

Author SHA1 Message Date
9056f3fc10 Update dependency @codemirror/language to v6.12.2
Some checks failed
continuous-integration/drone/pr Build is failing
2026-02-25 22:14:20 +00:00
1523f7be2c add unit tests
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 17:55:26 +01:00
b664c99944 fix sonar issues
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 16:27:53 +01:00
3d4a4a5f57 rewrite datepicker, v0.1.18
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is passing
2026-02-24 16:22:12 +01:00
f9864842b5 fix sonar bugs
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 12:18:55 +01:00
dd084369e9 update thresholds
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 12:10:36 +01:00
850eed0766 update ci
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 12:06:43 +01:00
4904bea29c update unit tests coverage
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 11:51:08 +01:00
4fc3738adf add unit tests
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 11:33:37 +01:00
e17c82de2f fix eslint ver
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 11:23:41 +01:00
a527ce27cd add code analysis
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 11:21:26 +01:00
623e45d241 update eslint
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 11:15:26 +01:00
5593746cf4 remove unused css
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-24 10:43:22 +01:00
b163fdaa62 update styles, v0.1.17
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2026-02-24 10:37:36 +01:00
ec63a10027 update ci
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2026-02-24 10:14:08 +01:00
5ada69773c update ci, datepicker, v0.1.16
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-24 10:06:41 +01:00
370d6e7e0a v0.1.15
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2026-02-23 23:53:32 +01:00
30 changed files with 4681 additions and 96 deletions

View File

@@ -13,26 +13,47 @@ trigger:
steps:
- name: install
image: node:22
image: node:25
commands:
- corepack enable
- corepack prepare yarn@1.22.22 --activate
- yarn install --frozen-lockfile
- name: lint
image: node:22
image: node:25
commands:
- corepack enable
- corepack prepare yarn@1.22.22 --activate
- yarn lint
- name: build
image: node:22
image: node:25
commands:
- corepack enable
- corepack prepare yarn@1.22.22 --activate
- 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
type: docker
@@ -46,14 +67,11 @@ trigger:
steps:
- name: publish-npm
image: node:22
image: node:25
environment:
NEXUS_NPM_TOKEN:
from_secret: nexus_npm_token
commands:
- corepack enable
- corepack prepare yarn@1.22.22 --activate
- yarn install --frozen-lockfile
- node -e "const pkg=require('./package.json').version; const tag=process.env.DRONE_TAG; if('v'+pkg!==tag){console.error('Tag/version mismatch: v'+pkg+' != '+tag); process.exit(1);}"
- npm config set //nexus.beatrice.wtf/repository/npm-hosted/:_authToken "$NEXUS_NPM_TOKEN"
- yarn publish:nexus

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@panic/web-ui",
"version": "0.1.14",
"version": "0.1.18",
"license": "AGPL-3.0-only",
"description": "Core components for panic.haus web applications",
"type": "module",
@@ -27,6 +27,9 @@
"scripts": {
"clean": "rm -rf dist",
"build": "yarn clean && vite build && tsc -p tsconfig.build.json && mkdir -p dist/styles && cp src/styles/base.css dist/styles/base.css && tailwindcss -c tailwind.build.config.cjs -i src/styles/components.css -o dist/styles/components.css --minify && tailwindcss -c tailwind.build.config.cjs -i src/styles/utilities.css -o dist/styles/utilities.css --minify && cp tailwind-preset.cjs dist/tailwind-preset.cjs",
"test": "vitest run",
"test:coverage": "vitest run --coverage --coverage.reporter=lcov --coverage.reporter=text-summary",
"test:watch": "vitest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier . --write",
@@ -64,13 +67,17 @@
"@storybook/react": "^10.2.10",
"@storybook/react-vite": "^10.2.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-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.0",
"@vitest/coverage-v8": "^4.0.18",
"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",
"globals": "^17.3.0",
"jsdom": "^28.1.0",
"postcss": "^8.4.49",
"prettier": "^3.8.1",
"react": "^19.0.0",
@@ -81,6 +88,10 @@
"typescript": "^5.6.2",
"typescript-eslint": "^8.56.0",
"vite": "^7.0.0",
"vitest": "^4.0.18",
"yjs": "^13.6.24"
},
"dependencies": {
"@types/node": "^25.3.0"
}
}

View File

@@ -11,7 +11,7 @@ const meta = {
docs: {
description: {
component:
'Date selection field with InputField-compatible API, supporting date/time/datetime-local values, size/layout variants, and validation state.',
'In-house date/time selection field with InputField-compatible API. Uses a custom popup (not native browser pickers) and supports date, time, and date-time modes.',
},
},
},
@@ -27,10 +27,10 @@ const meta = {
table: { type: { summary: 'string' } },
},
type: {
description: 'Native date input type.',
options: ['date', 'datetime-local', 'time'],
description: 'DatePicker mode.',
options: ['date', 'date-time', 'time'],
control: 'inline-radio',
table: { type: { summary: "'date' | 'datetime-local' | 'time'" } },
table: { type: { summary: "'date' | 'date-time' | 'time'" } },
},
size: {
description: 'Input size.',
@@ -55,6 +55,22 @@ const meta = {
control: 'text',
table: { type: { summary: 'string' } },
},
format: {
description:
'Optional input/output format. Supported tokens: `dd`, `mm`, `yyyy`, `HH` (for example `dd/mm/yyyy HH:mm`).',
control: 'text',
table: { type: { summary: 'string' } },
},
min: {
description: 'Optional minimum value in the same format as `value`.',
control: 'text',
table: { type: { summary: 'string' } },
},
max: {
description: 'Optional maximum value in the same format as `value`.',
control: 'text',
table: { type: { summary: 'string' } },
},
name: {
description: 'Native input `name` attribute.',
control: 'text',
@@ -108,7 +124,7 @@ const meta = {
},
args: {
label: 'Schedule at',
type: 'datetime-local',
type: 'date-time',
value: '',
size: 'md',
width: 'md',
@@ -124,8 +140,8 @@ export const DateOnly: Story = {
type: 'date',
label: 'Publish date',
},
render: (args) => {
const [value, setValue] = useState('2031-05-20');
render: function DateOnlyRender(args) {
const [value, setValue] = useState('2031/05/20');
return (
<DatePicker
{...args}
@@ -141,11 +157,11 @@ export const DateOnly: Story = {
export const DateTime: Story = {
args: {
type: 'datetime-local',
type: 'date-time',
label: 'Schedule at',
},
render: (args) => {
const [value, setValue] = useState('2031-05-20T14:30');
render: function DateTimeRender(args) {
const [value, setValue] = useState('2031/05/20 14:30');
return (
<DatePicker
{...args}
@@ -167,7 +183,7 @@ export const TimeOnlyInline: Story = {
size: 'sm',
rightIcon: <CalendarDaysIcon className="h-4 w-4 ui-body-secondary" />,
},
render: (args) => {
render: function TimeOnlyInlineRender(args) {
const [value, setValue] = useState('09:00');
return (
<DatePicker
@@ -182,9 +198,10 @@ export const TimeOnlyInline: Story = {
},
};
export const Error: Story = {
export const ErrorState: Story = {
name: 'Error',
args: {
type: 'datetime-local',
type: 'date-time',
label: 'Schedule at',
value: '',
error: 'Pick a valid future date and time',
@@ -193,20 +210,20 @@ export const Error: Story = {
export const Disabled: Story = {
args: {
type: 'datetime-local',
type: 'date-time',
label: 'Published at',
value: '2031-05-20T14:30',
value: '2031/05/20 14:30',
disabled: true,
},
};
export const SizeMatrix: Story = {
args: {
type: 'datetime-local',
type: 'date-time',
label: 'Schedule at',
},
render: (args) => {
const [value, setValue] = useState('2031-05-20T14:30');
render: function SizeMatrixRender(args) {
const [value, setValue] = useState('2031/05/20 14:30');
return (
<div className="grid grid-cols-1 gap-3">
<DatePicker
@@ -238,3 +255,26 @@ export const SizeMatrix: Story = {
);
},
};
export const CustomFormatWithRange: Story = {
args: {
type: 'date-time',
label: 'Starts at',
format: 'dd/mm/yyyy HH:mm',
min: '10/03/2026 09:00',
max: '24/03/2026 18:30',
},
render: function CustomFormatWithRangeRender(args) {
const [value, setValue] = useState('22/03/2026 14:30');
return (
<DatePicker
{...args}
value={value}
onChange={(event) => {
setValue(event.target.value);
args.onChange?.(event);
}}
/>
);
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -99,7 +99,7 @@ export default meta;
type Story = StoryObj<typeof meta>;
export const Stacked: Story = {
render: (args) => {
render: function StackedRender(args) {
const [value, setValue] = useState(args.value);
return (
<Dropdown
@@ -119,7 +119,7 @@ export const Inline: Story = {
layout: 'inline',
size: 'sm',
},
render: (args) => {
render: function InlineRender(args) {
const [value, setValue] = useState(args.value);
return (
<Dropdown
@@ -147,7 +147,7 @@ export const WithError: Story = {
};
export const SizeMatrix: Story = {
render: (args) => {
render: function SizeMatrixRender(args) {
const [value, setValue] = useState(args.value);
return (
<div className="grid grid-cols-1 gap-3">

View File

@@ -67,7 +67,7 @@ export const Basic: Story = {
};
export const WithActions: Story = {
render: (args) => {
render: function WithActionsRender(args) {
const [title, setTitle] = useState('Storybook powered CMS');
const [status, setStatus] = useState('draft');

View File

@@ -127,7 +127,7 @@ export const Text: Story = {
label: 'Title',
placeholder: 'Write a title',
},
render: (args) => {
render: function TextRender(args) {
const [value, setValue] = useState('Storybook integration');
return (
<InputField
@@ -148,7 +148,7 @@ export const PasswordWithToggle: Story = {
label: 'Password',
placeholder: 'Type a strong password',
},
render: (args) => {
render: function PasswordWithToggleRender(args) {
const [value, setValue] = useState('pa55word');
return (
<InputField
@@ -171,7 +171,7 @@ export const InlineWithIcon: Story = {
size: 'sm',
rightIcon: <MagnifyingGlassIcon className="h-4 w-4 ui-body-secondary" />,
},
render: (args) => {
render: function InlineWithIconRender(args) {
const [value, setValue] = useState('posts');
return (
<InputField
@@ -186,7 +186,8 @@ export const InlineWithIcon: Story = {
},
};
export const Error: Story = {
export const ErrorState: Story = {
name: 'Error',
args: {
type: 'email',
label: 'Email',
@@ -210,7 +211,7 @@ export const SizeMatrix: Story = {
label: 'Name',
placeholder: 'Enter value',
},
render: (args) => {
render: function SizeMatrixRender(args) {
const [value, setValue] = useState('Beatrice');
return (
<div className="grid grid-cols-1 gap-3">

View File

@@ -53,7 +53,8 @@ type Story = StoryObj<typeof meta>;
export const Body: Story = {};
export const Error: Story = {
export const ErrorState: Story = {
name: 'Error',
args: {
variant: 'error',
children: 'This field is required',

View File

@@ -115,7 +115,7 @@ export default meta;
type Story = StoryObj<typeof meta>;
export const Editable: Story = {
render: (args) => {
render: function EditableRender(args) {
const [markdown, setMarkdown] = useState(args.markdown);
return (
<div className="w-full max-w-2xl">

View File

@@ -183,7 +183,7 @@ export const Empty: Story = {
};
export const InteractiveSortingAndPagination: Story = {
render: () => {
render: function InteractiveSortingAndPaginationRender() {
const [sorting, setSorting] = useState<SortState | null>({
field: 'name',
direction: 'asc',

View File

@@ -10,6 +10,7 @@
--bg-page: #16121a;
--surface-bg: rgba(24, 24, 27, 0.45);
--surface-bg-strong: rgba(24, 24, 27, 0.62);
--datepicker-menu-bg: #18181b;
--surface-border: rgba(82, 82, 91, 0.6);
--surface-divider: rgba(63, 63, 70, 0.85);
--text-primary: #d5cfdf;
@@ -59,6 +60,7 @@
--bg-page: #f7f7fb;
--surface-bg: rgba(255, 255, 255, 0.9);
--surface-bg-strong: rgba(255, 255, 255, 0.98);
--datepicker-menu-bg: #ffffff;
--surface-border: rgba(161, 161, 170, 0.45);
--surface-divider: rgba(212, 212, 216, 0.9);
--text-primary: #52485c;

View File

@@ -30,24 +30,6 @@
-webkit-text-fill-color: var(--field-selection-text);
}
.field[type='date']:focus::-webkit-datetime-edit-day-field,
.field[type='date']:focus::-webkit-datetime-edit-month-field,
.field[type='date']:focus::-webkit-datetime-edit-year-field,
.field[type='time']:focus::-webkit-datetime-edit-hour-field,
.field[type='time']:focus::-webkit-datetime-edit-minute-field,
.field[type='time']:focus::-webkit-datetime-edit-ampm-field,
.field[type='datetime-local']:focus::-webkit-datetime-edit-day-field,
.field[type='datetime-local']:focus::-webkit-datetime-edit-month-field,
.field[type='datetime-local']:focus::-webkit-datetime-edit-year-field,
.field[type='datetime-local']:focus::-webkit-datetime-edit-hour-field,
.field[type='datetime-local']:focus::-webkit-datetime-edit-minute-field,
.field[type='datetime-local']:focus::-webkit-datetime-edit-ampm-field {
border-radius: 0.375rem;
background-color: var(--field-selection-bg) !important;
color: var(--field-selection-text) !important;
-webkit-text-fill-color: var(--field-selection-text) !important;
}
.field:disabled {
border-color: var(--field-disabled-border);
background-color: var(--field-disabled-bg);
@@ -345,3 +327,190 @@
color: var(--text-secondary);
@apply px-4 py-3 text-sm;
}
.datepicker-icon-btn {
color: var(--text-muted);
@apply absolute inset-y-0 right-2 my-auto inline-flex h-6 w-6 items-center justify-center rounded-md border border-transparent bg-transparent p-0 transition;
}
.datepicker-icon-btn:hover {
color: var(--text-primary);
background-color: var(--ghost-hover);
}
.datepicker-icon-btn:focus-visible {
outline: none;
border-color: rgb(var(--accent-400));
box-shadow: 0 0 0 2px rgb(var(--accent-400) / 0.3);
}
.datepicker-icon-btn:disabled {
color: var(--ghost-disabled-text);
background-color: transparent;
@apply cursor-not-allowed;
}
.datepicker-popup {
border: 1px solid var(--surface-divider);
background-color: var(--surface-bg-strong);
box-shadow: var(--shadow-glow);
color: var(--text-primary);
backdrop-filter: saturate(145%) blur(var(--auth-glass-blur));
-webkit-backdrop-filter: saturate(145%) blur(var(--auth-glass-blur));
will-change: backdrop-filter;
max-width: min(96vw, 440px);
@apply fixed z-[70] flex flex-col gap-3 rounded-xl p-3;
}
.datepicker-popup-top {
transform-origin: bottom center;
}
.datepicker-popup-bottom {
transform-origin: top center;
}
@screen sm {
.datepicker-popup {
@apply flex-row;
}
}
.datepicker-panel {
@apply min-w-0;
}
.datepicker-calendar-nav {
@apply mb-2 flex items-center justify-between gap-2;
}
.datepicker-nav-btn {
border: 1px solid var(--ghost-border);
background-color: var(--ghost-bg);
color: var(--text-secondary);
@apply inline-flex h-8 w-8 items-center justify-center rounded-lg transition;
}
.datepicker-nav-btn:hover {
background-color: var(--ghost-hover);
color: var(--text-primary);
}
.datepicker-heading-controls {
@apply relative flex items-center gap-2;
}
.datepicker-chooser {
@apply relative;
}
.datepicker-chooser-btn {
border: 1px solid var(--field-border);
background-color: var(--field-bg);
color: var(--text-primary);
@apply inline-flex h-8 items-center rounded-lg px-2.5 text-xs font-semibold;
}
.datepicker-chooser-menu {
border: 1px solid var(--surface-divider);
background-color: var(--datepicker-menu-bg);
box-shadow: var(--shadow-glow);
@apply absolute left-0 top-full z-20 mt-1 max-h-48 min-w-[9rem] overflow-y-auto rounded-lg p-1;
}
.datepicker-chooser-option {
color: var(--text-secondary);
@apply block w-full rounded-md px-2 py-1.5 text-left text-xs font-medium transition;
}
.datepicker-chooser-option:hover {
background-color: var(--ghost-hover);
color: var(--text-primary);
}
.datepicker-chooser-option.is-selected {
background-color: rgb(var(--accent-500) / 0.22);
color: rgb(var(--accent-300));
}
.datepicker-weekdays {
@apply mb-1 grid grid-cols-7 gap-1;
}
.datepicker-weekday {
color: var(--text-muted);
@apply text-center text-[0.65rem] font-semibold uppercase tracking-[0.08em];
}
.datepicker-grid {
@apply grid grid-cols-7 gap-1;
}
.datepicker-day {
color: var(--text-secondary);
@apply inline-flex h-8 w-8 items-center justify-center rounded-lg text-sm transition;
}
.datepicker-day:hover {
background-color: var(--ghost-hover);
color: var(--text-primary);
}
.datepicker-day.is-selected {
background-color: rgb(var(--accent-500));
color: rgb(var(--accent-contrast));
}
.datepicker-day.is-today {
border: 1px solid rgb(var(--accent-400) / 0.65);
}
.datepicker-day.is-outside-month {
color: var(--text-soft);
}
.datepicker-day:disabled {
color: var(--ghost-disabled-text);
background-color: transparent;
@apply cursor-not-allowed opacity-50;
}
.datepicker-time-root {
@apply grid min-w-[180px] grid-cols-2 gap-2;
}
.datepicker-time-column {
@apply flex min-w-0 flex-col gap-1;
}
.datepicker-time-title {
color: var(--text-muted);
@apply text-[0.65rem] font-semibold uppercase tracking-[0.08em];
}
.datepicker-time-list {
border: 1px solid var(--field-border);
background-color: var(--field-bg);
@apply max-h-52 overflow-y-auto rounded-lg p-1;
}
.datepicker-time-option {
color: var(--text-secondary);
@apply block w-full rounded-md px-2 py-1.5 text-left text-xs font-semibold transition;
}
.datepicker-time-option:hover {
background-color: var(--ghost-hover);
color: var(--text-primary);
}
.datepicker-time-option.is-selected {
background-color: rgb(var(--accent-500) / 0.22);
color: rgb(var(--accent-300));
}
.datepicker-time-option:disabled {
color: var(--ghost-disabled-text);
background-color: transparent;
@apply cursor-not-allowed opacity-55;
}

View File

@@ -0,0 +1,77 @@
import { HomeIcon } from '@heroicons/react/24/solid';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Button } from '../../src/components/Button';
import { renderWithRouter } from '../helpers/renderWithRouter';
describe('Button', () => {
it('renders native button with expected type and disabled state', () => {
render(<Button label="Save" type="solid" htmlType="submit" disabled />);
const button = screen.getByRole('button', { name: 'Save' });
expect(button.tagName).toBe('BUTTON');
expect(button).toHaveAttribute('type', 'submit');
expect(button).toBeDisabled();
expect(button).toHaveClass('btn-solid');
expect(button).toHaveClass('btn-primary');
});
it('defaults non-solid button variants to secondary', () => {
render(<Button label="Details" type="noborder" />);
expect(screen.getByRole('button', { name: 'Details' })).toHaveClass('btn-secondary');
});
it('uses explicit variant when provided', () => {
render(<Button label="Danger" type="outlined" variant="important" />);
expect(screen.getByRole('button', { name: 'Danger' })).toHaveClass('btn-important');
});
it('renders icon-only button and custom aria label', () => {
render(<Button type="solid" icon={HomeIcon} ariaLabel="Open home" />);
const button = screen.getByRole('button', { name: 'Open home' });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('!p-0');
expect(button.textContent).toBe('');
});
it('renders link button and prevents click when disabled', () => {
const onClick = vi.fn();
renderWithRouter(
<Button label="Go home" type="outlined" to="/home" onClick={onClick} disabled />,
);
const link = screen.getByRole('link', { name: 'Go home' });
fireEvent.click(link);
expect(link).toHaveAttribute('aria-disabled', 'true');
expect(link).toHaveAttribute('tabindex', '-1');
expect(onClick).not.toHaveBeenCalled();
});
it('calls onClick for enabled link buttons', () => {
const onClick = vi.fn();
renderWithRouter(<Button label="Profile" type="solid" to="/profile" onClick={onClick} />);
fireEvent.click(screen.getByRole('link', { name: 'Profile' }));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('renders icon+label spacing and custom class names', () => {
render(
<Button
label="Home"
type="outlined"
icon={HomeIcon}
className="custom-button"
width="full"
/>,
);
const button = screen.getByRole('button', { name: 'Home' });
expect(button).toHaveClass('gap-1.5');
expect(button).toHaveClass('custom-button');
expect(button).toHaveClass('w-full');
});
});

View File

@@ -0,0 +1,62 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Chip } from '../../src/components/Chip';
describe('Chip', () => {
it('renders default span with solid classes', () => {
render(<Chip>Default</Chip>);
const chip = screen.getByText('Default');
expect(chip.tagName).toBe('SPAN');
expect(chip.className).toContain('chip-solid');
});
it('supports outlined variant with valid tone', () => {
render(
<Chip as="div" variant="outlined" tone="indigo-700">
Custom
</Chip>,
);
const chip = screen.getByText('Custom');
expect(chip.tagName).toBe('DIV');
expect(chip.className).toContain('chip-outlined');
expect(chip).toHaveStyle({
borderColor: 'rgb(67, 56, 202)',
color: 'rgb(67, 56, 202)',
});
});
it('supports direct tone tokens without shades for solid variant', () => {
render(<Chip tone="white">Solid</Chip>);
expect(screen.getByText('Solid')).toHaveStyle({
borderColor: 'rgb(255, 255, 255)',
backgroundColor: 'rgb(255, 255, 255)',
color: 'rgb(255, 255, 255)',
});
});
it('ignores invalid/empty tones and keeps className', () => {
const { rerender } = render(
<Chip tone="not-a-token" className="chip-custom">
Invalid
</Chip>,
);
const invalid = screen.getByText('Invalid');
expect(invalid).toHaveClass('chip-custom');
expect(invalid.getAttribute('style')).toBeNull();
rerender(<Chip tone=" ">Blank</Chip>);
expect(screen.getByText('Blank').getAttribute('style')).toBeNull();
});
it('ignores unresolved direct palettes and unknown shades', () => {
const { rerender } = render(<Chip tone="indigo">Palette token</Chip>);
expect(screen.getByText('Palette token').getAttribute('style')).toBeNull();
rerender(<Chip tone="indigo-999">Unknown shade</Chip>);
expect(screen.getByText('Unknown shade').getAttribute('style')).toBeNull();
});
});

View File

@@ -0,0 +1,428 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { __datePickerTestUtils as utils } from '../../src/components/DatePicker';
type GlobalDescriptor = PropertyDescriptor | undefined;
function setGlobalProperty(name: string, descriptor: PropertyDescriptor): GlobalDescriptor {
const key = name as keyof typeof globalThis;
const original = Object.getOwnPropertyDescriptor(globalThis, key);
Object.defineProperty(globalThis, key, descriptor);
return original;
}
function restoreGlobalProperty(name: string, original: GlobalDescriptor): void {
const key = name as keyof typeof globalThis;
if (original) {
Object.defineProperty(globalThis, key, original);
return;
}
Reflect.deleteProperty(globalThis, key);
}
describe('DatePicker logic helpers', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('formats and clamps numeric values', () => {
expect(utils.pad2(3)).toBe('03');
expect(utils.pad4(12)).toBe('0012');
expect(utils.clampNumber(99, 0, 50)).toBe(50);
expect(utils.clampNumber(-1, 0, 50)).toBe(0);
});
it('validates calendar dates strictly', () => {
expect(utils.createValidatedDate(2026, 2, 29)).toBeNull();
expect(utils.createValidatedDate(2028, 2, 29)).toBeInstanceOf(Date);
});
it('resolves locale with and without navigator', () => {
const originalNavigator = setGlobalProperty('navigator', {
configurable: true,
value: undefined,
});
expect(utils.resolveLocale()).toBe('en-US');
restoreGlobalProperty('navigator', originalNavigator);
const locale = utils.resolveLocale();
expect(typeof locale).toBe('string');
expect(locale.length).toBeGreaterThan(0);
});
it('resolves week start across locale and fallback branches', () => {
const originalIntl = setGlobalProperty('Intl', {
configurable: true,
value: undefined,
});
expect(utils.resolveWeekStart('en-US')).toBe(0);
setGlobalProperty('Intl', {
configurable: true,
value: {
Locale: class MockLocale {
weekInfo = { firstDay: 2 };
},
},
});
expect(utils.resolveWeekStart('de-DE')).toBe(2);
setGlobalProperty('Intl', {
configurable: true,
value: {
Locale: class MockLocale {
weekInfo = { firstDay: 7 };
},
},
});
expect(utils.resolveWeekStart('en-US')).toBe(0);
setGlobalProperty('Intl', {
configurable: true,
value: {
Locale: class MockLocale {
weekInfo = { firstDay: 0 };
},
},
});
expect(utils.resolveWeekStart('en-US')).toBe(0);
setGlobalProperty('Intl', {
configurable: true,
value: {
Locale: class MockLocale {
weekInfo = { firstDay: 'x' as unknown as number };
},
},
});
expect(utils.resolveWeekStart('en-US')).toBe(0);
setGlobalProperty('Intl', {
configurable: true,
value: {
Locale: class MockLocale {
constructor() {
throw new Error('boom');
}
},
},
});
expect(utils.resolveWeekStart('en-US')).toBe(0);
restoreGlobalProperty('Intl', originalIntl);
});
it('builds and validates format configuration', () => {
expect(utils.buildFormatConfigOrNull('date-time', '')).toBeNull();
expect(utils.buildFormatConfigOrNull('date-time', 'yyyy/mm/dd HH')).toBeNull();
const config = utils.buildFormatConfigOrNull('date-time', 'dd/mm/yyyy HH:mm');
expect(config).not.toBeNull();
expect(config?.segments).toHaveLength(5);
expect(config?.totalLength).toBe(16);
});
it('falls back to default format when requested format is invalid', () => {
const config = utils.buildFormatConfig('date-time', 'yyyy/mm/dd');
expect(config.format).toBe('yyyy/mm/dd HH:mm');
});
it('parses valid values and rejects invalid input', () => {
const dateTimeConfig = utils.buildFormatConfig('date-time', 'dd/mm/yyyy HH:mm');
expect(utils.parsePickerValueWithFormat('22/02/2026 14:30', dateTimeConfig)).toEqual({
date: utils.createDateAtLocalMidnight(2026, 1, 22),
hour: 14,
minute: 30,
});
expect(utils.parsePickerValueWithFormat('2/2/2026 14:30', dateTimeConfig)).toBeNull();
expect(utils.parsePickerValueWithFormat('22-02-2026 14:30', dateTimeConfig)).toBeNull();
expect(utils.parsePickerValueWithFormat('aa/02/2026 14:30', dateTimeConfig)).toBeNull();
expect(utils.parsePickerValueWithFormat('31/02/2026 14:30', dateTimeConfig)).toBeNull();
expect(utils.parsePickerValueWithFormat('22/02/2026 24:30', dateTimeConfig)).toBeNull();
const timeConfig = utils.buildFormatConfig('time', 'HH:mm');
expect(utils.parsePickerValueWithFormat('09:00', timeConfig)).toEqual({
date: utils.startOfDay(new Date()),
hour: 9,
minute: 0,
});
expect(utils.parsePickerValueWithFormat('09:77', timeConfig)).toBeNull();
});
it('formats picker values with the chosen configuration', () => {
const config = utils.buildFormatConfig('date-time', 'dd/mm/yyyy HH:mm');
const text = utils.formatPickerValueWithFormat(
{
date: utils.createDateAtLocalMidnight(2027, 2, 11),
hour: 6,
minute: 5,
},
config,
);
expect(text).toBe('11/03/2027 06:05');
});
it('compares values by picker type', () => {
const date = utils.comparePickerValue(
{
date: utils.createDateAtLocalMidnight(2026, 1, 1),
hour: 23,
minute: 30,
},
{
date: utils.createDateAtLocalMidnight(2026, 1, 2),
hour: 1,
minute: 0,
},
'date',
);
expect(date).toBeLessThan(0);
const time = utils.comparePickerValue(
{
date: utils.createDateAtLocalMidnight(2026, 1, 1),
hour: 9,
minute: 0,
},
{
date: utils.createDateAtLocalMidnight(2026, 1, 1),
hour: 8,
minute: 59,
},
'time',
);
expect(time).toBeGreaterThan(0);
const dateTime = utils.comparePickerValue(
{
date: utils.createDateAtLocalMidnight(2026, 1, 1),
hour: 1,
minute: 0,
},
{
date: utils.createDateAtLocalMidnight(2026, 1, 1),
hour: 1,
minute: 1,
},
'date-time',
);
expect(dateTime).toBeLessThan(0);
});
it('normalizes and enforces picker ranges', () => {
const min = {
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 9,
minute: 0,
};
const max = {
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 10,
minute: 0,
};
const normalized = utils.normalizeRange(max, min, 'date-time');
expect(normalized.minValue).toEqual(min);
expect(normalized.maxValue).toEqual(max);
const below = utils.clampPickerToRange(
{
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 8,
minute: 0,
},
min,
max,
'date-time',
);
expect(below).toEqual(min);
const above = utils.clampPickerToRange(
{
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 10,
minute: 30,
},
min,
max,
'date-time',
);
expect(above).toEqual(max);
expect(utils.isWithinRange(min, min, max, 'date-time')).toBe(true);
expect(
utils.isWithinRange(
{
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 7,
minute: 59,
},
min,
max,
'date-time',
),
).toBe(false);
expect(
utils.isWithinRange(
{
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 10,
minute: 1,
},
min,
max,
'date-time',
),
).toBe(false);
});
it('applies segment edits across year, month, day, hour and minute', () => {
const base = {
date: utils.createDateAtLocalMidnight(2026, 1, 28),
hour: 14,
minute: 30,
};
expect(utils.applySegmentDigits(base, 'year', '0001').date.getFullYear()).toBe(1);
expect(utils.applySegmentDigits(base, 'month', '13').date.getMonth()).toBe(11);
expect(utils.applySegmentDigits(base, 'day', '99').date.getDate()).toBe(28);
expect(utils.applySegmentDigits(base, 'hour', '88').hour).toBe(23);
expect(utils.applySegmentDigits(base, 'minute', '99').minute).toBe(59);
const unchanged = utils.applySegmentDigits(base, 'day', 'not-a-number');
expect(unchanged).toEqual(base);
});
it('resolves segment index from caret position', () => {
const config = utils.buildFormatConfig('date-time', 'dd/mm/yyyy HH:mm');
expect(utils.findSegmentIndexByCaret([], 3)).toBe(0);
expect(utils.findSegmentIndexByCaret(config.segments, null)).toBe(0);
expect(utils.findSegmentIndexByCaret(config.segments, 0)).toBe(0);
expect(utils.findSegmentIndexByCaret(config.segments, 6)).toBe(2);
expect(utils.findSegmentIndexByCaret(config.segments, 99)).toBe(
config.segments[config.segments.length - 1].segmentIndex,
);
expect(utils.findSegmentIndexByCaret(config.segments, 2)).toBe(0);
const nonStandardSegments = [
{
type: 'segment',
kind: 'day',
token: 'dd',
length: 2,
start: 5,
end: 7,
segmentIndex: 0,
},
] as unknown as Parameters<typeof utils.findSegmentIndexByCaret>[0];
expect(utils.findSegmentIndexByCaret(nonStandardSegments, 1)).toBe(0);
});
it('checks date and hour selectability inside constrained ranges', () => {
const min = {
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 9,
minute: 30,
};
const max = {
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 10,
minute: 15,
};
expect(
utils.isDateSelectableForRange(
utils.createDateAtLocalMidnight(2026, 2, 9),
'date-time',
min,
max,
),
).toBe(false);
expect(
utils.isDateSelectableForRange(
utils.createDateAtLocalMidnight(2026, 2, 10),
'date-time',
min,
max,
),
).toBe(true);
expect(
utils.isDateSelectableForRange(
utils.createDateAtLocalMidnight(2026, 2, 11),
'date-time',
min,
max,
),
).toBe(false);
expect(
utils.isDateSelectableForRange(
utils.createDateAtLocalMidnight(2026, 2, 9),
'date',
{
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 0,
minute: 0,
},
{
date: utils.createDateAtLocalMidnight(2026, 2, 10),
hour: 0,
minute: 0,
},
),
).toBe(false);
expect(
utils.isHourSelectableForRange(
utils.createDateAtLocalMidnight(2026, 2, 10),
8,
'date-time',
min,
max,
),
).toBe(false);
expect(
utils.isHourSelectableForRange(
utils.createDateAtLocalMidnight(2026, 2, 10),
9,
'date-time',
min,
max,
),
).toBe(true);
expect(
utils.isHourSelectableForRange(
utils.createDateAtLocalMidnight(2026, 2, 10),
11,
'date-time',
min,
max,
),
).toBe(false);
});
it('builds month grids and joins class names', () => {
const month = utils.createDateAtLocalMidnight(2026, 2, 10);
const grid = utils.buildMonthGrid(month, 1);
expect(grid).toHaveLength(42);
expect(grid[0]).toBeInstanceOf(Date);
expect(grid[41]).toBeInstanceOf(Date);
expect(utils.joinClassNames('a', false, 'b', undefined, '', null, 'c')).toBe('a b c');
});
it('assigns refs for callback and object refs', () => {
const callback = vi.fn();
const objectRef = { current: null as HTMLInputElement | null };
const node = document.createElement('input');
utils.assignRef(callback, node);
utils.assignRef(objectRef, node);
utils.assignRef(undefined, node);
expect(callback).toHaveBeenCalledWith(node);
expect(objectRef.current).toBe(node);
});
});

View File

@@ -0,0 +1,740 @@
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { createRef, type FocusEvent as ReactFocusEvent, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { DatePicker } from '../../src/components/DatePicker';
type ControlledProps = {
type: 'date' | 'time' | 'date-time';
initialValue: string;
format?: string;
min?: string;
max?: string;
onValueChange?: (value: string) => void;
onBlur?: (event: ReactFocusEvent<HTMLInputElement>) => void;
disabled?: boolean;
};
function ControlledDatePicker({
type,
initialValue,
format,
min,
max,
onValueChange,
onBlur,
disabled = false,
}: Readonly<ControlledProps>) {
const [value, setValue] = useState(initialValue);
return (
<DatePicker
label="Schedule"
type={type}
value={value}
format={format}
min={min}
max={max}
disabled={disabled}
onBlur={onBlur}
onChange={(event) => {
setValue(event.target.value);
onValueChange?.(event.target.value);
}}
/>
);
}
function pickCurrentMonthDay(label: string): void {
const dayButtons = Array.from(document.querySelectorAll('.datepicker-day')) as HTMLButtonElement[];
const targetButton = dayButtons.find(
(button) => button.textContent === label && !button.classList.contains('is-outside-month'),
);
expect(targetButton).toBeDefined();
fireEvent.click(targetButton as HTMLButtonElement);
}
function getCurrentMonthDayButton(label: string): HTMLButtonElement {
const dayButtons = Array.from(document.querySelectorAll('.datepicker-day')) as HTMLButtonElement[];
const targetButton = dayButtons.find(
(button) => button.textContent === label && !button.classList.contains('is-outside-month'),
);
if (!targetButton) {
throw new Error(`Could not find day button for ${label}`);
}
return targetButton;
}
function createPasteEvent(text: string): Event {
const event = new Event('paste', { bubbles: true, cancelable: true });
Object.defineProperty(event, 'clipboardData', {
value: {
getData: () => text,
},
});
return event;
}
function createRect(left: number, top: number, width: number, height: number): DOMRect {
return {
x: left,
y: top,
left,
top,
width,
height,
right: left + width,
bottom: top + height,
toJSON: () => ({}),
} as DOMRect;
}
describe('DatePicker', () => {
it('opens popup from icon button and closes with Escape', () => {
render(<DatePicker label="Schedule" type="date-time" value="2031/05/20 14:30" onChange={() => {}} />);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
expect(input.type).toBe('text');
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
expect(
screen.getByRole('dialog', { name: 'Date and time picker popup' }),
).toBeInTheDocument();
fireEvent.keyDown(document, { key: 'Escape' });
expect(
screen.queryByRole('dialog', { name: 'Date and time picker popup' }),
).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
fireEvent.click(screen.getByRole('button', { name: 'Close date picker' }));
expect(
screen.queryByRole('dialog', { name: 'Date and time picker popup' }),
).not.toBeInTheDocument();
});
it('supports segment-by-segment editing with custom format and auto-advance', async () => {
const onValueChange = vi.fn();
render(
<ControlledDatePicker
type="date-time"
format="dd/mm/yyyy HH:mm"
initialValue="22/02/2026 14:30"
onValueChange={onValueChange}
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
fireEvent.focus(input);
await waitFor(() => {
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe(2);
});
fireEvent.keyDown(input, { key: '1' });
await waitFor(() => {
expect(input.value).toBe('01/02/2026 14:30');
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe(2);
});
fireEvent.keyDown(input, { key: '1' });
await waitFor(() => {
expect(input.value).toBe('11/02/2026 14:30');
expect(input.selectionStart).toBe(3);
expect(input.selectionEnd).toBe(5);
});
fireEvent.keyDown(input, { key: '0' });
fireEvent.keyDown(input, { key: '3' });
await waitFor(() => {
expect(input.value).toBe('11/03/2026 14:30');
expect(input.selectionStart).toBe(6);
expect(input.selectionEnd).toBe(10);
});
fireEvent.keyDown(input, { key: '2' });
fireEvent.keyDown(input, { key: '0' });
fireEvent.keyDown(input, { key: '2' });
fireEvent.keyDown(input, { key: '7' });
await waitFor(() => {
expect(input.value).toBe('11/03/2027 14:30');
expect(input.selectionStart).toBe(11);
expect(input.selectionEnd).toBe(13);
});
fireEvent.keyDown(input, { key: '1' });
fireEvent.keyDown(input, { key: '6' });
await waitFor(() => {
expect(input.value).toBe('11/03/2027 16:30');
expect(input.selectionStart).toBe(14);
expect(input.selectionEnd).toBe(16);
});
fireEvent.keyDown(input, { key: '4' });
fireEvent.keyDown(input, { key: '5' });
await waitFor(() => {
expect(input.value).toBe('11/03/2027 16:45');
});
expect(onValueChange).toHaveBeenCalled();
});
it('preserves year 0000 while editing and does not coerce to 19xx', async () => {
render(
<ControlledDatePicker
type="date-time"
format="dd/mm/yyyy HH:mm"
initialValue="22/02/2026 14:30"
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
fireEvent.focus(input);
await waitFor(() => {
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe(2);
});
fireEvent.keyDown(input, { key: '/' });
await waitFor(() => {
expect(input.selectionStart).toBe(3);
expect(input.selectionEnd).toBe(5);
});
fireEvent.keyDown(input, { key: '/' });
await waitFor(() => {
expect(input.selectionStart).toBe(6);
expect(input.selectionEnd).toBe(10);
});
fireEvent.keyDown(input, { key: '0' });
await waitFor(() => {
expect(input.value).toBe('22/02/0000 14:30');
expect(input.selectionStart).toBe(6);
expect(input.selectionEnd).toBe(10);
});
fireEvent.keyDown(input, { key: '0' });
fireEvent.keyDown(input, { key: '0' });
fireEvent.keyDown(input, { key: '0' });
await waitFor(() => {
expect(input.value).toBe('22/02/0000 14:30');
expect(input.selectionStart).toBe(11);
expect(input.selectionEnd).toBe(13);
});
});
it('uses separator keys to move between editable segments', async () => {
render(
<ControlledDatePicker
type="date-time"
format="dd/mm/yyyy HH:mm"
initialValue="22/02/2026 14:30"
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
fireEvent.focus(input);
await waitFor(() => {
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe(2);
});
fireEvent.keyDown(input, { key: '/' });
await waitFor(() => {
expect(input.selectionStart).toBe(3);
expect(input.selectionEnd).toBe(5);
});
fireEvent.keyDown(input, { key: '/' });
await waitFor(() => {
expect(input.selectionStart).toBe(6);
expect(input.selectionEnd).toBe(10);
});
fireEvent.keyDown(input, { key: ' ' });
await waitFor(() => {
expect(input.selectionStart).toBe(11);
expect(input.selectionEnd).toBe(13);
});
fireEvent.keyDown(input, { key: ':' });
await waitFor(() => {
expect(input.selectionStart).toBe(14);
expect(input.selectionEnd).toBe(16);
});
});
it('applies min/max constraints to calendar and time options', () => {
render(
<ControlledDatePicker
type="date-time"
initialValue="2026/03/10 10:00"
min="2026/03/10 09:30"
max="2026/03/10 10:15"
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
expect(getCurrentMonthDayButton('9')).toBeDisabled();
expect(getCurrentMonthDayButton('10')).toBeEnabled();
expect(getCurrentMonthDayButton('11')).toBeDisabled();
const hoursList = screen.getByRole('listbox', { name: 'Hours' });
expect(within(hoursList).getByRole('button', { name: '08' })).toBeDisabled();
expect(within(hoursList).getByRole('button', { name: '09' })).toBeEnabled();
expect(within(hoursList).getByRole('button', { name: '10' })).toBeEnabled();
expect(within(hoursList).getByRole('button', { name: '11' })).toBeDisabled();
const minutesList = screen.getByRole('listbox', { name: 'Minutes' });
expect(within(minutesList).getByRole('button', { name: '15' })).toBeEnabled();
expect(within(minutesList).getByRole('button', { name: '20' })).toBeDisabled();
});
it('renders date mode calendar only and auto-closes after day selection', () => {
const onValueChange = vi.fn();
render(
<ControlledDatePicker
type="date"
initialValue="2031/05/20"
onValueChange={onValueChange}
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
expect(screen.getByRole('dialog', { name: 'Date picker popup' })).toBeInTheDocument();
expect(screen.queryByText('Hours')).not.toBeInTheDocument();
expect(screen.queryByText('Minutes')).not.toBeInTheDocument();
pickCurrentMonthDay('15');
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
expect(input.value).toMatch(/^\d{4}\/\d{2}\/15$/);
expect(onValueChange).toHaveBeenCalled();
expect(screen.queryByRole('dialog', { name: 'Date picker popup' })).not.toBeInTheDocument();
});
it('commits calendar date changes in date-time mode without closing the popup', () => {
const onValueChange = vi.fn();
render(
<ControlledDatePicker
type="date-time"
initialValue="2031/05/20 14:30"
onValueChange={onValueChange}
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
pickCurrentMonthDay('15');
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
expect(input.value).toMatch(/^\d{4}\/\d{2}\/15 14:30$/);
expect(onValueChange).toHaveBeenCalled();
expect(screen.getByRole('dialog', { name: 'Date and time picker popup' })).toBeInTheDocument();
});
it('renders time mode selectors only and keeps popup open after selection', () => {
const onValueChange = vi.fn();
render(<ControlledDatePicker type="time" initialValue="09:00" onValueChange={onValueChange} />);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
expect(screen.getByRole('dialog', { name: 'Time picker popup' })).toBeInTheDocument();
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
const hoursList = screen.getByRole('listbox', { name: 'Hours' });
const minutesList = screen.getByRole('listbox', { name: 'Minutes' });
fireEvent.click(within(hoursList).getByRole('button', { name: '13' }));
fireEvent.click(within(minutesList).getByRole('button', { name: '45' }));
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
expect(input.value).toBe('13:45');
expect(onValueChange).toHaveBeenCalledTimes(2);
expect(screen.getByRole('dialog', { name: 'Time picker popup' })).toBeInTheDocument();
});
it('blocks popup interactions while disabled', () => {
render(<ControlledDatePicker type="date" initialValue="2031/05/20" disabled />);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
const iconButton = screen.getByRole('button', { name: 'Open date picker' });
expect(input).toBeDisabled();
expect(iconButton).toBeDisabled();
fireEvent.click(iconButton);
expect(screen.queryByRole('dialog', { name: 'Date picker popup' })).not.toBeInTheDocument();
});
it('renders right icon and error message', () => {
const { container } = render(
<DatePicker
label="Schedule"
type="date-time"
value="2031/05/20 14:30"
onChange={() => {}}
rightIcon={<span data-testid="right-icon">R</span>}
error="Invalid date"
inputClassName="custom-input"
/>,
);
const input = container.querySelector('input');
expect(input).toBeInstanceOf(HTMLInputElement);
expect(screen.getByTestId('right-icon')).toBeInTheDocument();
expect(input).toHaveClass('pr-10');
expect(input).toHaveClass('custom-input');
expect(screen.getByText('Invalid date')).toBeInTheDocument();
});
it('forwards inputRef for callback and object refs', () => {
const callbackRef = vi.fn();
const objectRef = createRef<HTMLInputElement>();
const { rerender } = render(
<DatePicker
label="Schedule"
type="date-time"
value="2031/05/20 14:30"
onChange={() => {}}
inputRef={callbackRef}
/>,
);
expect(callbackRef).toHaveBeenCalled();
const callbackNode = callbackRef.mock.calls.find(([node]) => node instanceof HTMLInputElement)?.[0];
expect(callbackNode).toBeInstanceOf(HTMLInputElement);
rerender(
<DatePicker
label="Schedule"
type="date-time"
value="2031/05/20 14:30"
onChange={() => {}}
inputRef={objectRef}
/>,
);
expect(objectRef.current).toBeInstanceOf(HTMLInputElement);
});
it('supports inline layout', () => {
const { container } = render(
<DatePicker label="Start time" type="time" value="09:00" onChange={() => {}} layout="inline" />,
);
expect(container.querySelector('label')).toHaveClass('inline-flex');
expect(container.querySelector('label > div')).not.toHaveClass('mt-1');
});
it('handles outside vs inside pointer interactions for popup close behavior', () => {
render(<DatePicker label="Schedule" type="date-time" value="2031/05/20 14:30" onChange={() => {}} />);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
const dialog = screen.getByRole('dialog', { name: 'Date and time picker popup' });
fireEvent.mouseDown(dialog);
expect(screen.getByRole('dialog', { name: 'Date and time picker popup' })).toBeInTheDocument();
fireEvent.mouseDown(document.body);
expect(
screen.queryByRole('dialog', { name: 'Date and time picker popup' }),
).not.toBeInTheDocument();
});
it('supports month/year chooser interactions and month navigation buttons', async () => {
render(<DatePicker label="Schedule" type="date" value="2031/05/20" onChange={() => {}} />);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
fireEvent.click(screen.getByRole('button', { name: 'Previous month' }));
fireEvent.click(screen.getByRole('button', { name: 'Next month' }));
const chooserButtons = document.querySelectorAll('.datepicker-chooser-btn');
const monthChooserButton = chooserButtons[0] as HTMLButtonElement;
const yearChooserButton = chooserButtons[1] as HTMLButtonElement;
const initialMonthText = monthChooserButton.textContent;
fireEvent.click(monthChooserButton);
const monthList = screen.getByRole('listbox', { name: 'Choose month' });
const monthOptions = within(monthList).getAllByRole('button');
const nextMonth = monthOptions.find((option) => option.textContent !== initialMonthText) ?? monthOptions[0];
fireEvent.click(nextMonth);
await waitFor(() => {
expect(screen.queryByRole('listbox', { name: 'Choose month' })).not.toBeInTheDocument();
});
expect((document.querySelectorAll('.datepicker-chooser-btn')[0] as HTMLButtonElement).textContent).toBe(
nextMonth.textContent,
);
const initialYearText = yearChooserButton.textContent;
fireEvent.click(yearChooserButton);
const yearList = screen.getByRole('listbox', { name: 'Choose year' });
const yearOptions = within(yearList).getAllByRole('button');
const nextYear = yearOptions.find((option) => option.textContent !== initialYearText) ?? yearOptions[0];
fireEvent.click(nextYear);
await waitFor(() => {
expect(screen.queryByRole('listbox', { name: 'Choose year' })).not.toBeInTheDocument();
});
expect((document.querySelectorAll('.datepicker-chooser-btn')[1] as HTMLButtonElement).textContent).toBe(
nextYear.textContent,
);
});
it('guards month navigation at absolute calendar boundaries', () => {
const first = render(
<DatePicker label="Schedule" type="date" value="0000/01/15" onChange={() => {}} />,
);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
fireEvent.click(screen.getByRole('button', { name: 'Previous month' }));
const lowerYearButton = document.querySelectorAll('.datepicker-chooser-btn')[1] as HTMLButtonElement;
expect(lowerYearButton.textContent).toBe('0');
first.unmount();
render(<DatePicker label="Schedule" type="date" value="9999/12/15" onChange={() => {}} />);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
fireEvent.click(screen.getByRole('button', { name: 'Next month' }));
const upperYearButton = document.querySelectorAll('.datepicker-chooser-btn')[1] as HTMLButtonElement;
expect(upperYearButton.textContent).toBe('9999');
});
it('supports keyboard navigation commands and segment reset shortcuts', async () => {
const onBlur = vi.fn();
render(
<ControlledDatePicker
type="date-time"
format="dd/mm/yyyy HH:mm"
initialValue="22/02/2026 14:30"
onBlur={onBlur}
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
fireEvent.focus(input);
await waitFor(() => {
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe(2);
});
fireEvent.keyDown(input, { key: '1' });
fireEvent.keyDown(input, { key: 'ArrowRight' });
await waitFor(() => {
expect(input.value.startsWith('01/')).toBe(true);
expect(input.selectionStart).toBe(3);
});
fireEvent.mouseUp(input);
fireEvent.click(input);
fireEvent.keyDown(input, { key: 'ArrowDown' });
expect(screen.getByRole('dialog', { name: 'Date and time picker popup' })).toBeInTheDocument();
fireEvent.keyDown(input, { key: 'ArrowRight' });
await waitFor(() => expect(input.selectionStart).toBe(3));
fireEvent.keyDown(input, { key: 'ArrowLeft' });
await waitFor(() => expect((input.selectionStart ?? 0) <= 3).toBe(true));
fireEvent.keyDown(input, { key: 'Tab' });
await waitFor(() => expect((input.selectionStart ?? 0) >= 0).toBe(true));
fireEvent.keyDown(input, { key: 'Tab', shiftKey: true });
await waitFor(() => expect((input.selectionStart ?? 0) >= 0).toBe(true));
fireEvent.keyDown(input, { key: 'Backspace' });
await waitFor(() => {
expect(input.value.startsWith('01/')).toBe(true);
});
fireEvent.keyDown(input, { key: '/' });
await waitFor(() => expect(input.selectionStart).toBe(3));
fireEvent.keyDown(input, { key: 'Delete' });
await waitFor(() => {
expect(input.value.slice(3, 5)).toBe('01');
});
fireEvent.keyDown(input, { key: '/' });
await waitFor(() => expect(input.selectionStart).toBe(6));
fireEvent.keyDown(input, { key: 'Delete' });
fireEvent.keyDown(input, { key: ' ' });
fireEvent.keyDown(input, { key: 'Delete' });
await waitFor(() => {
expect(/\s00:|:00$/.test(input.value)).toBe(true);
});
fireEvent.keyDown(input, { key: 'Enter' });
fireEvent.keyDown(input, { key: 'x' });
fireEvent.blur(input);
expect(onBlur).toHaveBeenCalledTimes(1);
});
it('handles paste validation and value commits', async () => {
render(
<ControlledDatePicker
type="date-time"
format="dd/mm/yyyy HH:mm"
initialValue="22/02/2026 14:30"
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
await act(async () => {
input.dispatchEvent(createPasteEvent('invalid value'));
});
expect(input.value).toBe('22/02/2026 14:30');
await act(async () => {
input.dispatchEvent(createPasteEvent('11/03/2027 16:45'));
});
expect(input.value).toBe('11/03/2027 16:45');
});
it('returns early from interaction handlers when disabled', () => {
render(
<DatePicker
label="Schedule"
type="date-time"
format="dd/mm/yyyy HH:mm"
value="22/02/2026 14:30"
disabled
onChange={() => {}}
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
fireEvent.focus(input);
fireEvent.mouseUp(input);
fireEvent.click(input);
fireEvent.keyDown(input, { key: 'ArrowDown' });
const disabledPaste = createPasteEvent('11/03/2027 16:45');
input.dispatchEvent(disabledPaste);
expect(disabledPaste.defaultPrevented).toBe(false);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('falls back to direct assignment and synthetic onChange when native dispatch is stubbed', async () => {
const onChange = vi.fn();
const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
render(
<DatePicker
label="Schedule"
type="date-time"
format="dd/mm/yyyy HH:mm"
value="22/02/2026 14:30"
onChange={onChange}
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
const nativeDispatchEvent = input.dispatchEvent.bind(input);
vi.spyOn(input, 'dispatchEvent').mockImplementation((event: Event) => {
if (event.type === 'change') {
return true;
}
return nativeDispatchEvent(event);
});
vi.spyOn(Object, 'getOwnPropertyDescriptor').mockImplementation((target, property) => {
if (target === HTMLInputElement.prototype && property === 'value') {
return {
configurable: true,
enumerable: true,
get: originalGetOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.get,
};
}
return originalGetOwnPropertyDescriptor(target, property);
});
fireEvent.focus(input);
input.setSelectionRange(0, 2);
fireEvent.keyDown(input, { key: '1' });
await waitFor(() => {
expect(onChange).toHaveBeenCalled();
});
});
it('safely handles pending segment selection during unmount', () => {
vi.useFakeTimers();
const { unmount } = render(
<DatePicker
label="Schedule"
type="date-time"
format="dd/mm/yyyy HH:mm"
value="22/02/2026 14:30"
onChange={() => {}}
/>,
);
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
fireEvent.focus(input);
unmount();
expect(() => {
vi.runAllTimers();
}).not.toThrow();
});
it('recalculates popup position on window resize and scroll', async () => {
const originalInnerWidth = Object.getOwnPropertyDescriptor(window, 'innerWidth');
const originalInnerHeight = Object.getOwnPropertyDescriptor(window, 'innerHeight');
const rectSpy = vi
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(function mockRect(this: HTMLElement) {
if (this.classList.contains('datepicker-popup')) {
return createRect(0, 0, 300, 200);
}
if (this.classList.contains('relative') && this.querySelector('input')) {
return createRect(500, 80, 120, 40);
}
return createRect(0, 0, 100, 30);
});
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 600 });
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 480 });
render(<DatePicker label="Schedule" type="date-time" value="2031/05/20 14:30" onChange={() => {}} />);
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
const dialog = screen.getByRole('dialog', { name: 'Date and time picker popup' });
expect(dialog).toBeInTheDocument();
await waitFor(() => {
expect(dialog.style.left).toBe('292px');
});
fireEvent(window, new Event('resize'));
fireEvent(window, new Event('scroll'));
expect(screen.getByRole('dialog', { name: 'Date and time picker popup' })).toBeInTheDocument();
expect(dialog.style.left).toBe('292px');
rectSpy.mockRestore();
if (originalInnerWidth) {
Object.defineProperty(window, 'innerWidth', originalInnerWidth);
}
if (originalInnerHeight) {
Object.defineProperty(window, 'innerHeight', originalInnerHeight);
}
});
});

View File

@@ -0,0 +1,60 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Dropdown } from '../../src/components/Dropdown';
const choices = [
{ id: 'USER', label: 'User' },
{ id: 'ADMIN', label: 'Admin' },
];
describe('Dropdown', () => {
it('calls onChange with selected value', () => {
const onChange = vi.fn();
render(<Dropdown label="Role" value="USER" choices={choices} onChange={onChange} />);
fireEvent.change(screen.getByLabelText('Role'), { target: { value: 'ADMIN' } });
expect(onChange).toHaveBeenCalledWith('ADMIN');
});
it('supports inline layout and disabled/required state', () => {
const { container } = render(
<Dropdown
label="Rows"
value="10"
choices={[{ id: '10', label: '10' }]}
layout="inline"
disabled
required
/>,
);
const select = screen.getByLabelText('Rows');
expect(select).toBeDisabled();
expect(select).toBeRequired();
expect(container.querySelector('label')).toHaveClass('inline-flex');
});
it('renders error and custom class names', () => {
const { container } = render(
<Dropdown
label="Role"
value="USER"
choices={choices}
error="Role is invalid"
className="custom-wrapper"
selectClassName="custom-select"
/>,
);
const select = container.querySelector('select');
expect(select).toBeInstanceOf(HTMLSelectElement);
expect(screen.getByText('Role is invalid')).toBeInTheDocument();
expect(select).toHaveClass('custom-select');
expect(screen.getByText('Role').closest('label')).toHaveClass('custom-wrapper');
});
it('supports rendering without a label', () => {
const { container } = render(<Dropdown value="USER" choices={choices} />);
expect(container.querySelector('label > span')).toBeNull();
});
});

View File

@@ -0,0 +1,28 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Form } from '../../src/components/Form';
describe('Form', () => {
it('renders title, title actions and children', () => {
render(
<Form title="User Details" titleBarRight={<button type="button">Action</button>}>
<div>Form child</div>
</Form>,
);
expect(screen.getByText('User Details')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
expect(screen.getByText('Form child')).toBeInTheDocument();
});
it('supports custom class names and optional title actions', () => {
const { container } = render(
<Form title="No Actions" className="form-custom">
<div>Child</div>
</Form>,
);
expect(container.firstElementChild).toHaveClass('form-custom');
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,97 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { createRef } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { InputField } from '../../src/components/InputField';
describe('InputField', () => {
it('supports email type and emits change/blur callbacks', () => {
const onChange = vi.fn();
const onBlur = vi.fn();
render(
<InputField
label="Email"
type="email"
value=""
onChange={onChange}
onBlur={onBlur}
required
/>,
);
const input = screen.getByLabelText('Email') as HTMLInputElement;
expect(input.type).toBe('email');
expect(input).toBeRequired();
fireEvent.change(input, { target: { value: 'new@example.com' } });
fireEvent.blur(input);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onBlur).toHaveBeenCalledTimes(1);
});
it('passes refs to input element', () => {
const inputRef = createRef<HTMLInputElement>();
render(<InputField label="Username" type="text" value="john" onChange={() => {}} inputRef={inputRef} />);
expect(inputRef.current).toBeInstanceOf(HTMLInputElement);
expect(inputRef.current?.value).toBe('john');
});
it('toggles password visibility', () => {
render(<InputField label="Password" type="password" value="abc" onChange={() => {}} />);
expect((screen.getByLabelText('Password') as HTMLInputElement).type).toBe('password');
fireEvent.click(screen.getByRole('button', { name: 'Show password' }));
expect((screen.getByLabelText('Password') as HTMLInputElement).type).toBe('text');
fireEvent.click(screen.getByRole('button', { name: 'Hide password' }));
expect((screen.getByLabelText('Password') as HTMLInputElement).type).toBe('password');
});
it('renders rightIcon for non-password input and displays errors', () => {
const { container } = render(
<InputField
label="Username"
type="text"
value="john"
onChange={() => {}}
rightIcon={<span data-testid="right-icon">R</span>}
error="Invalid username"
inputClassName="custom-input"
/>,
);
const input = container.querySelector('input');
expect(input).toBeInstanceOf(HTMLInputElement);
expect(input).toHaveClass('pr-10');
expect(input).toHaveClass('custom-input');
expect(screen.getByTestId('right-icon')).toBeInTheDocument();
expect(screen.getByText('Invalid username')).toBeInTheDocument();
});
it('disables password toggle when input is disabled', () => {
render(
<InputField
label="Password"
type="password"
value="secret"
onChange={() => {}}
disabled
/>,
);
expect(screen.getByRole('button', { name: 'Show password' })).toBeDisabled();
});
it('supports inline layout classes', () => {
const { container } = render(
<InputField label="Username" type="text" value="" onChange={() => {}} layout="inline" />,
);
expect(container.querySelector('label')).toHaveClass('inline-flex');
expect(container.querySelector('label > div')).not.toHaveClass('mt-1');
});
it('supports rendering without a label', () => {
const { container } = render(<InputField type="text" value="" onChange={() => {}} />);
expect(container.querySelector('label > span')).toBeNull();
});
});

View File

@@ -0,0 +1,25 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Label } from '../../src/components/Label';
describe('Label', () => {
it('uses default and variant-specific tags/classes', () => {
const { rerender } = render(<Label>Body</Label>);
expect(screen.getByText('Body').tagName).toBe('P');
expect(screen.getByText('Body')).toHaveClass('ui-body-primary');
rerender(<Label variant="h1">Title</Label>);
expect(screen.getByText('Title').tagName).toBe('H1');
expect(screen.getByText('Title')).toHaveClass('ui-title');
rerender(<Label variant="h4">Section</Label>);
expect(screen.getByText('Section').tagName).toBe('H3');
rerender(
<Label variant="h2" as="span">
Custom
</Label>,
);
expect(screen.getByText('Custom').tagName).toBe('SPAN');
});
});

View File

@@ -0,0 +1,82 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { MDXEditorField } from '../../src/components/MDXEditorField';
describe('MDXEditorField', () => {
it('renders label and change handler in editable mode', () => {
const onChange = vi.fn();
render(
<MDXEditorField
label="Content"
markdown=""
onChange={onChange}
themeClassName="light-theme"
editorClassName="extra-editor"
plugins={[]}
/>,
);
expect(screen.getByText('Content')).toHaveClass('ui-label');
fireEvent.change(screen.getByLabelText('Markdown Editor'), {
target: { value: '# Hello' },
});
expect(onChange).toHaveBeenCalledWith('# Hello');
expect(screen.getByLabelText('Markdown Editor')).toHaveAttribute(
'data-class-name',
'light-theme extra-editor',
);
});
it('renders preview and disabled classes when disabled', () => {
render(
<MDXEditorField
label="Content"
markdown="Disabled content"
disabled
themeClassName="dark-theme"
plugins={[]}
/>,
);
const label = screen.getByText('Content');
expect(label).toHaveClass('ui-label-disabled');
expect(screen.getByTestId('md-preview')).toHaveTextContent('Disabled content');
expect(screen.getByTestId('md-preview')).toHaveAttribute('data-class-name', 'dark-theme');
expect(screen.queryByLabelText('Markdown Editor')).not.toBeInTheDocument();
expect(document.querySelector('.post-mdx-editor--disabled')).toBeTruthy();
});
it('renders read-only preview without label when label is omitted', () => {
render(
<MDXEditorField
markdown="Read only content"
readOnly
themeClassName="dark-theme"
plugins={[]}
/>,
);
expect(screen.queryByText('Content')).not.toBeInTheDocument();
expect(screen.getByTestId('md-preview')).toHaveTextContent('Read only content');
});
it('supports wrapper style/class overrides and error rendering', () => {
render(
<MDXEditorField
label="Content"
markdown=""
themeClassName="light-theme"
plugins={[]}
editorWrapperClassName="editor-wrapper"
editorWrapperStyle={{ borderWidth: '2px' }}
error="Content is required"
/>,
);
const wrapper = document.querySelector('.editor-wrapper');
expect(wrapper).toHaveClass('post-mdx-editor--enabled');
expect(wrapper).toHaveStyle({ borderWidth: '2px' });
expect(screen.getByText('Content is required')).toHaveClass('ui-error');
});
});

View File

@@ -0,0 +1,55 @@
import { UserCircleIcon } from '@heroicons/react/24/outline';
import { fireEvent, screen } from '@testing-library/react';
import { Route, Routes } from 'react-router-dom';
import { describe, expect, it, vi } from 'vitest';
import { SidebarNavItem } from '../../src/components/SidebarNavItem';
import { renderWithRouter } from '../helpers/renderWithRouter';
describe('SidebarNavItem', () => {
it('renders active style and collapsed label behavior', () => {
renderWithRouter(
<Routes>
<Route
path="/users"
element={<SidebarNavItem to="/users" label="Users" icon={UserCircleIcon} collapsed />}
/>
</Routes>,
{ route: '/users' },
);
const link = screen.getByRole('link', { name: 'Users' });
expect(link.className).toContain('ui-accent-active');
expect(link.className).toContain('mx-auto');
expect(screen.getByText('Users').className).toContain('lg:hidden');
});
it('renders inactive style and triggers onClick', () => {
const onClick = vi.fn();
renderWithRouter(
<Routes>
<Route path="/users" element={<div>Users page</div>} />
<Route
path="/profile"
element={
<SidebarNavItem
to="/users"
label="Users"
icon={UserCircleIcon}
collapsed={false}
onClick={onClick}
/>
}
/>
</Routes>,
{ route: '/profile' },
);
const link = screen.getByRole('link', { name: 'Users' });
expect(link.className).toContain('hover:bg-zinc-500/15');
expect(link.className).toContain('lg:w-full');
expect(screen.getByText('Users').className).toContain('truncate');
fireEvent.click(link);
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,199 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Table, type TableHeader } from '../../src/components/Table';
import type { SortState } from '../../src/types/sort';
type Row = {
id: string;
name: string;
email: string;
};
const headers: TableHeader<Row>[] = [
{ label: 'Name', id: 'name', value: (row) => row.name, headerClassName: 'head-name' },
{ label: 'Email', id: 'email', value: (row) => row.email, cellClassName: 'cell-email' },
{ label: 'Static', id: 'static', value: 'Always', sortable: false },
];
const data: Row[] = [{ id: '1', name: 'Jane', email: 'jane@example.com' }];
describe('Table', () => {
it('renders loading state', () => {
render(<Table headers={headers} data={[]} isLoading rowKey={(row) => row.id} />);
expect(document.querySelector('.animate-spin')).toBeTruthy();
});
it('renders empty state message when no rows are available', () => {
render(
<Table
headers={headers}
data={[]}
emptyMessage="Nothing here"
rowKey={(row) => row.id}
/>,
);
expect(screen.getByText('Nothing here')).toBeInTheDocument();
});
it('renders row values from function and static header values', () => {
const rowKey = vi.fn((row: Row) => row.id);
render(<Table headers={headers} data={data} rowKey={rowKey} />);
expect(screen.getByText('Jane')).toBeInTheDocument();
expect(screen.getByText('jane@example.com')).toBeInTheDocument();
expect(screen.getByText('Always')).toBeInTheDocument();
expect(screen.getByText('Name').closest('th')).toHaveClass('head-name');
expect(screen.getByText('jane@example.com').closest('td')).toHaveClass('cell-email');
expect(rowKey).toHaveBeenCalledWith(data[0], 0);
});
it('shows sortable buttons only when sort config is complete', () => {
const onSortChange = vi.fn();
const sortableHeaders: TableHeader<Row>[] = [
{ label: 'No field', id: 'a', sortable: true, value: (row) => row.name },
{ label: 'Empty field', id: 'b', sortable: true, sortField: '', value: (row) => row.name },
{ label: 'Name', id: 'c', sortable: true, sortField: 'name', value: (row) => row.name },
];
const { rerender } = render(
<Table headers={sortableHeaders} data={data} rowKey={(row) => row.id} onSortChange={onSortChange} />,
);
expect(screen.queryByRole('button', { name: 'Sort by No field' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Sort by Empty field' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Sort by Name' })).toBeInTheDocument();
rerender(<Table headers={sortableHeaders} data={data} rowKey={(row) => row.id} />);
expect(screen.queryByRole('button', { name: 'Sort by Name' })).not.toBeInTheDocument();
});
it('renders sort states and notifies callback on header click', () => {
const onSortChange = vi.fn();
const sortableHeaders: TableHeader<Row>[] = [
{ label: 'Name', id: 'name', sortable: true, sortField: 'name', value: (row) => row.name },
{ label: 'Email', id: 'email', sortable: true, sortField: 'email', value: (row) => row.email },
];
const sorting: SortState = { field: 'name', direction: 'asc' };
const { rerender } = render(
<Table
headers={sortableHeaders}
data={data}
rowKey={(row) => row.id}
sorting={sorting}
onSortChange={onSortChange}
/>,
);
const nameSort = screen.getByRole('button', { name: 'Sort by Name' });
const emailSort = screen.getByRole('button', { name: 'Sort by Email' });
expect(nameSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'asc');
expect(emailSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'none');
fireEvent.click(nameSort);
expect(onSortChange).toHaveBeenCalledWith('name');
rerender(
<Table
headers={sortableHeaders}
data={data}
rowKey={(row) => row.id}
sorting={{ field: 'name', direction: 'desc' }}
onSortChange={onSortChange}
/>,
);
expect(nameSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'desc');
});
it('supports pagination controls and page-size changes', () => {
const onPageChange = vi.fn();
const onPageSizeChange = vi.fn();
render(
<Table
headers={headers}
data={data}
rowKey={(row) => row.id}
pagination={{
page: 2,
pageSize: 10,
total: 21,
totalPages: 3,
onPageChange,
onPageSizeChange,
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Previous page' }));
fireEvent.click(screen.getByRole('button', { name: 'Next page' }));
fireEvent.change(screen.getByLabelText('Rows'), { target: { value: '20' } });
expect(onPageChange).toHaveBeenNthCalledWith(1, 1);
expect(onPageChange).toHaveBeenNthCalledWith(2, 3);
expect(onPageSizeChange).toHaveBeenCalledWith(20);
});
it('hides rows selector when onPageSizeChange is absent and clamps page count display', () => {
render(
<Table
headers={headers}
data={data}
rowKey={(row) => row.id}
pagination={{
page: 1,
pageSize: 10,
total: 1,
totalPages: 0,
onPageChange: vi.fn(),
}}
/>,
);
expect(screen.queryByLabelText('Rows')).not.toBeInTheDocument();
expect(screen.getByText('Page 1 of 1')).toBeInTheDocument();
});
it('disables prev/next at bounds or while loading', () => {
const { rerender } = render(
<Table
headers={headers}
data={data}
rowKey={(row) => row.id}
pagination={{
page: 1,
pageSize: 10,
total: 10,
totalPages: 1,
onPageChange: vi.fn(),
}}
/>,
);
expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
rerender(
<Table
headers={headers}
data={data}
rowKey={(row) => row.id}
isLoading
pagination={{
page: 2,
pageSize: 10,
total: 100,
totalPages: 10,
onPageChange: vi.fn(),
onPageSizeChange: vi.fn(),
}}
/>,
);
expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
expect(screen.getByLabelText('Rows')).toBeDisabled();
});
});

View File

@@ -0,0 +1,12 @@
import type { ReactElement } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
type RenderWithRouterOptions = {
route?: string;
};
export function renderWithRouter(ui: ReactElement, options: RenderWithRouterOptions = {}) {
const { route = '/' } = options;
return render(<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>);
}

16
tests/index.test.ts Normal file
View File

@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';
import * as webUi from '../src/index';
describe('index exports', () => {
it('exposes runtime component exports', () => {
expect(typeof webUi.Button).toBe('function');
expect(typeof webUi.Chip).toBe('function');
expect(typeof webUi.DatePicker).toBe('function');
expect(typeof webUi.Dropdown).toBe('function');
expect(typeof webUi.Form).toBe('function');
expect(typeof webUi.InputField).toBe('function');
expect(typeof webUi.Label).toBe('function');
expect(typeof webUi.SidebarNavItem).toBe('function');
expect(typeof webUi.Table).toBe('function');
});
});

74
tests/mocks/mdxeditor.tsx Normal file
View File

@@ -0,0 +1,74 @@
import { forwardRef, useImperativeHandle, type ReactNode } from 'react';
export type MDXEditorMethods = {
setMarkdown: (value: string) => void;
};
export type MDXEditorProps = {
markdown: string;
onChange?: (value: string) => void;
readOnly?: boolean;
className?: string;
contentEditableClassName?: string;
plugins?: unknown[];
};
export const MDXEditor = forwardRef<MDXEditorMethods, MDXEditorProps>(function MDXEditor(
{ markdown, onChange, readOnly, className },
ref,
) {
useImperativeHandle(
ref,
() => ({
setMarkdown: () => undefined,
}),
[],
);
if (readOnly) {
return (
<div data-testid="md-preview" data-class-name={className}>
{markdown}
</div>
);
}
return (
<textarea
aria-label="Markdown Editor"
value={markdown}
data-class-name={className}
onChange={(event) => onChange?.(event.target.value)}
/>
);
});
function Control(): ReactNode {
return <span />;
}
function plugin(): Record<string, never> {
return {};
}
export const BlockTypeSelect = Control;
export const BoldItalicUnderlineToggles = Control;
export const CodeToggle = Control;
export const CreateLink = Control;
export const InsertCodeBlock = Control;
export const InsertTable = Control;
export const ListsToggle = Control;
export const Separator = Control;
export const UndoRedo = Control;
export const codeBlockPlugin = plugin;
export const codeMirrorPlugin = plugin;
export const headingsPlugin = plugin;
export const linkDialogPlugin = plugin;
export const linkPlugin = plugin;
export const listsPlugin = plugin;
export const markdownShortcutPlugin = plugin;
export const quotePlugin = plugin;
export const tablePlugin = plugin;
export const thematicBreakPlugin = plugin;
export const toolbarPlugin = plugin;

18
tests/setup.ts Normal file
View File

@@ -0,0 +1,18 @@
// 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';
vi.mock('@mdxeditor/editor', () => import('./mocks/mdxeditor'));
afterEach(() => {
cleanup();
const storage = globalThis.window?.localStorage ?? globalThis.localStorage;
if (typeof storage?.clear === 'function') {
storage.clear();
}
vi.restoreAllMocks();
vi.useRealTimers();
});

View File

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

582
yarn.lock
View File

@@ -2,6 +2,11 @@
# yarn lockfile v1
"@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"
@@ -12,6 +17,33 @@
resolved "https://nexus.beatrice.wtf/repository/npm-group/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@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"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c"
@@ -175,6 +207,18 @@
"@babel/helper-string-parser" "^7.27.1"
"@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"
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.3.2", "@codemirror/autocomplete@^6.4.0", "@codemirror/autocomplete@^6.7.1":
version "6.20.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz#db818c12dce892a93fb8abadc2426febb002f8c1"
@@ -460,9 +504,9 @@
"@codemirror/legacy-modes" "^6.4.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.11.3", "@codemirror/language@^6.3.0", "@codemirror/language@^6.3.2", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0":
version "6.12.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@codemirror/language/-/language-6.12.1.tgz#d615f7b099a39248312feaaf0bfafce4418aac1b"
integrity sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==
version "6.12.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@codemirror/language/-/language-6.12.2.tgz#7db5a46757411cf251e8f450474c05710c27d42c"
integrity sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
@@ -569,6 +613,39 @@
react-devtools-inline "4.4.0"
react-is "^17.0.2"
"@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":
version "0.27.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2"
@@ -752,6 +829,11 @@
"@eslint/core" "^1.1.0"
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==
"@floating-ui/core@^1.7.4":
version "1.7.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@floating-ui/core/-/core-1.7.4.tgz#4a006a6e01565c0f87ba222c317b056a2cffd2f4"
@@ -850,7 +932,7 @@
resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
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"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
@@ -1797,6 +1879,11 @@
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==
"@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==
"@stitches/core@^1.2.6":
version "1.2.8"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@stitches/core/-/core-1.2.8.tgz#dce3b8fdc764fbc6dbea30c83b73bfb52cf96173"
@@ -1898,7 +1985,7 @@
picocolors "1.1.1"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^6.6.3":
"@testing-library/jest-dom@^6.6.3", "@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==
@@ -1910,6 +1997,13 @@
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"
"@testing-library/user-event@^14.6.1":
version "14.6.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149"
@@ -2024,6 +2118,13 @@
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
"@types/node@^25.3.0":
version "25.3.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/node/-/node-25.3.0.tgz#749b1bd4058e51b72e22bd41e9eab6ebd0180470"
integrity sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==
dependencies:
undici-types "~7.18.0"
"@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"
@@ -2159,6 +2260,22 @@
"@types/babel__core" "^7.20.5"
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@3.2.4":
version "3.2.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433"
@@ -2170,6 +2287,27 @@
chai "^5.2.0"
tinyrainbow "^2.0.0"
"@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@3.2.4":
version "3.2.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4"
@@ -2177,6 +2315,30 @@
dependencies:
tinyrainbow "^2.0.0"
"@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@3.2.4":
version "3.2.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599"
@@ -2184,6 +2346,11 @@
dependencies:
tinyspy "^4.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@3.2.4":
version "3.2.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea"
@@ -2193,6 +2360,14 @@
loupe "^3.1.4"
tinyrainbow "^2.0.0"
"@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.0.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -2203,6 +2378,11 @@ acorn@^8.0.0, acorn@^8.15.0, acorn@^8.16.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
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:
version "6.14.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
@@ -2282,6 +2462,15 @@ ast-types@^0.16.1:
dependencies:
tslib "^2.0.1"
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"
axe-core@^4.2.0:
version "4.11.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/axe-core/-/axe-core-4.11.1.tgz#052ff9b2cbf543f5595028b583e4763b40c78ea7"
@@ -2302,6 +2491,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"
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"
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@@ -2353,9 +2549,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001759:
version "1.0.30001772"
resolved "https://nexus.beatrice.wtf/repository/npm-group/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz#aa8a176eba0006e78c965a8215c7a1ceb030122d"
integrity sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==
version "1.0.30001774"
resolved "https://nexus.beatrice.wtf/repository/npm-group/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz#0e576b6f374063abcd499d202b9ba1301be29b70"
integrity sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==
ccount@^2.0.0:
version "2.0.1"
@@ -2373,6 +2569,11 @@ chai@^5.2.0:
loupe "^3.1.0"
pathval "^2.0.0"
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==
character-entities-html4@^2.0.0:
version "2.1.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
@@ -2475,6 +2676,14 @@ cross-spawn@^7.0.6:
shebang-command "^2.0.0"
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"
@@ -2485,6 +2694,16 @@ cssesc@^3.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
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:
version "3.2.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
@@ -2498,13 +2717,26 @@ d@1, d@^1.0.1, d@^1.0.2:
es5-ext "^0.10.64"
type "^2.7.2"
debug@^4.0.0, 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.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.3:
version "4.4.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
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==
decode-named-character-reference@^1.0.0:
version "1.3.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz#3e40603760874c2e5867691b599d73a7da25b53f"
@@ -2615,6 +2847,16 @@ empathic@^2.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/empathic/-/empathic-2.0.0.tgz#71d3c2b94fad49532ef98a6c34be0386659f6131"
integrity sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==
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==
es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14:
version "0.10.64"
resolved "https://nexus.beatrice.wtf/repository/npm-group/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714"
@@ -2694,10 +2936,10 @@ escape-string-regexp@^5.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-plugin-react-hooks@^7.0.1:
version "7.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169"
integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==
eslint-plugin-react-hooks@^7.1.0-canary-ab18f33d-20260220:
version "7.1.0-canary-fd524fe0-20251121"
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-G5we0+XjZTKpjkLL9AgdWxzmo4mqelVDIYzoR1dBlhhiN8Lf5PQ+l8frr+BmX02nU4g0AEez3eGSF/LNfHokEw==
dependencies:
"@babel/core" "^7.24.4"
"@babel/parser" "^7.24.4"
@@ -2827,6 +3069,13 @@ estree-walker@^2.0.2:
resolved "https://nexus.beatrice.wtf/repository/npm-group/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
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:
version "2.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@@ -2840,6 +3089,11 @@ event-emitter@^0.3.5:
d "1"
es5-ext "~0.10.14"
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==
ext@^1.7.0:
version "1.7.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
@@ -2980,6 +3234,11 @@ globals@^17.3.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9"
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==
hasown@^2.0.2:
version "2.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
@@ -2999,6 +3258,34 @@ hermes-parser@^0.25.1:
dependencies:
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"
ieee754@^1.2.1:
version "1.2.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -3095,6 +3382,11 @@ is-number@^7.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
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==
is-wsl@^3.1.0:
version "3.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/is-wsl/-/is-wsl-3.1.1.tgz#327897b26832a3eb117da6c27492d04ca132594f"
@@ -3112,11 +3404,38 @@ isomorphic.js@^0.2.4:
resolved "https://nexus.beatrice.wtf/repository/npm-group/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88"
integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==
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"
jiti@^1.21.7:
version "1.21.7"
resolved "https://nexus.beatrice.wtf/repository/npm-group/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9"
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
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@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -3129,6 +3448,33 @@ js-yaml@4.1.1:
dependencies:
argparse "^2.0.1"
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:
version "3.1.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
@@ -3220,7 +3566,7 @@ loupe@^3.1.0, loupe@^3.1.4:
resolved "https://nexus.beatrice.wtf/repository/npm-group/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76"
integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==
lru-cache@^11.0.0:
lru-cache@^11.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==
@@ -3237,13 +3583,29 @@ lz-string@^1.4.4, lz-string@^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.0:
magic-string@^0.30.0, 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"
markdown-table@^3.0.0:
version "3.0.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a"
@@ -3414,6 +3776,11 @@ mdast-util-to-string@^4.0.0:
dependencies:
"@types/mdast" "^4.0.0"
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==
merge2@^1.3.0:
version "1.4.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -3872,6 +4239,11 @@ object-hash@^3.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
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==
open@^10.2.0:
version "10.2.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c"
@@ -3931,6 +4303,13 @@ parse-entities@^4.0.0:
is-decimal "^2.0.0"
is-hexadecimal "^2.0.0"
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:
version "4.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -3954,6 +4333,11 @@ path-scurry@^2.0.2:
lru-cache "^11.0.0"
minipass "^7.1.2"
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==
pathval@^2.0.0:
version "2.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d"
@@ -4069,7 +4453,7 @@ prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.13.1"
punycode@^2.1.0:
punycode@^2.1.0, punycode@^2.3.1:
version "2.3.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@@ -4221,6 +4605,11 @@ redent@^3.0.0:
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==
resolve@^1.1.7, resolve@^1.22.1, resolve@^1.22.8:
version "1.22.11"
resolved "https://nexus.beatrice.wtf/repository/npm-group/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262"
@@ -4288,6 +4677,13 @@ sade@^1.7.3:
dependencies:
mri "^1.1.0"
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:
version "0.27.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
@@ -4298,7 +4694,7 @@ semver@^6.3.1:
resolved "https://nexus.beatrice.wtf/repository/npm-group/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.7.3:
semver@^7.5.3, semver@^7.7.3:
version "7.7.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
@@ -4320,7 +4716,12 @@ shebang-regex@^3.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
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"
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==
@@ -4330,6 +4731,11 @@ source-map@~0.6.1:
resolved "https://nexus.beatrice.wtf/repository/npm-group/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
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==
static-browser-server@1.0.3:
version "1.0.3"
resolved "https://nexus.beatrice.wtf/repository/npm-group/static-browser-server/-/static-browser-server-1.0.3.tgz#9030d141b99ed92c8eec1a7546b87548fd036f5d"
@@ -4340,6 +4746,11 @@ static-browser-server@1.0.3:
mime-db "^1.52.0"
outvariant "^1.3.0"
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==
storybook@^10.2.10:
version "10.2.10"
resolved "https://nexus.beatrice.wtf/repository/npm-group/storybook/-/storybook-10.2.10.tgz#b0a008c239690889fc52ae95a35305d4c64b4306"
@@ -4406,11 +4817,23 @@ sucrase@^3.35.0:
tinyglobby "^0.2.11"
ts-interface-checker "^0.1.9"
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"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
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==
tabbable@^6.0.0:
version "6.4.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581"
@@ -4463,6 +4886,16 @@ tiny-invariant@^1.3.3:
resolved "https://nexus.beatrice.wtf/repository/npm-group/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
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.11, tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
@@ -4476,11 +4909,28 @@ tinyrainbow@^2.0.0:
resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294"
integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==
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==
tinyspy@^4.0.3:
version "4.0.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78"
integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==
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"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -4488,6 +4938,20 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
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:
version "2.4.0"
resolved "https://nexus.beatrice.wtf/repository/npm-group/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8"
@@ -4544,6 +5008,16 @@ typescript@^5.6.2:
resolved "https://nexus.beatrice.wtf/repository/npm-group/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
undici-types@~7.18.0:
version "7.18.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9"
integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==
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==
unidiff@^1.0.2:
version "1.0.4"
resolved "https://nexus.beatrice.wtf/repository/npm-group/unidiff/-/unidiff-1.0.4.tgz#45096a898285821c51e22e84be4215c05d6511cd"
@@ -4657,7 +5131,7 @@ vfile-message@^4.0.0:
"@types/unist" "^3.0.0"
unist-util-stringify-position "^4.0.0"
vite@^7.0.0:
"vite@^6.0.0 || ^7.0.0", vite@^7.0.0:
version "7.3.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507"
integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==
@@ -4671,16 +5145,68 @@ vite@^7.0.0:
optionalDependencies:
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-keyname@^2.2.4:
version "2.2.8"
resolved "https://nexus.beatrice.wtf/repository/npm-group/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
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==
webpack-virtual-modules@^0.6.2:
version "0.6.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
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:
version "2.0.2"
resolved "https://nexus.beatrice.wtf/repository/npm-group/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -4688,6 +5214,14 @@ which@^2.0.1:
dependencies:
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:
version "1.2.5"
resolved "https://nexus.beatrice.wtf/repository/npm-group/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
@@ -4705,6 +5239,16 @@ wsl-utils@^0.1.0:
dependencies:
is-wsl "^3.1.0"
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:
version "3.1.1"
resolved "https://nexus.beatrice.wtf/repository/npm-group/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"