setAmount(e.target.value)}
+ currency="USD"
+ min="$1"
+ max="$10000"
+ placeholder="Enter amount"
+ />
+ );
+}
+```
+
+### With react-hook-form
+
+```tsx
+import { Controller, useForm } from 'react-hook-form';
+import { MoneyInput } from '@thesis-co/cent-react';
+
+function CheckoutForm() {
+ const { control, handleSubmit } = useForm();
+
+ return (
+
+ );
+}
+```
+
+### useMoney Hook
+
+```tsx
+import { useMoney, MoneyDisplay } from '@thesis-co/cent-react';
+
+function TipCalculator() {
+ const bill = useMoney({ currency: 'USD' });
+ const tip = bill.money?.multiply(0.18) ?? null;
+
+ return (
+
+
+ {bill.error &&
{bill.error.message} }
+
+
Tip (18%):
+
Total:
+
+ );
+}
+```
+
+### MoneyProvider
+
+Set default configuration for all descendant components:
+
+```tsx
+import { MoneyProvider } from '@thesis-co/cent-react';
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+### useExchangeRate Hook
+
+```tsx
+import { useExchangeRate, MoneyDisplay } from '@thesis-co/cent-react';
+import { Money } from '@thesis-co/cent';
+
+function CurrencyConverter() {
+ const [usd, setUsd] = useState(Money.zero('USD'));
+
+ const { convert, isLoading, isStale, refetch } = useExchangeRate({
+ from: 'USD',
+ to: 'EUR',
+ pollInterval: 60000, // Refresh every minute
+ staleThreshold: 300000, // Stale after 5 minutes
+ });
+
+ const eur = convert(usd);
+
+ return (
+
+ setUsd(e.target.value)} currency="USD" />
+
+ {isLoading ? (
+ Loading...
+ ) : (
+
+ )}
+
+ {isStale && (
+ Rate may be outdated. Refresh?
+ )}
+
+ );
+}
+```
+
+**Note:** `useExchangeRate` requires an `exchangeRateResolver` to be provided via `MoneyProvider`:
+
+```tsx
+ {
+ const response = await fetch(`/api/rates/${from}/${to}`);
+ const data = await response.json();
+ return new ExchangeRate(from, to, data.rate);
+ }}
+>
+
+
+```
+
+### MoneyDiff
+
+Display the difference between two money values:
+
+```tsx
+import { MoneyDiff } from '@thesis-co/cent-react';
+import { Money } from '@thesis-co/cent';
+
+// Basic difference
+
+// → "+$20.00"
+
+// With percentage change
+
+// → "+$20.00 (+20.00%)"
+
+// Custom rendering
+
+ {({ direction, formatted }) => (
+
+ {formatted.difference}
+
+ )}
+
+```
+
+## API Reference
+
+### Components
+
+| Component | Description |
+|-----------|-------------|
+| `MoneyDisplay` | Display formatted money values |
+| `MoneyInput` | Controlled input for money values |
+| `MoneyDiff` | Display difference between two values |
+| `MoneyProvider` | Context provider for default configuration |
+
+### Hooks
+
+| Hook | Description |
+|------|-------------|
+| `useMoney` | Manage money state with validation |
+| `useExchangeRate` | Fetch and manage exchange rates |
+| `useMoneyConfig` | Access MoneyProvider context |
+
+## Requirements
+
+- React 17.0.0 or later
+- @thesis-co/cent 0.0.5 or later
diff --git a/packages/cent-react/jest.config.js b/packages/cent-react/jest.config.js
new file mode 100644
index 0000000..798a558
--- /dev/null
+++ b/packages/cent-react/jest.config.js
@@ -0,0 +1,15 @@
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'jsdom',
+ testMatch: ['**/test/**/*.test.ts', '**/test/**/*.test.tsx'],
+ setupFilesAfterEnv: ['/test/setup.ts'],
+ transform: {
+ '^.+\\.tsx?$': [
+ 'ts-jest',
+ {
+ tsconfig: './tsconfig.json',
+ },
+ ],
+ },
+}
diff --git a/packages/cent-react/package.json b/packages/cent-react/package.json
new file mode 100644
index 0000000..e9c5275
--- /dev/null
+++ b/packages/cent-react/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@thesis-co/cent-react",
+ "version": "0.0.1",
+ "description": "React bindings for @thesis-co/cent - display, input, and manage money values",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thesis/cent.git",
+ "directory": "packages/cent-react"
+ },
+ "keywords": [
+ "react",
+ "money",
+ "currency",
+ "finance",
+ "input",
+ "form",
+ "cents"
+ ],
+ "author": "Matt Luongo (@mhluongo)",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "lint": "pnpx @biomejs/biome check",
+ "lint:fix": "pnpx @biomejs/biome check --write",
+ "build": "tsc",
+ "test": "jest",
+ "prepublishOnly": "pnpm run build && pnpm run test && pnpm run lint"
+ },
+ "devDependencies": {
+ "@thesis-co/cent": "workspace:*",
+ "@testing-library/jest-dom": "^6.4.0",
+ "@testing-library/react": "^14.2.0",
+ "@testing-library/user-event": "^14.5.0",
+ "@types/jest": "^29.5.12",
+ "@types/node": "^20.11.24",
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "ts-jest": "^29.1.2"
+ },
+ "peerDependencies": {
+ "@thesis-co/cent": ">=0.0.5",
+ "react": ">=17.0.0"
+ }
+}
diff --git a/packages/cent-react/src/components/MoneyDiff.tsx b/packages/cent-react/src/components/MoneyDiff.tsx
new file mode 100644
index 0000000..217bcda
--- /dev/null
+++ b/packages/cent-react/src/components/MoneyDiff.tsx
@@ -0,0 +1,246 @@
+import { Money, MoneyClass, FixedPointNumber } from '@thesis-co/cent'
+import { type ReactNode, useMemo } from 'react'
+
+/** Type alias for Money instance */
+type MoneyInstance = InstanceType
+
+/** Options for formatting Money to string */
+interface MoneyFormatOptions {
+ locale?: string
+ compact?: boolean
+ maxDecimals?: number | bigint
+ minDecimals?: number | bigint
+ preferredUnit?: string
+ preferSymbol?: boolean
+ preferFractionalSymbol?: boolean
+ excludeCurrency?: boolean
+}
+
+/**
+ * Render props for MoneyDiff custom rendering
+ */
+export interface MoneyDiffRenderProps {
+ /** The current value */
+ current: MoneyInstance
+ /** The comparison value */
+ compareTo: MoneyInstance
+ /** The difference (current - compareTo) */
+ difference: MoneyInstance
+ /** Percentage change as string for precision (null if compareTo is zero) */
+ percentageChange: string | null
+ /** Direction of change */
+ direction: 'increase' | 'decrease' | 'unchanged'
+ /** Formatted strings */
+ formatted: {
+ current: string
+ compareTo: string
+ difference: string
+ percentage: string
+ }
+}
+
+export interface MoneyDiffProps {
+ /** Current/new value */
+ value: MoneyInstance | string
+
+ /** Value to compare against (previous/baseline) */
+ compareTo: MoneyInstance | string
+
+ /** Formatting options */
+ formatOptions?: MoneyFormatOptions
+
+ /** Show percentage change */
+ showPercentage?: boolean
+
+ /** Number of decimal places for percentage */
+ percentageDecimals?: number
+
+ /** CSS class name */
+ className?: string
+
+ /** Inline styles */
+ style?: React.CSSProperties
+
+ /** Element type to render (default: "span") */
+ as?: React.ElementType
+
+ /** Custom render function */
+ children?: (props: MoneyDiffRenderProps) => ReactNode
+}
+
+/**
+ * Coerce a value to Money
+ */
+function toMoney(value: MoneyInstance | string): MoneyInstance {
+ if (typeof value === 'string') {
+ return Money(value) as MoneyInstance
+ }
+ return value
+}
+
+/**
+ * Calculate percentage change using FixedPointNumber for precision.
+ * Returns the percentage as a string to preserve precision.
+ */
+function calculatePercentageChange(current: MoneyInstance, compareTo: MoneyInstance, decimals: number): string | null {
+ if (compareTo.isZero()) {
+ return null
+ }
+
+ try {
+ // (current - compareTo) / |compareTo| * 100
+ const diff = current.subtract(compareTo)
+ const diffStr = diff.toString({ excludeCurrency: true }).replace(/,/g, '')
+ const compareStr = compareTo.absolute().toString({ excludeCurrency: true }).replace(/,/g, '')
+
+ const diffFP = FixedPointNumber.fromDecimalString(diffStr)
+ const compareFP = FixedPointNumber.fromDecimalString(compareStr)
+
+ if (compareFP.amount === 0n) return null
+
+ // Align to same decimal scale
+ const maxDecimals = diffFP.decimals > compareFP.decimals ? diffFP.decimals : compareFP.decimals
+ const diffScaled = diffFP.amount * 10n ** (maxDecimals - diffFP.decimals)
+ const compareScaled = compareFP.amount * 10n ** (maxDecimals - compareFP.decimals)
+
+ // For percentage with N decimal places, we need extra precision
+ // percentage = (diff / compare) * 100
+ // We compute: (diff * 100 * 10^(decimals+1)) / compare, then round
+ const extraPrecision = BigInt(decimals + 1)
+ const multiplier = 100n * 10n ** extraPrecision
+
+ const rawResult = (diffScaled * multiplier) / compareScaled
+
+ // Create a FixedPointNumber with the result and use its toString for formatting
+ const resultFP = new FixedPointNumber(rawResult, extraPrecision)
+
+ // Normalize to desired decimals (truncating extra precision)
+ const targetFP = new FixedPointNumber(0n, BigInt(decimals))
+ const normalizedFP = resultFP.normalize(targetFP, true) // unsafe=true to allow truncation
+
+ return normalizedFP.toString()
+ } catch {
+ return null
+ }
+}
+
+/**
+ * Format percentage string with sign
+ */
+function formatPercentage(value: string | null): string {
+ if (value === null) return ''
+
+ const isNegative = value.startsWith('-')
+ const isZero = value.replace(/[^1-9]/g, '') === ''
+ const absValue = isNegative ? value.slice(1) : value
+
+ if (isZero) {
+ return `${absValue}%`
+ } else if (isNegative) {
+ return `-${absValue}%`
+ } else {
+ return `+${absValue}%`
+ }
+}
+
+/**
+ * Display the difference between two Money values.
+ *
+ * @example
+ * // Basic usage
+ *
+ * // → "+$20.00"
+ *
+ * @example
+ * // With percentage
+ *
+ * // → "+$20.00 (+20.00%)"
+ *
+ * @example
+ * // Custom rendering
+ *
+ * {({ direction, formatted }) => (
+ *
+ * {formatted.difference}
+ *
+ * )}
+ *
+ */
+export function MoneyDiff({
+ value,
+ compareTo,
+ formatOptions,
+ showPercentage = false,
+ percentageDecimals = 2,
+ className,
+ style,
+ as: Component = 'span',
+ children,
+ ...rest
+}: MoneyDiffProps & React.HTMLAttributes): ReactNode {
+ const renderProps = useMemo(() => {
+ const current = toMoney(value)
+ const compare = toMoney(compareTo)
+ const difference = current.subtract(compare)
+
+ const percentageChange = calculatePercentageChange(current, compare, percentageDecimals)
+
+ let direction: 'increase' | 'decrease' | 'unchanged'
+ if (difference.isPositive()) {
+ direction = 'increase'
+ } else if (difference.isNegative()) {
+ direction = 'decrease'
+ } else {
+ direction = 'unchanged'
+ }
+
+ // Format the difference with sign
+ const absDiff = difference.absolute()
+ const diffFormatted = absDiff.toString(formatOptions)
+ const signedDiff =
+ direction === 'increase'
+ ? `+${diffFormatted}`
+ : direction === 'decrease'
+ ? `-${diffFormatted}`
+ : diffFormatted
+
+ return {
+ current,
+ compareTo: compare,
+ difference,
+ percentageChange,
+ direction,
+ formatted: {
+ current: current.toString(formatOptions),
+ compareTo: compare.toString(formatOptions),
+ difference: signedDiff,
+ percentage: formatPercentage(percentageChange),
+ },
+ }
+ }, [value, compareTo, formatOptions, percentageDecimals])
+
+ // Custom render
+ if (children) {
+ return (
+
+ {children(renderProps)}
+
+ )
+ }
+
+ // Default render
+ const { formatted, direction } = renderProps
+ const displayText = showPercentage
+ ? `${formatted.difference} (${formatted.percentage})`
+ : formatted.difference
+
+ return (
+
+ {displayText}
+
+ )
+}
diff --git a/packages/cent-react/src/components/MoneyDisplay.tsx b/packages/cent-react/src/components/MoneyDisplay.tsx
new file mode 100644
index 0000000..13f960d
--- /dev/null
+++ b/packages/cent-react/src/components/MoneyDisplay.tsx
@@ -0,0 +1,293 @@
+import { Money, MoneyClass } from '@thesis-co/cent'
+import { type ReactNode, useMemo } from 'react'
+
+/** Type alias for Money instance */
+type MoneyInstance = InstanceType
+
+/** Options for formatting Money to string */
+interface MoneyFormatOptions {
+ locale?: string
+ compact?: boolean
+ maxDecimals?: number | bigint
+ minDecimals?: number | bigint
+ preferredUnit?: string
+ preferSymbol?: boolean
+ preferFractionalSymbol?: boolean
+ excludeCurrency?: boolean
+}
+
+/**
+ * Parts of a formatted money value for custom rendering
+ */
+export interface MoneyParts {
+ /** The fully formatted string */
+ formatted: string
+ /** Individual parts parsed from the formatted string (precision-safe) */
+ parts: Array<{ type: string; value: string }>
+ /** Whether the value is negative */
+ isNegative: boolean
+ /** Whether the value is zero */
+ isZero: boolean
+ /** The original Money instance */
+ money: MoneyInstance
+}
+
+export interface MoneyDisplayProps {
+ /** The Money instance to display */
+ value: MoneyInstance | string | null | undefined
+
+ /** Display locale (default: "en-US") */
+ locale?: string
+
+ /** Use compact notation (e.g., $1M instead of $1,000,000) */
+ compact?: boolean
+
+ /** Maximum number of decimal places */
+ maxDecimals?: number | bigint
+
+ /** Minimum number of decimal places */
+ minDecimals?: number | bigint
+
+ /** Preferred fractional unit for crypto (e.g., "sat" for BTC) */
+ preferredUnit?: string
+
+ /** Use fractional unit symbol (e.g., "§10K" instead of "10K sats") */
+ preferFractionalSymbol?: boolean
+
+ /** Exclude currency symbol/code from output */
+ excludeCurrency?: boolean
+
+ /** Sign display mode */
+ showSign?: 'always' | 'negative' | 'never'
+
+ /** CSS class name */
+ className?: string
+
+ /** Inline styles */
+ style?: React.CSSProperties
+
+ /** Element type to render (default: "span") */
+ as?: React.ElementType
+
+ /** Content to show when value is null/undefined */
+ placeholder?: ReactNode
+
+ /** Custom render function for full control over rendering */
+ children?: (parts: MoneyParts) => ReactNode
+}
+
+/**
+ * Coerce a value to a Money instance
+ */
+function toMoney(value: MoneyInstance | string | null | undefined): MoneyInstance | null {
+ if (value == null) return null
+ if (typeof value === 'string') {
+ try {
+ return Money(value) as MoneyInstance
+ } catch {
+ return null
+ }
+ }
+ return value
+}
+
+/**
+ * Parse a formatted money string into parts.
+ * This preserves full precision by parsing the string output from Money.toString()
+ * rather than converting to JavaScript Number.
+ */
+function parseFormattedParts(formatted: string): Array<{ type: string; value: string }> {
+ const parts: Array<{ type: string; value: string }> = []
+ let remaining = formatted
+ let i = 0
+
+ while (i < remaining.length) {
+ const char = remaining[i]
+
+ // Minus sign
+ if (char === '-') {
+ parts.push({ type: 'minusSign', value: '-' })
+ i++
+ continue
+ }
+
+ // Plus sign
+ if (char === '+') {
+ parts.push({ type: 'plusSign', value: '+' })
+ i++
+ continue
+ }
+
+ // Digits (collect consecutive digits as integer or fraction based on context)
+ if (/\d/.test(char)) {
+ let digits = ''
+ while (i < remaining.length && /\d/.test(remaining[i])) {
+ digits += remaining[i]
+ i++
+ }
+ // Determine if this is integer or fraction based on whether we've seen a decimal
+ const hasDecimalBefore = parts.some(p => p.type === 'decimal')
+ parts.push({ type: hasDecimalBefore ? 'fraction' : 'integer', value: digits })
+ continue
+ }
+
+ // Decimal separator (period or comma depending on locale)
+ if (char === '.' || char === ',') {
+ // Check if this is a decimal or group separator
+ // If followed by exactly 3 digits and more content, likely group separator
+ // If followed by digits at end or non-digit, likely decimal
+ const afterChar = remaining.slice(i + 1)
+ const nextDigits = afterChar.match(/^(\d+)/)
+
+ if (nextDigits && nextDigits[1].length === 3 && afterChar.length > 3 && /[\d,.]/.test(afterChar[3])) {
+ // Likely a group separator (thousands)
+ parts.push({ type: 'group', value: char })
+ } else {
+ // Likely a decimal separator
+ parts.push({ type: 'decimal', value: char })
+ }
+ i++
+ continue
+ }
+
+ // Whitespace
+ if (/\s/.test(char)) {
+ let ws = ''
+ while (i < remaining.length && /\s/.test(remaining[i])) {
+ ws += remaining[i]
+ i++
+ }
+ parts.push({ type: 'literal', value: ws })
+ continue
+ }
+
+ // Currency symbols and other characters
+ // Collect consecutive non-digit, non-separator characters as currency
+ let other = ''
+ while (i < remaining.length && !/[\d.,\s+-]/.test(remaining[i])) {
+ other += remaining[i]
+ i++
+ }
+ if (other) {
+ parts.push({ type: 'currency', value: other })
+ }
+ }
+
+ return parts
+}
+
+/**
+ * Get formatted parts from a Money value.
+ * Uses Money.toString() for precision-safe formatting, then parses into parts.
+ */
+function getMoneyParts(
+ money: MoneyInstance,
+ options: MoneyFormatOptions,
+ showSign: 'always' | 'negative' | 'never'
+): MoneyParts {
+ const formatted = money.toString(options)
+ const isNegative = money.isNegative()
+ const isZero = money.isZero()
+
+ // Handle sign display
+ let finalFormatted = formatted
+ if (showSign === 'always' && !isNegative && !isZero) {
+ finalFormatted = `+${formatted}`
+ } else if (showSign === 'never' && isNegative) {
+ finalFormatted = formatted.replace(/^-/, '')
+ }
+
+ // Parse the formatted string into parts (preserves full precision)
+ const parts = parseFormattedParts(finalFormatted)
+
+ return {
+ formatted: finalFormatted,
+ parts,
+ isNegative,
+ isZero,
+ money,
+ }
+}
+
+/**
+ * Display a Money value with formatting options.
+ *
+ * @example
+ * // Basic usage
+ *
+ * // → "$1,234.56"
+ *
+ * @example
+ * // Compact notation
+ *
+ * // → "$1.5M"
+ *
+ * @example
+ * // Custom parts rendering
+ *
+ * {({ parts }) => (
+ *
+ * {parts.map((p, i) => (
+ * {p.value}
+ * ))}
+ *
+ * )}
+ *
+ */
+export function MoneyDisplay({
+ value,
+ locale,
+ compact,
+ maxDecimals,
+ minDecimals,
+ preferredUnit,
+ preferFractionalSymbol,
+ excludeCurrency,
+ showSign = 'negative',
+ className,
+ style,
+ as: Component = 'span',
+ placeholder,
+ children,
+ ...rest
+}: MoneyDisplayProps & React.HTMLAttributes): ReactNode {
+ const money = useMemo(() => toMoney(value), [value])
+
+ const formatOptions: MoneyFormatOptions = useMemo(
+ () => ({
+ locale,
+ compact,
+ maxDecimals,
+ minDecimals,
+ preferredUnit,
+ preferFractionalSymbol,
+ excludeCurrency,
+ }),
+ [locale, compact, maxDecimals, minDecimals, preferredUnit, preferFractionalSymbol, excludeCurrency]
+ )
+
+ const parts = useMemo(() => {
+ if (!money) return null
+ return getMoneyParts(money, formatOptions, showSign)
+ }, [money, formatOptions, showSign])
+
+ // Handle null/undefined value
+ if (!money || !parts) {
+ if (placeholder != null) {
+ return {placeholder}
+ }
+ return null
+ }
+
+ // Custom render via children function
+ if (children) {
+ return {children(parts)}
+ }
+
+ // Default render
+ return (
+
+ {parts.formatted}
+
+ )
+}
diff --git a/packages/cent-react/src/components/MoneyInput.tsx b/packages/cent-react/src/components/MoneyInput.tsx
new file mode 100644
index 0000000..93a262e
--- /dev/null
+++ b/packages/cent-react/src/components/MoneyInput.tsx
@@ -0,0 +1,286 @@
+import { Money, MoneyClass } from '@thesis-co/cent'
+import {
+ type InputHTMLAttributes,
+ forwardRef,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from 'react'
+
+/** Type alias for Money instance */
+type MoneyInstance = InstanceType
+
+/**
+ * Change event for MoneyInput, compatible with react-hook-form and formik
+ */
+export interface MoneyInputChangeEvent {
+ target: {
+ name: string
+ value: MoneyInstance | null
+ }
+}
+
+/**
+ * Blur event for MoneyInput
+ */
+export interface MoneyInputBlurEvent {
+ target: {
+ name: string
+ value: MoneyInstance | null
+ }
+}
+
+export interface MoneyInputProps
+ extends Omit<
+ InputHTMLAttributes,
+ 'value' | 'onChange' | 'onBlur' | 'type' | 'defaultValue' | 'min' | 'max'
+ > {
+ /** Current Money value (controlled) */
+ value?: MoneyInstance | null
+
+ /** Field name - required for form integration */
+ name: string
+
+ /** Currency for parsing input (required) */
+ currency: string
+
+ /**
+ * onChange handler - designed for form library compatibility
+ *
+ * For react-hook-form:
+ * } />
+ *
+ * For formik:
+ *
+ */
+ onChange?: (event: MoneyInputChangeEvent) => void
+
+ /** Alternative: direct value handler */
+ onValueChange?: (value: MoneyInstance | null) => void
+
+ /** Blur handler */
+ onBlur?: (event: MoneyInputBlurEvent) => void
+
+ /** Minimum allowed value */
+ min?: MoneyInstance | string
+
+ /** Maximum allowed value */
+ max?: MoneyInstance | string
+
+ /** Format the display on blur (default: true) */
+ formatOnBlur?: boolean
+
+ /** Display locale for formatting */
+ locale?: string
+
+ /** Allow negative values (default: true) */
+ allowNegative?: boolean
+
+ /** Select all text on focus (default: true) */
+ selectOnFocus?: boolean
+}
+
+/**
+ * Parse a string input to Money, with fallback to currency
+ */
+function parseInput(input: string, currency: string, allowNegative: boolean): MoneyInstance | null {
+ if (!input.trim()) {
+ return null
+ }
+
+ try {
+ // Try parsing with currency prefix/suffix
+ const result = MoneyClass.parse(input)
+ if (result.ok) {
+ const money = result.value
+ if (!allowNegative && money.isNegative()) {
+ return money.absolute()
+ }
+ return money
+ }
+
+ // Try parsing as a plain number with the specified currency
+ const cleaned = input.replace(/[,\s]/g, '')
+ const numMatch = cleaned.match(/^-?[\d.]+$/)
+ if (numMatch) {
+ // Pass as string to preserve precision
+ const money = Money(`${cleaned} ${currency}`) as MoneyInstance
+ if (!allowNegative && money.isNegative()) {
+ return money.absolute()
+ }
+ return money
+ }
+
+ return null
+ } catch {
+ return null
+ }
+}
+
+/**
+ * Format Money for display in the input
+ */
+function formatForDisplay(money: MoneyInstance | null, locale?: string): string {
+ if (!money) return ''
+ return money.toString({
+ locale,
+ excludeCurrency: true,
+ })
+}
+
+/**
+ * A controlled money input component compatible with react-hook-form and formik.
+ *
+ * @example
+ * // Basic usage
+ * const [amount, setAmount] = useState(null);
+ * setAmount(e.target.value)}
+ * currency="USD"
+ * />
+ *
+ * @example
+ * // With react-hook-form
+ * (
+ *
+ * )}
+ * />
+ */
+export const MoneyInput = forwardRef(function MoneyInput(
+ {
+ value,
+ name,
+ currency,
+ onChange,
+ onValueChange,
+ onBlur,
+ min,
+ max,
+ formatOnBlur = true,
+ locale,
+ allowNegative = true,
+ selectOnFocus = true,
+ placeholder,
+ disabled,
+ className,
+ style,
+ ...rest
+ },
+ ref
+) {
+ const inputRef = useRef(null)
+ useImperativeHandle(ref, () => inputRef.current!)
+
+ // Track whether we're currently editing
+ const [isEditing, setIsEditing] = useState(false)
+ const [displayValue, setDisplayValue] = useState(() => formatForDisplay(value ?? null, locale))
+
+ // Sync display value with controlled value when not editing
+ // Only sync if value is explicitly controlled (not undefined)
+ useEffect(() => {
+ if (!isEditing && value !== undefined) {
+ setDisplayValue(formatForDisplay(value, locale))
+ }
+ }, [value, isEditing, locale])
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const raw = e.target.value
+ setDisplayValue(raw)
+
+ const parsed = parseInput(raw, currency, allowNegative)
+
+ // Validate min/max
+ let validatedValue = parsed
+ if (validatedValue) {
+ try {
+ if (min) {
+ const minMoney = typeof min === 'string' ? (Money(min) as MoneyInstance) : min
+ if (validatedValue.lessThan(minMoney)) {
+ // Allow the input but mark as invalid (validation is external)
+ }
+ }
+ if (max) {
+ const maxMoney = typeof max === 'string' ? (Money(max) as MoneyInstance) : max
+ if (validatedValue.greaterThan(maxMoney)) {
+ // Allow the input but mark as invalid (validation is external)
+ }
+ }
+ } catch {
+ // Ignore validation errors for mismatched currencies
+ }
+ }
+
+ if (onChange) {
+ onChange({ target: { name, value: validatedValue } })
+ }
+ if (onValueChange) {
+ onValueChange(validatedValue)
+ }
+ },
+ [currency, allowNegative, min, max, name, onChange, onValueChange]
+ )
+
+ const handleFocus = useCallback(
+ (_e: React.FocusEvent) => {
+ setIsEditing(true)
+
+ // Show raw value for editing (without formatting)
+ if (value) {
+ const rawValue = value.toString({ excludeCurrency: true })
+ // Remove thousand separators for easier editing
+ setDisplayValue(rawValue.replace(/,/g, ''))
+ }
+
+ if (selectOnFocus) {
+ // Use setTimeout to ensure the value is set before selecting
+ setTimeout(() => {
+ inputRef.current?.select()
+ }, 0)
+ }
+ },
+ [value, selectOnFocus]
+ )
+
+ const handleBlur = useCallback(
+ (_e: React.FocusEvent) => {
+ setIsEditing(false)
+
+ // Format on blur if enabled
+ if (formatOnBlur && value) {
+ setDisplayValue(formatForDisplay(value, locale))
+ }
+
+ if (onBlur) {
+ onBlur({ target: { name, value: value ?? null } })
+ }
+ },
+ [formatOnBlur, value, locale, name, onBlur]
+ )
+
+ return (
+
+ )
+})
diff --git a/packages/cent-react/src/components/index.ts b/packages/cent-react/src/components/index.ts
new file mode 100644
index 0000000..2d44c42
--- /dev/null
+++ b/packages/cent-react/src/components/index.ts
@@ -0,0 +1,8 @@
+export { MoneyDisplay } from './MoneyDisplay'
+export type { MoneyDisplayProps, MoneyParts } from './MoneyDisplay'
+
+export { MoneyInput } from './MoneyInput'
+export type { MoneyInputProps, MoneyInputChangeEvent, MoneyInputBlurEvent } from './MoneyInput'
+
+export { MoneyDiff } from './MoneyDiff'
+export type { MoneyDiffProps, MoneyDiffRenderProps } from './MoneyDiff'
diff --git a/packages/cent-react/src/context/MoneyProvider.tsx b/packages/cent-react/src/context/MoneyProvider.tsx
new file mode 100644
index 0000000..aff34d7
--- /dev/null
+++ b/packages/cent-react/src/context/MoneyProvider.tsx
@@ -0,0 +1,120 @@
+import { type CentConfig, type ExchangeRate, getConfig } from '@thesis-co/cent'
+import { type ReactNode, createContext, useContext, useMemo } from 'react'
+
+/**
+ * Function to resolve exchange rates
+ */
+export type ExchangeRateResolver = (
+ from: string,
+ to: string
+) => Promise | ExchangeRate | null
+
+/**
+ * Context value for MoneyProvider
+ */
+export interface MoneyContextValue {
+ /** Default locale for formatting */
+ locale: string
+
+ /** Default currency for inputs */
+ defaultCurrency: string | null
+
+ /** Exchange rate resolver */
+ exchangeRateResolver: ExchangeRateResolver | null
+
+ /** Cent library config */
+ config: CentConfig
+}
+
+/**
+ * Props for MoneyProvider
+ */
+export interface MoneyProviderProps {
+ children: ReactNode
+
+ /** Default locale for all money formatting (default: "en-US") */
+ locale?: string
+
+ /** Default currency for inputs */
+ defaultCurrency?: string
+
+ /** Exchange rate resolver for conversions */
+ exchangeRateResolver?: ExchangeRateResolver
+
+ /** Cent library config overrides */
+ config?: Partial
+}
+
+const MoneyContext = createContext(null)
+
+/**
+ * Provider for default Money configuration.
+ *
+ * @example
+ * // Set defaults for all descendant components
+ *
+ *
+ *
+ *
+ * @example
+ * // With exchange rate resolver
+ * {
+ * const rate = await fetchExchangeRate(from, to)
+ * return new ExchangeRate(from, to, rate)
+ * }}
+ * >
+ *
+ *
+ */
+export function MoneyProvider({
+ children,
+ locale = 'en-US',
+ defaultCurrency,
+ exchangeRateResolver,
+ config: configOverrides,
+}: MoneyProviderProps): ReactNode {
+ const parentContext = useContext(MoneyContext)
+
+ const contextValue = useMemo(() => {
+ // Get base config from Cent library
+ const baseConfig = getConfig()
+
+ // Merge with overrides
+ const mergedConfig: CentConfig = configOverrides
+ ? { ...baseConfig, ...configOverrides }
+ : baseConfig
+
+ return {
+ locale: locale ?? parentContext?.locale ?? 'en-US',
+ defaultCurrency: defaultCurrency ?? parentContext?.defaultCurrency ?? null,
+ exchangeRateResolver: exchangeRateResolver ?? parentContext?.exchangeRateResolver ?? null,
+ config: mergedConfig,
+ }
+ }, [locale, defaultCurrency, exchangeRateResolver, configOverrides, parentContext])
+
+ return {children}
+}
+
+/**
+ * Default context value when no provider is present
+ */
+function getDefaultContextValue(): MoneyContextValue {
+ return {
+ locale: 'en-US',
+ defaultCurrency: null,
+ exchangeRateResolver: null,
+ config: getConfig(),
+ }
+}
+
+/**
+ * Hook to access the Money context.
+ *
+ * @example
+ * const { locale, defaultCurrency } = useMoneyConfig()
+ */
+export function useMoneyContext(): MoneyContextValue {
+ const context = useContext(MoneyContext)
+ return context ?? getDefaultContextValue()
+}
diff --git a/packages/cent-react/src/context/index.ts b/packages/cent-react/src/context/index.ts
new file mode 100644
index 0000000..423b1c8
--- /dev/null
+++ b/packages/cent-react/src/context/index.ts
@@ -0,0 +1,6 @@
+export { MoneyProvider, useMoneyContext } from './MoneyProvider'
+export type {
+ MoneyProviderProps,
+ MoneyContextValue,
+ ExchangeRateResolver,
+} from './MoneyProvider'
diff --git a/packages/cent-react/src/hooks/index.ts b/packages/cent-react/src/hooks/index.ts
new file mode 100644
index 0000000..d7cabaf
--- /dev/null
+++ b/packages/cent-react/src/hooks/index.ts
@@ -0,0 +1,7 @@
+export { useMoney } from './useMoney'
+export type { UseMoneyOptions, UseMoneyReturn } from './useMoney'
+
+export { useExchangeRate } from './useExchangeRate'
+export type { UseExchangeRateOptions, UseExchangeRateReturn } from './useExchangeRate'
+
+export { useMoneyConfig } from './useMoneyConfig'
diff --git a/packages/cent-react/src/hooks/useExchangeRate.ts b/packages/cent-react/src/hooks/useExchangeRate.ts
new file mode 100644
index 0000000..de226a2
--- /dev/null
+++ b/packages/cent-react/src/hooks/useExchangeRate.ts
@@ -0,0 +1,198 @@
+import { type ExchangeRate, MoneyClass } from '@thesis-co/cent'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { useMoneyContext } from '../context/MoneyProvider'
+
+/** Type alias for Money instance */
+type MoneyInstance = InstanceType
+
+export interface UseExchangeRateOptions {
+ /** Base currency code */
+ from: string
+
+ /** Quote currency code */
+ to: string
+
+ /** Auto-refresh interval in milliseconds (0 = disabled) */
+ pollInterval?: number
+
+ /** Time in ms after which rate is considered stale */
+ staleThreshold?: number
+
+ /** Whether to enable fetching (default: true) */
+ enabled?: boolean
+}
+
+export interface UseExchangeRateReturn {
+ /** Current exchange rate */
+ rate: ExchangeRate | null
+
+ /** Whether a fetch is in progress */
+ isLoading: boolean
+
+ /** Whether the rate is stale */
+ isStale: boolean
+
+ /** Error from the last fetch attempt */
+ error: Error | null
+
+ /** Time since last successful fetch in ms */
+ age: number
+
+ /** Convert a Money value using the current rate */
+ convert: (money: MoneyInstance) => MoneyInstance | null
+
+ /** Manually trigger a refetch */
+ refetch: () => Promise
+}
+
+/**
+ * Hook for fetching and managing exchange rates.
+ *
+ * Uses the exchangeRateResolver from MoneyProvider context.
+ *
+ * @example
+ * // Basic usage
+ * const { rate, convert, isLoading } = useExchangeRate({
+ * from: 'USD',
+ * to: 'EUR'
+ * })
+ *
+ * const eurAmount = convert(usdAmount)
+ *
+ * @example
+ * // With polling
+ * const { rate, isStale, refetch } = useExchangeRate({
+ * from: 'BTC',
+ * to: 'USD',
+ * pollInterval: 30000, // 30 seconds
+ * staleThreshold: 60000 // 1 minute
+ * })
+ */
+export function useExchangeRate(options: UseExchangeRateOptions): UseExchangeRateReturn {
+ const { from, to, pollInterval = 0, staleThreshold = 300000, enabled = true } = options
+
+ const { exchangeRateResolver } = useMoneyContext()
+
+ const [rate, setRate] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [lastFetchTime, setLastFetchTime] = useState(null)
+ const [age, setAge] = useState(0)
+
+ const abortControllerRef = useRef(null)
+
+ // Fetch the exchange rate
+ const fetchRate = useCallback(async () => {
+ if (!exchangeRateResolver) {
+ setError(new Error('No exchange rate resolver configured in MoneyProvider'))
+ return
+ }
+
+ // Cancel any pending request
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort()
+ }
+ abortControllerRef.current = new AbortController()
+
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const result = await exchangeRateResolver(from, to)
+ setRate(result)
+ setLastFetchTime(Date.now())
+ setError(null)
+ } catch (e) {
+ if (e instanceof Error && e.name === 'AbortError') {
+ // Ignore abort errors
+ return
+ }
+ setError(e instanceof Error ? e : new Error('Failed to fetch exchange rate'))
+ } finally {
+ setIsLoading(false)
+ }
+ }, [exchangeRateResolver, from, to])
+
+ // Initial fetch
+ useEffect(() => {
+ if (enabled) {
+ if (exchangeRateResolver) {
+ fetchRate()
+ } else {
+ // Set error when no resolver is configured
+ setError(new Error('No exchange rate resolver configured in MoneyProvider'))
+ }
+ }
+
+ return () => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort()
+ }
+ }
+ }, [enabled, exchangeRateResolver, from, to, fetchRate])
+
+ // Polling
+ useEffect(() => {
+ if (!enabled || pollInterval <= 0) {
+ return
+ }
+
+ const intervalId = setInterval(fetchRate, pollInterval)
+ return () => clearInterval(intervalId)
+ }, [enabled, pollInterval, fetchRate])
+
+ // Update age
+ useEffect(() => {
+ if (!lastFetchTime) {
+ setAge(0)
+ return
+ }
+
+ const updateAge = () => {
+ setAge(Date.now() - lastFetchTime)
+ }
+
+ updateAge()
+ const intervalId = setInterval(updateAge, 1000)
+ return () => clearInterval(intervalId)
+ }, [lastFetchTime])
+
+ // Calculate staleness
+ const isStale = lastFetchTime ? age > staleThreshold : false
+
+ // Convert function
+ const convert = useCallback(
+ (money: MoneyInstance): MoneyInstance | null => {
+ if (!rate) return null
+
+ try {
+ // Use the ExchangeRate's convert method if available
+ if ('convert' in rate && typeof rate.convert === 'function') {
+ return rate.convert(money) as MoneyInstance
+ }
+
+ // Fallback: manual conversion
+ // This assumes the rate is a simple numeric multiplier
+ if ('rate' in rate) {
+ const rateValue = rate.rate
+ return money.multiply(rateValue.toString()) as MoneyInstance
+ }
+
+ return null
+ } catch {
+ return null
+ }
+ },
+ [rate]
+ )
+
+ return {
+ rate,
+ isLoading,
+ isStale,
+ error,
+ age,
+ convert,
+ refetch: fetchRate,
+ }
+}
diff --git a/packages/cent-react/src/hooks/useMoney.ts b/packages/cent-react/src/hooks/useMoney.ts
new file mode 100644
index 0000000..f5d4164
--- /dev/null
+++ b/packages/cent-react/src/hooks/useMoney.ts
@@ -0,0 +1,266 @@
+import { Money, MoneyClass } from '@thesis-co/cent'
+import { useCallback, useMemo, useState } from 'react'
+
+/** Type alias for Money instance */
+type MoneyInstance = InstanceType
+
+/** Options for formatting Money to string */
+interface MoneyFormatOptions {
+ locale?: string
+ compact?: boolean
+ maxDecimals?: number | bigint
+ minDecimals?: number | bigint
+ preferredUnit?: string
+ preferSymbol?: boolean
+ preferFractionalSymbol?: boolean
+ excludeCurrency?: boolean
+}
+
+export interface UseMoneyOptions {
+ /** Initial Money value */
+ initialValue?: MoneyInstance | string | null
+
+ /** Currency for parsing string inputs */
+ currency?: string
+
+ /** Minimum allowed value for validation */
+ min?: MoneyInstance | string
+
+ /** Maximum allowed value for validation */
+ max?: MoneyInstance | string
+}
+
+export interface UseMoneyReturn {
+ /** Current Money value */
+ money: MoneyInstance | null
+
+ /** Set Money value from various inputs */
+ setMoney: (value: MoneyInstance | string | number | null) => void
+
+ /** Format the current value */
+ format: (options?: MoneyFormatOptions) => string
+
+ /** Whether the current value is valid */
+ isValid: boolean
+
+ /** Current validation error, if any */
+ error: Error | null
+
+ /** Reset to initial value */
+ reset: () => void
+
+ /** Clear the value */
+ clear: () => void
+
+ /**
+ * Props to spread on a native input element
+ * @example
+ * const { inputProps } = useMoney({ currency: 'USD' })
+ *
+ */
+ inputProps: {
+ value: string
+ onChange: (e: React.ChangeEvent) => void
+ onBlur: () => void
+ }
+}
+
+/**
+ * Parse a value to Money
+ */
+function parseMoney(value: MoneyInstance | string | number | null, currency?: string): MoneyInstance | null {
+ if (value == null) return null
+
+ if (typeof value === 'object' && 'currency' in value) {
+ // Already a Money instance
+ return value as MoneyInstance
+ }
+
+ if (typeof value === 'number') {
+ if (!currency) {
+ throw new Error('Currency is required when setting from a number')
+ }
+ // Convert number to string to preserve precision
+ return Money(`${value} ${currency}`) as MoneyInstance
+ }
+
+ if (typeof value === 'string') {
+ if (!value.trim()) return null
+
+ // Try parsing with embedded currency
+ const result = MoneyClass.parse(value)
+ if (result.ok) {
+ return result.value
+ }
+
+ // Try parsing as number with provided currency
+ if (currency) {
+ const cleaned = value.replace(/[,\s]/g, '')
+ const numMatch = cleaned.match(/^-?[\d.]+$/)
+ if (numMatch) {
+ // Pass as string to preserve precision
+ return Money(`${cleaned} ${currency}`) as MoneyInstance
+ }
+ }
+
+ return null
+ }
+
+ return null
+}
+
+/**
+ * Validate Money against constraints
+ */
+function validateMoney(
+ money: MoneyInstance | null,
+ min?: MoneyInstance | string,
+ max?: MoneyInstance | string
+): Error | null {
+ if (!money) return null
+
+ try {
+ if (min) {
+ const minMoney = typeof min === 'string' ? (Money(min) as MoneyInstance) : min
+ if (money.lessThan(minMoney)) {
+ return new Error(`Value must be at least ${minMoney.toString()}`)
+ }
+ }
+
+ if (max) {
+ const maxMoney = typeof max === 'string' ? (Money(max) as MoneyInstance) : max
+ if (money.greaterThan(maxMoney)) {
+ return new Error(`Value must be at most ${maxMoney.toString()}`)
+ }
+ }
+ } catch (e) {
+ // Currency mismatch or other error
+ return e instanceof Error ? e : new Error('Validation error')
+ }
+
+ return null
+}
+
+/**
+ * Hook for managing Money state with validation.
+ *
+ * @example
+ * // Basic usage
+ * const { money, setMoney, format } = useMoney({ currency: 'USD' })
+ *
+ * @example
+ * // With validation
+ * const { money, isValid, error } = useMoney({
+ * currency: 'USD',
+ * min: '$0.01',
+ * max: '$1000'
+ * })
+ *
+ * @example
+ * // With native input binding
+ * const { inputProps } = useMoney({ currency: 'USD' })
+ *
+ */
+export function useMoney(options: UseMoneyOptions = {}): UseMoneyReturn {
+ const { initialValue, currency, min, max } = options
+
+ const [money, setMoneyState] = useState(() => {
+ if (initialValue == null) return null
+ try {
+ return parseMoney(initialValue, currency)
+ } catch {
+ return null
+ }
+ })
+
+ const [displayValue, setDisplayValue] = useState(() => {
+ return money?.toString({ excludeCurrency: true }) ?? ''
+ })
+
+ // Validation
+ const error = useMemo(() => validateMoney(money, min, max), [money, min, max])
+ const isValid = error === null
+
+ // Set money from various inputs
+ const setMoney = useCallback(
+ (value: MoneyInstance | string | number | null) => {
+ try {
+ const parsed = parseMoney(value, currency)
+ setMoneyState(parsed)
+ setDisplayValue(parsed?.toString({ excludeCurrency: true }) ?? '')
+ } catch {
+ setMoneyState(null)
+ setDisplayValue('')
+ }
+ },
+ [currency]
+ )
+
+ // Format the current value
+ const format = useCallback(
+ (formatOptions?: MoneyFormatOptions) => {
+ if (!money) return ''
+ return money.toString(formatOptions)
+ },
+ [money]
+ )
+
+ // Reset to initial value
+ const reset = useCallback(() => {
+ try {
+ const initial = initialValue != null ? parseMoney(initialValue, currency) : null
+ setMoneyState(initial)
+ setDisplayValue(initial?.toString({ excludeCurrency: true }) ?? '')
+ } catch {
+ setMoneyState(null)
+ setDisplayValue('')
+ }
+ }, [initialValue, currency])
+
+ // Clear the value
+ const clear = useCallback(() => {
+ setMoneyState(null)
+ setDisplayValue('')
+ }, [])
+
+ // Input props for native input binding
+ const inputProps = useMemo(
+ () => ({
+ value: displayValue,
+ onChange: (e: React.ChangeEvent) => {
+ const raw = e.target.value
+ setDisplayValue(raw)
+
+ if (!raw.trim()) {
+ setMoneyState(null)
+ return
+ }
+
+ try {
+ const parsed = parseMoney(raw, currency)
+ setMoneyState(parsed)
+ } catch {
+ // Keep display value but don't update money
+ }
+ },
+ onBlur: () => {
+ // Format on blur
+ if (money) {
+ setDisplayValue(money.toString({ excludeCurrency: true }))
+ }
+ },
+ }),
+ [displayValue, currency, money]
+ )
+
+ return {
+ money,
+ setMoney,
+ format,
+ isValid,
+ error,
+ reset,
+ clear,
+ inputProps,
+ }
+}
diff --git a/packages/cent-react/src/hooks/useMoneyConfig.ts b/packages/cent-react/src/hooks/useMoneyConfig.ts
new file mode 100644
index 0000000..864bd3e
--- /dev/null
+++ b/packages/cent-react/src/hooks/useMoneyConfig.ts
@@ -0,0 +1,2 @@
+// Re-export from context for convenience
+export { useMoneyContext as useMoneyConfig } from '../context/MoneyProvider'
diff --git a/packages/cent-react/src/index.ts b/packages/cent-react/src/index.ts
new file mode 100644
index 0000000..ec012f2
--- /dev/null
+++ b/packages/cent-react/src/index.ts
@@ -0,0 +1,22 @@
+// Components
+export { MoneyDisplay } from './components/MoneyDisplay'
+export type { MoneyDisplayProps, MoneyParts } from './components/MoneyDisplay'
+
+export { MoneyInput } from './components/MoneyInput'
+export type { MoneyInputProps, MoneyInputChangeEvent } from './components/MoneyInput'
+
+export { MoneyDiff } from './components/MoneyDiff'
+export type { MoneyDiffProps, MoneyDiffRenderProps } from './components/MoneyDiff'
+
+// Hooks
+export { useMoney } from './hooks/useMoney'
+export type { UseMoneyOptions, UseMoneyReturn } from './hooks/useMoney'
+
+export { useExchangeRate } from './hooks/useExchangeRate'
+export type { UseExchangeRateOptions, UseExchangeRateReturn } from './hooks/useExchangeRate'
+
+export { useMoneyConfig } from './hooks/useMoneyConfig'
+
+// Context
+export { MoneyProvider } from './context/MoneyProvider'
+export type { MoneyProviderProps, MoneyContextValue } from './context/MoneyProvider'
diff --git a/packages/cent-react/test/components/MoneyDiff.test.tsx b/packages/cent-react/test/components/MoneyDiff.test.tsx
new file mode 100644
index 0000000..dce612b
--- /dev/null
+++ b/packages/cent-react/test/components/MoneyDiff.test.tsx
@@ -0,0 +1,123 @@
+import { render, screen } from '@testing-library/react'
+import { Money } from '@thesis-co/cent'
+import { MoneyDiff } from '../../src/components/MoneyDiff'
+
+describe('MoneyDiff', () => {
+ describe('basic rendering', () => {
+ it('renders positive difference with plus sign', () => {
+ render( )
+ expect(screen.getByText('+$20.00')).toBeInTheDocument()
+ })
+
+ it('renders negative difference with minus sign', () => {
+ render( )
+ expect(screen.getByText('-$20.00')).toBeInTheDocument()
+ })
+
+ it('renders zero difference without sign', () => {
+ render( )
+ expect(screen.getByText('$0.00')).toBeInTheDocument()
+ })
+
+ it('accepts string values', () => {
+ render( )
+ expect(screen.getByText('+$50.00')).toBeInTheDocument()
+ })
+ })
+
+ describe('percentage display', () => {
+ it('shows percentage when showPercentage is true', () => {
+ render( )
+ expect(screen.getByText('+$20.00 (+20.00%)')).toBeInTheDocument()
+ })
+
+ it('shows negative percentage for decrease', () => {
+ render( )
+ expect(screen.getByText('-$20.00 (-20.00%)')).toBeInTheDocument()
+ })
+
+ it('respects percentageDecimals option', () => {
+ render(
+
+ )
+ expect(screen.getByText(/33\.3%/)).toBeInTheDocument()
+ })
+ })
+
+ describe('data-direction attribute', () => {
+ it('sets data-direction to increase for positive diff', () => {
+ render( )
+ expect(screen.getByTestId('diff')).toHaveAttribute('data-direction', 'increase')
+ })
+
+ it('sets data-direction to decrease for negative diff', () => {
+ render( )
+ expect(screen.getByTestId('diff')).toHaveAttribute('data-direction', 'decrease')
+ })
+
+ it('sets data-direction to unchanged for zero diff', () => {
+ render( )
+ expect(screen.getByTestId('diff')).toHaveAttribute('data-direction', 'unchanged')
+ })
+ })
+
+ describe('custom rendering', () => {
+ it('passes render props to children function', () => {
+ render(
+
+ {({ direction, formatted, percentageChange }) => (
+
+ {direction}: {formatted.difference} ({percentageChange}%)
+
+ )}
+
+ )
+
+ expect(screen.getByTestId('custom')).toHaveTextContent('increase: +$50.00 (50.00%)')
+ })
+
+ it('provides Money instances in render props', () => {
+ let receivedCurrent: Money | null = null
+ let receivedDiff: Money | null = null
+
+ render(
+
+ {({ current, difference }) => {
+ receivedCurrent = current
+ receivedDiff = difference
+ return test
+ }}
+
+ )
+
+ expect(receivedCurrent?.toString()).toBe('$120.00')
+ expect(receivedDiff?.toString()).toBe('$20.00')
+ })
+ })
+
+ describe('styling', () => {
+ it('applies className', () => {
+ render(
+
+ )
+ expect(screen.getByTestId('diff')).toHaveClass('my-diff')
+ })
+
+ it('renders as different element type', () => {
+ render(
+
+ )
+ expect(screen.getByTestId('diff').tagName).toBe('DIV')
+ })
+ })
+})
diff --git a/packages/cent-react/test/components/MoneyDisplay.test.tsx b/packages/cent-react/test/components/MoneyDisplay.test.tsx
new file mode 100644
index 0000000..cbacc31
--- /dev/null
+++ b/packages/cent-react/test/components/MoneyDisplay.test.tsx
@@ -0,0 +1,120 @@
+import { render, screen } from '@testing-library/react'
+import { Money } from '@thesis-co/cent'
+import { MoneyDisplay } from '../../src/components/MoneyDisplay'
+
+describe('MoneyDisplay', () => {
+ describe('basic rendering', () => {
+ it('renders formatted money value', () => {
+ render( )
+ expect(screen.getByText('$100.50')).toBeInTheDocument()
+ })
+
+ it('renders with string value', () => {
+ render( )
+ expect(screen.getByText('$200.00')).toBeInTheDocument()
+ })
+
+ it('renders nothing for null value without placeholder', () => {
+ const { container } = render( )
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ it('renders placeholder for null value', () => {
+ render( )
+ expect(screen.getByText('—')).toBeInTheDocument()
+ })
+
+ it('renders placeholder for undefined value', () => {
+ render( )
+ expect(screen.getByText('N/A')).toBeInTheDocument()
+ })
+ })
+
+ describe('formatting options', () => {
+ it('applies compact notation', () => {
+ render( )
+ // Compact notation varies by browser, just check it renders
+ expect(screen.getByText(/\$1\.5M|\$1,500K/)).toBeInTheDocument()
+ })
+
+ it('applies maxDecimals', () => {
+ render( )
+ expect(screen.getByText('$101.00')).toBeInTheDocument()
+ })
+
+ it('excludes currency when requested', () => {
+ render( )
+ expect(screen.getByText('100.00')).toBeInTheDocument()
+ })
+ })
+
+ describe('showSign prop', () => {
+ it('shows negative sign by default for negative values', () => {
+ render( )
+ expect(screen.getByText(/-\$50\.00|-50\.00/)).toBeInTheDocument()
+ })
+
+ it('shows positive sign when showSign is always', () => {
+ render( )
+ expect(screen.getByText(/\+\$50\.00/)).toBeInTheDocument()
+ })
+
+ it('hides sign when showSign is never', () => {
+ render( )
+ expect(screen.getByText('$50.00')).toBeInTheDocument()
+ })
+ })
+
+ describe('custom rendering', () => {
+ it('passes parts to children function', () => {
+ render(
+
+ {({ formatted, isNegative, isZero }) => (
+
+ {formatted} - neg:{String(isNegative)} - zero:{String(isZero)}
+
+ )}
+
+ )
+
+ const element = screen.getByTestId('custom')
+ expect(element).toHaveTextContent('$99.99')
+ expect(element).toHaveTextContent('neg:false')
+ expect(element).toHaveTextContent('zero:false')
+ })
+
+ it('provides money instance in parts', () => {
+ let receivedMoney: Money | null = null
+
+ render(
+
+ {({ money }) => {
+ receivedMoney = money
+ return test
+ }}
+
+ )
+
+ expect(receivedMoney).not.toBeNull()
+ expect(receivedMoney!.toString()).toBe('$100.00')
+ })
+ })
+
+ describe('styling', () => {
+ it('applies className', () => {
+ render( )
+ expect(screen.getByText('$100.00')).toHaveClass('my-class')
+ })
+
+ it('applies style', () => {
+ render( )
+ expect(screen.getByText('$100.00')).toHaveStyle({ color: 'red' })
+ })
+
+ it('renders as different element type', () => {
+ render( )
+ const element = screen.getByTestId('money')
+ expect(element.tagName).toBe('DIV')
+ })
+ })
+})
diff --git a/packages/cent-react/test/components/MoneyInput.test.tsx b/packages/cent-react/test/components/MoneyInput.test.tsx
new file mode 100644
index 0000000..3d8e5e6
--- /dev/null
+++ b/packages/cent-react/test/components/MoneyInput.test.tsx
@@ -0,0 +1,141 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { Money } from '@thesis-co/cent'
+import { MoneyInput } from '../../src/components/MoneyInput'
+
+describe('MoneyInput', () => {
+ describe('basic rendering', () => {
+ it('renders an input element', () => {
+ render( )
+ expect(screen.getByRole('textbox')).toBeInTheDocument()
+ })
+
+ it('displays controlled value', () => {
+ render( )
+ expect(screen.getByRole('textbox')).toHaveValue('100.00')
+ })
+
+ it('displays placeholder', () => {
+ render( )
+ expect(screen.getByPlaceholderText('Enter amount')).toBeInTheDocument()
+ })
+ })
+
+ describe('user input', () => {
+ it('calls onChange with parsed Money value', async () => {
+ const user = userEvent.setup()
+ const onChange = jest.fn()
+
+ render( )
+
+ const input = screen.getByRole('textbox')
+ await user.type(input, '50.00')
+
+ expect(onChange).toHaveBeenCalled()
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
+ expect(lastCall.target.name).toBe('amount')
+ expect(lastCall.target.value).not.toBeNull()
+ })
+
+ it('calls onValueChange with Money value', async () => {
+ const user = userEvent.setup()
+ const onValueChange = jest.fn()
+
+ render( )
+
+ const input = screen.getByRole('textbox')
+ await user.type(input, '75')
+
+ expect(onValueChange).toHaveBeenCalled()
+ })
+
+ it('handles clearing input', async () => {
+ const user = userEvent.setup()
+ const onChange = jest.fn()
+
+ render( )
+
+ const input = screen.getByRole('textbox')
+ await user.clear(input)
+
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
+ expect(lastCall.target.value).toBeNull()
+ })
+ })
+
+ describe('formatting', () => {
+ it('formats value on blur when formatOnBlur is true with controlled value', () => {
+ const value = Money('$1234.50')
+ render(
+
+ )
+
+ const input = screen.getByRole('textbox')
+ // Controlled value should display formatted (without currency since excludeCurrency)
+ expect(input).toHaveValue('1,234.50')
+ })
+
+ it('displays unformatted value when controlled value is set', () => {
+ const value = Money('$100')
+ render(
+
+ )
+
+ const input = screen.getByRole('textbox')
+ expect(input).toHaveValue('100.00')
+ })
+ })
+
+ describe('validation', () => {
+ it('allows negative values by default', async () => {
+ const user = userEvent.setup()
+ const onChange = jest.fn()
+
+ render( )
+
+ const input = screen.getByRole('textbox')
+ await user.type(input, '-50')
+
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
+ expect(lastCall.target.value?.isNegative()).toBe(true)
+ })
+
+ it('converts negative to positive when allowNegative is false', async () => {
+ const user = userEvent.setup()
+ const onChange = jest.fn()
+
+ render( )
+
+ const input = screen.getByRole('textbox')
+ await user.type(input, '-50')
+
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0]
+ expect(lastCall.target.value?.isNegative()).toBe(false)
+ })
+ })
+
+ describe('props passthrough', () => {
+ it('passes disabled prop', () => {
+ render( )
+ expect(screen.getByRole('textbox')).toBeDisabled()
+ })
+
+ it('passes className', () => {
+ render( )
+ expect(screen.getByRole('textbox')).toHaveClass('my-input')
+ })
+
+ it('sets inputMode to decimal', () => {
+ render( )
+ expect(screen.getByRole('textbox')).toHaveAttribute('inputMode', 'decimal')
+ })
+ })
+
+ describe('ref forwarding', () => {
+ it('forwards ref to input element', () => {
+ const ref = { current: null as HTMLInputElement | null }
+ render( )
+ expect(ref.current).toBeInstanceOf(HTMLInputElement)
+ })
+ })
+})
diff --git a/packages/cent-react/test/context/MoneyProvider.test.tsx b/packages/cent-react/test/context/MoneyProvider.test.tsx
new file mode 100644
index 0000000..f1e3511
--- /dev/null
+++ b/packages/cent-react/test/context/MoneyProvider.test.tsx
@@ -0,0 +1,125 @@
+import { render, screen } from '@testing-library/react'
+import { MoneyProvider, useMoneyContext } from '../../src/context/MoneyProvider'
+
+// Test component that displays context values
+function ContextDisplay() {
+ const { locale, defaultCurrency } = useMoneyContext()
+ return (
+
+ {locale}
+ {defaultCurrency ?? 'none'}
+
+ )
+}
+
+describe('MoneyProvider', () => {
+ describe('default values', () => {
+ it('provides default locale without provider', () => {
+ render( )
+ expect(screen.getByTestId('locale')).toHaveTextContent('en-US')
+ })
+
+ it('provides null defaultCurrency without provider', () => {
+ render( )
+ expect(screen.getByTestId('currency')).toHaveTextContent('none')
+ })
+ })
+
+ describe('with provider', () => {
+ it('provides custom locale', () => {
+ render(
+
+
+
+ )
+ expect(screen.getByTestId('locale')).toHaveTextContent('de-DE')
+ })
+
+ it('provides custom defaultCurrency', () => {
+ render(
+
+
+
+ )
+ expect(screen.getByTestId('currency')).toHaveTextContent('EUR')
+ })
+
+ it('provides multiple values', () => {
+ render(
+
+
+
+ )
+ expect(screen.getByTestId('locale')).toHaveTextContent('fr-FR')
+ expect(screen.getByTestId('currency')).toHaveTextContent('CHF')
+ })
+ })
+
+ describe('nesting', () => {
+ it('nested provider overrides parent values', () => {
+ render(
+
+
+
+
+
+ )
+ expect(screen.getByTestId('locale')).toHaveTextContent('de-DE')
+ expect(screen.getByTestId('currency')).toHaveTextContent('EUR')
+ })
+
+ it('nested provider inherits unspecified values from parent', () => {
+ render(
+
+
+
+
+
+ )
+ expect(screen.getByTestId('locale')).toHaveTextContent('de-DE')
+ expect(screen.getByTestId('currency')).toHaveTextContent('GBP')
+ })
+ })
+
+ describe('exchangeRateResolver', () => {
+ it('provides resolver to context', () => {
+ const mockResolver = jest.fn()
+ let receivedResolver: typeof mockResolver | null = null
+
+ function ResolverCapture() {
+ const { exchangeRateResolver } = useMoneyContext()
+ receivedResolver = exchangeRateResolver
+ return null
+ }
+
+ render(
+
+
+
+ )
+
+ expect(receivedResolver).toBe(mockResolver)
+ })
+
+ it('inherits resolver from parent when not specified', () => {
+ const mockResolver = jest.fn()
+ let receivedResolver: typeof mockResolver | null = null
+
+ function ResolverCapture() {
+ const { exchangeRateResolver } = useMoneyContext()
+ receivedResolver = exchangeRateResolver
+ return null
+ }
+
+ render(
+
+
+
+
+
+ )
+
+ expect(receivedResolver).toBe(mockResolver)
+ })
+ })
+})
diff --git a/packages/cent-react/test/hooks/useExchangeRate.test.tsx b/packages/cent-react/test/hooks/useExchangeRate.test.tsx
new file mode 100644
index 0000000..a8bddd7
--- /dev/null
+++ b/packages/cent-react/test/hooks/useExchangeRate.test.tsx
@@ -0,0 +1,113 @@
+import { renderHook, act, waitFor } from '@testing-library/react'
+import { ExchangeRate, Money } from '@thesis-co/cent'
+import type { ReactNode } from 'react'
+import { MoneyProvider } from '../../src/context/MoneyProvider'
+import { useExchangeRate } from '../../src/hooks/useExchangeRate'
+
+describe('useExchangeRate', () => {
+ const mockResolver = jest.fn()
+
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ )
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe('initial state', () => {
+ it('starts with null rate', () => {
+ mockResolver.mockResolvedValue(null)
+
+ const { result } = renderHook(
+ () => useExchangeRate({ from: 'USD', to: 'EUR', enabled: false }),
+ { wrapper }
+ )
+
+ expect(result.current.rate).toBeNull()
+ expect(result.current.error).toBeNull()
+ })
+
+ it('returns error when no resolver configured', () => {
+ const { result } = renderHook(() => useExchangeRate({ from: 'USD', to: 'EUR' }))
+
+ // Should have error since no provider
+ expect(result.current.error).not.toBeNull()
+ expect(result.current.error?.message).toContain('No exchange rate resolver')
+ })
+ })
+
+ describe('fetching', () => {
+ it('does not fetch when enabled is false', () => {
+ const { result } = renderHook(
+ () => useExchangeRate({ from: 'USD', to: 'EUR', enabled: false }),
+ { wrapper }
+ )
+
+ expect(mockResolver).not.toHaveBeenCalled()
+ expect(result.current.rate).toBeNull()
+ })
+
+ it('calls resolver with correct currencies', async () => {
+ mockResolver.mockResolvedValue(null)
+
+ renderHook(
+ () => useExchangeRate({ from: 'USD', to: 'EUR' }),
+ { wrapper }
+ )
+
+ await waitFor(() => {
+ expect(mockResolver).toHaveBeenCalledWith('USD', 'EUR')
+ })
+ })
+ })
+
+ describe('staleness', () => {
+ it('isStale is false initially when no rate fetched', () => {
+ const { result } = renderHook(
+ () => useExchangeRate({ from: 'USD', to: 'EUR', enabled: false, staleThreshold: 1000 }),
+ { wrapper }
+ )
+
+ expect(result.current.isStale).toBe(false)
+ })
+ })
+
+ describe('convert', () => {
+ it('returns null when no rate available', () => {
+ const { result } = renderHook(
+ () => useExchangeRate({ from: 'USD', to: 'EUR', enabled: false }),
+ { wrapper }
+ )
+
+ const usd = Money('$100')
+ expect(result.current.convert(usd)).toBeNull()
+ })
+ })
+
+ describe('options', () => {
+ it('respects enabled option', () => {
+ const { result, rerender } = renderHook(
+ ({ enabled }) => useExchangeRate({ from: 'USD', to: 'EUR', enabled }),
+ { wrapper, initialProps: { enabled: false } }
+ )
+
+ expect(mockResolver).not.toHaveBeenCalled()
+
+ // Enable and check if it fetches
+ rerender({ enabled: true })
+
+ // Just verify the hook doesn't crash
+ expect(result.current.rate).toBeNull()
+ })
+
+ it('provides refetch function', () => {
+ const { result } = renderHook(
+ () => useExchangeRate({ from: 'USD', to: 'EUR', enabled: false }),
+ { wrapper }
+ )
+
+ expect(typeof result.current.refetch).toBe('function')
+ })
+ })
+})
diff --git a/packages/cent-react/test/hooks/useMoney.test.ts b/packages/cent-react/test/hooks/useMoney.test.ts
new file mode 100644
index 0000000..1f77896
--- /dev/null
+++ b/packages/cent-react/test/hooks/useMoney.test.ts
@@ -0,0 +1,204 @@
+import { renderHook, act } from '@testing-library/react'
+import { Money } from '@thesis-co/cent'
+import { useMoney } from '../../src/hooks/useMoney'
+
+describe('useMoney', () => {
+ describe('initialization', () => {
+ it('initializes with null when no initialValue provided', () => {
+ const { result } = renderHook(() => useMoney({ currency: 'USD' }))
+ expect(result.current.money).toBeNull()
+ })
+
+ it('initializes with Money from string initialValue', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$100.00', currency: 'USD' })
+ )
+ expect(result.current.money).not.toBeNull()
+ expect(result.current.money?.toString()).toBe('$100.00')
+ })
+
+ it('initializes with Money instance', () => {
+ const initial = Money('$50.00')
+ const { result } = renderHook(() => useMoney({ initialValue: initial }))
+ expect(result.current.money?.toString()).toBe('$50.00')
+ })
+ })
+
+ describe('setMoney', () => {
+ it('sets money from string', () => {
+ const { result } = renderHook(() => useMoney({ currency: 'USD' }))
+
+ act(() => {
+ result.current.setMoney('$200.00')
+ })
+
+ expect(result.current.money?.toString()).toBe('$200.00')
+ })
+
+ it('sets money from string with currency', () => {
+ const { result } = renderHook(() => useMoney({ currency: 'USD' }))
+
+ act(() => {
+ result.current.setMoney('150')
+ })
+
+ expect(result.current.money?.toString()).toBe('$150.00')
+ })
+
+ it('sets money to null', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$100', currency: 'USD' })
+ )
+
+ act(() => {
+ result.current.setMoney(null)
+ })
+
+ expect(result.current.money).toBeNull()
+ })
+
+ it('sets money from Money instance', () => {
+ const { result } = renderHook(() => useMoney({ currency: 'USD' }))
+
+ act(() => {
+ result.current.setMoney(Money('€50.00'))
+ })
+
+ expect(result.current.money?.toString()).toBe('€50.00')
+ })
+ })
+
+ describe('validation', () => {
+ it('returns isValid true when no constraints violated', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$50', currency: 'USD', min: '$10', max: '$100' })
+ )
+
+ expect(result.current.isValid).toBe(true)
+ expect(result.current.error).toBeNull()
+ })
+
+ it('returns error when value below min', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$5', currency: 'USD', min: '$10' })
+ )
+
+ expect(result.current.isValid).toBe(false)
+ expect(result.current.error).not.toBeNull()
+ expect(result.current.error?.message).toContain('at least')
+ })
+
+ it('returns error when value above max', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$150', currency: 'USD', max: '$100' })
+ )
+
+ expect(result.current.isValid).toBe(false)
+ expect(result.current.error).not.toBeNull()
+ expect(result.current.error?.message).toContain('at most')
+ })
+
+ it('isValid is true for null value', () => {
+ const { result } = renderHook(() =>
+ useMoney({ currency: 'USD', min: '$10' })
+ )
+
+ expect(result.current.money).toBeNull()
+ expect(result.current.isValid).toBe(true)
+ })
+ })
+
+ describe('format', () => {
+ it('formats money with default options', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$1234.56', currency: 'USD' })
+ )
+
+ expect(result.current.format()).toBe('$1,234.56')
+ })
+
+ it('formats with custom options', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$1500000', currency: 'USD' })
+ )
+
+ expect(result.current.format({ compact: true })).toMatch(/\$1\.5M|\$1,500K/)
+ })
+
+ it('returns empty string for null money', () => {
+ const { result } = renderHook(() => useMoney({ currency: 'USD' }))
+
+ expect(result.current.format()).toBe('')
+ })
+ })
+
+ describe('reset and clear', () => {
+ it('reset returns to initial value', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$100', currency: 'USD' })
+ )
+
+ act(() => {
+ result.current.setMoney('$500')
+ })
+ expect(result.current.money?.toString()).toBe('$500.00')
+
+ act(() => {
+ result.current.reset()
+ })
+ expect(result.current.money?.toString()).toBe('$100.00')
+ })
+
+ it('clear sets money to null', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$100', currency: 'USD' })
+ )
+
+ act(() => {
+ result.current.clear()
+ })
+ expect(result.current.money).toBeNull()
+ })
+ })
+
+ describe('inputProps', () => {
+ it('provides value as string', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$100', currency: 'USD' })
+ )
+
+ expect(result.current.inputProps.value).toBe('100.00')
+ })
+
+ it('provides empty string for null money', () => {
+ const { result } = renderHook(() => useMoney({ currency: 'USD' }))
+
+ expect(result.current.inputProps.value).toBe('')
+ })
+
+ it('onChange updates money', () => {
+ const { result } = renderHook(() => useMoney({ currency: 'USD' }))
+
+ act(() => {
+ result.current.inputProps.onChange({
+ target: { value: '75.50' },
+ } as React.ChangeEvent)
+ })
+
+ expect(result.current.money?.toString()).toBe('$75.50')
+ })
+
+ it('onBlur formats the display value', () => {
+ const { result } = renderHook(() =>
+ useMoney({ initialValue: '$1234.5', currency: 'USD' })
+ )
+
+ act(() => {
+ result.current.inputProps.onBlur()
+ })
+
+ // After blur, should be formatted with proper decimals
+ expect(result.current.inputProps.value).toBe('1,234.50')
+ })
+ })
+})
diff --git a/packages/cent-react/test/setup.ts b/packages/cent-react/test/setup.ts
new file mode 100644
index 0000000..84b6513
--- /dev/null
+++ b/packages/cent-react/test/setup.ts
@@ -0,0 +1,9 @@
+import '@testing-library/jest-dom'
+import { configure } from '@thesis-co/cent'
+
+// Configure Cent with strict settings to catch any precision issues in tests
+configure({
+ numberInputMode: 'never', // Disallow Number inputs entirely
+ defaultRoundingMode: 'none', // No implicit rounding
+ strictPrecision: true, // Throw on any precision loss
+})
diff --git a/packages/cent-react/tsconfig.json b/packages/cent-react/tsconfig.json
new file mode 100644
index 0000000..499309a
--- /dev/null
+++ b/packages/cent-react/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM"],
+ "jsx": "react-jsx"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fa655c8..0ce11b8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -37,6 +37,48 @@ importers:
specifier: ^29.1.2
version: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.9))(typescript@5.5.4)
+ packages/cent-react:
+ devDependencies:
+ '@testing-library/jest-dom':
+ specifier: ^6.4.0
+ version: 6.9.1
+ '@testing-library/react':
+ specifier: ^14.2.0
+ version: 14.3.1(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@testing-library/user-event':
+ specifier: ^14.5.0
+ version: 14.6.1(@testing-library/dom@9.3.4)
+ '@thesis-co/cent':
+ specifier: workspace:*
+ version: link:../cent
+ '@types/jest':
+ specifier: ^29.5.12
+ version: 29.5.14
+ '@types/node':
+ specifier: ^20.11.24
+ version: 20.19.9
+ '@types/react':
+ specifier: ^18.2.0
+ version: 18.3.27
+ '@types/react-dom':
+ specifier: ^18.2.0
+ version: 18.3.7(@types/react@18.3.27)
+ jest:
+ specifier: ^29.7.0
+ version: 29.7.0(@types/node@20.19.9)
+ jest-environment-jsdom:
+ specifier: ^29.7.0
+ version: 29.7.0
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
+ ts-jest:
+ specifier: ^29.1.2
+ version: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.9))(typescript@5.5.4)
+
packages/cent-zod:
devDependencies:
'@thesis-co/cent':
@@ -60,6 +102,9 @@ importers:
packages:
+ '@adobe/css-tools@4.4.4':
+ resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
+
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@@ -214,6 +259,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/runtime@7.28.4':
+ resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@@ -378,11 +427,39 @@ packages:
'@sinonjs/fake-timers@10.3.0':
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
+ '@testing-library/dom@9.3.4':
+ resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==}
+ engines: {node: '>=14'}
+
+ '@testing-library/jest-dom@6.9.1':
+ resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+
+ '@testing-library/react@14.3.1':
+ resolution: {integrity: sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+
+ '@testing-library/user-event@14.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
'@thesis-co/biome-config@https://codeload.github.com/thesis/biome-config/tar.gz/6e8586bfa74c62c9ede2ca12abbcac1dc0ad4606':
resolution: {tarball: https://codeload.github.com/thesis/biome-config/tar.gz/6e8586bfa74c62c9ede2ca12abbcac1dc0ad4606}
version: 0.0.1
engines: {node: '>=14.0.0'}
+ '@tootallnate/once@2.0.0':
+ resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
+ engines: {node: '>= 10'}
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -410,18 +487,55 @@ packages:
'@types/jest@29.5.14':
resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==}
+ '@types/jsdom@20.0.1':
+ resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==}
+
'@types/node@20.19.9':
resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==}
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
+ '@types/react-dom@18.3.7':
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
+ peerDependencies:
+ '@types/react': ^18.0.0
+
+ '@types/react@18.3.27':
+ resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==}
+
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
+ '@types/tough-cookie@4.0.5':
+ resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
'@types/yargs@17.0.33':
resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
+ abab@2.0.6:
+ resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
+ deprecated: Use your platform's native atob() and btoa() methods instead
+
+ acorn-globals@7.0.1:
+ resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
+
+ acorn-walk@8.3.4:
+ resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
+ engines: {node: '>=0.4.0'}
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
@@ -445,9 +559,27 @@ packages:
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+ aria-query@5.1.3:
+ resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
babel-jest@29.7.0:
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -501,6 +633,18 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bind@1.0.8:
+ resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -549,6 +693,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -564,6 +712,26 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ cssom@0.3.8:
+ resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==}
+
+ cssom@0.5.0:
+ resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==}
+
+ cssstyle@2.3.0:
+ resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==}
+ engines: {node: '>=8'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ data-urls@3.0.2:
+ resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==}
+ engines: {node: '>=12'}
+
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -573,6 +741,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
dedent@1.6.0:
resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==}
peerDependencies:
@@ -581,10 +752,26 @@ packages:
babel-plugin-macros:
optional: true
+ deep-equal@2.2.3:
+ resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
+ engines: {node: '>= 0.4'}
+
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
detect-newline@3.1.0:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'}
@@ -593,6 +780,21 @@ packages:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
+ domexception@4.0.0:
+ resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
+ engines: {node: '>=12'}
+ deprecated: Use your platform's native DOMException instead
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
engines: {node: '>=0.10.0'}
@@ -608,9 +810,32 @@ packages:
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-get-iterator@1.1.3:
+ resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -619,11 +844,24 @@ packages:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
+ escodegen@2.1.0:
+ resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
+ engines: {node: '>=6.0'}
+ hasBin: true
+
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@@ -653,6 +891,14 @@ packages:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -664,6 +910,9 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -672,10 +921,18 @@ packages:
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'}
+
get-package-type@0.1.0:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
@@ -684,24 +941,59 @@ packages:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
+ html-encoding-sniffer@3.0.0:
+ resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
+ engines: {node: '>=12'}
+
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ http-proxy-agent@5.0.0:
+ resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
+ engines: {node: '>= 6'}
+
+ https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
import-local@3.2.0:
resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
engines: {node: '>=8'}
@@ -711,6 +1003,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
@@ -718,13 +1014,41 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ is-arguments@1.2.0:
+ resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
+ engines: {node: '>= 0.4'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
@@ -733,14 +1057,56 @@ packages:
resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
engines: {node: '>=6'}
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -815,6 +1181,15 @@ packages:
resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ jest-environment-jsdom@29.7.0:
+ resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ peerDependencies:
+ canvas: ^2.5.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jest-environment-node@29.7.0:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -909,6 +1284,15 @@ packages:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
+ jsdom@20.0.3:
+ resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ canvas: ^2.5.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -940,9 +1324,17 @@ packages:
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
@@ -953,6 +1345,10 @@ packages:
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -960,10 +1356,22 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -991,6 +1399,25 @@ packages:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
+ nwsapi@2.2.23:
+ resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-is@1.1.6:
+ resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
+ engines: {node: '>= 0.4'}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -1018,6 +1445,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1048,6 +1478,14 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
pretty-format@29.7.0:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -1056,16 +1494,49 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
+ psl@1.15.0:
+ resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
pure-rand@6.1.0:
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
+ querystringify@2.2.0:
+ resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
+
+ react-dom@18.3.1:
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+
+ 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@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
+ requires-port@1.0.0:
+ resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+
resolve-cwd@3.0.0:
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
engines: {node: '>=8'}
@@ -1083,6 +1554,20 @@ packages:
engines: {node: '>= 0.4'}
hasBin: true
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
+ scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -1092,6 +1577,14 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -1100,6 +1593,22 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@@ -1124,6 +1633,10 @@ packages:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
string-length@4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
engines: {node: '>=10'}
@@ -1144,6 +1657,10 @@ packages:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -1160,6 +1677,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
@@ -1171,6 +1691,14 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ tough-cookie@4.1.4:
+ resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
+ engines: {node: '>=6'}
+
+ tr46@3.0.0:
+ resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
+ engines: {node: '>=12'}
+
ts-jest@29.4.0:
resolution: {integrity: sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==}
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
@@ -1252,19 +1780,59 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ universalify@0.2.0:
+ resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
+ engines: {node: '>= 4.0.0'}
+
update-browserslist-db@1.1.3:
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
+ url-parse@1.5.10:
+ resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+
v8-to-istanbul@9.3.0:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
+ w3c-xmlserializer@4.0.0:
+ resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
+ engines: {node: '>=14'}
+
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
+ webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+
+ whatwg-encoding@2.0.0:
+ resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
+ engines: {node: '>=12'}
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
+
+ whatwg-mimetype@3.0.0:
+ resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+ engines: {node: '>=12'}
+
+ whatwg-url@11.0.0:
+ resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
+ engines: {node: '>=12'}
+
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.19:
+ resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
+ engines: {node: '>= 0.4'}
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -1281,6 +1849,25 @@ packages:
resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ ws@8.19.0:
+ resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ xml-name-validator@4.0.0:
+ resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
+ engines: {node: '>=12'}
+
+ 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'}
@@ -1305,6 +1892,8 @@ packages:
snapshots:
+ '@adobe/css-tools@4.4.4': {}
+
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.12
@@ -1474,6 +2063,8 @@ snapshots:
'@babel/core': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
+ '@babel/runtime@7.28.4': {}
+
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@@ -1730,10 +2321,48 @@ snapshots:
dependencies:
'@sinonjs/commons': 3.0.1
+ '@testing-library/dom@9.3.4':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/runtime': 7.28.4
+ '@types/aria-query': 5.0.4
+ aria-query: 5.1.3
+ chalk: 4.1.2
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@6.9.1':
+ dependencies:
+ '@adobe/css-tools': 4.4.4
+ aria-query: 5.3.2
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ picocolors: 1.1.1
+ redent: 3.0.0
+
+ '@testing-library/react@14.3.1(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.28.4
+ '@testing-library/dom': 9.3.4
+ '@types/react-dom': 18.3.7(@types/react@18.3.27)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ transitivePeerDependencies:
+ - '@types/react'
+
+ '@testing-library/user-event@14.6.1(@testing-library/dom@9.3.4)':
+ dependencies:
+ '@testing-library/dom': 9.3.4
+
'@thesis-co/biome-config@https://codeload.github.com/thesis/biome-config/tar.gz/6e8586bfa74c62c9ede2ca12abbcac1dc0ad4606':
dependencies:
'@biomejs/biome': 2.1.1
+ '@tootallnate/once@2.0.0': {}
+
+ '@types/aria-query@5.0.4': {}
+
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.28.0
@@ -1774,18 +2403,56 @@ snapshots:
expect: 29.7.0
pretty-format: 29.7.0
+ '@types/jsdom@20.0.1':
+ dependencies:
+ '@types/node': 20.19.9
+ '@types/tough-cookie': 4.0.5
+ parse5: 7.3.0
+
'@types/node@20.19.9':
dependencies:
undici-types: 6.21.0
+ '@types/prop-types@15.7.15': {}
+
+ '@types/react-dom@18.3.7(@types/react@18.3.27)':
+ dependencies:
+ '@types/react': 18.3.27
+
+ '@types/react@18.3.27':
+ dependencies:
+ '@types/prop-types': 15.7.15
+ csstype: 3.2.3
+
'@types/stack-utils@2.0.3': {}
+ '@types/tough-cookie@4.0.5': {}
+
'@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.33':
dependencies:
'@types/yargs-parser': 21.0.3
+ abab@2.0.6: {}
+
+ acorn-globals@7.0.1:
+ dependencies:
+ acorn: 8.15.0
+ acorn-walk: 8.3.4
+
+ acorn-walk@8.3.4:
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ agent-base@6.0.2:
+ dependencies:
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
@@ -1807,8 +2474,25 @@ snapshots:
dependencies:
sprintf-js: 1.0.3
+ aria-query@5.1.3:
+ dependencies:
+ deep-equal: 2.2.3
+
+ aria-query@5.3.2: {}
+
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
async@3.2.6: {}
+ asynckit@0.4.0: {}
+
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
babel-jest@29.7.0(@babel/core@7.28.0):
dependencies:
'@babel/core': 7.28.0
@@ -1896,6 +2580,23 @@ snapshots:
buffer-from@1.1.2: {}
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bind@1.0.8:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
callsites@3.1.0: {}
camelcase@5.3.1: {}
@@ -1931,6 +2632,10 @@ snapshots:
color-name@1.1.4: {}
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
@@ -1956,18 +2661,87 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ css.escape@1.5.1: {}
+
+ cssom@0.3.8: {}
+
+ cssom@0.5.0: {}
+
+ cssstyle@2.3.0:
+ dependencies:
+ cssom: 0.3.8
+
+ csstype@3.2.3: {}
+
+ data-urls@3.0.2:
+ dependencies:
+ abab: 2.0.6
+ whatwg-mimetype: 3.0.0
+ whatwg-url: 11.0.0
+
debug@4.4.1:
dependencies:
ms: 2.1.3
+ decimal.js@10.6.0: {}
+
dedent@1.6.0: {}
+ deep-equal@2.2.3:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.8
+ es-get-iterator: 1.1.3
+ get-intrinsic: 1.3.0
+ is-arguments: 1.2.0
+ is-array-buffer: 3.0.5
+ is-date-object: 1.1.0
+ is-regex: 1.2.1
+ is-shared-array-buffer: 1.0.4
+ isarray: 2.0.5
+ object-is: 1.1.6
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ regexp.prototype.flags: 1.5.4
+ side-channel: 1.1.0
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.19
+
deepmerge@4.3.1: {}
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
+ delayed-stream@1.0.0: {}
+
detect-newline@3.1.0: {}
diff-sequences@29.6.3: {}
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
+ domexception@4.0.0:
+ dependencies:
+ webidl-conversions: 7.0.0
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
ejs@3.1.10:
dependencies:
jake: 10.9.2
@@ -1978,16 +2752,57 @@ snapshots:
emoji-regex@8.0.0: {}
+ entities@6.0.1: {}
+
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-get-iterator@1.1.3:
+ dependencies:
+ call-bind: 1.0.8
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ is-arguments: 1.2.0
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-string: 1.1.1
+ isarray: 2.0.5
+ stop-iteration-iterator: 1.1.0
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
escalade@3.2.0: {}
escape-string-regexp@2.0.0: {}
+ escodegen@2.1.0:
+ dependencies:
+ esprima: 4.0.1
+ estraverse: 5.3.0
+ esutils: 2.0.3
+ optionalDependencies:
+ source-map: 0.6.1
+
esprima@4.0.1: {}
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
execa@5.1.1:
dependencies:
cross-spawn: 7.0.6
@@ -2029,6 +2844,18 @@ snapshots:
locate-path: 5.0.0
path-exists: 4.0.0
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
fs.realpath@1.0.0: {}
fsevents@2.3.3:
@@ -2036,12 +2863,32 @@ snapshots:
function-bind@1.1.2: {}
+ functions-have-names@1.2.3: {}
+
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
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
get-package-type@0.1.0: {}
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
get-stream@6.0.1: {}
glob@7.2.3:
@@ -2053,18 +2900,55 @@ snapshots:
once: 1.4.0
path-is-absolute: 1.0.1
+ gopd@1.2.0: {}
+
graceful-fs@4.2.11: {}
+ has-bigints@1.1.0: {}
+
has-flag@4.0.0: {}
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
+ html-encoding-sniffer@3.0.0:
+ dependencies:
+ whatwg-encoding: 2.0.0
+
html-escaper@2.0.2: {}
+ http-proxy-agent@5.0.0:
+ dependencies:
+ '@tootallnate/once': 2.0.0
+ agent-base: 6.0.2
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@5.0.1:
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
human-signals@2.1.0: {}
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
import-local@3.2.0:
dependencies:
pkg-dir: 4.2.0
@@ -2072,6 +2956,8 @@ snapshots:
imurmurhash@0.1.4: {}
+ indent-string@4.0.0: {}
+
inflight@1.0.6:
dependencies:
once: 1.4.0
@@ -2079,20 +2965,95 @@ snapshots:
inherits@2.0.4: {}
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.2
+ side-channel: 1.1.0
+
+ is-arguments@1.2.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
is-arrayish@0.2.1: {}
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-callable@1.2.7: {}
+
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-fullwidth-code-point@3.0.0: {}
is-generator-fn@2.1.0: {}
+ is-map@2.0.3: {}
+
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-number@7.0.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
is-stream@2.0.1: {}
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
+ is-weakmap@2.0.2: {}
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ isarray@2.0.5: {}
+
isexe@2.0.0: {}
istanbul-lib-coverage@3.2.2: {}
@@ -2243,6 +3204,21 @@ snapshots:
jest-util: 29.7.0
pretty-format: 29.7.0
+ jest-environment-jsdom@29.7.0:
+ dependencies:
+ '@jest/environment': 29.7.0
+ '@jest/fake-timers': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/jsdom': 20.0.1
+ '@types/node': 20.19.9
+ jest-mock: 29.7.0
+ jest-util: 29.7.0
+ jsdom: 20.0.3
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
jest-environment-node@29.7.0:
dependencies:
'@jest/environment': 29.7.0
@@ -2458,6 +3434,39 @@ snapshots:
argparse: 1.0.10
esprima: 4.0.1
+ jsdom@20.0.3:
+ dependencies:
+ abab: 2.0.6
+ acorn: 8.15.0
+ acorn-globals: 7.0.1
+ cssom: 0.5.0
+ cssstyle: 2.3.0
+ data-urls: 3.0.2
+ decimal.js: 10.6.0
+ domexception: 4.0.0
+ escodegen: 2.1.0
+ form-data: 4.0.5
+ html-encoding-sniffer: 3.0.0
+ http-proxy-agent: 5.0.0
+ https-proxy-agent: 5.0.1
+ is-potential-custom-element-name: 1.0.1
+ nwsapi: 2.2.23
+ parse5: 7.3.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 4.1.4
+ w3c-xmlserializer: 4.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 2.0.0
+ whatwg-mimetype: 3.0.0
+ whatwg-url: 11.0.0
+ ws: 8.19.0
+ xml-name-validator: 4.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
jsesc@3.1.0: {}
json-parse-even-better-errors@2.3.1: {}
@@ -2476,10 +3485,16 @@ snapshots:
lodash.memoize@4.1.2: {}
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
+ lz-string@1.5.0: {}
+
make-dir@4.0.0:
dependencies:
semver: 7.7.2
@@ -2490,6 +3505,8 @@ snapshots:
dependencies:
tmpl: 1.0.5
+ math-intrinsics@1.1.0: {}
+
merge-stream@2.0.0: {}
micromatch@4.0.8:
@@ -2497,8 +3514,16 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
mimic-fn@2.1.0: {}
+ min-indent@1.0.1: {}
+
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -2521,6 +3546,26 @@ snapshots:
dependencies:
path-key: 3.1.1
+ nwsapi@2.2.23: {}
+
+ object-inspect@1.13.4: {}
+
+ object-is@1.1.6:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -2550,6 +3595,10 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
@@ -2568,6 +3617,14 @@ snapshots:
dependencies:
find-up: 4.1.0
+ possible-typed-array-names@1.1.0: {}
+
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
pretty-format@29.7.0:
dependencies:
'@jest/schemas': 29.6.3
@@ -2579,12 +3636,48 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
+ psl@1.15.0:
+ dependencies:
+ punycode: 2.3.1
+
+ punycode@2.3.1: {}
+
pure-rand@6.1.0: {}
+ querystringify@2.2.0: {}
+
+ react-dom@18.3.1(react@18.3.1):
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+
+ react-is@17.0.2: {}
+
react-is@18.3.1: {}
+ react@18.3.1:
+ dependencies:
+ loose-envify: 1.4.0
+
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
require-directory@2.1.1: {}
+ requires-port@1.0.0: {}
+
resolve-cwd@3.0.0:
dependencies:
resolve-from: 5.0.0
@@ -2599,16 +3692,76 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
+ safer-buffer@2.1.2: {}
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
+ scheduler@0.23.2:
+ dependencies:
+ loose-envify: 1.4.0
+
semver@6.3.1: {}
semver@7.7.2: {}
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
signal-exit@3.0.7: {}
sisteransi@1.0.5: {}
@@ -2628,6 +3781,11 @@ snapshots:
dependencies:
escape-string-regexp: 2.0.0
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
string-length@4.0.2:
dependencies:
char-regex: 1.0.2
@@ -2647,6 +3805,10 @@ snapshots:
strip-final-newline@2.0.0: {}
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
+
strip-json-comments@3.1.1: {}
supports-color@7.2.0:
@@ -2659,6 +3821,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ symbol-tree@3.2.4: {}
+
test-exclude@6.0.0:
dependencies:
'@istanbuljs/schema': 0.1.3
@@ -2671,6 +3835,17 @@ snapshots:
dependencies:
is-number: 7.0.0
+ tough-cookie@4.1.4:
+ dependencies:
+ psl: 1.15.0
+ punycode: 2.3.1
+ universalify: 0.2.0
+ url-parse: 1.5.10
+
+ tr46@3.0.0:
+ dependencies:
+ punycode: 2.3.1
+
ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.9))(typescript@5.5.4):
dependencies:
bs-logger: 0.2.6
@@ -2728,22 +3903,71 @@ snapshots:
undici-types@6.21.0: {}
+ universalify@0.2.0: {}
+
update-browserslist-db@1.1.3(browserslist@4.25.1):
dependencies:
browserslist: 4.25.1
escalade: 3.2.0
picocolors: 1.1.1
+ url-parse@1.5.10:
+ dependencies:
+ querystringify: 2.2.0
+ requires-port: 1.0.0
+
v8-to-istanbul@9.3.0:
dependencies:
'@jridgewell/trace-mapping': 0.3.29
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
+ w3c-xmlserializer@4.0.0:
+ dependencies:
+ xml-name-validator: 4.0.0
+
walker@1.0.8:
dependencies:
makeerror: 1.0.12
+ webidl-conversions@7.0.0: {}
+
+ whatwg-encoding@2.0.0:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@3.0.0: {}
+
+ whatwg-url@11.0.0:
+ dependencies:
+ tr46: 3.0.0
+ webidl-conversions: 7.0.0
+
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.19:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
which@2.0.2:
dependencies:
isexe: 2.0.0
@@ -2761,6 +3985,12 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 3.0.7
+ ws@8.19.0: {}
+
+ xml-name-validator@4.0.0: {}
+
+ xmlchars@2.2.0: {}
+
y18n@5.0.8: {}
yallist@3.1.1: {}