diff --git a/.gitignore b/.gitignore index 25cbf7b..3363dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .DS_Store -CLAUDE.md +.vscode/ dist/ -node_modules/ +node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index b642196..95af95a 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,50 @@ Lightweight library to block user interactions in browsers. +## ⚠️ Breaking Changes in v0.3.0 + +Version 0.3.0 introduces significant API changes from v0.2.x: + +- **Factory function instead of singleton**: `blokr()` returns an instance instead of being a singleton object +- **Options-based API**: `lock({ timeout, scope })` instead of separate `setTimeout()` method +- **No reference counting**: Multiple `lock()` calls return `false` instead of incrementing a counter +- **No `setTimeout()` method**: Use `lock({ timeout })` option instead +- **No `unlock(abort)` parameter**: `unlock()` always releases the lock immediately + +**Migration guide:** See [Migration from v0.2.x](#migration-from-v02x) below. + +**Note:** This library is under active development. Future versions may introduce additional breaking changes. Please refer to the changelog before upgrading. + ## Features +- **Factory-based API**: Support for both global and element-specific locks +- **Scope filtering**: Control which events to block (`inside`, `outside`, `self`) - **No overlay elements**: Blocks interactions without adding elements to the DOM -- **Event blocking**: Prevents mouse, keyboard, and touch interactions -- **Auto-timeout**: Configurable timeout to prevent permanent blocking (default: 10 seconds) -- **Lightweight**: Minimal footprint with no dependencies +- **All interaction types**: Blocks mouse, keyboard, touch, and wheel events +- **Per-lock timeout**: Optional automatic unlock after specified time +- **No dependencies**: Zero external dependencies - **TypeScript**: Full type support included -- **Singleton**: Simple, predictable API -## Use Cases +## Why Blokr? + +### Problems with CSS-based Solutions -- **POST processing**: Block interactions during data submission -- **Form submission**: Prevent double-submission -- **Animations**: Disable interaction during transitions -- **Game pausing**: Temporarily disable game controls +While CSS `pointer-events: none` can disable interactions, it has several limitations: + +1. **Cannot block keyboard events**: Tab navigation and keyboard shortcuts still work +2. **No timeout protection**: No automatic unlock if code fails to re-enable interactions +3. **Requires DOM manipulation**: Must add/remove CSS classes or inline styles +4. **Cannot scope events**: Cannot selectively block events inside/outside an element +5. **z-index issues**: Overlay approaches require careful z-index management + +### How Blokr Solves These Problems + +- ✅ **Blocks all interaction types**: Mouse, keyboard, touch, and wheel events +- ✅ **Optional timeout protection**: Automatically unlock after specified time +- ✅ **No DOM changes**: Works via event listeners only +- ✅ **Flexible scoping**: Block events inside, outside, or only on specific elements +- ✅ **No z-index conflicts**: No overlay elements needed +- ✅ **TypeScript support**: Full type definitions included ## Installation @@ -29,109 +58,187 @@ npm install blokr ## Usage -### ES Modules +### Basic Usage (ES Modules) ```typescript import blokr from 'blokr'; -// Block user interactions -blokr.lock(); +// Global lock - blocks all user interactions +const instance = blokr(); +instance.lock(); -// Check if blocked -if (blokr.isLocked()) { +// Check if locked +if (instance.isLocked()) { console.log('User interactions are blocked'); } -// Unblock after some work -setTimeout(() => { - blokr.unlock(); -}, 2000); +// Unlock +instance.unlock(); +``` + +### Element-specific Locking + +```typescript +import blokr from 'blokr'; + +const container = document.querySelector('.container'); +const instance = blokr(container); + +// Block events inside the container (default scope) +instance.lock(); + +// Or explicitly specify scope +instance.lock({ scope: 'inside' }); // Block events inside container +instance.lock({ scope: 'outside' }); // Block events outside container +instance.lock({ scope: 'self' }); // Block events on container itself only +``` + +### Auto-timeout + +```typescript +import blokr from 'blokr'; + +const instance = blokr(); + +// Auto-unlock after 5 seconds +instance.lock({ timeout: 5000 }); + +// Disable timeout (lock indefinitely) +instance.lock({ timeout: 0 }); ``` -### CDN +### CDN Usage (UMD) ```html ``` -## API +### CDN Usage (ES Modules) + +```html + +``` + +## API Reference + +### `blokr(target?: Element): BlokrInstance` + +Returns a Blokr instance. If no target is specified, creates a global instance that blocks all events. If the same target is provided multiple times, returns the cached instance. + +**Parameters:** +- `target` (optional): DOM element to scope the lock to -### `blokr.lock()` +**Returns:** `BlokrInstance` -Blocks user interactions. Multiple calls are counted internally, requiring the same number of `unlock()` calls to fully unblock. +**Examples:** ```typescript -blokr.lock(); // Call count: 1 -blokr.lock(); // Call count: 2 -blokr.unlock(); // Call count: 1 (still blocked) -blokr.unlock(); // Call count: 0 (unblocked) +// Global instance (blocks all events) +const global = blokr(); + +// Element-specific instance +const container = document.querySelector('.modal'); +const modal = blokr(container); + +// Same element returns same instance +const modal2 = blokr(container); +console.log(modal === modal2); // true ``` -### `blokr.unlock(abort?: boolean)` +### `instance.lock(options?: Options): boolean` -Unblocks user interactions. By default, decrements the internal counter. When `abort` is `true`, immediately resets the counter to zero and releases all locks. +Locks user interactions. Returns `true` if lock was applied, `false` if already locked. **Parameters:** +- `options.timeout` (optional): Auto-unlock timeout in milliseconds. Default: `0` (no timeout) +- `options.scope` (optional): Event blocking scope. Default: `'inside'` + - `'inside'`: Block events inside target element (default) + - `'outside'`: Block events outside target element + - `'self'`: Block events on target element itself only + +**Returns:** `true` if lock was applied, `false` if already locked -- `abort` (optional): When `true`, immediately unlocks all locks. Default: `false` +**Examples:** ```typescript -// Normal unlock behavior (decrements counter) -blokr.lock(); // Lock count: 1 -blokr.lock(); // Lock count: 2 +const instance = blokr(); -blokr.unlock(); // Lock count: 1 (still locked) -blokr.unlock(); // Lock count: 0 (unlocked) +// Basic lock +instance.lock(); // Returns true -// Emergency unlock with abort -blokr.lock(); // Lock count: 1 -blokr.lock(); // Lock count: 2 -blokr.unlock(true); // Lock count: 0 (immediately unlocked) +// Already locked +instance.lock(); // Returns false + +// Lock with timeout +instance.lock({ timeout: 5000 }); + +// Lock with scope (requires target element) +const container = document.querySelector('.panel'); +const panelInstance = blokr(container); +panelInstance.lock({ scope: 'inside' }); ``` -### `blokr.isLocked(): boolean` +### `instance.unlock(): void` + +Unlocks user interactions and clears any pending timeout. Safe to call even when not locked. -Returns `true` if user interactions are currently blocked. +**Examples:** ```typescript -blokr.isLocked(); // false -blokr.lock(); -blokr.isLocked(); // true +const instance = blokr(); +instance.lock(); +instance.unlock(); + +// Safe to call multiple times +instance.unlock(); +instance.unlock(); ``` -### `blokr.setTimeout(timeout: number): boolean` +### `instance.isLocked(): boolean` + +Returns `true` if user interactions are currently locked. + +**Returns:** `boolean` -Sets the auto-unlock timeout in milliseconds (default: 10000). Cannot be changed while locked. Returns `true` if successfully set, `false` if currently locked. +**Examples:** ```typescript -// Set 5-second timeout (only works when unlocked) -blokr.setTimeout(5000); +const instance = blokr(); +console.log(instance.isLocked()); // false -// Disable auto-timeout -blokr.setTimeout(0); +instance.lock(); +console.log(instance.isLocked()); // true -// Cannot change timeout while locked -blokr.lock(); -blokr.setTimeout(1000); // returns false +instance.unlock(); +console.log(instance.isLocked()); // false ``` -## Example: POST Processing +## Examples + +### POST Processing with Timeout ```typescript import blokr from 'blokr'; -async function saveUserProfile(formData) { - // Block all interactions during save - blokr.lock(); +async function saveUserProfile(formData: FormData) { + const instance = blokr(); + + // Block all interactions with 10-second timeout + instance.lock({ timeout: 10000 }); try { const response = await fetch('/api/profile', { @@ -143,34 +250,133 @@ async function saveUserProfile(formData) { showSuccessMessage(); } } finally { - blokr.unlock(); + instance.unlock(); } } ``` -## Example: Animation Blocking +### Modal Dialog ```typescript import blokr from 'blokr'; -function slidePanel() { - // Block interactions during animation - blokr.lock(); +function openModal() { + const modal = document.querySelector('.modal'); + const instance = blokr(modal); - // Start CSS animation - panel.classList.add('sliding'); + modal.classList.add('visible'); - // Re-enable interactions when animation completes - setTimeout(() => { - blokr.unlock(); - }, 500); + // Block all interactions outside the modal + instance.lock({ scope: 'outside' }); +} + +function closeModal() { + const modal = document.querySelector('.modal'); + const instance = blokr(modal); + + modal.classList.remove('visible'); + instance.unlock(); +} +``` + +### Form Panel Lock + +```typescript +import blokr from 'blokr'; + +function disableFormPanel() { + const panel = document.querySelector('.settings-panel'); + const instance = blokr(panel); + + // Disable interactions only inside the panel + instance.lock({ scope: 'inside' }); +} + +function enableFormPanel() { + const panel = document.querySelector('.settings-panel'); + const instance = blokr(panel); + + instance.unlock(); +} +``` + +### Loading Overlay Alternative + +```typescript +import blokr from 'blokr'; + +async function loadData() { + const instance = blokr(); + + // No overlay element needed! + instance.lock({ timeout: 30000 }); + + try { + const data = await fetch('/api/data').then(r => r.json()); + renderData(data); + } finally { + instance.unlock(); + } } ``` +## Migration from v0.2.x + +### API Changes + +| v0.2.x | v0.3.0 | +|--------|--------| +| `blokr.lock()` | `blokr().lock()` | +| `blokr.unlock()` | `blokr().unlock()` | +| `blokr.unlock(true)` | `blokr().unlock()` (always immediate) | +| `blokr.setTimeout(ms)` | `blokr().lock({ timeout: ms })` | +| `blokr.isLocked()` | `blokr().isLocked()` | +| `window.Blokr` (UMD) | `window.blokr` (UMD) | + +### Reference Counting Removed + +In v0.2.x, multiple `lock()` calls incremented a counter: + +```typescript +// v0.2.x +blokr.lock(); // Count: 1 +blokr.lock(); // Count: 2 +blokr.unlock(); // Count: 1 (still locked) +blokr.unlock(); // Count: 0 (unlocked) +``` + +In v0.3.0, `lock()` returns `false` if already locked: + +```typescript +// v0.3.0 +const instance = blokr(); +instance.lock(); // Returns true +instance.lock(); // Returns false (already locked) +instance.unlock(); // Unlocked +``` + +### Element-specific Locking (New Feature) + +```typescript +// v0.3.0 only - new feature not available in v0.2.x +const container = document.querySelector('.container'); +const instance = blokr(container); + +// Block events inside container +instance.lock({ scope: 'inside' }); + +// Block events outside container +instance.lock({ scope: 'outside' }); + +// Block events on container itself only +instance.lock({ scope: 'self' }); +``` + ## Limitations -- Only blocks genuine user interactions. Programmatically triggered events (e.g., `element.click()`) are not blocked. -- May not work when used with event delegation libraries. Loading Blokr before other libraries may resolve this issue. +- **Only blocks genuine user interactions**: Programmatically triggered events (e.g., `element.click()`) are not blocked. +- **Event listener priority**: Event listeners are registered at the capture phase. May not work correctly when used with event delegation libraries. Loading Blokr before other libraries may resolve this issue. +- **Target-specific locks accept Elements only**: The `blokr(target)` factory function only accepts DOM `Element` nodes. To block interactions across the entire page, use the global lock: `blokr()` (without a target parameter). ## License diff --git a/eslint.config.js b/eslint.config.js index f7cbab3..098189c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,110 +1,61 @@ +// @ts-check +import { defineConfig } from 'eslint/config'; +import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; +import stylistic from '@stylistic/eslint-plugin'; -export default tseslint.config( +export default defineConfig( { - ignores: ['dist'] + ignores: [ + 'dist', + 'eslint.config.js', + 'vite.config.ts', + 'vitest.config.ts' + ] }, - ...tseslint.configs.strict, - ...tseslint.configs.stylistic, + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, { - files: ['src/**/*.ts'], + plugins: { + '@stylistic': stylistic + }, languageOptions: { ecmaVersion: 2015, sourceType: 'module', - globals: { - self: 'readonly' - }, parserOptions: { - project: './tsconfig.json' + projectService: true } }, rules: { + '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], '@typescript-eslint/no-extraneous-class': 'off', - '@typescript-eslint/no-unused-vars': ['error', { 'caughtErrors': 'none' }], - '@typescript-eslint/unified-signatures': ['error', { 'ignoreDifferentlyNamedParameters': true }], - 'array-bracket-spacing': ['warn', 'never'], - 'array-callback-return': 'error', - 'constructor-super': 'error', - 'for-direction': 'error', - 'getter-return': 'error', - 'no-async-promise-executor': 'error', - 'no-class-assign': 'error', - 'no-compare-neg-zero': 'error', - 'no-cond-assign': 'error', - 'no-const-assign': 'error', - 'no-constant-binary-expression': 'error', - 'no-constant-condition': 'error', - 'no-constructor-return': 'error', - 'no-control-regex': 'error', - 'no-debugger': 'error', - 'no-dupe-args': 'error', - 'no-dupe-class-members': 'error', - 'no-dupe-else-if': 'error', - 'no-dupe-keys': 'error', - 'no-duplicate-case': 'error', - 'no-empty-character-class': 'error', - 'no-empty-pattern': 'error', - 'no-ex-assign': 'error', - 'no-fallthrough': 'error', - 'no-func-assign': 'error', - 'no-import-assign': 'error', - 'no-inner-declarations': 'error', - 'no-invalid-regexp': 'error', - 'no-irregular-whitespace': 'error', - 'no-loss-of-precision': 'error', - 'no-misleading-character-class': 'error', - 'no-new-native-nonconstructor': 'error', - 'no-new-symbol': 'error', - 'no-obj-calls': 'error', - 'no-self-assign': 'error', - 'no-self-compare': 'error', - 'no-setter-return': 'error', - 'no-sparse-arrays': 'error', - 'no-template-curly-in-string': 'error', - 'no-this-before-super': 'error', - 'no-undef': 'error', - 'no-unexpected-multiline': 'error', - 'no-unmodified-loop-condition': 'error', - 'no-unreachable': 'error', - 'no-unreachable-loop': 'error', - 'no-unsafe-finally': 'error', - 'no-unsafe-negation': 'error', - 'no-unsafe-optional-chaining': 'error', - 'no-unused-private-class-members': 'error', - 'no-use-before-define': 'error', - 'no-useless-backreference': 'error', - 'require-atomic-updates': 'error', - 'use-isnan': 'error', - 'valid-typeof': 'error', + '@typescript-eslint/no-unused-vars': ['error', { caughtErrors: 'none' }], + '@typescript-eslint/restrict-template-expressions': ['error', { allowNever: true }], + '@typescript-eslint/unified-signatures': ['error', { ignoreDifferentlyNamedParameters: true }], + 'accessor-pairs': 'error', + 'array-callback-return': 'error', 'block-scoped-var': 'error', 'consistent-return': 'error', 'curly': 'error', 'default-case-last': 'error', - 'dot-notation': 'error', 'eqeqeq': ['error', 'smart'], 'func-name-matching': 'error', - 'func-style': ['error', 'expression', { 'overrides': { 'namedExports': 'ignore' } }], + 'func-style': ['error', 'expression', { overrides: { namedExports: 'ignore' } }], 'grouped-accessor-pairs': 'error', - 'id-denylist': 'error', - 'id-match': 'error', 'max-depth': 'error', 'max-nested-callbacks': 'error', 'new-cap': 'error', - 'no-array-constructor': 'error', 'no-caller': 'error', - 'no-delete-var': 'error', + 'no-constructor-return': 'error', 'no-div-regex': 'error', 'no-else-return': 'error', 'no-empty-static-block': 'error', 'no-eval': 'error', 'no-extend-native': 'error', 'no-extra-bind': 'error', - 'no-extra-boolean-cast': 'error', 'no-extra-label': 'error', - 'no-extra-semi': 'error', - 'no-floating-decimal': 'error', - 'no-global-assign': 'error', 'no-implicit-globals': 'error', 'no-implied-eval': 'error', 'no-iterator': 'error', @@ -112,103 +63,93 @@ export default tseslint.config( 'no-labels': 'error', 'no-lone-blocks': 'error', 'no-lonely-if': 'error', - 'no-loop-func': 'error', - 'no-multi-assign': ['error', { 'ignoreNonDeclaration': true }], + 'no-multi-assign': ['error', { ignoreNonDeclaration: true }], 'no-multi-str': 'error', 'no-negated-condition': 'error', 'no-new': 'error', 'no-new-func': 'error', - 'no-new-object': 'error', 'no-new-wrappers': 'error', - 'no-nonoctal-decimal-escape': 'error', - 'no-octal': 'error', + 'no-object-constructor': 'error', 'no-octal-escape': 'error', 'no-proto': 'error', - 'no-regex-spaces': 'error', - 'no-restricted-exports': 'error', - 'no-restricted-globals': 'error', - 'no-restricted-imports': 'error', - 'no-restricted-properties': 'error', - 'no-restricted-syntax': 'error', 'no-return-assign': 'error', - 'no-return-await': 'error', 'no-script-url': 'error', + 'no-self-compare': 'error', 'no-sequences': 'error', - 'no-shadow': 'error', 'no-shadow-restricted-names': 'error', - 'no-throw-literal': 'error', + 'no-template-curly-in-string': 'error', 'no-undef-init': 'error', + 'no-unmodified-loop-condition': 'error', 'no-unneeded-ternary': 'error', - 'no-unused-expressions': 'error', - 'no-unused-labels': 'error', + 'no-unreachable-loop': 'error', 'no-useless-call': 'error', - 'no-useless-catch': 'error', 'no-useless-computed-key': 'error', 'no-useless-concat': 'error', - 'no-useless-escape': 'error', 'no-useless-rename': 'error', 'no-useless-return': 'error', 'no-void': 'error', - 'no-warning-comments': 'warn', - 'no-with': 'error', + 'no-warning-comments': 'error', 'operator-assignment': 'error', - 'prefer-const': 'error', 'prefer-exponentiation-operator': 'error', 'prefer-numeric-literals': 'error', 'prefer-object-has-own': 'error', 'prefer-object-spread': 'error', - 'prefer-promise-reject-errors': ['error', { 'allowEmptyReject': true }], + 'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }], 'prefer-regex-literals': 'error', 'prefer-rest-params': 'error', 'prefer-spread': 'error', 'radix': 'error', - 'require-await': 'error', - 'require-yield': 'error', + 'require-atomic-updates': 'error', 'symbol-description': 'error', + 'unicode-bom': 'error', 'yoda': 'error', - 'arrow-spacing': 'warn', - 'block-spacing': 'warn', - 'comma-dangle': 'warn', - 'comma-spacing': 'warn', - 'comma-style': 'warn', - 'computed-property-spacing': 'warn', - 'dot-location': ['warn', 'property'], - 'eol-last': 'warn', - 'func-call-spacing': 'warn', - 'generator-star-spacing': 'warn', - 'implicit-arrow-linebreak': 'warn', - 'indent': ['warn', 2, { 'ignoreComments': true }], - 'jsx-quotes': 'warn', - 'key-spacing': 'warn', - 'keyword-spacing': 'warn', - 'linebreak-style': 'warn', - 'lines-between-class-members': 'warn', - 'new-parens': 'warn', - 'no-extra-parens': ['warn', 'functions'], - 'no-mixed-spaces-and-tabs': 'warn', - 'no-multi-spaces': ['warn', { 'ignoreEOLComments': true }], - 'no-tabs': 'warn', - 'no-trailing-spaces': 'warn', - 'no-whitespace-before-property': 'warn', - 'nonblock-statement-body-position': 'warn', - 'object-curly-newline': 'warn', - 'object-curly-spacing': ['warn', 'always'], - 'padding-line-between-statements': 'warn', - 'quotes': ['warn', 'single'], - 'rest-spread-spacing': 'warn', - 'semi': 'warn', - 'semi-spacing': 'warn', - 'semi-style': 'warn', - 'space-before-blocks': 'warn', - 'space-in-parens': 'warn', - 'space-infix-ops': 'warn', - 'space-unary-ops': 'warn', - 'switch-colon-spacing': 'warn', - 'template-curly-spacing': 'warn', - 'template-tag-spacing': 'warn', - 'unicode-bom': 'warn', - 'wrap-iife': ['warn', 'any'], - 'yield-star-spacing': 'warn' + + '@stylistic/array-bracket-spacing': ['warn', 'never'], + '@stylistic/arrow-spacing': 'warn', + '@stylistic/block-spacing': 'warn', + '@stylistic/comma-dangle': 'warn', + '@stylistic/comma-spacing': 'warn', + '@stylistic/comma-style': 'warn', + '@stylistic/computed-property-spacing': 'warn', + '@stylistic/dot-location': ['warn', 'property'], + '@stylistic/eol-last': 'warn', + '@stylistic/function-call-spacing': 'warn', + '@stylistic/generator-star-spacing': 'warn', + '@stylistic/implicit-arrow-linebreak': 'warn', + '@stylistic/indent': ['warn', 2, { ignoreComments: true }], + '@stylistic/jsx-quotes': 'warn', + '@stylistic/key-spacing': 'warn', + '@stylistic/keyword-spacing': 'warn', + '@stylistic/linebreak-style': 'warn', + '@stylistic/lines-between-class-members': 'warn', + '@stylistic/member-delimiter-style': ['warn', { multiline: { delimiter: 'semi', requireLast: true }, singleline: { delimiter: 'semi', requireLast: false } }], + '@stylistic/new-parens': 'warn', + '@stylistic/no-extra-parens': ['warn', 'functions'], + '@stylistic/no-extra-semi': 'warn', + '@stylistic/no-floating-decimal': 'warn', + '@stylistic/no-mixed-spaces-and-tabs': 'warn', + '@stylistic/no-multi-spaces': ['warn', { ignoreEOLComments: true }], + '@stylistic/no-tabs': 'warn', + '@stylistic/no-trailing-spaces': 'warn', + '@stylistic/no-whitespace-before-property': 'warn', + '@stylistic/nonblock-statement-body-position': 'warn', + '@stylistic/object-curly-newline': 'warn', + '@stylistic/object-curly-spacing': ['warn', 'always'], + '@stylistic/padding-line-between-statements': 'warn', + '@stylistic/quotes': ['warn', 'single'], + '@stylistic/rest-spread-spacing': 'warn', + '@stylistic/semi': 'warn', + '@stylistic/semi-spacing': 'warn', + '@stylistic/semi-style': 'warn', + '@stylistic/space-before-blocks': 'warn', + '@stylistic/space-in-parens': 'warn', + '@stylistic/space-infix-ops': 'warn', + '@stylistic/space-unary-ops': 'warn', + '@stylistic/switch-colon-spacing': 'warn', + '@stylistic/template-curly-spacing': 'warn', + '@stylistic/template-tag-spacing': 'warn', + '@stylistic/wrap-iife': ['warn', 'any'], + '@stylistic/yield-star-spacing': 'warn' } } ); diff --git a/package-lock.json b/package-lock.json index a462f15..bc3a209 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,25 @@ { "name": "blokr", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blokr", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@rollup/plugin-terser": "^0.4.4", - "@vitest/browser": "^4.0.12", - "@vitest/browser-playwright": "^4.0.12", - "eslint": "^9.39.1", - "playwright": "^1.56.1", + "@stylistic/eslint-plugin": "^5.6.1", + "@vitest/browser": "^4.0.16", + "@vitest/browser-playwright": "^4.0.16", + "eslint": "^9.39.2", + "playwright": "^1.57.0", "rollup-plugin-license": "^3.6.0", - "typescript-eslint": "^8.47.0", - "vite": "^7.2.4", + "typescript-eslint": "^8.51.0", + "vite": "^7.3.0", "vite-plugin-dts": "^4.5.4", - "vitest": "^4.0.12" + "vitest": "^4.0.16" } }, "node_modules/@babel/helper-string-parser": { @@ -72,9 +73,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -89,9 +90,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -106,9 +107,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -123,9 +124,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -140,9 +141,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -157,9 +158,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -174,9 +175,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -191,9 +192,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -208,9 +209,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -225,9 +226,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -242,9 +243,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -259,9 +260,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -276,9 +277,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -293,9 +294,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -310,9 +311,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -327,9 +328,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -344,9 +345,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -361,9 +362,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -378,9 +379,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -395,9 +396,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -412,9 +413,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -429,9 +430,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -446,9 +447,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -463,9 +464,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -480,9 +481,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -497,9 +498,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -621,9 +622,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -923,44 +924,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1451,12 +1414,33 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.6.1.tgz", + "integrity": "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.0", + "@typescript-eslint/types": "^8.47.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, "node_modules/@types/argparse": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", @@ -1497,21 +1481,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", - "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/type-utils": "8.47.0", - "@typescript-eslint/utils": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1521,7 +1504,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.47.0", + "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1537,16 +1520,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", - "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "engines": { @@ -1562,14 +1545,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", - "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", + "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.47.0", - "@typescript-eslint/types": "^8.47.0", + "@typescript-eslint/tsconfig-utils": "^8.51.0", + "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "engines": { @@ -1584,14 +1567,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", - "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1602,9 +1585,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", - "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", + "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", "dev": true, "license": "MIT", "engines": { @@ -1619,17 +1602,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", - "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1644,9 +1627,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", - "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -1658,22 +1641,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", - "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.47.0", - "@typescript-eslint/tsconfig-utils": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1713,16 +1695,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", - "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1737,13 +1719,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", - "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1755,14 +1737,14 @@ } }, "node_modules/@vitest/browser": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.12.tgz", - "integrity": "sha512-8zE2ksJ7V4B7Mc++L6rBRZOZHnE/f9URvj7oLYKIS5wcDaSi6EhfalN0EG6+R/OlTYZarbK6RqmhKDLYNC9KfQ==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.16.tgz", + "integrity": "sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/mocker": "4.0.12", - "@vitest/utils": "4.0.12", + "@vitest/mocker": "4.0.16", + "@vitest/utils": "4.0.16", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", @@ -1774,18 +1756,18 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.12" + "vitest": "4.0.16" } }, "node_modules/@vitest/browser-playwright": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.12.tgz", - "integrity": "sha512-TCFuvEHVLHDuK4HmKJNSuYQwefpXBnWNuf3J8pLsXF3Q2sgRZJs+L+aUF2xjtJT5OBEJsgX5jENvlGKc596XNw==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.16.tgz", + "integrity": "sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/browser": "4.0.12", - "@vitest/mocker": "4.0.12", + "@vitest/browser": "4.0.16", + "@vitest/mocker": "4.0.16", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1793,7 +1775,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "4.0.12" + "vitest": "4.0.16" }, "peerDependenciesMeta": { "playwright": { @@ -1802,16 +1784,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.12.tgz", - "integrity": "sha512-is+g0w8V3/ZhRNrRizrJNr8PFQKwYmctWlU4qg8zy5r9aIV5w8IxXLlfbbxJCwSpsVl2PXPTm2/zruqTqz3QSg==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.12", - "@vitest/utils": "4.0.12", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -1820,13 +1802,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.12.tgz", - "integrity": "sha512-GsmA/tD5Ht3RUFoz41mZsMU1AXch3lhmgbTnoSPTdH231g7S3ytNN1aU0bZDSyxWs8WA7KDyMPD5L4q6V6vj9w==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.12", + "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1847,9 +1829,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.12.tgz", - "integrity": "sha512-R7nMAcnienG17MvRN8TPMJiCG8rrZJblV9mhT7oMFdBXvS0x+QD6S1G4DxFusR2E0QIS73f7DqSR1n87rrmE+g==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1860,13 +1842,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.12.tgz", - "integrity": "sha512-hDlCIJWuwlcLumfukPsNfPDOJokTv79hnOlf11V+n7E14rHNPz0Sp/BO6h8sh9qw4/UjZiKyYpVxK2ZNi+3ceQ==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.12", + "@vitest/utils": "4.0.16", "pathe": "^2.0.3" }, "funding": { @@ -1874,13 +1856,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.12.tgz", - "integrity": "sha512-2jz9zAuBDUSbnfyixnyOd1S2YDBrZO23rt1bicAb6MA/ya5rHdKFRikPIDpBj/Dwvh6cbImDmudegnDAkHvmRQ==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.12", + "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1889,9 +1871,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.12.tgz", - "integrity": "sha512-GZjI9PPhiOYNX8Nsyqdw7JQB+u0BptL5fSnXiottAUBHlcMzgADV58A7SLTXXQwcN1yZ6gfd1DH+2bqjuUlCzw==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", "dev": true, "license": "MIT", "funding": { @@ -1899,13 +1881,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.12.tgz", - "integrity": "sha512-DVS/TLkLdvGvj1avRy0LSmKfrcI9MNFvNGN6ECjTUHWJdlcgPDOXhjMis5Dh7rBH62nAmSXnkPbE+DZ5YD75Rw==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.12", + "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" }, "funding": { @@ -2192,19 +2174,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2223,9 +2192,9 @@ } }, "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -2372,9 +2341,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2385,32 +2354,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escape-string-regexp": { @@ -2427,9 +2396,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -2439,7 +2408,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2591,9 +2560,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2614,36 +2583,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2675,16 +2614,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2716,19 +2645,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2840,13 +2756,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2966,16 +2875,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3139,43 +3038,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3281,6 +3143,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3444,13 +3317,13 @@ } }, "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.1" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -3463,9 +3336,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3551,27 +3424,6 @@ ], "license": "MIT" }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3623,17 +3475,6 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { "version": "4.48.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz", @@ -3697,30 +3538,6 @@ "rollup": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4011,11 +3828,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -4044,19 +3864,6 @@ "node": ">=14.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4068,9 +3875,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", + "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", "dev": true, "license": "MIT", "engines": { @@ -4109,16 +3916,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.47.0.tgz", - "integrity": "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", + "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.47.0", - "@typescript-eslint/parser": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0" + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4160,13 +3967,13 @@ } }, "node_modules/vite": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", - "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -4277,28 +4084,28 @@ } }, "node_modules/vitest": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.12.tgz", - "integrity": "sha512-pmW4GCKQ8t5Ko1jYjC3SqOr7TUKN7uHOHB/XGsAIb69eYu6d1ionGSsb5H9chmPf+WeXt0VE7jTXsB1IvWoNbw==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.12", - "@vitest/mocker": "4.0.12", - "@vitest/pretty-format": "4.0.12", - "@vitest/runner": "4.0.12", - "@vitest/snapshot": "4.0.12", - "@vitest/spy": "4.0.12", - "@vitest/utils": "4.0.12", - "debug": "^4.4.3", + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", + "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", @@ -4316,12 +4123,11 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.12", - "@vitest/browser-preview": "4.0.12", - "@vitest/browser-webdriverio": "4.0.12", - "@vitest/ui": "4.0.12", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, @@ -4332,9 +4138,6 @@ "@opentelemetry/api": { "optional": true }, - "@types/debug": { - "optional": true - }, "@types/node": { "optional": true }, diff --git a/package.json b/package.json index a1ccbc5..d3412da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blokr", - "version": "0.2.1", + "version": "0.3.0", "description": "Lightweight library to block user interactions in browsers", "keywords": [ "event", @@ -9,7 +9,13 @@ "disable", "interaction", "ui", - "browser" + "browser", + "factory", + "scope", + "element", + "modal", + "overlay", + "pointer-events" ], "author": "KNOWLEDGECODE", "license": "MIT", @@ -44,14 +50,15 @@ }, "devDependencies": { "@rollup/plugin-terser": "^0.4.4", - "@vitest/browser": "^4.0.12", - "@vitest/browser-playwright": "^4.0.12", - "eslint": "^9.39.1", - "playwright": "^1.56.1", + "@stylistic/eslint-plugin": "^5.6.1", + "@vitest/browser": "^4.0.16", + "@vitest/browser-playwright": "^4.0.16", + "eslint": "^9.39.2", + "playwright": "^1.57.0", "rollup-plugin-license": "^3.6.0", - "typescript-eslint": "^8.47.0", - "vite": "^7.2.4", + "typescript-eslint": "^8.51.0", + "vite": "^7.3.0", "vite-plugin-dts": "^4.5.4", - "vitest": "^4.0.12" + "vitest": "^4.0.16" } } diff --git a/src/blokr.ts b/src/blokr.ts index 4fb941f..8594fb7 100644 --- a/src/blokr.ts +++ b/src/blokr.ts @@ -1,84 +1,95 @@ import lock from './lock.ts'; +import type { Filter } from './lock.ts'; + +export type Scope = 'inside' | 'outside' | 'self'; + +export interface Options { + scope?: Scope; + timeout?: number; +} + +const blokrs = new WeakMap(); class Blokr { - private _timeout: number; + private _target: Element | undefined; - private _timerId: number; + private _timerId: number | undefined; - private _counter: number; + private _filter: Filter | undefined; /** * Creates the Blokr singleton instance. */ - constructor () { - this._timeout = 10000; // Default timeout of 10 seconds - this._timerId = 0; - this._counter = 0; + constructor (target?: Element) { + this._target = target; + this._timerId = undefined; } /** - * Prevents user interactions. + * Locks user interactions with optional timeout and scope configuration. + * Returns false if already locked without making any changes. + * @param [options] - Lock configuration options. + * @returns true if lock was applied, false if already locked. */ - lock () { - lock.on(); - this._counter++; - - if (this._timerId) { - self.clearTimeout(this._timerId); - this._timerId = 0; + lock (options?: Options) { + if (this.isLocked()) { + return false; } - if (this._timeout) { - this._timerId = self.setTimeout(() => { - this._timerId = 0; - this._counter = 0; - lock.off(); - }, this._timeout); + + const scope = options?.scope ?? 'inside'; + const timeout = options?.timeout ?? 0; + + this._filter = (eventTarget: Element) => { + if (this._target) { + if (scope === 'self') { + return this._target === eventTarget; + } + const contains = this._target.contains(eventTarget); + // For 'outside' scope, block events outside target; otherwise block events inside target + return scope === 'outside' ? !contains : contains; + } + // No target specified: block all events + return true; + }; + lock.register(this._filter); + + if (timeout > 0) { + this._timerId = globalThis.setTimeout(() => this.unlock(), timeout); } + + return true; } /** - * Checks if user interactions are currently prevented. - * @returns {boolean} Returns true if interactions are blocked, false otherwise. + * Checks if user interactions are currently locked. + * @returns true if locked, false otherwise. */ isLocked () { - return this._counter > 0; + return !!this._filter; } /** - * Sets the timeout duration for automatic unlock. - * @param timeout - The timeout in milliseconds. Set to 0 to disable automatic unlock. Negative values are treated as 0. - * @returns {boolean} Returns true if the timeout was set successfully, false if currently locked. + * Unlocks user interactions and clears any pending timeout. + * Safe to call even when not locked. */ - setTimeout(timeout: number) { - if (!this.isLocked()) { - this._timeout = timeout < 0 ? 0 : timeout; - return true; + unlock () { + if (this._timerId) { + globalThis.clearTimeout(this._timerId); + this._timerId = undefined; } - return false; - } - - /** - * Decrements the internal counter and releases the lock when the counter reaches zero. - * If abort is true, the counter is reset to zero immediately, effectively releasing the lock. - * Clears any pending timeout and triggers the unlock event. - * @param abort - If true, immediately resets the counter to zero and releases the lock - */ - unlock (abort = false) { - if (this._counter > 0) { - this._counter--; - - if (abort) { - this._counter = 0; - } - if (this._counter === 0) { - if (this._timerId) { - self.clearTimeout(this._timerId); - this._timerId = 0; - } - lock.off(); - } + if (this._filter) { + lock.unregister(this._filter); } + this._filter = undefined; } } -export default new Blokr(); +const blokr = (target?: Element) => { + return blokrs.get(target ?? globalThis) ?? (() => { + const instance = new Blokr(target); + blokrs.set(target ?? globalThis, instance); + return instance; + })(); +}; + +export default blokr; diff --git a/src/lock.ts b/src/lock.ts index ef9d0dd..20a5511 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -1,42 +1,58 @@ +export type Filter = (eventTarget: Element) => boolean; + const eventNames = [ 'contextmenu', 'keydown', 'mousedown', 'touchmove', 'touchstart', 'wheel' ]; -const options = { capture: true, passive: false }; - class Lock { - private _locked: boolean; + private _filters: Set; + /** + * Creates the Lock singleton instance. + */ constructor () { - this._locked = false; - this._listener = this._listener.bind(this); - if (typeof self !== 'undefined') { - eventNames.forEach(eventName => self.addEventListener(eventName, this._listener, options)); + this._filters = new Set(); + + if ('addEventListener' in globalThis) { + eventNames.forEach(eventName => globalThis.addEventListener( + eventName, + this._listener.bind(this), + { capture: true, passive: false } + )); } } + /** + * Blocks user interactions when the lock is active. + * @param evt - The event to be blocked. + */ private _listener (evt: Event) { - if (this._locked) { - evt.stopImmediatePropagation(); - evt.stopPropagation(); - evt.preventDefault(); + if (evt.target instanceof Element) { + for (const filter of this._filters.values()) { + if (filter(evt.target)) { + evt.stopImmediatePropagation(); + evt.stopPropagation(); + evt.preventDefault(); + break; + } + } } } - on () { - if (this._locked) { - return false; - } - this._locked = true; - - return true; + /** + * Registers a filter function to block events matching the filter criteria. + * @param filter - Filter function that determines which events to block. + */ + register (filter: Filter) { + this._filters.add(filter); } - off () { - if (!this._locked) { - return; - } - this._locked = false; + /** + * Unregisters a previously registered filter function. + * @param filter - The filter function to remove. + */ + unregister (filter: Filter) { + this._filters.delete(filter); } } diff --git a/tests/blokr.test.ts b/tests/blokr.test.ts index 774861f..4ef49a8 100644 --- a/tests/blokr.test.ts +++ b/tests/blokr.test.ts @@ -1,251 +1,293 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, afterEach, vi } from 'vitest'; import blokr from '../src/blokr.ts'; -describe('Blokr Singleton', () => { - beforeEach(() => { - // Reset the singleton state - blokr.setTimeout(10000); - // Unlock any existing locks - while (blokr.isLocked()) { - blokr.unlock(); - } - }); - +describe('Blokr Factory Function', () => { afterEach(() => { // Clean up any locks after each test - while (blokr.isLocked()) { - blokr.unlock(); + const globalInstance = blokr(); + if (globalInstance.isLocked()) { + globalInstance.unlock(); } }); - describe('Singleton Instance', () => { - it('should be available as singleton', () => { - expect(blokr).toBeDefined(); - expect(typeof blokr.lock).toBe('function'); - expect(typeof blokr.unlock).toBe('function'); - expect(typeof blokr.isLocked).toBe('function'); - expect(typeof blokr.setTimeout).toBe('function'); + describe('Factory Behavior', () => { + it('should return a Blokr instance', () => { + const instance = blokr(); + expect(instance).toBeDefined(); + expect(typeof instance.lock).toBe('function'); + expect(typeof instance.unlock).toBe('function'); + expect(typeof instance.isLocked).toBe('function'); + }); + + it('should return the same instance for global (no target)', () => { + const instance1 = blokr(); + const instance2 = blokr(); + expect(instance1).toBe(instance2); + }); + + it('should return the same instance for same element', () => { + const element = document.createElement('div'); + const instance1 = blokr(element); + const instance2 = blokr(element); + expect(instance1).toBe(instance2); + }); + + it('should return different instances for different elements', () => { + const element1 = document.createElement('div'); + const element2 = document.createElement('div'); + const instance1 = blokr(element1); + const instance2 = blokr(element2); + expect(instance1).not.toBe(instance2); }); }); describe('lock()', () => { - it('should increment counter on first lock', () => { - blokr.lock(); - // We can test the behavior indirectly by checking unlock behavior - blokr.unlock(); - // If lock/unlock worked correctly, no exception should be thrown - expect(() => blokr.unlock()).not.toThrow(); + it('should lock and return true on first call', () => { + const instance = blokr(); + const result = instance.lock(); + expect(result).toBe(true); + expect(instance.isLocked()).toBe(true); + instance.unlock(); }); - it('should increment counter on multiple locks', () => { - blokr.lock(); - blokr.lock(); - blokr.lock(); - // Counter should be 3, test through unlock behavior - blokr.unlock(); - blokr.unlock(); - blokr.unlock(); - // Should be fully unlocked after 3 unlocks - expect(() => blokr.unlock()).not.toThrow(); + it('should return false if already locked', () => { + const instance = blokr(); + const result1 = instance.lock(); + expect(result1).toBe(true); + + const result2 = instance.lock(); + expect(result2).toBe(false); + expect(instance.isLocked()).toBe(true); + + instance.unlock(); }); - it('should set timeout when timeout > 0', () => { - const setTimeoutSpy = vi.spyOn(self, 'setTimeout'); - blokr.setTimeout(5000); + it('should accept timeout option', () => { + const instance = blokr(); + const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout'); - blokr.lock(); + instance.lock({ timeout: 5000 }); expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5000); + expect(instance.isLocked()).toBe(true); + setTimeoutSpy.mockRestore(); + instance.unlock(); }); it('should not set timeout when timeout is 0', () => { - const setTimeoutSpy = vi.spyOn(self, 'setTimeout'); - blokr.setTimeout(0); + const instance = blokr(); + const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout'); - blokr.lock(); + instance.lock({ timeout: 0 }); expect(setTimeoutSpy).not.toHaveBeenCalled(); + expect(instance.isLocked()).toBe(true); + setTimeoutSpy.mockRestore(); + instance.unlock(); }); - it('should clear existing timeout on subsequent locks', () => { - const clearTimeoutSpy = vi.spyOn(self, 'clearTimeout'); - blokr.setTimeout(5000); + it('should accept scope option', () => { + const instance = blokr(document.createElement('div')); - blokr.lock(); - blokr.lock(); + const result1 = instance.lock({ scope: 'inside' }); + expect(result1).toBe(true); + instance.unlock(); - expect(clearTimeoutSpy).toHaveBeenCalled(); - clearTimeoutSpy.mockRestore(); + const result2 = instance.lock({ scope: 'outside' }); + expect(result2).toBe(true); + instance.unlock(); + + const result3 = instance.lock({ scope: 'self' }); + expect(result3).toBe(true); + instance.unlock(); + }); + + it('should use default scope "inside" if not specified', () => { + const instance = blokr(document.createElement('div')); + const result = instance.lock(); + expect(result).toBe(true); + expect(instance.isLocked()).toBe(true); + instance.unlock(); }); }); describe('unlock()', () => { - it('should decrement counter when locked', () => { - blokr.lock(); - blokr.unlock(); - // Counter should be 0 now, verify by checking unlock doesn't throw - expect(() => blokr.unlock()).not.toThrow(); + it('should unlock when locked', () => { + const instance = blokr(); + instance.lock(); + expect(instance.isLocked()).toBe(true); + + instance.unlock(); + expect(instance.isLocked()).toBe(false); }); - it('should not decrement counter when not locked', () => { - blokr.unlock(); // Should do nothing - blokr.unlock(); // Should do nothing - // Should not throw errors when unlocking without locking - expect(() => blokr.unlock()).not.toThrow(); + it('should do nothing when not locked', () => { + const instance = blokr(); + expect(instance.isLocked()).toBe(false); + + instance.unlock(); + expect(instance.isLocked()).toBe(false); + + // Should not throw + expect(() => instance.unlock()).not.toThrow(); }); - it('should clear timeout when counter reaches 0', () => { - const clearTimeoutSpy = vi.spyOn(self, 'clearTimeout'); - blokr.setTimeout(5000); + it('should clear timeout when unlocking', () => { + const instance = blokr(); + const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout'); - blokr.lock(); - blokr.unlock(); + instance.lock({ timeout: 5000 }); + instance.unlock(); expect(clearTimeoutSpy).toHaveBeenCalled(); clearTimeoutSpy.mockRestore(); }); - it('should handle multiple lock/unlock cycles', () => { - blokr.lock(); - blokr.lock(); - blokr.lock(); // Counter = 3 + it('should clear timeout only when timeout was set', () => { + const instance = blokr(); + const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout'); - blokr.unlock(); // Counter = 2 - blokr.unlock(); // Counter = 1 - blokr.unlock(); // Counter = 0, should unlock + instance.lock({ timeout: 0 }); + instance.unlock(); - blokr.unlock(); // Should do nothing (counter already 0) - - // Verify final state is correct - expect(() => blokr.unlock()).not.toThrow(); + expect(clearTimeoutSpy).not.toHaveBeenCalled(); + clearTimeoutSpy.mockRestore(); }); + }); - it('should immediately unlock when abort is true', () => { - blokr.lock(); - blokr.lock(); - blokr.lock(); // Counter = 3 - - blokr.unlock(true); // Should reset counter to 0 immediately + describe('isLocked()', () => { + it('should return false when not locked', () => { + const instance = blokr(); + expect(instance.isLocked()).toBe(false); + }); - expect(blokr.isLocked()).toBe(false); + it('should return true when locked', () => { + const instance = blokr(); + instance.lock(); + expect(instance.isLocked()).toBe(true); + instance.unlock(); }); - it('should clear timeout when abort is true', () => { - const clearTimeoutSpy = vi.spyOn(self, 'clearTimeout'); - blokr.setTimeout(5000); + it('should return false after unlock', () => { + const instance = blokr(); + instance.lock(); + instance.unlock(); + expect(instance.isLocked()).toBe(false); + }); + }); - blokr.lock(); - blokr.lock(); // Counter = 2 - blokr.unlock(true); // Should clear timeout and reset counter + describe('Timeout Behavior', () => { + it('should auto-unlock after timeout', () => { + vi.useFakeTimers(); - expect(clearTimeoutSpy).toHaveBeenCalled(); - expect(blokr.isLocked()).toBe(false); - clearTimeoutSpy.mockRestore(); - }); + const instance = blokr(); + instance.lock({ timeout: 1000 }); - it('should work normally when abort is false (default)', () => { - blokr.lock(); - blokr.lock(); - blokr.lock(); // Counter = 3 + expect(instance.isLocked()).toBe(true); - blokr.unlock(false); // Counter = 2 - expect(blokr.isLocked()).toBe(true); + // Fast-forward time + vi.advanceTimersByTime(1000); - blokr.unlock(); // Counter = 1 (default false) - expect(blokr.isLocked()).toBe(true); + // Should be unlocked after timeout + expect(instance.isLocked()).toBe(false); - blokr.unlock(); // Counter = 0 - expect(blokr.isLocked()).toBe(false); + vi.useRealTimers(); }); - }); - describe('setTimeout()', () => { - it('should set timeout when not locked', () => { - const result = blokr.setTimeout(5000); - expect(result).toBe(true); - }); + it('should not set timeout when timeout is 0', () => { + const instance = blokr(); + const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout'); - it('should not set timeout when locked', () => { - blokr.lock(); - const result = blokr.setTimeout(5000); - expect(result).toBe(false); - }); + instance.lock({ timeout: 0 }); - it('should handle negative timeout values', () => { - const result = blokr.setTimeout(-1000); - expect(result).toBe(true); - // Verify timeout was set to 0 by checking lock behavior - const setTimeoutSpy = vi.spyOn(self, 'setTimeout'); - blokr.lock(); expect(setTimeoutSpy).not.toHaveBeenCalled(); + setTimeoutSpy.mockRestore(); + instance.unlock(); }); - }); - describe('isLocked()', () => { - it('should return false when not locked', () => { - expect(blokr.isLocked()).toBe(false); - }); + it('should clear timeout on manual unlock', () => { + vi.useFakeTimers(); - it('should return true when locked', () => { - blokr.lock(); - expect(blokr.isLocked()).toBe(true); - }); + const instance = blokr(); + instance.lock({ timeout: 5000 }); - it('should return false after unlock', () => { - blokr.lock(); - blokr.unlock(); - expect(blokr.isLocked()).toBe(false); - }); - - it('should return true until all locks are released', () => { - blokr.lock(); - blokr.lock(); - blokr.lock(); - - expect(blokr.isLocked()).toBe(true); - blokr.unlock(); - expect(blokr.isLocked()).toBe(true); - blokr.unlock(); - expect(blokr.isLocked()).toBe(true); - blokr.unlock(); - expect(blokr.isLocked()).toBe(false); + // Unlock before timeout + instance.unlock(); + + // Fast-forward past the timeout + vi.advanceTimersByTime(5000); + + // Should still be unlocked (timeout was cleared) + expect(instance.isLocked()).toBe(false); + + vi.useRealTimers(); }); }); - describe('timeout behavior', () => { - it('should auto-unlock after timeout', async () => { - vi.useFakeTimers(); + describe('Independent Instances', () => { + it('should support independent locks on different elements', () => { + const element1 = document.createElement('div'); + const element2 = document.createElement('div'); - blokr.setTimeout(1000); - blokr.lock(); + const instance1 = blokr(element1); + const instance2 = blokr(element2); - // Fast-forward time - vi.advanceTimersByTime(1000); + instance1.lock(); + expect(instance1.isLocked()).toBe(true); + expect(instance2.isLocked()).toBe(false); - // Should be unlocked after timeout - expect(blokr.isLocked()).toBe(false); + instance2.lock(); + expect(instance1.isLocked()).toBe(true); + expect(instance2.isLocked()).toBe(true); - vi.useRealTimers(); + instance1.unlock(); + expect(instance1.isLocked()).toBe(false); + expect(instance2.isLocked()).toBe(true); + + instance2.unlock(); }); - it('should reset counter to 0 on timeout', async () => { + it('should maintain independent timeout states', () => { vi.useFakeTimers(); - blokr.setTimeout(1000); - blokr.lock(); - blokr.lock(); - blokr.lock(); // Counter = 3 + const element1 = document.createElement('div'); + const element2 = document.createElement('div'); + + const instance1 = blokr(element1); + const instance2 = blokr(element2); + + instance1.lock({ timeout: 1000 }); + instance2.lock({ timeout: 2000 }); - // Fast-forward time vi.advanceTimersByTime(1000); + expect(instance1.isLocked()).toBe(false); + expect(instance2.isLocked()).toBe(true); - // After timeout, should be fully unlocked - expect(blokr.isLocked()).toBe(false); + vi.advanceTimersByTime(1000); + expect(instance1.isLocked()).toBe(false); + expect(instance2.isLocked()).toBe(false); vi.useRealTimers(); }); }); -}); \ No newline at end of file + + describe('Global Instance', () => { + it('should block all events when no target specified', () => { + const instance = blokr(); + instance.lock(); + expect(instance.isLocked()).toBe(true); + instance.unlock(); + }); + + it('should use default scope "inside" for global instance', () => { + const instance = blokr(); + // Global instance with no target should still work (no filtering) + const result = instance.lock(); + expect(result).toBe(true); + instance.unlock(); + }); + }); +}); diff --git a/tests/event-blocking.test.ts b/tests/event-blocking.test.ts index 3525fb1..b879116 100644 --- a/tests/event-blocking.test.ts +++ b/tests/event-blocking.test.ts @@ -1,188 +1,378 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, afterEach, vi } from 'vitest'; import blokr from '../src/blokr.ts'; describe('Event Blocking Integration', () => { - let testElement: HTMLElement; - - beforeEach(() => { - // Reset singleton state - while (blokr.isLocked()) { - blokr.unlock(); - } - testElement = document.createElement('button'); - testElement.textContent = 'Test Button'; - document.body.appendChild(testElement); - }); - afterEach(() => { - // Clean up locks - while (blokr.isLocked()) { - blokr.unlock(); + // Clean up any locks after each test + const globalInstance = blokr(); + if (globalInstance.isLocked()) { + globalInstance.unlock(); } - document.body.removeChild(testElement); }); - describe('Mousedown Events', () => { - it('should block mousedown events when locked', () => { - const mousedownHandler = vi.fn(); - testElement.addEventListener('mousedown', mousedownHandler); + describe('Global Event Blocking', () => { + it('should block mousedown events when locked globally', () => { + const handler = vi.fn(); + const element = document.createElement('button'); + element.addEventListener('mousedown', handler); + document.body.appendChild(element); - blokr.lock(); + const instance = blokr(); + instance.lock(); - // Simulate mousedown event - const mousedownEvent = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); - testElement.dispatchEvent(mousedownEvent); + element.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); - expect(mousedownHandler).not.toHaveBeenCalled(); + instance.unlock(); + document.body.removeChild(element); }); it('should allow mousedown events when unlocked', () => { - const mousedownHandler = vi.fn(); - testElement.addEventListener('mousedown', mousedownHandler); + const handler = vi.fn(); + const element = document.createElement('button'); + element.addEventListener('mousedown', handler); + document.body.appendChild(element); // Don't lock - // Simulate mousedown event - const mousedownEvent = new MouseEvent('mousedown', { - bubbles: true, - cancelable: true - }); - - testElement.dispatchEvent(mousedownEvent); - - expect(mousedownHandler).toHaveBeenCalled(); - }); - }); - - describe('Keyboard Events', () => { - it('should block keydown events when locked', () => { - const keydownHandler = vi.fn(); - document.addEventListener('keydown', keydownHandler); - - blokr.lock(); - - // Simulate keydown event - const keydownEvent = new KeyboardEvent('keydown', { - key: 'Enter', + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); - document.dispatchEvent(keydownEvent); + element.dispatchEvent(event); - expect(keydownHandler).not.toHaveBeenCalled(); + expect(handler).toHaveBeenCalled(); - document.removeEventListener('keydown', keydownHandler); + document.body.removeChild(element); }); - it('should allow keydown events when unlocked', () => { - const keydownHandler = vi.fn(); - document.addEventListener('keydown', keydownHandler); + it('should block keydown events when locked globally', () => { + const handler = vi.fn(); + const element = document.createElement('input'); + element.addEventListener('keydown', handler); + document.body.appendChild(element); - // Don't lock + const instance = blokr(); + instance.lock(); - // Simulate keydown event - const keydownEvent = new KeyboardEvent('keydown', { + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }); - document.dispatchEvent(keydownEvent); + element.dispatchEvent(event); - expect(keydownHandler).toHaveBeenCalled(); + expect(handler).not.toHaveBeenCalled(); - document.removeEventListener('keydown', keydownHandler); + instance.unlock(); + document.body.removeChild(element); }); - }); - describe('Mouse Events', () => { + it('should block contextmenu events when locked globally', () => { + const handler = vi.fn(); + const element = document.createElement('div'); + element.addEventListener('contextmenu', handler); + document.body.appendChild(element); - it('should block contextmenu events when locked', () => { - const contextmenuHandler = vi.fn(); - testElement.addEventListener('contextmenu', contextmenuHandler); + const instance = blokr(); + instance.lock(); - blokr.lock(); - - // Simulate contextmenu event - const contextmenuEvent = new MouseEvent('contextmenu', { + const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true }); - testElement.dispatchEvent(contextmenuEvent); + element.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); - expect(contextmenuHandler).not.toHaveBeenCalled(); + instance.unlock(); + document.body.removeChild(element); }); - it('should block wheel events when locked', () => { - const wheelHandler = vi.fn(); - testElement.addEventListener('wheel', wheelHandler); + it('should block wheel events when locked globally', () => { + const handler = vi.fn(); + const element = document.createElement('div'); + element.addEventListener('wheel', handler); + document.body.appendChild(element); - blokr.lock(); + const instance = blokr(); + instance.lock(); - // Simulate wheel event - const wheelEvent = new WheelEvent('wheel', { + const event = new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaY: 100 }); - testElement.dispatchEvent(wheelEvent); + element.dispatchEvent(event); - expect(wheelHandler).not.toHaveBeenCalled(); + expect(handler).not.toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(element); }); - }); - describe('Touch Events', () => { - it('should block touchstart events when locked', () => { - const touchstartHandler = vi.fn(); - testElement.addEventListener('touchstart', touchstartHandler); + it('should block touchstart events when locked globally', () => { + const handler = vi.fn(); + const element = document.createElement('div'); + element.addEventListener('touchstart', handler); + document.body.appendChild(element); - blokr.lock(); + const instance = blokr(); + instance.lock(); - // Simulate touchstart event - const touchstartEvent = new TouchEvent('touchstart', { + const event = new TouchEvent('touchstart', { bubbles: true, cancelable: true, touches: [new Touch({ identifier: 0, - target: testElement, + target: element, clientX: 100, clientY: 100 })] }); - testElement.dispatchEvent(touchstartEvent); + element.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); - expect(touchstartHandler).not.toHaveBeenCalled(); + instance.unlock(); + document.body.removeChild(element); }); - it('should block touchmove events when locked', () => { - const touchmoveHandler = vi.fn(); - testElement.addEventListener('touchmove', touchmoveHandler); + it('should block touchmove events when locked globally', () => { + const handler = vi.fn(); + const element = document.createElement('div'); + element.addEventListener('touchmove', handler); + document.body.appendChild(element); - blokr.lock(); + const instance = blokr(); + instance.lock(); - // Simulate touchmove event - const touchmoveEvent = new TouchEvent('touchmove', { + const event = new TouchEvent('touchmove', { bubbles: true, cancelable: true, touches: [new Touch({ identifier: 0, - target: testElement, + target: element, clientX: 110, clientY: 110 })] }); - testElement.dispatchEvent(touchmoveEvent); + element.dispatchEvent(event); - expect(touchmoveHandler).not.toHaveBeenCalled(); + expect(handler).not.toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(element); + }); + }); + + describe('Target-specific Event Blocking (scope: inside)', () => { + it('should block events inside target with default scope', () => { + const container = document.createElement('div'); + const button = document.createElement('button'); + const handler = vi.fn(); + button.addEventListener('mousedown', handler); + container.appendChild(button); + document.body.appendChild(container); + + const instance = blokr(container); + instance.lock(); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + button.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(container); + }); + + it('should allow events inside target when unlocked', () => { + const container = document.createElement('div'); + const button = document.createElement('button'); + const handler = vi.fn(); + button.addEventListener('mousedown', handler); + container.appendChild(button); + document.body.appendChild(container); + + const instance = blokr(container); + instance.lock(); + instance.unlock(); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + button.dispatchEvent(event); + + expect(handler).toHaveBeenCalled(); + + document.body.removeChild(container); + }); + + it('should block events on child elements of target', () => { + const container = document.createElement('div'); + const child = document.createElement('span'); + const handler = vi.fn(); + child.addEventListener('mousedown', handler); + container.appendChild(child); + document.body.appendChild(container); + + const instance = blokr(container); + instance.lock({ scope: 'inside' }); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + child.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(container); + }); + }); + + describe('Target-specific Event Blocking (scope: outside)', () => { + it('should block events outside target', () => { + const container = document.createElement('div'); + const outside = document.createElement('button'); + const handler = vi.fn(); + outside.addEventListener('mousedown', handler); + document.body.appendChild(container); + document.body.appendChild(outside); + + const instance = blokr(container); + instance.lock({ scope: 'outside' }); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + outside.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(container); + document.body.removeChild(outside); + }); + + it('should allow events inside target with scope outside', () => { + const container = document.createElement('div'); + const button = document.createElement('button'); + const handler = vi.fn(); + button.addEventListener('mousedown', handler); + container.appendChild(button); + document.body.appendChild(container); + + const instance = blokr(container); + instance.lock({ scope: 'outside' }); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + button.dispatchEvent(event); + + expect(handler).toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(container); + }); + }); + + describe('Target-specific Event Blocking (scope: self)', () => { + it('should block events on target itself', () => { + const target = document.createElement('button'); + const handler = vi.fn(); + target.addEventListener('mousedown', handler); + document.body.appendChild(target); + + const instance = blokr(target); + instance.lock({ scope: 'self' }); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + target.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(target); + }); + + it('should allow events on child elements with scope self', () => { + const parent = document.createElement('div'); + const child = document.createElement('button'); + const handler = vi.fn(); + child.addEventListener('mousedown', handler); + parent.appendChild(child); + document.body.appendChild(parent); + + const instance = blokr(parent); + instance.lock({ scope: 'self' }); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + child.dispatchEvent(event); + + expect(handler).toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(parent); + }); + + it('should allow events outside target with scope self', () => { + const target = document.createElement('div'); + const outside = document.createElement('button'); + const handler = vi.fn(); + outside.addEventListener('mousedown', handler); + document.body.appendChild(target); + document.body.appendChild(outside); + + const instance = blokr(target); + instance.lock({ scope: 'self' }); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + outside.dispatchEvent(event); + + expect(handler).toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(target); + document.body.removeChild(outside); }); }); @@ -190,74 +380,200 @@ describe('Event Blocking Integration', () => { it('should stop immediate propagation of blocked events', () => { const handler1 = vi.fn(); const handler2 = vi.fn(); + const element = document.createElement('button'); + element.addEventListener('mousedown', handler1); + element.addEventListener('mousedown', handler2); + document.body.appendChild(element); - testElement.addEventListener('mousedown', handler1); - testElement.addEventListener('mousedown', handler2); - - blokr.lock(); + const instance = blokr(); + instance.lock(); - const mousedownEvent = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); - testElement.dispatchEvent(mousedownEvent); + element.dispatchEvent(event); expect(handler1).not.toHaveBeenCalled(); expect(handler2).not.toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(element); }); it('should prevent default behavior of blocked events', () => { - blokr.lock(); + const element = document.createElement('a'); + element.href = '#'; + document.body.appendChild(element); - const mousedownEvent = new MouseEvent('mousedown', { + const instance = blokr(); + instance.lock(); + + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); - const preventDefaultSpy = vi.spyOn(mousedownEvent, 'preventDefault'); - - testElement.dispatchEvent(mousedownEvent); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + element.dispatchEvent(event); expect(preventDefaultSpy).toHaveBeenCalled(); + + instance.unlock(); + document.body.removeChild(element); + }); + }); + + describe('Multiple Independent Locks', () => { + it('should support independent locks on different targets', () => { + const container1 = document.createElement('div'); + const button1 = document.createElement('button'); + const handler1 = vi.fn(); + button1.addEventListener('mousedown', handler1); + container1.appendChild(button1); + document.body.appendChild(container1); + + const container2 = document.createElement('div'); + const button2 = document.createElement('button'); + const handler2 = vi.fn(); + button2.addEventListener('mousedown', handler2); + container2.appendChild(button2); + document.body.appendChild(container2); + + const instance1 = blokr(container1); + const instance2 = blokr(container2); + + // Lock only container1 + instance1.lock(); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + button1.dispatchEvent(event); + button2.dispatchEvent(event); + + expect(handler1).not.toHaveBeenCalled(); + expect(handler2).toHaveBeenCalled(); + + // Lock container2 + instance2.lock(); + handler2.mockClear(); + + button1.dispatchEvent(event); + button2.dispatchEvent(event); + + expect(handler1).not.toHaveBeenCalled(); + expect(handler2).not.toHaveBeenCalled(); + + instance1.unlock(); + instance2.unlock(); + document.body.removeChild(container1); + document.body.removeChild(container2); + }); + + it('should respect each instance scope independently', () => { + const container1 = document.createElement('div'); + const button1 = document.createElement('button'); + const handler1 = vi.fn(); + button1.addEventListener('mousedown', handler1); + container1.appendChild(button1); + document.body.appendChild(container1); + + const container2 = document.createElement('div'); + const button2 = document.createElement('button'); + const handler2 = vi.fn(); + button2.addEventListener('mousedown', handler2); + container2.appendChild(button2); + document.body.appendChild(container2); + + const outside = document.createElement('button'); + const outsideHandler = vi.fn(); + outside.addEventListener('mousedown', outsideHandler); + document.body.appendChild(outside); + + const instance1 = blokr(container1); + const instance2 = blokr(container2); + + // Container1: scope inside (blocks events inside container1) + instance1.lock({ scope: 'inside' }); + // Container2: scope outside (blocks events outside container2) + instance2.lock({ scope: 'outside' }); + + const event = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true + }); + + // Event inside container1 - should be blocked by instance1 + button1.dispatchEvent(event); + expect(handler1).not.toHaveBeenCalled(); + + // Event inside container2 - should not be blocked (outside scope) + button2.dispatchEvent(event); + expect(handler2).toHaveBeenCalled(); + + // Event outside both containers - should be blocked by instance2 + outside.dispatchEvent(event); + expect(outsideHandler).not.toHaveBeenCalled(); + + instance1.unlock(); + instance2.unlock(); + document.body.removeChild(container1); + document.body.removeChild(container2); + document.body.removeChild(outside); }); }); describe('Lock/Unlock State Changes', () => { it('should allow events after unlocking', () => { - const mousedownHandler = vi.fn(); - testElement.addEventListener('mousedown', mousedownHandler); + const handler = vi.fn(); + const element = document.createElement('button'); + element.addEventListener('mousedown', handler); + document.body.appendChild(element); - blokr.lock(); - blokr.unlock(); + const instance = blokr(); + instance.lock(); + instance.unlock(); - const mousedownEvent = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); - testElement.dispatchEvent(mousedownEvent); + element.dispatchEvent(event); + + expect(handler).toHaveBeenCalled(); - expect(mousedownHandler).toHaveBeenCalled(); + document.body.removeChild(element); }); it('should handle rapid lock/unlock cycles', () => { - const mousedownHandler = vi.fn(); - testElement.addEventListener('mousedown', mousedownHandler); + const handler = vi.fn(); + const element = document.createElement('button'); + element.addEventListener('mousedown', handler); + document.body.appendChild(element); - blokr.lock(); - blokr.unlock(); - blokr.lock(); - blokr.unlock(); + const instance = blokr(); - const mousedownEvent = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); - testElement.dispatchEvent(mousedownEvent); + // Rapid cycle + instance.lock(); + instance.unlock(); + instance.lock(); + instance.unlock(); + + element.dispatchEvent(event); + + expect(handler).toHaveBeenCalled(); - expect(mousedownHandler).toHaveBeenCalled(); + document.body.removeChild(element); }); }); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json index 8091116..38fa989 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -105,5 +105,5 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src"] + "include": ["src", "tests"] } diff --git a/vite.config.ts b/vite.config.ts index 0e3517d..a43ea70 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ minify: false, lib: { entry: 'src/index.ts', - name: 'Blokr', + name: 'blokr', formats: ['es', 'umd'], fileName: format => format === 'es' ? 'index.js' : 'blokr.js', }, @@ -16,15 +16,17 @@ export default defineConfig({ output: { exports: 'default', plugins: [ - terser(), license({ banner: '@license\nCopyright 2025 KNOWLEDGECODE\nSPDX-License-Identifier: MIT' - }) + }), + terser() ] } } }, plugins: [ - dts() + dts({ + include: ['src/**/*'] + }) ] });