diff --git a/packages/benchmarks/.gitignore b/packages/benchmarks/.gitignore index 24cefbf..d61b80a 100644 --- a/packages/benchmarks/.gitignore +++ b/packages/benchmarks/.gitignore @@ -2,3 +2,5 @@ traces/ # Benchmark JSON results and reports (generated by bench:json / bench:report) results/ +# Reassure performance test output (generated by perf / perf:baseline) +.reassure/ diff --git a/packages/benchmarks/package.json b/packages/benchmarks/package.json index 7ec9791..ef4910b 100644 --- a/packages/benchmarks/package.json +++ b/packages/benchmarks/package.json @@ -6,9 +6,13 @@ "type": "module", "scripts": { "bench": "vitest bench --run", - "bench:browser": "vitest bench --config vitest.config.browser.ts --run" + "bench:browser": "vitest bench --config vitest.config.browser.ts --run", + "perf": "rm -f .reassure/current.perf && vitest --run --config vitest.config.perf.ts", + "perf:baseline": "rm -f .reassure/baseline.perf && REASSURE_OUTPUT_FILE=.reassure/baseline.perf vitest --run --config vitest.config.perf.ts", + "perf:compare": "node scripts/perf-compare.mjs" }, "devDependencies": { + "@callstack/reassure-compare": "^1.4.1", "@codspeed/vitest-plugin": "^5.2.0", "@omniview/base-ui": "workspace:*", "@omniview/editors": "workspace:*", @@ -22,6 +26,7 @@ "jsdom": "^25.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "reassure": "^1.4.1", "typescript": "^5.9.2", "vite": "^7.3.1", "vitest": "^4.0.0" diff --git a/packages/benchmarks/scripts/perf-compare.mjs b/packages/benchmarks/scripts/perf-compare.mjs new file mode 100644 index 0000000..204dbf9 --- /dev/null +++ b/packages/benchmarks/scripts/perf-compare.mjs @@ -0,0 +1,16 @@ +/** + * Compare Reassure performance results between baseline and current runs. + * + * Usage: + * pnpm perf:baseline # run on base branch, writes .reassure/baseline.perf + * pnpm perf # run on current branch, writes .reassure/current.perf + * pnpm perf:compare # compare and output report + */ +import { compare } from '@callstack/reassure-compare'; + +await compare({ + baselineFile: '.reassure/baseline.perf', + currentFile: '.reassure/current.perf', + outputFile: '.reassure/output.json', + outputFormat: 'all', +}); diff --git a/packages/benchmarks/src/__perf__/base-ui/Accordion.perf-test.tsx b/packages/benchmarks/src/__perf__/base-ui/Accordion.perf-test.tsx new file mode 100644 index 0000000..3aa335b --- /dev/null +++ b/packages/benchmarks/src/__perf__/base-ui/Accordion.perf-test.tsx @@ -0,0 +1,49 @@ +import { test } from 'vitest'; +import { measureRenders } from 'reassure'; +import { fireEvent, screen } from '@testing-library/react'; +import { Accordion } from '@omniview/base-ui'; +import { ThemeWrapper } from '../utils/theme-wrapper'; + +function FiveItemAccordion() { + return ( + + {Array.from({ length: 5 }, (_, i) => ( + + Content for section {i} + + ))} + + ); +} + +test('Accordion: mount 5 items', async () => { + await measureRenders(, { wrapper: ThemeWrapper }); +}); + +/** + * Expanding one section should NOT re-render sibling sections. + * This catches shared-context over-notification patterns. + */ +test('Accordion: expand one section', async () => { + await measureRenders(, { + wrapper: ThemeWrapper, + scenario: async () => { + fireEvent.click(screen.getByText('Section 2')); + }, + }); +}); + +/** + * Expanding then collapsing — tests the full toggle cycle. + * Render count should be similar to a single expand. + */ +test('Accordion: expand then collapse', async () => { + await measureRenders(, { + wrapper: ThemeWrapper, + scenario: async () => { + const trigger = screen.getByText('Section 2'); + fireEvent.click(trigger); + fireEvent.click(trigger); + }, + }); +}); diff --git a/packages/benchmarks/src/__perf__/base-ui/Button.perf-test.tsx b/packages/benchmarks/src/__perf__/base-ui/Button.perf-test.tsx new file mode 100644 index 0000000..a33554c --- /dev/null +++ b/packages/benchmarks/src/__perf__/base-ui/Button.perf-test.tsx @@ -0,0 +1,22 @@ +import { test } from 'vitest'; +import { measureRenders } from 'reassure'; +import { Button } from '@omniview/base-ui'; +import { ThemeWrapper } from '../utils/theme-wrapper'; + +/** + * Button is the simplest interactive component — its render count + * serves as the baseline for all other components. + * Expected: 1 render on mount, 0 issues. + */ +test('Button: mount', async () => { + await measureRenders(, { wrapper: ThemeWrapper }); +}); + +test('Button: mount with decorators', async () => { + await measureRenders( + , + { wrapper: ThemeWrapper }, + ); +}); diff --git a/packages/benchmarks/src/__perf__/base-ui/Checkbox.perf-test.tsx b/packages/benchmarks/src/__perf__/base-ui/Checkbox.perf-test.tsx new file mode 100644 index 0000000..5f3c972 --- /dev/null +++ b/packages/benchmarks/src/__perf__/base-ui/Checkbox.perf-test.tsx @@ -0,0 +1,49 @@ +import { test } from 'vitest'; +import { measureRenders } from 'reassure'; +import { fireEvent, screen } from '@testing-library/react'; +import { Checkbox } from '@omniview/base-ui'; +import { ThemeWrapper } from '../utils/theme-wrapper'; + +test('Checkbox: mount', async () => { + await measureRenders( + Accept terms, + { wrapper: ThemeWrapper }, + ); +}); + +test('Checkbox: toggle checked', async () => { + await measureRenders( + Accept terms, + { + wrapper: ThemeWrapper, + scenario: async () => { + const checkbox = screen.getByRole('checkbox'); + fireEvent.click(checkbox); + }, + }, + ); +}); + +/** + * Critical scenario: toggling one checkbox in a list. + * If memoization is missing, ALL checkboxes re-render on a single toggle. + */ +test('Checkbox: toggle one in list of 20', async () => { + const items = Array.from({ length: 20 }, (_, i) => `Option ${i}`); + await measureRenders( +
+ {items.map((label) => ( + + {label} + + ))} +
, + { + wrapper: ThemeWrapper, + scenario: async () => { + const checkboxes = screen.getAllByRole('checkbox'); + fireEvent.click(checkboxes[0]!); + }, + }, + ); +}); diff --git a/packages/benchmarks/src/__perf__/base-ui/Dialog.perf-test.tsx b/packages/benchmarks/src/__perf__/base-ui/Dialog.perf-test.tsx new file mode 100644 index 0000000..30533e6 --- /dev/null +++ b/packages/benchmarks/src/__perf__/base-ui/Dialog.perf-test.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import { test } from 'vitest'; +import { measureRenders } from 'reassure'; +import { fireEvent, screen } from '@testing-library/react'; +import { Dialog, Button } from '@omniview/base-ui'; +import { ThemeWrapper } from '../utils/theme-wrapper'; + +function DialogWithTrigger() { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} size="md"> + Confirm Action + Are you sure you want to proceed? + + + + + + + + ); +} + +/** + * Mount cost includes the trigger but NOT the dialog body (open=false). + * Render count should be minimal — just the trigger + null dialog. + */ +test('Dialog: mount (closed)', async () => { + await measureRenders(, { wrapper: ThemeWrapper }); +}); + +/** + * Opening the dialog triggers portal mount, backdrop, and content render. + * High render counts here signal excessive setState chains in useEffect. + */ +test('Dialog: open', async () => { + await measureRenders(, { + wrapper: ThemeWrapper, + scenario: async () => { + fireEvent.click(screen.getByText('Open Dialog')); + }, + }); +}); + +/** + * Full open/close cycle. Render count should be ~2x the open-only count. + * Significantly more signals cleanup-related re-renders. + */ +test('Dialog: open then close', async () => { + await measureRenders(, { + wrapper: ThemeWrapper, + scenario: async () => { + fireEvent.click(screen.getByText('Open Dialog')); + fireEvent.click(screen.getByText('Cancel')); + }, + }); +}); diff --git a/packages/benchmarks/src/__perf__/base-ui/Input.perf-test.tsx b/packages/benchmarks/src/__perf__/base-ui/Input.perf-test.tsx new file mode 100644 index 0000000..5867d66 --- /dev/null +++ b/packages/benchmarks/src/__perf__/base-ui/Input.perf-test.tsx @@ -0,0 +1,63 @@ +import { test } from 'vitest'; +import { type ChangeEvent, useState } from 'react'; +import { measureRenders } from 'reassure'; +import { fireEvent, screen } from '@testing-library/react'; +import { Input, TextField } from '@omniview/base-ui'; +import { ThemeWrapper } from '../utils/theme-wrapper'; + +test('Input: mount', async () => { + await measureRenders( + + Username + + , + { wrapper: ThemeWrapper }, + ); +}); + +test('TextField: mount', async () => { + await measureRenders( + + Email + + , + { wrapper: ThemeWrapper }, + ); +}); + +/** + * Controlled input wrapper so each fireEvent.change triggers a state update + * and re-render, simulating real per-keystroke behavior. + */ +function ControlledInput() { + const [value, setValue] = useState(''); + return ( + + Username + ) => setValue(e.target.value)} + /> + + ); +} + +/** + * Typing in an input field — each keystroke should cause minimal re-renders. + * High counts here signal uncontrolled→controlled mismatches or expensive + * parent re-renders from onChange. + */ +test('Input: type 10 characters', async () => { + await measureRenders(, { + wrapper: ThemeWrapper, + scenario: async () => { + const input = screen.getByTestId('input'); + const chars = 'helloworld'; + for (let i = 0; i < chars.length; i++) { + fireEvent.change(input, { target: { value: chars.slice(0, i + 1) } }); + } + }, + }); +}); diff --git a/packages/benchmarks/src/__perf__/base-ui/Select.perf-test.tsx b/packages/benchmarks/src/__perf__/base-ui/Select.perf-test.tsx new file mode 100644 index 0000000..ad1471d --- /dev/null +++ b/packages/benchmarks/src/__perf__/base-ui/Select.perf-test.tsx @@ -0,0 +1,67 @@ +import { test } from 'vitest'; +import { measureRenders } from 'reassure'; +import { fireEvent, screen } from '@testing-library/react'; +import { Select } from '@omniview/base-ui'; +import { makeOptions } from '../../utils/factories'; +import { ThemeWrapper } from '../utils/theme-wrapper'; + +const options = makeOptions(20); + +test('Select: mount with 20 options', async () => { + await measureRenders( + , + { wrapper: ThemeWrapper }, + ); +}); + +/** + * Opening a Select triggers portal mount, positioner layout, and popup render. + * High render counts here signal excessive context notifications. + */ +test('Select: open dropdown', async () => { + await measureRenders( + , + { + wrapper: ThemeWrapper, + scenario: async () => { + const trigger = screen.getByRole('combobox'); + fireEvent.click(trigger); + await screen.findByRole('listbox'); + }, + }, + ); +}); diff --git a/packages/benchmarks/src/__perf__/base-ui/Tabs.perf-test.tsx b/packages/benchmarks/src/__perf__/base-ui/Tabs.perf-test.tsx new file mode 100644 index 0000000..cbc8725 --- /dev/null +++ b/packages/benchmarks/src/__perf__/base-ui/Tabs.perf-test.tsx @@ -0,0 +1,54 @@ +import { test } from 'vitest'; +import { measureRenders } from 'reassure'; +import { fireEvent, screen } from '@testing-library/react'; +import { Tabs } from '@omniview/base-ui'; +import { ThemeWrapper } from '../utils/theme-wrapper'; + +test('Tabs: mount with 5 tabs', async () => { + await measureRenders( + + + {Array.from({ length: 5 }, (_, i) => ( + + Tab {i} + + ))} + + {Array.from({ length: 5 }, (_, i) => ( + + Content for tab {i} + + ))} + , + { wrapper: ThemeWrapper }, + ); +}); + +/** + * Switching tabs should ideally only re-render the old and new panels, + * not the entire tree. High render counts here signal over-rendering. + */ +test('Tabs: switch active tab', async () => { + await measureRenders( + + + {Array.from({ length: 5 }, (_, i) => ( + + Tab {i} + + ))} + + {Array.from({ length: 5 }, (_, i) => ( + + Content for tab {i} + + ))} + , + { + wrapper: ThemeWrapper, + scenario: async () => { + fireEvent.click(screen.getByText('Tab 3')); + }, + }, + ); +}); diff --git a/packages/benchmarks/src/__perf__/utils/theme-wrapper.tsx b/packages/benchmarks/src/__perf__/utils/theme-wrapper.tsx new file mode 100644 index 0000000..fc9e35b --- /dev/null +++ b/packages/benchmarks/src/__perf__/utils/theme-wrapper.tsx @@ -0,0 +1,10 @@ +import type { ReactElement } from 'react'; +import { ThemeProvider } from '@omniview/base-ui'; + +/** + * Wrapper for Reassure tests — provides the ThemeProvider context + * required by all @omniview components. + */ +export function ThemeWrapper({ children }: { children: ReactElement }) { + return {children}; +} diff --git a/packages/benchmarks/src/base-ui/Accordion.bench.tsx b/packages/benchmarks/src/base-ui/Accordion.bench.tsx new file mode 100644 index 0000000..4ed6296 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Accordion.bench.tsx @@ -0,0 +1,34 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Accordion } from '@omniview/base-ui'; + +describe('Accordion', () => { + benchRender( + 'mount with 3 items', + () => ( + + Content A + Content B + Content C + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'expand toggle', + { + initialProps: { expanded: [] as string[] }, + updatedProps: { expanded: ['a'] as string[] }, + }, + (props) => ( + + Content A + Content B + Content C + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/ActionList.bench.tsx b/packages/benchmarks/src/base-ui/ActionList.bench.tsx new file mode 100644 index 0000000..fd337b6 --- /dev/null +++ b/packages/benchmarks/src/base-ui/ActionList.bench.tsx @@ -0,0 +1,38 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ActionList } from '@omniview/base-ui'; + +describe('ActionList', () => { + benchRender( + 'mount with items', + () => ( + + + + + + + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'disabled toggle', + { + initialProps: { disabled: false }, + updatedProps: { disabled: true }, + }, + (props) => ( + + + + + + + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/AlertDialog.bench.tsx b/packages/benchmarks/src/base-ui/AlertDialog.bench.tsx new file mode 100644 index 0000000..e49a80e --- /dev/null +++ b/packages/benchmarks/src/base-ui/AlertDialog.bench.tsx @@ -0,0 +1,44 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { AlertDialog } from '@omniview/base-ui'; + +describe('AlertDialog', () => { + benchRender( + 'mount (open)', + () => ( + + + + + Confirm + Are you sure? + Cancel + + + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'open toggle', + { + initialProps: { open: true }, + updatedProps: { open: false }, + }, + (props) => ( + + + + + Confirm + Are you sure? + Cancel + + + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/AppShell.bench.tsx b/packages/benchmarks/src/base-ui/AppShell.bench.tsx new file mode 100644 index 0000000..60a8156 --- /dev/null +++ b/packages/benchmarks/src/base-ui/AppShell.bench.tsx @@ -0,0 +1,34 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { AppShell } from '@omniview/base-ui'; + +describe('AppShell', () => { + benchRender( + 'mount with Header/Sidebar/Content', + () => ( + + Header + Sidebar + Content + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'sidebar visibility toggle', + { + initialProps: { sidebarCollapsed: false }, + updatedProps: { sidebarCollapsed: true }, + }, + (props) => ( + + Header + Sidebar + Content + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Autocomplete.bench.tsx b/packages/benchmarks/src/base-ui/Autocomplete.bench.tsx new file mode 100644 index 0000000..f9b4f44 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Autocomplete.bench.tsx @@ -0,0 +1,53 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeOptions, type Option } from '../utils/factories'; +import { Autocomplete } from '@omniview/base-ui'; + +const options100 = makeOptions(100); +const options200 = makeOptions(200); +const options10 = makeOptions(10); + +function AutocompleteBench({ options }: { options: Option[] }) { + return ( + + + + + + + + {(item: Option) => ( + + {item.label} + + )} + + + + + + ); +} + +describe('Autocomplete', () => { + benchRender( + 'mount with 100 options', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'options change (100 → 200)', + { initialProps: { options: options100 }, updatedProps: { options: options200 } }, + (props) => , + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 100 instances (10 options each)', + 100, + (i) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Banner.bench.tsx b/packages/benchmarks/src/base-ui/Banner.bench.tsx new file mode 100644 index 0000000..4a480db --- /dev/null +++ b/packages/benchmarks/src/base-ui/Banner.bench.tsx @@ -0,0 +1,39 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Banner } from '@omniview/base-ui'; + +describe('Banner', () => { + benchRender( + 'mount', + () => ( + + Notice + This is a banner message. + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'variant change', + { + initialProps: { variant: 'soft' as const }, + updatedProps: { variant: 'outline' as const }, + }, + (props) => ( + + Notice + This is a banner message. + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 50', 50, (i) => ( + + Notice {i} + Message {i} + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/BasicList.bench.tsx b/packages/benchmarks/src/base-ui/BasicList.bench.tsx new file mode 100644 index 0000000..f33e372 --- /dev/null +++ b/packages/benchmarks/src/base-ui/BasicList.bench.tsx @@ -0,0 +1,43 @@ +import { describe } from 'vitest'; +import { BasicList } from '@omniview/base-ui'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeRows, type Row } from '../utils/factories'; + +// Pre-generate data +const rows200 = makeRows(200); +const rows300 = makeRows(300); + +// --------------------------------------------------------------------------- +// Wrapper +// --------------------------------------------------------------------------- + +function BasicListBench({ data }: { data: Row[] }) { + return ( + + + {data.map((row) => ( + + {row.name} + {row.status} + + ))} + + + ); +} + +// --------------------------------------------------------------------------- +// Benchmarks +// --------------------------------------------------------------------------- + +describe('BasicList', () => { + benchRender('mount 200 items', () => , TIER_1_OPTIONS); + + benchRerender( + 'data change (200 -> 300 items)', + { initialProps: { data: rows200 }, updatedProps: { data: rows300 } }, + (props) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Box.bench.tsx b/packages/benchmarks/src/base-ui/Box.bench.tsx index 3294ad9..a694ffa 100644 --- a/packages/benchmarks/src/base-ui/Box.bench.tsx +++ b/packages/benchmarks/src/base-ui/Box.bench.tsx @@ -1,11 +1,10 @@ import { describe } from 'vitest'; import { benchRender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; import { Box } from '@omniview/base-ui'; describe('Box', () => { - benchRender('mount div', () => Content); - - benchRender('mount section', () => Content); - - benchMountMany('mount 1000', 1000, (i) => Item {i}); + benchRender('mount div', () => Content, TIER_2_OPTIONS); + benchRender('mount section', () => Content, TIER_2_OPTIONS); + benchMountMany('mount 1000', 1000, (i) => Item {i}, TIER_2_OPTIONS); }); diff --git a/packages/benchmarks/src/base-ui/Breadcrumbs.bench.tsx b/packages/benchmarks/src/base-ui/Breadcrumbs.bench.tsx new file mode 100644 index 0000000..c25600a --- /dev/null +++ b/packages/benchmarks/src/base-ui/Breadcrumbs.bench.tsx @@ -0,0 +1,51 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Breadcrumbs } from '@omniview/base-ui'; + +function threeItems() { + return ( + <> + Home + Docs + Current + + ); +} + +function fourItems() { + return ( + <> + Home + Docs + API + Current + + ); +} + +describe('Breadcrumbs', () => { + benchRender( + 'mount with 3 items', + () => {threeItems()}, + TIER_2_OPTIONS, + ); + + benchRerender( + 'items change', + { + initialProps: { children: threeItems() }, + updatedProps: { children: fourItems() }, + }, + (props) => , + TIER_2_OPTIONS, + ); + + benchMountMany('mount 50', 50, (i) => ( + + Home + Section + Page {i} + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Button.bench.tsx b/packages/benchmarks/src/base-ui/Button.bench.tsx index 36c78fb..6874c1a 100644 --- a/packages/benchmarks/src/base-ui/Button.bench.tsx +++ b/packages/benchmarks/src/base-ui/Button.bench.tsx @@ -1,21 +1,20 @@ import { describe } from 'vitest'; import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; import { Button } from '@omniview/base-ui'; describe('Button', () => { - benchRender('mount', () => ); - + benchRender('mount', () => , TIER_2_OPTIONS); benchRender('mount with decorators', () => ( - )); - + ), TIER_2_OPTIONS); benchRerender( 'variant change', { initialProps: { variant: 'solid' as const }, updatedProps: { variant: 'outline' as const } }, (props) => , + TIER_2_OPTIONS, ); - - benchMountMany('mount 1000', 1000, (i) => ); + benchMountMany('mount 1000', 1000, (i) => , TIER_2_OPTIONS); }); diff --git a/packages/benchmarks/src/base-ui/ButtonGroup.bench.tsx b/packages/benchmarks/src/base-ui/ButtonGroup.bench.tsx new file mode 100644 index 0000000..abba5ca --- /dev/null +++ b/packages/benchmarks/src/base-ui/ButtonGroup.bench.tsx @@ -0,0 +1,34 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ButtonGroup } from '@omniview/base-ui'; + +describe('ButtonGroup', () => { + benchRender( + 'mount with 3 buttons', + () => ( + + One + Two + Three + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'variant change', + { + initialProps: { variant: 'soft' as const }, + updatedProps: { variant: 'outline' as const }, + }, + (props) => ( + + One + Two + Three + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Card.bench.tsx b/packages/benchmarks/src/base-ui/Card.bench.tsx new file mode 100644 index 0000000..5b8b8cc --- /dev/null +++ b/packages/benchmarks/src/base-ui/Card.bench.tsx @@ -0,0 +1,43 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Card } from '@omniview/base-ui'; + +describe('Card', () => { + benchRender( + 'mount with Header/Title/Body', + () => ( + + + Title + + Body content + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'variant change', + { + initialProps: { variant: 'soft' as const }, + updatedProps: { variant: 'outline' as const }, + }, + (props) => ( + + + Title + + Body content + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => ( + + Card {i} + Content {i} + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Checkbox.bench.tsx b/packages/benchmarks/src/base-ui/Checkbox.bench.tsx index 5786a9a..486d6aa 100644 --- a/packages/benchmarks/src/base-ui/Checkbox.bench.tsx +++ b/packages/benchmarks/src/base-ui/Checkbox.bench.tsx @@ -1,15 +1,15 @@ import { describe } from 'vitest'; import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; import { Checkbox } from '@omniview/base-ui'; describe('Checkbox', () => { - benchRender('mount', () => ); - + benchRender('mount', () => , TIER_2_OPTIONS); benchRerender( 'checked toggle', { initialProps: { checked: false }, updatedProps: { checked: true } }, (props) => , + TIER_2_OPTIONS, ); - - benchMountMany('mount 1000', 1000, (i) => ); + benchMountMany('mount 200', 200, (i) => , TIER_2_OPTIONS); }); diff --git a/packages/benchmarks/src/base-ui/CheckboxGroup.bench.tsx b/packages/benchmarks/src/base-ui/CheckboxGroup.bench.tsx new file mode 100644 index 0000000..1f12b05 --- /dev/null +++ b/packages/benchmarks/src/base-ui/CheckboxGroup.bench.tsx @@ -0,0 +1,27 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { CheckboxGroup } from '@omniview/base-ui'; + +describe('CheckboxGroup', () => { + benchRender('mount with 3 items', () => ( + + Option A + Option B + Option C + + ), TIER_2_OPTIONS); + + benchRerender( + 'value change', + { initialProps: { value: ['a'] as string[] }, updatedProps: { value: ['a', 'b'] as string[] } }, + (props) => ( + + Option A + Option B + Option C + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Chip.bench.tsx b/packages/benchmarks/src/base-ui/Chip.bench.tsx new file mode 100644 index 0000000..c3459aa --- /dev/null +++ b/packages/benchmarks/src/base-ui/Chip.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Chip } from '@omniview/base-ui'; + +describe('Chip', () => { + benchRender( + 'mount', + () => Label, + TIER_2_OPTIONS, + ); + + benchRerender( + 'variant change', + { + initialProps: { variant: 'soft' as const }, + updatedProps: { variant: 'outline' as const }, + }, + (props) => Label, + TIER_2_OPTIONS, + ); + + benchMountMany('mount 200', 200, (i) => Tag {i}, TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/ClipboardText.bench.tsx b/packages/benchmarks/src/base-ui/ClipboardText.bench.tsx new file mode 100644 index 0000000..b8f6113 --- /dev/null +++ b/packages/benchmarks/src/base-ui/ClipboardText.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ClipboardText } from '@omniview/base-ui'; + +describe('ClipboardText', () => { + benchRender( + 'mount', + () => , + TIER_2_OPTIONS, + ); + + benchRerender( + 'value change', + { + initialProps: { value: 'kubectl get pods' }, + updatedProps: { value: 'kubectl get nodes' }, + }, + (props) => , + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => , TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/CodeBlock.bench.tsx b/packages/benchmarks/src/base-ui/CodeBlock.bench.tsx new file mode 100644 index 0000000..9d3fce3 --- /dev/null +++ b/packages/benchmarks/src/base-ui/CodeBlock.bench.tsx @@ -0,0 +1,25 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { CodeBlock } from '@omniview/base-ui'; + +const codeA = 'const x = 1;\nconsole.log(x);'; +const codeB = 'const y = 2;\nconsole.log(y);'; + +describe('CodeBlock', () => { + benchRender( + 'mount', + () => {codeA}, + TIER_2_OPTIONS, + ); + + benchRerender( + 'code change', + { + initialProps: { children: codeA }, + updatedProps: { children: codeB }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Collapsible.bench.tsx b/packages/benchmarks/src/base-ui/Collapsible.bench.tsx new file mode 100644 index 0000000..6d652dd --- /dev/null +++ b/packages/benchmarks/src/base-ui/Collapsible.bench.tsx @@ -0,0 +1,39 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@omniview/base-ui'; + +describe('Collapsible', () => { + benchRender( + 'mount', + () => ( + + Toggle + Collapsible content here + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'open toggle', + { + initialProps: { open: false }, + updatedProps: { open: true }, + }, + (props) => ( + + Toggle + Collapsible content here + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => ( + + Section {i} + Content {i} + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Combobox.bench.tsx b/packages/benchmarks/src/base-ui/Combobox.bench.tsx new file mode 100644 index 0000000..b94f8e3 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Combobox.bench.tsx @@ -0,0 +1,58 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeOptions, type Option } from '../utils/factories'; +import { Combobox } from '@omniview/base-ui'; + +const options100 = makeOptions(100); +const options200 = makeOptions(200); +const options10 = makeOptions(10); + +function ComboboxBench({ options }: { options: Option[] }) { + return ( + item.label} + itemToStringValue={(item: Option) => item.value} + defaultOpen + > + + + + + + + {(item: Option) => ( + + {item.label} + + )} + + + + + + ); +} + +describe('Combobox', () => { + benchRender( + 'mount with 100 options', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'options change (100 → 200)', + { initialProps: { options: options100 }, updatedProps: { options: options200 } }, + (props) => , + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 100 instances (10 options each)', + 100, + (i) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/CommandList.bench.tsx b/packages/benchmarks/src/base-ui/CommandList.bench.tsx new file mode 100644 index 0000000..dbba346 --- /dev/null +++ b/packages/benchmarks/src/base-ui/CommandList.bench.tsx @@ -0,0 +1,54 @@ +import { describe } from 'vitest'; +import { CommandList } from '@omniview/base-ui'; +import type { CommandItemMeta } from '@omniview/base-ui'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeCommandItems, type CommandItem } from '../utils/factories'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const itemKey = (item: CommandItem) => item.id; +const getTextValue = (item: CommandItem) => item.label; +const renderItem = (item: CommandItem, meta: CommandItemMeta) => ( + + {item.label} + +); + +// Pre-generate data +const items200 = makeCommandItems(200); +const items300 = makeCommandItems(300); + +// --------------------------------------------------------------------------- +// Wrapper +// --------------------------------------------------------------------------- + +function CommandListBench({ items }: { items: CommandItem[] }) { + return ( + + + + ); +} + +// --------------------------------------------------------------------------- +// Benchmarks +// --------------------------------------------------------------------------- + +describe('CommandList', () => { + benchRender('mount 200 items', () => , TIER_1_OPTIONS); + + benchRerender( + 'data change (200 -> 300 items)', + { initialProps: { items: items200 }, updatedProps: { items: items300 } }, + (props) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/ConfirmButton.bench.tsx b/packages/benchmarks/src/base-ui/ConfirmButton.bench.tsx new file mode 100644 index 0000000..9420a2d --- /dev/null +++ b/packages/benchmarks/src/base-ui/ConfirmButton.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ConfirmButton } from '@omniview/base-ui'; + +const noop = () => {}; + +describe('ConfirmButton', () => { + benchRender( + 'mount', + () => Delete, + TIER_2_OPTIONS, + ); + + benchRerender( + 'disabled toggle', + { + initialProps: { disabled: false, onConfirm: noop, children: 'Delete' }, + updatedProps: { disabled: true, onConfirm: noop, children: 'Delete' }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/ContextMenu.bench.tsx b/packages/benchmarks/src/base-ui/ContextMenu.bench.tsx new file mode 100644 index 0000000..3b1e528 --- /dev/null +++ b/packages/benchmarks/src/base-ui/ContextMenu.bench.tsx @@ -0,0 +1,75 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { ContextMenu } from '@omniview/base-ui'; + +describe('ContextMenu', () => { + benchRender( + 'mount with items', + () => ( + + +
Right-click me
+
+ + + + Cut + Copy + Paste + + Delete + + + +
+ ), + TIER_1_OPTIONS, + ); + + benchRerender( + 'disabled toggle on item', + { + initialProps: { itemDisabled: false }, + updatedProps: { itemDisabled: true }, + }, + (props) => ( + + +
Right-click me
+
+ + + + Cut + Copy + Paste + + + +
+ ), + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 50 context menus', + 50, + (i) => ( + + +
Trigger {i}
+
+ + + + Action A + Action B + + + +
+ ), + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/DataTable.bench.tsx b/packages/benchmarks/src/base-ui/DataTable.bench.tsx index a3a3a87..e294402 100644 --- a/packages/benchmarks/src/base-ui/DataTable.bench.tsx +++ b/packages/benchmarks/src/base-ui/DataTable.bench.tsx @@ -6,14 +6,9 @@ import { type ColumnDef, } from '@tanstack/react-table'; import { DataTable } from '@omniview/base-ui'; -import { benchRender } from '../utils/bench-render'; - -interface Row { - id: number; - name: string; - status: string; - value: number; -} +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeRows, type Row } from '../utils/factories'; const columnHelper = createColumnHelper(); @@ -24,23 +19,9 @@ const columns: ColumnDef[] = [ columnHelper.accessor('value', { header: 'Value' }), ]; -function generateRows(count: number): Row[] { - return Array.from({ length: count }, (_, i) => ({ - id: i, - name: `Row ${i}`, - status: i % 3 === 0 ? 'active' : i % 3 === 1 ? 'pending' : 'inactive', - value: (i * 7 + 13) % 1000, - })); -} - -// Pre-generate data so row construction isn't measured -const rows100 = generateRows(100); -const rows1000 = generateRows(1000); +const rows100 = makeRows(100); +const rows200 = makeRows(200); -/** - * Wrapper component that calls useReactTable internally and renders - * DataTable with its compound children. - */ function DataTableBench({ data }: { data: Row[] }) { const table = useReactTable({ data, @@ -59,6 +40,12 @@ function DataTableBench({ data }: { data: Row[] }) { } describe('DataTable', () => { - benchRender('mount 100 rows', () => ); - benchRender('mount 1000 rows', () => ); + benchRender('mount 100 rows', () => , TIER_1_OPTIONS); + + benchRerender( + 'data change (100 → 200 rows)', + { initialProps: { data: rows100 }, updatedProps: { data: rows200 } }, + (props) => , + TIER_1_OPTIONS, + ); }); diff --git a/packages/benchmarks/src/base-ui/DescriptionList.bench.tsx b/packages/benchmarks/src/base-ui/DescriptionList.bench.tsx new file mode 100644 index 0000000..9cf5f97 --- /dev/null +++ b/packages/benchmarks/src/base-ui/DescriptionList.bench.tsx @@ -0,0 +1,43 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { DescriptionList } from '@omniview/base-ui'; + +function threeItems() { + return ( + <> + Widget + Active + 1.0.0 + + ); +} + +function fourItems() { + return ( + <> + Widget + Active + 1.0.0 + US-East + + ); +} + +describe('DescriptionList', () => { + benchRender( + 'mount with 3 items', + () => {threeItems()}, + TIER_2_OPTIONS, + ); + + benchRerender( + 'items change', + { + initialProps: { children: threeItems() }, + updatedProps: { children: fourItems() }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Dialog.bench.tsx b/packages/benchmarks/src/base-ui/Dialog.bench.tsx new file mode 100644 index 0000000..25f5c36 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Dialog.bench.tsx @@ -0,0 +1,53 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { Dialog } from '@omniview/base-ui'; + +const noop = () => {}; + +describe('Dialog', () => { + benchRender( + 'mount (open)', + () => ( + + Title + Body content + + + + + + ), + TIER_1_OPTIONS, + ); + + benchRerender( + 'open/close toggle', + { + initialProps: { open: true }, + updatedProps: { open: false }, + }, + (props) => ( + + Title + Body content + + + + + ), + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 50 dialogs', + 50, + (i) => ( + + Dialog {i} + Content {i} + + ), + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/DockLayout.bench.tsx b/packages/benchmarks/src/base-ui/DockLayout.bench.tsx new file mode 100644 index 0000000..ef4dd34 --- /dev/null +++ b/packages/benchmarks/src/base-ui/DockLayout.bench.tsx @@ -0,0 +1,105 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { DockLayout, type DockNode } from '@omniview/base-ui'; + +function makeTwoPanel(): DockNode { + return { + type: 'split', + direction: 'horizontal', + children: [ + { + type: 'leaf', + id: 'left', + tabs: [ + { id: 'tab-1', title: 'File A', content:
Content A
}, + { id: 'tab-2', title: 'File B', content:
Content B
}, + ], + activeTab: 'tab-1', + }, + { + type: 'leaf', + id: 'right', + tabs: [ + { id: 'tab-3', title: 'File C', content:
Content C
}, + ], + activeTab: 'tab-3', + }, + ], + sizes: [1, 1], + }; +} + +function makeThreePanelVertical(): DockNode { + return { + type: 'split', + direction: 'vertical', + children: [ + { + type: 'leaf', + id: 'top', + tabs: [{ id: 'tab-a', title: 'Top', content:
Top
}], + activeTab: 'tab-a', + }, + { + type: 'leaf', + id: 'middle', + tabs: [{ id: 'tab-b', title: 'Middle', content:
Middle
}], + activeTab: 'tab-b', + }, + { + type: 'leaf', + id: 'bottom', + tabs: [{ id: 'tab-c', title: 'Bottom', content:
Bottom
}], + activeTab: 'tab-c', + }, + ], + sizes: [1, 1, 1], + }; +} + +describe('DockLayout', () => { + benchRender( + 'mount 2-panel layout', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'layout change (2-panel → 3-panel vertical)', + { + initialProps: { layout: makeTwoPanel() }, + updatedProps: { layout: makeThreePanelVertical() }, + }, + (props) => , + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 20 dock layouts', + 20, + (i) => { + const layout: DockNode = { + type: 'split', + direction: 'horizontal', + children: [ + { + type: 'leaf', + id: `left-${i}`, + tabs: [{ id: `t-${i}-a`, title: `Tab A ${i}`, content:
A{i}
}], + activeTab: `t-${i}-a`, + }, + { + type: 'leaf', + id: `right-${i}`, + tabs: [{ id: `t-${i}-b`, title: `Tab B ${i}`, content:
B{i}
}], + activeTab: `t-${i}-b`, + }, + ], + sizes: [1, 1], + }; + return ; + }, + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Drawer.bench.tsx b/packages/benchmarks/src/base-ui/Drawer.bench.tsx new file mode 100644 index 0000000..4edeffe --- /dev/null +++ b/packages/benchmarks/src/base-ui/Drawer.bench.tsx @@ -0,0 +1,43 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { Drawer } from '@omniview/base-ui'; + +const noop = () => {}; + +describe('Drawer', () => { + benchRender( + 'mount (open)', + () => ( + + Drawer content + + ), + TIER_1_OPTIONS, + ); + + benchRerender( + 'open/close toggle', + { + initialProps: { open: true }, + updatedProps: { open: false }, + }, + (props) => ( + + Drawer content + + ), + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 50 drawers', + 50, + (i) => ( + + Content {i} + + ), + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/EditableList.bench.tsx b/packages/benchmarks/src/base-ui/EditableList.bench.tsx new file mode 100644 index 0000000..ed50815 --- /dev/null +++ b/packages/benchmarks/src/base-ui/EditableList.bench.tsx @@ -0,0 +1,57 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { EditableList } from '@omniview/base-ui'; + +const noop = () => {}; + +function threeItems() { + return ( + <> + + Item A + + + + + + Item B + + + + + + Item C + + + + + + ); +} + +describe('EditableList', () => { + benchRender( + 'mount with items', + () => ( + + {threeItems()} + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'editable toggle', + { + initialProps: { editable: true }, + updatedProps: { editable: false }, + }, + (props) => ( + + {threeItems()} + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/EditorTabs.bench.tsx b/packages/benchmarks/src/base-ui/EditorTabs.bench.tsx new file mode 100644 index 0000000..fdfff59 --- /dev/null +++ b/packages/benchmarks/src/base-ui/EditorTabs.bench.tsx @@ -0,0 +1,36 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { EditorTabs } from '@omniview/base-ui'; +import { makeEditorTabs } from '../utils/factories'; + +const tabs20 = makeEditorTabs(20); +const tabs40 = makeEditorTabs(40); + +describe('EditorTabs', () => { + benchRender( + 'mount 20 tabs', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'tabs change (20 → 40)', + { + initialProps: { tabs: tabs20 }, + updatedProps: { tabs: tabs40 }, + }, + (props) => , + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 30 tab bars (5 tabs each)', + 30, + (i) => { + const tabs = makeEditorTabs(5, `bar${i}-`); + return ; + }, + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/EmptyState.bench.tsx b/packages/benchmarks/src/base-ui/EmptyState.bench.tsx new file mode 100644 index 0000000..8814de8 --- /dev/null +++ b/packages/benchmarks/src/base-ui/EmptyState.bench.tsx @@ -0,0 +1,22 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { EmptyState } from '@omniview/base-ui'; + +describe('EmptyState', () => { + benchRender( + 'mount', + () => , + TIER_2_OPTIONS, + ); + + benchRerender( + 'title change', + { + initialProps: { title: 'No results' }, + updatedProps: { title: 'Nothing found' }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/FilterBar.bench.tsx b/packages/benchmarks/src/base-ui/FilterBar.bench.tsx new file mode 100644 index 0000000..5474e45 --- /dev/null +++ b/packages/benchmarks/src/base-ui/FilterBar.bench.tsx @@ -0,0 +1,44 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeTags } from '../utils/factories'; +import { FilterBar } from '@omniview/base-ui'; + +const filters10 = makeTags(10); +const filters20 = makeTags(20); +const filters5 = makeTags(5); + +function FilterBarBench({ filters }: { filters: string[] }) { + return ( + + {filters.map((filter) => ( + {}}> + {filter} + + ))} + + + ); +} + +describe('FilterBar', () => { + benchRender( + 'mount with 10 filters', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'filters change (10 → 20)', + { initialProps: { filters: filters10 }, updatedProps: { filters: filters20 } }, + (props) => , + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 100 instances (5 filters each)', + 100, + (i) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/FindBar.bench.tsx b/packages/benchmarks/src/base-ui/FindBar.bench.tsx new file mode 100644 index 0000000..c14e45f --- /dev/null +++ b/packages/benchmarks/src/base-ui/FindBar.bench.tsx @@ -0,0 +1,22 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { FindBar } from '@omniview/base-ui'; + +describe('FindBar', () => { + benchRender( + 'mount', + () => , + TIER_2_OPTIONS, + ); + + benchRerender( + 'value change', + { + initialProps: { open: true as const, query: '' }, + updatedProps: { open: true as const, query: 'search term' }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/FormField.bench.tsx b/packages/benchmarks/src/base-ui/FormField.bench.tsx new file mode 100644 index 0000000..83d9d1a --- /dev/null +++ b/packages/benchmarks/src/base-ui/FormField.bench.tsx @@ -0,0 +1,32 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { FormField } from '@omniview/base-ui'; + +describe('FormField', () => { + benchRender('mount', () => ( + + + + ), TIER_2_OPTIONS); + + benchRerender( + 'error toggle', + { + initialProps: { error: undefined as string | undefined }, + updatedProps: { error: 'This field is required' }, + }, + (props) => ( + + + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => ( + + + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/IconButton.bench.tsx b/packages/benchmarks/src/base-ui/IconButton.bench.tsx new file mode 100644 index 0000000..113384f --- /dev/null +++ b/packages/benchmarks/src/base-ui/IconButton.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { IconButton } from '@omniview/base-ui'; + +describe('IconButton', () => { + benchRender( + 'mount', + () => E, + TIER_2_OPTIONS, + ); + + benchRerender( + 'variant change', + { + initialProps: { variant: 'soft' as const, 'aria-label': 'Edit' }, + updatedProps: { variant: 'outline' as const, 'aria-label': 'Edit' }, + }, + (props) => E, + TIER_2_OPTIONS, + ); + + benchMountMany('mount 200', 200, (i) => E, TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Input.bench.tsx b/packages/benchmarks/src/base-ui/Input.bench.tsx new file mode 100644 index 0000000..b8a3663 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Input.bench.tsx @@ -0,0 +1,32 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Input } from '@omniview/base-ui'; + +describe('Input', () => { + benchRender('mount', () => ( + + Label + + + ), TIER_2_OPTIONS); + + benchRerender( + 'disabled toggle', + { initialProps: { disabled: false }, updatedProps: { disabled: true } }, + (props) => ( + + Label + + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => ( + + Field {i} + + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/List.bench.tsx b/packages/benchmarks/src/base-ui/List.bench.tsx new file mode 100644 index 0000000..b5d2122 --- /dev/null +++ b/packages/benchmarks/src/base-ui/List.bench.tsx @@ -0,0 +1,40 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { List } from '@omniview/base-ui'; +import type { ReactNode } from 'react'; + +const threeItems = ( + <> + Alpha + Beta + Gamma + +); + +const fourItems = ( + <> + Alpha + Beta + Gamma + Delta + +); + +describe('List', () => { + benchRender( + 'mount with 3 items', + () => {threeItems}, + TIER_2_OPTIONS, + ); + + benchRerender( + 'items change', + { + initialProps: { children: threeItems as ReactNode }, + updatedProps: { children: fourItems as ReactNode }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Menu.bench.tsx b/packages/benchmarks/src/base-ui/Menu.bench.tsx new file mode 100644 index 0000000..163edd5 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Menu.bench.tsx @@ -0,0 +1,48 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Menu } from '@omniview/base-ui'; + +describe('Menu', () => { + benchRender( + 'mount (closed)', + () => ( + + Open + + + + Item 1 + Item 2 + Item 3 + + + + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'open toggle', + { + initialProps: { open: false }, + updatedProps: { open: true }, + }, + (props) => ( + + Open + + + + Item 1 + Item 2 + Item 3 + + + + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Meter.bench.tsx b/packages/benchmarks/src/base-ui/Meter.bench.tsx new file mode 100644 index 0000000..ac0830a --- /dev/null +++ b/packages/benchmarks/src/base-ui/Meter.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Meter } from '@omniview/base-ui'; + +describe('Meter', () => { + benchRender( + 'mount', + () => , + TIER_2_OPTIONS, + ); + + benchRerender( + 'value change', + { + initialProps: { value: 60, label: 'CPU Usage' as const }, + updatedProps: { value: 85, label: 'CPU Usage' as const }, + }, + (props) => , + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => , TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/MultiSelect.bench.tsx b/packages/benchmarks/src/base-ui/MultiSelect.bench.tsx new file mode 100644 index 0000000..404ce2e --- /dev/null +++ b/packages/benchmarks/src/base-ui/MultiSelect.bench.tsx @@ -0,0 +1,40 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeOptions, type Option } from '../utils/factories'; +import { MultiSelect } from '@omniview/base-ui'; + +const options100 = makeOptions(100); +const noSelection: Option[] = []; +const selected20 = options100.slice(0, 20); + +function MultiSelectBench({ options, value }: { options: Option[]; value?: Option[] }) { + return ( + + items={options} + value={value} + getItemLabel={(item) => item.label} + getItemValue={(item) => item.value} + placeholder="Select options" + /> + ); +} + +describe('MultiSelect', () => { + benchRender( + 'mount with 100 options', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'selection change (0 → 20 selected)', + { + initialProps: { options: options100, value: noSelection }, + updatedProps: { options: options100, value: selected20 }, + }, + (props) => , + TIER_1_OPTIONS, + ); + +}); diff --git a/packages/benchmarks/src/base-ui/NavList.bench.tsx b/packages/benchmarks/src/base-ui/NavList.bench.tsx new file mode 100644 index 0000000..eed4e65 --- /dev/null +++ b/packages/benchmarks/src/base-ui/NavList.bench.tsx @@ -0,0 +1,46 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { NavList } from '@omniview/base-ui'; + +describe('NavList', () => { + benchRender( + 'mount with items', + () => ( + + + Home + + + Settings + + + Profile + + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'active item change', + { + initialProps: { activeKey: 'home' }, + updatedProps: { activeKey: 'settings' }, + }, + (props) => ( + + + Home + + + Settings + + + Profile + + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/NumberInput.bench.tsx b/packages/benchmarks/src/base-ui/NumberInput.bench.tsx new file mode 100644 index 0000000..bc4994d --- /dev/null +++ b/packages/benchmarks/src/base-ui/NumberInput.bench.tsx @@ -0,0 +1,44 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { NumberInput } from '@omniview/base-ui'; + +describe('NumberInput', () => { + benchRender('mount', () => ( + + Quantity + + + + + + + ), TIER_2_OPTIONS); + + benchRerender( + 'disabled toggle', + { initialProps: { disabled: false }, updatedProps: { disabled: true } }, + (props) => ( + + Quantity + + + + + + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 50', 50, (i) => ( + + Qty {i} + + + + + + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Pagination.bench.tsx b/packages/benchmarks/src/base-ui/Pagination.bench.tsx new file mode 100644 index 0000000..6fde980 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Pagination.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Pagination } from '@omniview/base-ui'; + +const noop = () => {}; + +describe('Pagination', () => { + benchRender( + 'mount', + () => , + TIER_2_OPTIONS, + ); + + benchRerender( + 'page change', + { + initialProps: { page: 1 }, + updatedProps: { page: 5 }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Popover.bench.tsx b/packages/benchmarks/src/base-ui/Popover.bench.tsx new file mode 100644 index 0000000..8cf374d --- /dev/null +++ b/packages/benchmarks/src/base-ui/Popover.bench.tsx @@ -0,0 +1,48 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { Popover } from '@omniview/base-ui'; + +describe('Popover', () => { + benchRender( + 'mount (open)', + () => ( + + Open + + + + Info + Popover content here. + Close + + + + + ), + TIER_1_OPTIONS, + ); + + benchRerender( + 'open/close toggle', + { + initialProps: { open: true }, + updatedProps: { open: false }, + }, + (props) => ( + + Open + + + + Info + Content + + + + + ), + TIER_1_OPTIONS, + ); + +}); diff --git a/packages/benchmarks/src/base-ui/Progress.bench.tsx b/packages/benchmarks/src/base-ui/Progress.bench.tsx new file mode 100644 index 0000000..d96ea5d --- /dev/null +++ b/packages/benchmarks/src/base-ui/Progress.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Progress } from '@omniview/base-ui'; + +describe('Progress', () => { + benchRender( + 'mount', + () => , + TIER_2_OPTIONS, + ); + + benchRerender( + 'value change', + { + initialProps: { value: 50 }, + updatedProps: { value: 80 }, + }, + (props) => , + TIER_2_OPTIONS, + ); + + benchMountMany('mount 200', 200, (i) => , TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Radio.bench.tsx b/packages/benchmarks/src/base-ui/Radio.bench.tsx new file mode 100644 index 0000000..761abfe --- /dev/null +++ b/packages/benchmarks/src/base-ui/Radio.bench.tsx @@ -0,0 +1,21 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Radio } from '@omniview/base-ui'; + +describe('Radio', () => { + benchRender('mount', () => ( + Option 1 + ), TIER_2_OPTIONS); + + benchRerender( + 'disabled toggle', + { initialProps: { disabled: false }, updatedProps: { disabled: true } }, + (props) => ( + Option 1 + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 200', 200, (i) => Option {i}, TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/RadioGroup.bench.tsx b/packages/benchmarks/src/base-ui/RadioGroup.bench.tsx new file mode 100644 index 0000000..3775199 --- /dev/null +++ b/packages/benchmarks/src/base-ui/RadioGroup.bench.tsx @@ -0,0 +1,27 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { RadioGroup } from '@omniview/base-ui'; + +describe('RadioGroup', () => { + benchRender('mount with 3 items', () => ( + + Option A + Option B + Option C + + ), TIER_2_OPTIONS); + + benchRerender( + 'value change', + { initialProps: { value: 'a' }, updatedProps: { value: 'b' } }, + (props) => ( + + Option A + Option B + Option C + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/ResizableSplitPane.bench.tsx b/packages/benchmarks/src/base-ui/ResizableSplitPane.bench.tsx new file mode 100644 index 0000000..26b35a7 --- /dev/null +++ b/packages/benchmarks/src/base-ui/ResizableSplitPane.bench.tsx @@ -0,0 +1,41 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { ResizableSplitPane, type SplitDirection } from '@omniview/base-ui'; + +describe('ResizableSplitPane', () => { + benchRender( + 'mount horizontal', + () => ( + + {[
Left
,
Right
]} +
+ ), + TIER_1_OPTIONS, + ); + + benchRerender( + 'direction change (horizontal → vertical)', + { + initialProps: { direction: 'horizontal' as SplitDirection }, + updatedProps: { direction: 'vertical' as SplitDirection }, + }, + (props) => ( + + {[
Pane 1
,
Pane 2
]} +
+ ), + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 30 split panes', + 30, + (i) => ( + + {[
Left {i}
,
Right {i}
]} +
+ ), + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/RowList.bench.tsx b/packages/benchmarks/src/base-ui/RowList.bench.tsx new file mode 100644 index 0000000..ee89a35 --- /dev/null +++ b/packages/benchmarks/src/base-ui/RowList.bench.tsx @@ -0,0 +1,58 @@ +import { describe } from 'vitest'; +import { RowList } from '@omniview/base-ui'; +import type { ColumnDef } from '@omniview/base-ui'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeRows, type Row } from '../utils/factories'; + +// --------------------------------------------------------------------------- +// Column definitions +// --------------------------------------------------------------------------- + +const columns: ColumnDef[] = [ + { id: 'id', header: 'ID', width: '60px' }, + { id: 'name', header: 'Name', width: '1fr' }, + { id: 'status', header: 'Status', width: '100px' }, + { id: 'value', header: 'Value', width: '80px', align: 'end' }, +]; + +// Pre-generate data +const rows200 = makeRows(200); +const rows300 = makeRows(300); + +// --------------------------------------------------------------------------- +// Wrapper +// --------------------------------------------------------------------------- + +function RowListBench({ data }: { data: Row[] }) { + return ( + + + + {data.map((row) => ( + + {row.id} + {row.name} + {row.status} + {row.value} + + ))} + + + ); +} + +// --------------------------------------------------------------------------- +// Benchmarks +// --------------------------------------------------------------------------- + +describe('RowList', () => { + benchRender('mount 200 rows', () => , TIER_1_OPTIONS); + + benchRerender( + 'data change (200 -> 300 rows)', + { initialProps: { data: rows200 }, updatedProps: { data: rows300 } }, + (props) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/ScrollArea.bench.tsx b/packages/benchmarks/src/base-ui/ScrollArea.bench.tsx new file mode 100644 index 0000000..480f429 --- /dev/null +++ b/packages/benchmarks/src/base-ui/ScrollArea.bench.tsx @@ -0,0 +1,30 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ScrollArea } from '@omniview/base-ui'; +import type { ReactNode } from 'react'; + +const contentA =
Scrollable content block A
; +const contentB =
Scrollable content block B with extra text
; + +describe('ScrollArea', () => { + benchRender( + 'mount', + () => ( + + {contentA} + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'content change', + { + initialProps: { children: contentA as ReactNode }, + updatedProps: { children: contentB as ReactNode }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/SearchInput.bench.tsx b/packages/benchmarks/src/base-ui/SearchInput.bench.tsx new file mode 100644 index 0000000..1d999ef --- /dev/null +++ b/packages/benchmarks/src/base-ui/SearchInput.bench.tsx @@ -0,0 +1,21 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { SearchInput } from '@omniview/base-ui'; + +const noop = () => {}; + +describe('SearchInput', () => { + benchRender('mount', () => ( + + ), TIER_2_OPTIONS); + + benchRerender( + 'value change', + { initialProps: { value: '' }, updatedProps: { value: 'search query' } }, + (props) => ( + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/SegmentedControl.bench.tsx b/packages/benchmarks/src/base-ui/SegmentedControl.bench.tsx new file mode 100644 index 0000000..4a648f3 --- /dev/null +++ b/packages/benchmarks/src/base-ui/SegmentedControl.bench.tsx @@ -0,0 +1,34 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { SegmentedControl } from '@omniview/base-ui'; + +describe('SegmentedControl', () => { + benchRender( + 'mount with 3 items', + () => ( + + Option A + Option B + Option C + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'value change', + { + initialProps: { value: 'a' as string }, + updatedProps: { value: 'c' as string }, + }, + (props) => ( + + Option A + Option B + Option C + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Select.bench.tsx b/packages/benchmarks/src/base-ui/Select.bench.tsx new file mode 100644 index 0000000..d4e26ba --- /dev/null +++ b/packages/benchmarks/src/base-ui/Select.bench.tsx @@ -0,0 +1,75 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { makeOptions } from '../utils/factories'; +import { Select } from '@omniview/base-ui'; + +const options = makeOptions(20); + +describe('Select', () => { + benchRender('mount with 20 options', () => ( + + ), TIER_2_OPTIONS); + + benchRerender( + 'disabled toggle', + { initialProps: { disabled: false }, updatedProps: { disabled: true } }, + (props) => ( + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 50', 50, (i) => ( + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/SelectableList.bench.tsx b/packages/benchmarks/src/base-ui/SelectableList.bench.tsx new file mode 100644 index 0000000..0e9bcab --- /dev/null +++ b/packages/benchmarks/src/base-ui/SelectableList.bench.tsx @@ -0,0 +1,60 @@ +import { describe } from 'vitest'; +import { SelectableList } from '@omniview/base-ui'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeRows, type Row } from '../utils/factories'; + +// Pre-generate data +const rows200 = makeRows(200); + +// Pre-compute selection sets +const noSelection = new Set(); +const fiftySelected = new Set(Array.from({ length: 50 }, (_, i) => i)); + +// --------------------------------------------------------------------------- +// Wrapper +// --------------------------------------------------------------------------- + +function SelectableListBench({ + data, + selectedKeys, +}: { + data: Row[]; + selectedKeys?: ReadonlySet; +}) { + return ( + + + {data.map((row) => ( + + + {row.name} + {row.status} + + ))} + + + ); +} + +// --------------------------------------------------------------------------- +// Benchmarks +// --------------------------------------------------------------------------- + +describe('SelectableList', () => { + benchRender( + 'mount 200 items', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'selection change (0 -> 50 selected)', + { + initialProps: { data: rows200, selectedKeys: noSelection }, + updatedProps: { data: rows200, selectedKeys: fiftySelected }, + }, + (props) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Sheet.bench.tsx b/packages/benchmarks/src/base-ui/Sheet.bench.tsx new file mode 100644 index 0000000..bd0cd18 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Sheet.bench.tsx @@ -0,0 +1,42 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { Sheet } from '@omniview/base-ui'; +import type { SurfaceElevation } from '@omniview/base-ui'; + +describe('Sheet', () => { + benchRender( + 'mount', + () => ( + +

Sheet content

+
+ ), + TIER_1_OPTIONS, + ); + + benchRerender( + 'elevation change', + { + initialProps: { elevation: 0 as SurfaceElevation }, + updatedProps: { elevation: 3 as SurfaceElevation }, + }, + (props) => ( + +

Sheet content

+
+ ), + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 50 sheets', + 50, + (i) => ( + +

Content {i}

+
+ ), + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Slider.bench.tsx b/packages/benchmarks/src/base-ui/Slider.bench.tsx new file mode 100644 index 0000000..81c62f5 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Slider.bench.tsx @@ -0,0 +1,33 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Slider } from '@omniview/base-ui'; + +describe('Slider', () => { + benchRender('mount', () => ( + + + + + + + + + ), TIER_2_OPTIONS); + + benchRerender( + 'value change', + { initialProps: { value: 25 }, updatedProps: { value: 75 } }, + (props) => ( + + + + + + + + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Spinner.bench.tsx b/packages/benchmarks/src/base-ui/Spinner.bench.tsx new file mode 100644 index 0000000..b7a51b6 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Spinner.bench.tsx @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Spinner } from '@omniview/base-ui'; + +describe('Spinner', () => { + benchRender( + 'mount', + () => , + TIER_2_OPTIONS, + ); + + benchRerender( + 'size change', + { + initialProps: { size: 'md' as const }, + updatedProps: { size: 'lg' as const }, + }, + (props) => , + TIER_2_OPTIONS, + ); + + benchMountMany('mount 200', 200, (i) => , TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/SplitButton.bench.tsx b/packages/benchmarks/src/base-ui/SplitButton.bench.tsx new file mode 100644 index 0000000..71b3e40 --- /dev/null +++ b/packages/benchmarks/src/base-ui/SplitButton.bench.tsx @@ -0,0 +1,36 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { SplitButton } from '@omniview/base-ui'; + +describe('SplitButton', () => { + benchRender( + 'mount', + () => ( + + Save + +
Option 1
+
+
+ ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'disabled toggle', + { + initialProps: { disabled: false }, + updatedProps: { disabled: true }, + }, + (props) => ( + + Save + +
Option 1
+
+
+ ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/StatRow.bench.tsx b/packages/benchmarks/src/base-ui/StatRow.bench.tsx new file mode 100644 index 0000000..bea684c --- /dev/null +++ b/packages/benchmarks/src/base-ui/StatRow.bench.tsx @@ -0,0 +1,46 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { StatRow } from '@omniview/base-ui'; +import type { ReactNode } from 'react'; + +const itemsA = ( + <> + CPU: 45% + Mem: 2.1 GB + Pods: 12 + +); + +const itemsB = ( + <> + CPU: 72% + Mem: 3.4 GB + Pods: 18 + +); + +describe('StatRow', () => { + benchRender( + 'mount', + () => {itemsA}, + TIER_2_OPTIONS, + ); + + benchRerender( + 'value change', + { + initialProps: { children: itemsA as ReactNode }, + updatedProps: { children: itemsB as ReactNode }, + }, + (props) => , + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => ( + + CPU: {i}% + Mem: {i} GB + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/StatusBar.bench.tsx b/packages/benchmarks/src/base-ui/StatusBar.bench.tsx new file mode 100644 index 0000000..a2e3183 --- /dev/null +++ b/packages/benchmarks/src/base-ui/StatusBar.bench.tsx @@ -0,0 +1,39 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { StatusBar } from '@omniview/base-ui'; +import type { ReactNode } from 'react'; + +const contentA = ( + + main + + UTF-8 + +); + +const contentB = ( + + develop + + UTF-16 + +); + +describe('StatusBar', () => { + benchRender( + 'mount', + () => {contentA}, + TIER_2_OPTIONS, + ); + + benchRerender( + 'content change', + { + initialProps: { children: contentA as ReactNode }, + updatedProps: { children: contentB as ReactNode }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Stepper.bench.tsx b/packages/benchmarks/src/base-ui/Stepper.bench.tsx new file mode 100644 index 0000000..d00a1f1 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Stepper.bench.tsx @@ -0,0 +1,34 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Stepper } from '@omniview/base-ui'; + +describe('Stepper', () => { + benchRender( + 'mount with 3 steps', + () => ( + + + + + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'active step change', + { + initialProps: { activeStep: 0 as number }, + updatedProps: { activeStep: 2 as number }, + }, + (props) => ( + + + + + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Switch.bench.tsx b/packages/benchmarks/src/base-ui/Switch.bench.tsx new file mode 100644 index 0000000..8670c97 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Switch.bench.tsx @@ -0,0 +1,21 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Switch } from '@omniview/base-ui'; + +describe('Switch', () => { + benchRender('mount', () => ( + Enable notifications + ), TIER_2_OPTIONS); + + benchRerender( + 'checked toggle', + { initialProps: { checked: false }, updatedProps: { checked: true } }, + (props) => ( + Enable notifications + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 200', 200, (i) => Setting {i}, TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Table.bench.tsx b/packages/benchmarks/src/base-ui/Table.bench.tsx new file mode 100644 index 0000000..6ad570a --- /dev/null +++ b/packages/benchmarks/src/base-ui/Table.bench.tsx @@ -0,0 +1,71 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Table } from '@omniview/base-ui'; +import type { ReactNode } from 'react'; + +const rowsA = ( + + + Alice + 42 + + + Bob + 37 + + + Charlie + 29 + + +); + +const rowsB = ( + + + Diana + 55 + + + Eve + 31 + + + Frank + 48 + + +); + +const header = ( + + + Name + Age + + +); + +describe('Table', () => { + benchRender( + 'mount with rows', + () => ( + + {header} + {rowsA} +
+ ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'rows change', + { + initialProps: { children: <>{header}{rowsA} as ReactNode }, + updatedProps: { children: <>{header}{rowsB} as ReactNode }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Tabs.bench.tsx b/packages/benchmarks/src/base-ui/Tabs.bench.tsx new file mode 100644 index 0000000..0505482 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Tabs.bench.tsx @@ -0,0 +1,44 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Tabs } from '@omniview/base-ui'; + +describe('Tabs', () => { + benchRender( + 'mount with 3 tabs', + () => ( + + + Tab 1 + Tab 2 + Tab 3 + + Panel 1 + Panel 2 + Panel 3 + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'active tab change', + { + initialProps: { value: 0 as number }, + updatedProps: { value: 2 as number }, + }, + (props) => ( + + + Tab 1 + Tab 2 + Tab 3 + + Panel 1 + Panel 2 + Panel 3 + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/TagInput.bench.tsx b/packages/benchmarks/src/base-ui/TagInput.bench.tsx new file mode 100644 index 0000000..0d32e3e --- /dev/null +++ b/packages/benchmarks/src/base-ui/TagInput.bench.tsx @@ -0,0 +1,37 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; +import { makeTags } from '../utils/factories'; +import { TagInput } from '@omniview/base-ui'; + +const tags10 = makeTags(10); +const tags20 = makeTags(20); +const tags5 = makeTags(5); + +const noop = () => {}; + +function TagInputBench({ tags }: { tags: string[] }) { + return ; +} + +describe('TagInput', () => { + benchRender( + 'mount with 10 tags', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'tags change (10 → 20)', + { initialProps: { tags: tags10 }, updatedProps: { tags: tags20 } }, + (props) => , + TIER_1_OPTIONS, + ); + + benchMountMany( + 'mount 100 instances (5 tags each)', + 100, + (i) => , + TIER_1_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/TextArea.bench.tsx b/packages/benchmarks/src/base-ui/TextArea.bench.tsx new file mode 100644 index 0000000..4aca204 --- /dev/null +++ b/packages/benchmarks/src/base-ui/TextArea.bench.tsx @@ -0,0 +1,32 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { TextArea } from '@omniview/base-ui'; + +describe('TextArea', () => { + benchRender('mount', () => ( + + ), TIER_2_OPTIONS); + + benchRerender( + 'disabled toggle', + { initialProps: { disabled: false }, updatedProps: { disabled: true } }, + (props) => ( + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 50', 50, (i) => ( + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/TextField.bench.tsx b/packages/benchmarks/src/base-ui/TextField.bench.tsx new file mode 100644 index 0000000..755f0d5 --- /dev/null +++ b/packages/benchmarks/src/base-ui/TextField.bench.tsx @@ -0,0 +1,32 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { TextField } from '@omniview/base-ui'; + +describe('TextField', () => { + benchRender('mount', () => ( + + Label + + + ), TIER_2_OPTIONS); + + benchRerender( + 'disabled toggle', + { initialProps: { disabled: false }, updatedProps: { disabled: true } }, + (props) => ( + + Label + + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => ( + + Field {i} + + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/Timeline.bench.tsx b/packages/benchmarks/src/base-ui/Timeline.bench.tsx new file mode 100644 index 0000000..aa7d59a --- /dev/null +++ b/packages/benchmarks/src/base-ui/Timeline.bench.tsx @@ -0,0 +1,43 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Timeline } from '@omniview/base-ui'; + +function threeItems() { + return ( + <> + Deployed v1.0 + Health check passed + Rollout complete + + ); +} + +function fourItems() { + return ( + <> + Deployed v1.0 + Health check passed + Rollout complete + Metrics stabilized + + ); +} + +describe('Timeline', () => { + benchRender( + 'mount with 3 items', + () => {threeItems()}, + TIER_2_OPTIONS, + ); + + benchRerender( + 'items change', + { + initialProps: { children: threeItems() }, + updatedProps: { children: fourItems() }, + }, + (props) => , + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Toast.bench.tsx b/packages/benchmarks/src/base-ui/Toast.bench.tsx new file mode 100644 index 0000000..ae654a7 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Toast.bench.tsx @@ -0,0 +1,30 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ToastProvider } from '@omniview/base-ui'; + +describe('ToastProvider', () => { + benchRender( + 'mount provider', + () => ( + +
App content
+
+ ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'position change', + { + initialProps: { position: 'bottom-right' as const }, + updatedProps: { position: 'top-left' as const }, + }, + (props) => ( + +
App content
+
+ ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/ToggleButton.bench.tsx b/packages/benchmarks/src/base-ui/ToggleButton.bench.tsx new file mode 100644 index 0000000..bd2491c --- /dev/null +++ b/packages/benchmarks/src/base-ui/ToggleButton.bench.tsx @@ -0,0 +1,28 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ToggleButton } from '@omniview/base-ui'; + +describe('ToggleButton', () => { + benchRender( + 'mount', + () => Bold, + TIER_2_OPTIONS, + ); + + benchRerender( + 'pressed toggle', + { + initialProps: { pressed: false }, + updatedProps: { pressed: true }, + }, + (props) => ( + + Bold + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 200', 200, (i) => Option {i}, TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/ToggleButtonGroup.bench.tsx b/packages/benchmarks/src/base-ui/ToggleButtonGroup.bench.tsx new file mode 100644 index 0000000..d0b51bd --- /dev/null +++ b/packages/benchmarks/src/base-ui/ToggleButtonGroup.bench.tsx @@ -0,0 +1,34 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { ToggleButtonGroup } from '@omniview/base-ui'; + +describe('ToggleButtonGroup', () => { + benchRender( + 'mount with 3 items', + () => ( + + Bold + Italic + Underline + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'value change', + { + initialProps: { value: ['bold'] as string[] }, + updatedProps: { value: ['italic'] as string[] }, + }, + (props) => ( + + Bold + Italic + Underline + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Toolbar.bench.tsx b/packages/benchmarks/src/base-ui/Toolbar.bench.tsx new file mode 100644 index 0000000..e99a69b --- /dev/null +++ b/packages/benchmarks/src/base-ui/Toolbar.bench.tsx @@ -0,0 +1,38 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Toolbar, Button } from '@omniview/base-ui'; + +describe('Toolbar', () => { + benchRender( + 'mount with buttons', + () => ( + + + + + + + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'disabled toggle', + { + initialProps: { 'aria-disabled': undefined }, + updatedProps: { 'aria-disabled': 'true' }, + }, + (props) => ( + + + + + + + + ), + TIER_2_OPTIONS, + ); +}); diff --git a/packages/benchmarks/src/base-ui/Tooltip.bench.tsx b/packages/benchmarks/src/base-ui/Tooltip.bench.tsx new file mode 100644 index 0000000..7afabd2 --- /dev/null +++ b/packages/benchmarks/src/base-ui/Tooltip.bench.tsx @@ -0,0 +1,51 @@ +import { describe } from 'vitest'; +import { benchRender, benchRerender, benchMountMany } from '../utils/bench-render'; +import { TIER_2_OPTIONS } from '../utils/bench-options'; +import { Tooltip } from '@omniview/base-ui'; + +describe('Tooltip', () => { + benchRender( + 'mount', + () => ( + + Hover me + + + Tooltip content + + + + ), + TIER_2_OPTIONS, + ); + + benchRerender( + 'open toggle', + { + initialProps: { open: false }, + updatedProps: { open: true }, + }, + (props) => ( + + Hover me + + + Tooltip content + + + + ), + TIER_2_OPTIONS, + ); + + benchMountMany('mount 100', 100, (i) => ( + + Item {i} + + + Tooltip {i} + + + + ), TIER_2_OPTIONS); +}); diff --git a/packages/benchmarks/src/base-ui/TreeList.bench.tsx b/packages/benchmarks/src/base-ui/TreeList.bench.tsx new file mode 100644 index 0000000..0d781d4 --- /dev/null +++ b/packages/benchmarks/src/base-ui/TreeList.bench.tsx @@ -0,0 +1,91 @@ +import { describe } from 'vitest'; +import { TreeList } from '@omniview/base-ui'; +import type { TreeNodeMeta } from '@omniview/base-ui'; +import { benchRender, benchRerender } from '../utils/bench-render'; +import { TIER_1_OPTIONS } from '../utils/bench-options'; + +// --------------------------------------------------------------------------- +// Data factories +// --------------------------------------------------------------------------- + +interface TreeItem { + id: string; + label: string; + children?: TreeItem[]; +} + +function makeFlatItems(count: number): TreeItem[] { + return Array.from({ length: count }, (_, i) => ({ + id: `item-${i}`, + label: `Item ${i}`, + })); +} + +function makeNestedItems(parents: number, childrenPerParent: number): TreeItem[] { + return Array.from({ length: parents }, (_, i) => ({ + id: `parent-${i}`, + label: `Parent ${i}`, + children: Array.from({ length: childrenPerParent }, (_, j) => ({ + id: `parent-${i}-child-${j}`, + label: `Child ${i}-${j}`, + })), + })); +} + +const itemKey = (item: TreeItem) => item.id; +const getChildren = (item: TreeItem) => item.children ?? []; +const isBranch = (item: TreeItem) => (item.children?.length ?? 0) > 0; +const getTextValue = (item: TreeItem) => item.label; +const renderItem = (item: TreeItem, node: TreeNodeMeta) => ( + + + {node.isBranch && } + {item.label} + +); + +// Pre-generate data +const flat500 = makeFlatItems(500); +const nested50x2 = makeNestedItems(50, 2); +const flat100 = makeFlatItems(100); +const flat150 = makeFlatItems(150); +// --------------------------------------------------------------------------- +// Wrapper +// --------------------------------------------------------------------------- + +function TreeListBench({ items }: { items: TreeItem[] }) { + return ( + + + + ); +} + +// --------------------------------------------------------------------------- +// Benchmarks +// --------------------------------------------------------------------------- + +describe('TreeList', () => { + benchRender('mount 500 flat nodes', () => , TIER_1_OPTIONS); + + benchRender( + 'mount nested (50 parents x 2 children)', + () => , + TIER_1_OPTIONS, + ); + + benchRerender( + 'data change (100 -> 150 nodes)', + { initialProps: { items: flat100 }, updatedProps: { items: flat150 } }, + (props) => , + TIER_1_OPTIONS, + ); + +}); diff --git a/packages/benchmarks/src/setup-perf.ts b/packages/benchmarks/src/setup-perf.ts new file mode 100644 index 0000000..472da93 --- /dev/null +++ b/packages/benchmarks/src/setup-perf.ts @@ -0,0 +1,20 @@ +import { configure } from 'reassure'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Ensure .reassure directory exists +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const reassureDir = path.resolve(__dirname, '../.reassure'); +if (!fs.existsSync(reassureDir)) { + fs.mkdirSync(reassureDir, { recursive: true }); +} + +// Tell Reassure to use @testing-library/react explicitly +configure({ + testingLibrary: 'react', + runs: 20, + warmupRuns: 3, +}); + diff --git a/packages/benchmarks/src/setup.ts b/packages/benchmarks/src/setup.ts index bb02c60..cdca5e9 100644 --- a/packages/benchmarks/src/setup.ts +++ b/packages/benchmarks/src/setup.ts @@ -1 +1,62 @@ import '@testing-library/jest-dom/vitest'; + +// ── Browser API stubs for jsdom ── +// Components like ResizableSplitPane, DockLayout, ScrollArea depend on these. + +if (typeof globalThis.ResizeObserver === 'undefined') { + globalThis.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + } as unknown as typeof ResizeObserver; +} + +if (typeof window !== 'undefined' && typeof window.matchMedia !== 'function') { + window.matchMedia = (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }) as MediaQueryList; +} + +// @base-ui/react uses PointerEvent in click handlers (checkbox, button, etc.) +if (typeof globalThis.PointerEvent === 'undefined') { + globalThis.PointerEvent = class PointerEvent extends MouseEvent { + readonly pointerId: number; + readonly width: number; + readonly height: number; + readonly pressure: number; + readonly tiltX: number; + readonly tiltY: number; + readonly pointerType: string; + readonly isPrimary: boolean; + constructor(type: string, params: PointerEventInit = {}) { + super(type, params); + this.pointerId = params.pointerId ?? 0; + this.width = params.width ?? 1; + this.height = params.height ?? 1; + this.pressure = params.pressure ?? 0; + this.tiltX = params.tiltX ?? 0; + this.tiltY = params.tiltY ?? 0; + this.pointerType = params.pointerType ?? ''; + this.isPrimary = params.isPrimary ?? false; + } + } as unknown as typeof PointerEvent; +} + +if (typeof globalThis.IntersectionObserver === 'undefined') { + globalThis.IntersectionObserver = class IntersectionObserver { + readonly root = null; + readonly rootMargin = '0px'; + readonly thresholds = [0]; + observe() {} + unobserve() {} + disconnect() {} + takeRecords() { return []; } + } as unknown as typeof IntersectionObserver; +} diff --git a/packages/benchmarks/src/utils/bench-options.ts b/packages/benchmarks/src/utils/bench-options.ts index 71f9374..a0fa345 100644 --- a/packages/benchmarks/src/utils/bench-options.ts +++ b/packages/benchmarks/src/utils/bench-options.ts @@ -13,3 +13,15 @@ const DEFAULT_OPTIONS: BenchRenderOptions = { export function resolveOptions(options?: BenchRenderOptions): BenchRenderOptions { return { ...DEFAULT_OPTIONS, ...options }; } + +/** Tier 1: deep benchmarks (mount + rerender + mountMany). */ +export const TIER_1_OPTIONS: BenchRenderOptions = { + iterations: 30, + warmupIterations: 3, +}; + +/** Tier 2: light benchmarks (mount + rerender only). */ +export const TIER_2_OPTIONS: BenchRenderOptions = { + iterations: 20, + warmupIterations: 2, +}; diff --git a/packages/benchmarks/src/utils/factories.ts b/packages/benchmarks/src/utils/factories.ts new file mode 100644 index 0000000..6a5fada --- /dev/null +++ b/packages/benchmarks/src/utils/factories.ts @@ -0,0 +1,152 @@ +/** + * Shared data factories for benchmark tests. + * Pre-generate data outside the benchmark loop so construction cost isn't measured. + */ + +// ── Row data (DataTable, BasicList, SelectableList, RowList) ── + +export interface Row { + id: number; + name: string; + status: string; + value: number; +} + +export function makeRows(count: number): Row[] { + return Array.from({ length: count }, (_, i) => ({ + id: i, + name: `Row ${i}`, + status: i % 3 === 0 ? 'active' : i % 3 === 1 ? 'pending' : 'inactive', + value: (i * 7 + 13) % 1000, + })); +} + +// ── Tree node data (TreeList) ── + +export interface TreeNode { + id: string; + label: string; + children?: TreeNode[]; +} + +export function makeTreeNodes(count: number, depth: number = 2): TreeNode[] { + let created = 0; + const roots: TreeNode[] = []; + const queue: { node: TreeNode; depth: number }[] = []; + + // Create root-level nodes + while (created < count) { + const node: TreeNode = { + id: `node-${created}`, + label: `Node ${created}`, + }; + created++; + roots.push(node); + if (depth > 0) queue.push({ node, depth: 1 }); + } + + // Breadth-first: attach children until budget exhausted + while (queue.length > 0 && created < count) { + const { node, depth: d } = queue.shift()!; + const children: TreeNode[] = []; + const childCount = Math.min(2, count - created); + for (let i = 0; i < childCount; i++) { + const child: TreeNode = { + id: `node-${created}`, + label: `Node ${created}`, + }; + created++; + children.push(child); + if (d < depth) queue.push({ node: child, depth: d + 1 }); + } + node.children = children; + } + + return roots; +} + +// ── Option data (Select, Autocomplete, Combobox, MultiSelect) ── + +export interface Option { + value: string; + label: string; +} + +export function makeOptions(count: number): Option[] { + return Array.from({ length: count }, (_, i) => ({ + value: `option-${i}`, + label: `Option ${i}`, + })); +} + +// ── Column definitions (DataTable, RowList) ── + +export interface Column { + id: string; + header: string; + accessorKey: keyof T; +} + +export function makeColumns(): Column[] { + return [ + { id: 'id', header: 'ID', accessorKey: 'id' }, + { id: 'name', header: 'Name', accessorKey: 'name' }, + { id: 'status', header: 'Status', accessorKey: 'status' }, + { id: 'value', header: 'Value', accessorKey: 'value' }, + ]; +} + +// ── Tag data (TagInput, FilterBar) ── + +export function makeTags(count: number): string[] { + return Array.from({ length: count }, (_, i) => `tag-${i}`); +} + +// ── Command items (CommandList) ── + +export interface CommandItem { + id: string; + label: string; + group?: string; + keywords?: string[]; +} + +export function makeCommandItems(count: number): CommandItem[] { + const groups = ['File', 'Edit', 'View', 'Navigate', 'Run']; + return Array.from({ length: count }, (_, i) => ({ + id: `cmd-${i}`, + label: `Command ${i}`, + group: groups[i % groups.length], + keywords: [`keyword-${i}`], + })); +} + +// ── Tab data (EditorTabs) ── + +export interface TabItem { + id: string; + label: string; + closable?: boolean; +} + +export function makeTabs(count: number): TabItem[] { + return Array.from({ length: count }, (_, i) => ({ + id: `tab-${i}`, + label: `Tab ${i}`, + closable: true, + })); +} + +export interface EditorTabItem { + id: string; + title: string; + closable?: boolean; +} + +export function makeEditorTabs(count: number, prefix = ''): EditorTabItem[] { + return Array.from({ length: count }, (_, i) => ({ + id: `${prefix}tab-${i}`, + title: `File ${prefix}${i}.ts`, + closable: true, + })); +} diff --git a/packages/benchmarks/vitest.config.perf.ts b/packages/benchmarks/vitest.config.perf.ts new file mode 100644 index 0000000..446294d --- /dev/null +++ b/packages/benchmarks/vitest.config.perf.ts @@ -0,0 +1,64 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Vitest config for Reassure performance tests. + * + * These are regular test() calls (not bench()) that use Reassure's measureRenders + * to track render counts and durations per interaction scenario. The output goes + * to .reassure/current.perf (or baseline.perf) for cross-branch comparison. + * + * V8 flags match Reassure's recommendations: disable JIT for stable measurements. + * + * Note: async factory + cast mirrors vitest.config.ts — needed because Vitest's + * InlineConfig type doesn't include `forks` even though the runtime supports it. + */ +export default defineConfig(async () => { + return { + plugins: [ + react({ + babel: { + plugins: [['babel-plugin-react-compiler', {}]], + }, + }), + ], + resolve: { + alias: [ + { find: '@omniview/base-ui', replacement: path.resolve(__dirname, '../base-ui/src/index.ts') }, + { find: '@omniview/editors', replacement: path.resolve(__dirname, '../editors/src/index.ts') }, + { + find: /^react-syntax-highlighter(\/.*)?$/, + replacement: path.resolve(__dirname, 'src/stubs/react-syntax-highlighter.ts'), + }, + ], + }, + test: { + include: ['src/**/*.perf-test.{ts,tsx}'], + environment: 'jsdom', + // Reassure's writeTestStats uses expect.getState().currentTestName + globals: true, + setupFiles: ['./src/setup.ts', './src/setup-perf.ts'], + pool: 'forks', + fileParallelism: false, + // Reassure recommends --no-turbofan --no-sparkplug for stable measurements. + // --expose-gc lets Reassure force GC between runs to reduce noise. + forks: { + execArgv: [ + '--expose-gc', + '--no-turbofan', + '--no-sparkplug', + '--hash-seed=1', + '--random-seed=1', + '--max-old-space-size=4096', + ], + }, + // Reassure tests are slow (20 runs + warmup per test). Generous timeout. + testTimeout: 60_000, + }, + }; +}) as ReturnType; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55a968b..2ccac93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,9 @@ importers: packages/benchmarks: devDependencies: + '@callstack/reassure-compare': + specifier: ^1.4.1 + version: 1.4.1 '@codspeed/vitest-plugin': specifier: ^5.2.0 version: 5.2.0(tinybench@2.9.0)(vite@7.3.1(@types/node@22.19.15)(yaml@2.8.2))(vitest@4.0.18(@types/node@22.19.15)(jsdom@25.0.1)(yaml@2.8.2)) @@ -247,6 +250,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) + reassure: + specifier: ^1.4.1 + version: 1.4.1(react@19.2.4) typescript: specifier: ^5.9.2 version: 5.9.3 @@ -473,6 +479,24 @@ packages: '@types/react': optional: true + '@callstack/reassure-cli@1.4.1': + resolution: {integrity: sha512-NKJi/WoLiDkn3FnYJhOk+FhWsdvrytrAfHRqexrCeyVfwpgbM+laPWto2+EKbfKdrHWBqWgbBOWyFoMiOAXcVg==} + hasBin: true + + '@callstack/reassure-compare@1.4.1': + resolution: {integrity: sha512-qJgASbKlBWA37XSN5b/uVAvc524dd9s3grumCKabI2GHkA5w5G6WWI11DnJPzjBnoZ/oIgNSrOm2sm6UMEvsVQ==} + + '@callstack/reassure-danger@1.4.1': + resolution: {integrity: sha512-TTfliolOt8Tfq4yu8vdj4lRdki5Vuvwxj22K2+xpAjxiF2rT4BL6gwM3YIPVUwuQMN3kQ8d8TuGJ4Wlc3zpAUA==} + + '@callstack/reassure-logger@1.4.1': + resolution: {integrity: sha512-2uB6OBk0/IdSXUpdTsMcN40d3ly5xWWPRjs0G4zEr37tyHB0lmJVuQIR5elUjIs+fTuK48PVuqW/4+Yce7WlFA==} + + '@callstack/reassure-measure@1.4.1': + resolution: {integrity: sha512-EPUKuMgzz0bccf/nn+6A5PhunZS/K3h0rpvhdOsPzGl3abjROoPi7bQN/ipURsLwle+OonqLCigcOK/CnndLEg==} + peerDependencies: + react: '>=18.0.0' + '@codspeed/core@5.2.0': resolution: {integrity: sha512-CmDhpWjcOJg2iBOQ/BmBnSBq8qxlM3r4h8uvYDkoUaba+EKRT3T73BZtKuml/48jZMsB+4/FG2UbTBinDWtuvw==} @@ -764,6 +788,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4': resolution: {integrity: sha512-6PyZBYKnnVNqOSB0YFly+62R7dmov8segT27A+RVTBVd4iAE6kbW9QBJGlyR2yG4D4ohzhZSTIu7BK1UTtmFFA==} peerDependencies: @@ -789,6 +817,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@mdx-js/react@3.1.1': resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: @@ -975,6 +1009,9 @@ packages: '@rushstack/ts-command-line@5.3.3': resolution: {integrity: sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==} + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1537,6 +1574,10 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1554,6 +1595,9 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + complex.js@2.4.3: + resolution: {integrity: sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1677,6 +1721,9 @@ packages: electron-to-chromium@1.5.307: resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -1733,6 +1780,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-latex@1.2.0: + resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1857,6 +1907,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1893,6 +1947,9 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} @@ -1920,6 +1977,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2058,6 +2119,11 @@ packages: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2127,6 +2193,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -2212,6 +2282,9 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} @@ -2277,6 +2350,10 @@ packages: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2332,6 +2409,11 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mathjs@15.1.1: + resolution: {integrity: sha512-rM668DTtpSzMVoh/cKAllyQVEbBApM5g//IMGD8vD7YlrIz9ITRr3SrdhjaDxcBNTdyETWwPebj2unZyHD7ZdA==} + engines: {node: '>= 18'} + hasBin: true + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -2591,6 +2673,10 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2599,6 +2685,10 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -2607,6 +2697,10 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2653,6 +2747,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -2680,6 +2778,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -2725,6 +2827,9 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-markdown@9.1.0: resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} peerDependencies: @@ -2745,6 +2850,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + reassure@1.4.1: + resolution: {integrity: sha512-EeW23/ci4mGHsv4WDSK9jTzzi5yCu3Gdf3GgugKvi5NrqxCiVubiTlcz8bjfpipSFEUK+mpDtlhyJI/6Kmkgjg==} + hasBin: true + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -2782,6 +2891,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2789,10 +2902,18 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -2840,6 +2961,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2893,6 +3017,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-git@3.33.0: + resolution: {integrity: sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2930,6 +3057,10 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -2952,6 +3083,10 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -2992,6 +3127,9 @@ packages: tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + tiny-emitter@2.1.0: + resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3049,6 +3187,14 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} + ts-markdown-builder@0.5.0: + resolution: {integrity: sha512-/2pzAFjGwk5fUR8ftFMhCOm/zmoqtpYWK6209j3YPfgWF2I+eYY6PKpR0mBJte6s8McnkQ/T92fV+IJg6bdxyw==} + engines: {node: '>= 18.0.0'} + + ts-regex-builder@1.8.2: + resolution: {integrity: sha512-Y8HovHFheDKm/jgLIWSO8o81xA/I9O5AGc3/vNG1sVSskatOifr3SQzAsatBXGLjL3nYhQif1MpwQRS5GF8ADg==} + engines: {node: '>= 18.0.0'} + tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -3076,6 +3222,10 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typed-function@4.2.2: + resolution: {integrity: sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==} + engines: {node: '>= 18'} + typescript-eslint@8.57.0: resolution: {integrity: sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3308,6 +3458,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -3331,6 +3485,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3342,6 +3500,14 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3350,6 +3516,9 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3503,6 +3672,36 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@callstack/reassure-cli@1.4.1': + dependencies: + '@callstack/reassure-compare': 1.4.1 + '@callstack/reassure-logger': 1.4.1 + chalk: 4.1.2 + simple-git: 3.33.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@callstack/reassure-compare@1.4.1': + dependencies: + '@callstack/reassure-logger': 1.4.1 + ts-markdown-builder: 0.5.0 + ts-regex-builder: 1.8.2 + zod: 4.3.6 + + '@callstack/reassure-danger@1.4.1': {} + + '@callstack/reassure-logger@1.4.1': + dependencies: + chalk: 4.1.2 + + '@callstack/reassure-measure@1.4.1(react@19.2.4)': + dependencies: + '@callstack/reassure-logger': 1.4.1 + mathjs: 15.1.1 + pretty-format: 30.3.0 + react: 19.2.4 + '@codspeed/core@5.2.0': dependencies: axios: 1.13.6 @@ -3725,6 +3924,10 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.48 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(yaml@2.8.2))': dependencies: glob: 13.0.6 @@ -3752,6 +3955,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: '@types/mdx': 2.0.13 @@ -3918,6 +4129,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@sinclair/typebox@0.34.48': {} + '@standard-schema/spec@1.1.0': {} '@storybook/addon-a11y@10.2.17(storybook@10.2.17(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': @@ -4586,6 +4799,12 @@ snapshots: check-error@2.1.3: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4600,6 +4819,8 @@ snapshots: compare-versions@6.1.1: {} + complex.js@2.4.3: {} + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -4713,6 +4934,8 @@ snapshots: electron-to-chromium@1.5.307: {} + emoji-regex@8.0.0: {} + empathic@2.0.0: {} entities@6.0.1: {} @@ -4854,6 +5077,8 @@ snapshots: escalade@3.2.0: {} + escape-latex@1.2.0: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -4992,6 +5217,11 @@ snapshots: dependencies: flat-cache: 4.0.1 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -5025,6 +5255,8 @@ snapshots: format@0.2.2: {} + fraction.js@5.3.4: {} + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 @@ -5051,6 +5283,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5242,6 +5476,11 @@ snapshots: import-lazy@4.0.0: {} + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -5311,6 +5550,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -5398,6 +5639,8 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + javascript-natural-sort@0.7.1: {} + jju@1.4.0: {} js-tokens@4.0.0: {} @@ -5478,6 +5721,10 @@ snapshots: pkg-types: 2.3.0 quansync: 0.2.11 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -5525,6 +5772,18 @@ snapshots: math-intrinsics@1.1.0: {} + mathjs@15.1.1: + dependencies: + '@babel/runtime': 7.28.6 + complex.js: 2.4.3 + decimal.js: 10.6.0 + escape-latex: 1.2.0 + fraction.js: 5.3.4 + javascript-natural-sort: 0.7.1 + seedrandom: 3.0.5 + tiny-emitter: 2.1.0 + typed-function: 4.2.2 + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -6018,6 +6277,10 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -6026,6 +6289,10 @@ snapshots: dependencies: yocto-queue: 1.2.2 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -6034,6 +6301,8 @@ snapshots: dependencies: p-limit: 4.0.0 + p-try@2.2.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6075,6 +6344,10 @@ snapshots: picomatch@4.0.3: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -6105,6 +6378,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + prismjs@1.30.0: {} prop-types@15.8.1: @@ -6153,6 +6432,8 @@ snapshots: react-is@17.0.2: {} + react-is@18.3.1: {} + react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.4): dependencies: '@types/hast': 3.0.4 @@ -6185,6 +6466,17 @@ snapshots: react@19.2.4: {} + reassure@1.4.1(react@19.2.4): + dependencies: + '@callstack/reassure-cli': 1.4.1 + '@callstack/reassure-compare': 1.4.1 + '@callstack/reassure-danger': 1.4.1 + '@callstack/reassure-measure': 1.4.1(react@19.2.4) + import-local: 3.2.0 + transitivePeerDependencies: + - react + - supports-color + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -6270,12 +6562,20 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} reselect@5.1.1: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -6355,6 +6655,8 @@ snapshots: scheduler@0.27.0: {} + seedrandom@3.0.5: {} + semver@6.3.1: {} semver@7.5.4: @@ -6421,6 +6723,14 @@ snapshots: siginfo@2.0.0: {} + simple-git@3.33.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -6463,6 +6773,12 @@ snapshots: string-argv@0.3.2: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -6512,6 +6828,10 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-bom@3.0.0: {} strip-indent@3.0.0: @@ -6544,6 +6864,8 @@ snapshots: tabbable@6.4.0: {} + tiny-emitter@2.1.0: {} + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -6585,6 +6907,10 @@ snapshots: ts-dedent@2.2.0: {} + ts-markdown-builder@0.5.0: {} + + ts-regex-builder@1.8.2: {} + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -6630,6 +6956,8 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typed-function@4.2.2: {} + typescript-eslint@8.57.0(eslint@9.39.4)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) @@ -6883,6 +7211,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + ws@8.19.0: {} wsl-utils@0.1.0: @@ -6893,14 +7227,30 @@ snapshots: xmlchars@2.2.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yallist@4.0.0: {} yaml@2.8.2: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} yocto-queue@1.2.2: {} + zod@4.3.6: {} + zwitch@2.0.4: {}