From 9041065bf916edf183d03d8383a7af3aa144431f Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:41:59 +0900 Subject: [PATCH 1/9] feat: enhance core event blocking implementation - Add dynamic event listener activation/deactivation (_activate() and _deactivate() methods) - Expand blocked event types from 6 to 13 events - Added: click, dragstart, mouseup, pointerdown, pointermove, pointerup, selectstart - Optimize performance by removing listeners when no filters are registered - Improve JSDoc comments for better developer experience --- src/lock.ts | 79 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/src/lock.ts b/src/lock.ts index 20a5511..57aa8ff 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -1,58 +1,97 @@ export type Filter = (eventTarget: Element) => boolean; const eventNames = [ - 'contextmenu', 'keydown', 'mousedown', 'touchmove', 'touchstart', 'wheel' + 'click', 'contextmenu', 'dragstart', 'keydown', 'mousedown', 'mouseup', + 'pointerdown', 'pointermove', 'pointerup', 'selectstart', 'touchmove', + 'touchstart', 'wheel' ]; - class Lock { + /** + * Event listener that blocks events based on registered filters. + */ + private _listener: (ev: Event) => void; + + /** + * Indicates whether the global event listener is currently active. + */ + private _isActive: boolean; + + /** + * Set of registered filter functions to determine which events to block. + */ private _filters: Set; /** * Creates the Lock singleton instance. */ constructor () { + this._listener = (ev: Event) => { + if (ev.target instanceof Element) { + for (const filter of this._filters.values()) { + if (filter(ev.target)) { + ev.stopImmediatePropagation(); + ev.stopPropagation(); + ev.preventDefault(); + break; + } + } + } + }; + this._isActive = false; this._filters = new Set(); + } - if ('addEventListener' in globalThis) { - eventNames.forEach(eventName => globalThis.addEventListener( - eventName, - this._listener.bind(this), - { capture: true, passive: false } - )); + /** + * Activates the global event listener to block events based on registered filters. + */ + private _activate () { + if (!this._isActive) { + if ('addEventListener' in globalThis) { + eventNames.forEach(eventName => globalThis.addEventListener( + eventName, + this._listener, + { capture: true, passive: false } + )); + this._isActive = true; + } } } /** - * Blocks user interactions when the lock is active. - * @param evt - The event to be blocked. + * Deactivates the global event listener when no filters are registered. */ - private _listener (evt: Event) { - if (evt.target instanceof Element) { - for (const filter of this._filters.values()) { - if (filter(evt.target)) { - evt.stopImmediatePropagation(); - evt.stopPropagation(); - evt.preventDefault(); - break; - } + private _deactivate () { + if (this._isActive) { + if ('removeEventListener' in globalThis) { + eventNames.forEach(eventName => globalThis.removeEventListener( + eventName, + this._listener, + { capture: true } + )); + this._isActive = false; } } } /** - * Registers a filter function to block events matching the filter criteria. + * Registers a filter function to block events on matching targets. * @param filter - Filter function that determines which events to block. */ register (filter: Filter) { + this._activate(); this._filters.add(filter); } /** * Unregisters a previously registered filter function. + * If no filters remain, the global event listener is deactivated. * @param filter - The filter function to remove. */ unregister (filter: Filter) { this._filters.delete(filter); + if (this._filters.size === 0) { + this._deactivate(); + } } } From fd1851dbe0ea6bcf31c41927879703af8af9091a Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:42:13 +0900 Subject: [PATCH 2/9] feat: improve core API with documentation and type safety - Add comprehensive JSDoc comments for private properties and factory function - Improve timer ID type from number to ReturnType for cross-platform compatibility - Simplify timer calls from globalThis.setTimeout to setTimeout - Add type exports to src/index.ts (export type * from './blokr.ts') --- src/blokr.ts | 21 ++++++++++++++++++--- src/index.ts | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/blokr.ts b/src/blokr.ts index 8594fb7..5f989a2 100644 --- a/src/blokr.ts +++ b/src/blokr.ts @@ -11,10 +11,19 @@ export interface Options { const blokrs = new WeakMap(); class Blokr { + /** + * The target element whose interactions are to be managed. + */ private _target: Element | undefined; - private _timerId: number | undefined; + /** + * Timer ID for the optional timeout to auto-unlock interactions. + */ + private _timerId: ReturnType | undefined; + /** + * Filter function to determine which events to block. + */ private _filter: Filter | undefined; /** @@ -54,7 +63,7 @@ class Blokr { lock.register(this._filter); if (timeout > 0) { - this._timerId = globalThis.setTimeout(() => this.unlock(), timeout); + this._timerId = setTimeout(() => this.unlock(), timeout); } return true; @@ -74,7 +83,7 @@ class Blokr { */ unlock () { if (this._timerId) { - globalThis.clearTimeout(this._timerId); + clearTimeout(this._timerId); this._timerId = undefined; } if (this._filter) { @@ -84,6 +93,12 @@ class Blokr { } } +/** + * Factory function to get or create a Blokr instance for a given target element. + * @param target - Optional target element to manage interactions for. + * @returns Blokr instance associated with the target element. + * @public + */ const blokr = (target?: Element) => { return blokrs.get(target ?? globalThis) ?? (() => { const instance = new Blokr(target); diff --git a/src/index.ts b/src/index.ts index 3d2a29a..a29464e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export { default } from './blokr.ts'; +export type * from './blokr.ts'; From dc6e299e8aa4dcbb2535fed9473a40884ffbb050 Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:42:34 +0900 Subject: [PATCH 3/9] feat: add React Hook implementation - Implement useBlokr Hook for element-scoped or global user interaction blocking - Provide target ref, lock(), unlock(), and isLocked() methods - Support allowGlobal parameter for global lock mode - Enable event blocking at element scope or global scope --- src/react.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/react.ts diff --git a/src/react.ts b/src/react.ts new file mode 100644 index 0000000..a85d038 --- /dev/null +++ b/src/react.ts @@ -0,0 +1,39 @@ +import { useCallback, useRef } from 'react'; +import type { RefObject } from 'react'; +import blokr from './index.ts'; +import type { Options } from './index.ts'; + +/** + * React hook to block user interaction on a target element. + * @template T - The type of the target element. + * @param allowGlobal - If true, allows lock/unlock operations even when target ref is not attached. + * @returns An object containing the target ref and lock management functions. + * - `target`: A ref object to be attached to the target element. + * - `lock(options?)`: Function to lock the target element with optional settings. + * - `unlock()`: Function to unlock the target element. + * - `isLocked()`: Function to check if the target element is currently locked. + * @public + */ +export const useBlokr: (allowGlobal?: boolean) => { + target: RefObject; + lock: (options?: Options) => boolean; + unlock: () => void; + isLocked: () => boolean; +} = (allowGlobal = false) => { + const target = useRef(null); + const lock = useCallback( + (options?: Options) => target.current || allowGlobal ? blokr(target.current ?? undefined).lock(options) : false, + [allowGlobal] + ); + const unlock = useCallback(() => { + if (target.current || allowGlobal) { + blokr(target.current ?? undefined).unlock(); + } + }, [allowGlobal]); + const isLocked = useCallback( + () => target.current || allowGlobal ? blokr(target.current ?? undefined).isLocked() : false, + [allowGlobal] + ); + + return { target, lock, unlock, isLocked }; +}; From de8827a13c211955e2f5b817859adeb900f91660 Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:42:46 +0900 Subject: [PATCH 4/9] test: add comprehensive tests for React Hook - Add comprehensive test suite for useBlokr Hook (834 lines, ~60 test cases) - Cover basic behavior, ref assignment, function memoization, integration tests, and edge cases - Ensure full test coverage for React Hook implementation --- tests/useBlokr.test.tsx | 834 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 834 insertions(+) create mode 100644 tests/useBlokr.test.tsx diff --git a/tests/useBlokr.test.tsx b/tests/useBlokr.test.tsx new file mode 100644 index 0000000..d801e66 --- /dev/null +++ b/tests/useBlokr.test.tsx @@ -0,0 +1,834 @@ +import { describe, it, expect, vi } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import type { RefObject } from 'react'; +import { useBlokr } from '../src/react.ts'; +import blokr from '../src/index.ts'; + +/** + * Test helper to set ref.current value. + * In production, React automatically sets ref.current, but in tests we need to set it manually. + */ +const setRef = (ref: RefObject, element: T): void => { + Object.defineProperty(ref, 'current', { + value: element, + writable: true, + configurable: true + }); +}; + +describe('useBlokr Hook', () => { + describe('Basic Behavior', () => { + it('should return a tuple with ref and three functions', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock, isLocked } = result.current; + + expect(target).toBeDefined(); + expect(typeof lock).toBe('function'); + expect(typeof unlock).toBe('function'); + expect(typeof isLocked).toBe('function'); + + unmount(); + }); + + it('should have ref.current as null initially', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target } = result.current; + + expect(target.current).toBeNull(); + + unmount(); + }); + + it('should have lock, unlock, isLocked as functions', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { lock, unlock, isLocked } = result.current; + + expect(lock).toBeInstanceOf(Function); + expect(unlock).toBeInstanceOf(Function); + expect(isLocked).toBeInstanceOf(Function); + + unmount(); + }); + }); + + describe('Ref Assignment', () => { + it('should assign ref to DOM element correctly', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + expect(target.current).toBe(div); + + unmount(); + }); + + it('should handle multiple elements', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target } = result.current; + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + setRef(target, div1); + expect(target.current).toBe(div1); + + setRef(target, div2); + expect(target.current).toBe(div2); + + unmount(); + }); + + it('should support different element types via generics', () => { + const { result: resultDiv, unmount: unmountDiv } = renderHook(() => useBlokr()); + const { result: resultButton, unmount: unmountButton } = renderHook(() => useBlokr()); + + const div = document.createElement('div'); + const button = document.createElement('button'); + + setRef(resultDiv.current.target, div); + setRef(resultButton.current.target, button); + + expect(resultDiv.current.target.current).toBe(div); + expect(resultButton.current.target.current).toBe(button); + + unmountDiv(); + unmountButton(); + }); + }); + + describe('Lock Function', () => { + it('should return false if ref.current is null', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { lock } = result.current; + + let returnValue = true; + + act(() => { + returnValue = lock(); + }); + + expect(returnValue).toBe(false); + + unmount(); + }); + + it('should return true when ref.current is set and lock succeeds', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + let returnValue = false; + + act(() => { + returnValue = lock(); + }); + + expect(returnValue).toBe(true); + + act(() => { + unlock(); + }); + unmount(); + }); + + it('should pass options to blokr().lock()', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + const spy = vi.spyOn(blokr(div), 'lock'); + + act(() => { + lock({ scope: 'inside', timeout: 5000 }); + }); + + expect(spy).toHaveBeenCalledWith({ scope: 'inside', timeout: 5000 }); + + act(() => { + unlock(); + }); + spy.mockRestore(); + unmount(); + }); + + it('should accept scope option with all valid values', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + const spy = vi.spyOn(blokr(div), 'lock'); + + act(() => { + lock({ scope: 'inside' }); + }); + + expect(spy).toHaveBeenCalledWith({ scope: 'inside' }); + + act(() => { + unlock(); + }); + + act(() => { + lock({ scope: 'outside' }); + }); + + expect(spy).toHaveBeenCalledWith({ scope: 'outside' }); + + act(() => { + unlock(); + }); + + act(() => { + lock({ scope: 'self' }); + }); + + expect(spy).toHaveBeenCalledWith({ scope: 'self' }); + + act(() => { + unlock(); + }); + + spy.mockRestore(); + unmount(); + }); + + it('should return false if already locked', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + let firstLock: boolean | undefined; + let secondLock: boolean | undefined; + + act(() => { + firstLock = lock(); + }); + + act(() => { + secondLock = lock(); + }); + + expect(firstLock).toBe(true); + expect(secondLock).toBe(false); + + act(() => { + unlock(); + }); + unmount(); + }); + + it('should support timeout option', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + const spy = vi.spyOn(blokr(div), 'lock'); + + act(() => { + lock({ timeout: 3000 }); + }); + + expect(spy).toHaveBeenCalledWith({ timeout: 3000 }); + + act(() => { + unlock(); + }); + spy.mockRestore(); + unmount(); + }); + }); + + describe('Unlock Function', () => { + it('should not throw error if ref.current is null', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { unlock } = result.current; + + expect(() => { + act(() => { + unlock(); + }); + }).not.toThrow(); + + unmount(); + }); + + it('should unlock after lock', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock, isLocked } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + act(() => { + lock(); + }); + + let locked = isLocked(); + expect(locked).toBe(true); + + act(() => { + unlock(); + }); + + locked = isLocked(); + expect(locked).toBe(false); + + unmount(); + }); + + it('should not throw error if not locked', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + expect(() => { + act(() => { + unlock(); + }); + }).not.toThrow(); + + unmount(); + }); + }); + + describe('IsLocked Function', () => { + it('should return false if ref.current is null', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { isLocked } = result.current; + + const locked = isLocked(); + expect(locked).toBe(false); + + unmount(); + }); + + it('should return false when not locked', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, isLocked } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + const locked = isLocked(); + expect(locked).toBe(false); + + unmount(); + }); + + it('should return true when locked', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock, isLocked } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + act(() => { + lock(); + }); + + const locked = isLocked(); + expect(locked).toBe(true); + + act(() => { + unlock(); + }); + unmount(); + }); + + it('should return false after unlock', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock, isLocked } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + act(() => { + lock(); + }); + + act(() => { + unlock(); + }); + + const locked = isLocked(); + expect(locked).toBe(false); + + unmount(); + }); + }); + + describe('Function Memoization with useCallback', () => { + it('should memoize all functions across re-renders', () => { + const { result, rerender, unmount } = renderHook(() => useBlokr()); + const { lock: lock1, unlock: unlock1, isLocked: isLocked1 } = result.current; + + rerender(); + const { lock: lock2, unlock: unlock2, isLocked: isLocked2 } = result.current; + + expect(lock1).toBe(lock2); + expect(unlock1).toBe(unlock2); + expect(isLocked1).toBe(isLocked2); + + unmount(); + }); + + it('should maintain memoization through multiple re-renders', () => { + const { result, rerender, unmount } = renderHook(() => useBlokr()); + const { lock: lock1, unlock: unlock1, isLocked: isLocked1 } = result.current; + + for (let i = 0; i < 5; i++) { + rerender(); + } + + const { lock: lock2, unlock: unlock2, isLocked: isLocked2 } = result.current; + + expect(lock1).toBe(lock2); + expect(unlock1).toBe(unlock2); + expect(isLocked1).toBe(isLocked2); + + unmount(); + }); + }); + + describe('Integration Tests', () => { + it('should actually block events when locked', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + document.body.appendChild(div); + setRef(target, div); + + const handler = vi.fn(); + div.addEventListener('mousedown', handler); + + act(() => { + lock(); + }); + + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); + div.dispatchEvent(event); + + expect(handler).not.toHaveBeenCalled(); + + act(() => { + unlock(); + }); + div.removeEventListener('mousedown', handler); + document.body.removeChild(div); + unmount(); + }); + + it('should allow events when not locked', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target } = result.current; + + const div = document.createElement('div'); + document.body.appendChild(div); + setRef(target, div); + + const handler = vi.fn(); + div.addEventListener('mousedown', handler); + + const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); + div.dispatchEvent(event); + + expect(handler).toHaveBeenCalled(); + + // Cleanup + div.removeEventListener('mousedown', handler); + document.body.removeChild(div); + unmount(); + }); + + it('should block events with "inside" scope', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + const child = document.createElement('span'); + div.appendChild(child); + document.body.appendChild(div); + setRef(target, div); + + const divHandler = vi.fn(); + const childHandler = vi.fn(); + div.addEventListener('mousedown', divHandler); + child.addEventListener('mousedown', childHandler); + + act(() => { + lock({ scope: 'inside' }); + }); + + const divEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); + const childEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true }); + div.dispatchEvent(divEvent); + child.dispatchEvent(childEvent); + + expect(divHandler).not.toHaveBeenCalled(); + expect(childHandler).not.toHaveBeenCalled(); + + act(() => { + unlock(); + }); + div.removeEventListener('mousedown', divHandler); + child.removeEventListener('mousedown', childHandler); + document.body.removeChild(div); + unmount(); + }); + + it('should work independently with multiple instances', () => { + const { result: result1, unmount: unmount1 } = renderHook(() => useBlokr()); + const { result: result2, unmount: unmount2 } = renderHook(() => useBlokr()); + + const { target: target1, lock: lock1, unlock: unlock1, isLocked: isLocked1 } = result1.current; + const { target: target2, lock: lock2, unlock: unlock2, isLocked: isLocked2 } = result2.current; + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + setRef(target1, div1); + setRef(target2, div2); + + act(() => { + lock1(); + }); + + expect(isLocked1()).toBe(true); + expect(isLocked2()).toBe(false); + + act(() => { + lock2(); + }); + + expect(isLocked1()).toBe(true); + expect(isLocked2()).toBe(true); + + act(() => { + unlock1(); + }); + + expect(isLocked1()).toBe(false); + expect(isLocked2()).toBe(true); + + act(() => { + unlock2(); + }); + + unmount1(); + unmount2(); + }); + }); + + describe('Edge Cases', () => { + it('should handle rapid mount/unmount cycles', () => { + for (let i = 0; i < 10; i++) { + const { unmount } = renderHook(() => useBlokr()); + unmount(); + } + }); + + it('should clean up properly on unmount', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + act(() => { + lock(); + }); + + expect(result.current.isLocked()).toBe(true); + + act(() => { + unlock(); + }); + unmount(); + + expect(() => { + act(() => { + unlock(); + }); + }).not.toThrow(); + }); + + it('should be safe to call lock before element mount', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { lock } = result.current; + + let returnValue = true; + + act(() => { + returnValue = lock(); + }); + + expect(returnValue).toBe(false); + + unmount(); + }); + + it('should support generic type parameter with specific types', () => { + const { result: resultDiv, unmount: unmountDiv } = renderHook(() => useBlokr()); + const { result: resultButton, unmount: unmountButton } = renderHook(() => useBlokr()); + const { result: resultInput, unmount: unmountInput } = renderHook(() => useBlokr()); + + const div = document.createElement('div'); + const button = document.createElement('button'); + const input = document.createElement('input'); + + setRef(resultDiv.current.target, div); + setRef(resultButton.current.target, button); + setRef(resultInput.current.target, input); + + expect(resultDiv.current.target.current?.tagName).toBe('DIV'); + expect(resultButton.current.target.current?.tagName).toBe('BUTTON'); + expect(resultInput.current.target.current?.tagName).toBe('INPUT'); + + unmountDiv(); + unmountButton(); + unmountInput(); + }); + + it('should handle ref change gracefully', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock, isLocked } = result.current; + + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + setRef(target, div1); + + act(() => { + lock(); + }); + + expect(isLocked()).toBe(true); + + // Change ref to different element + setRef(target, div2); + + // New element should not be locked + const newIsLocked = isLocked(); + expect(newIsLocked).toBe(false); + + act(() => { + unlock(); + }); + unmount(); + }); + + it('should work with default generic parameter', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { target, lock, unlock } = result.current; + + const element = document.createElement('div'); + setRef(target, element); + + act(() => { + lock(); + }); + + expect(result.current.isLocked()).toBe(true); + + act(() => { + unlock(); + }); + unmount(); + }); + }); + + describe('allowGlobal Option', () => { + it('should work with allowGlobal=true when ref.current is null', () => { + const { result, unmount } = renderHook(() => useBlokr(true)); + const { lock, unlock, isLocked } = result.current; + + let returnValue = false; + + act(() => { + returnValue = lock(); + }); + + expect(returnValue).toBe(true); + expect(isLocked()).toBe(true); + + act(() => { + unlock(); + }); + + expect(isLocked()).toBe(false); + + unmount(); + }); + + it('should use global blokr instance when allowGlobal=true and ref is null', () => { + const { result, unmount } = renderHook(() => useBlokr(true)); + const { lock, unlock } = result.current; + + const spy = vi.spyOn(blokr(undefined), 'lock'); + + act(() => { + lock({ timeout: 5000 }); + }); + + expect(spy).toHaveBeenCalledWith({ timeout: 5000 }); + + act(() => { + unlock(); + }); + + spy.mockRestore(); + unmount(); + }); + + it('should prefer ref element over global when both allowGlobal=true and ref is set', () => { + const { result, unmount } = renderHook(() => useBlokr(true)); + const { target, lock, unlock } = result.current; + + const div = document.createElement('div'); + setRef(target, div); + + const globalSpy = vi.spyOn(blokr(undefined), 'lock'); + const elementSpy = vi.spyOn(blokr(div), 'lock'); + + act(() => { + lock(); + }); + + expect(elementSpy).toHaveBeenCalled(); + expect(globalSpy).not.toHaveBeenCalled(); + + act(() => { + unlock(); + }); + + globalSpy.mockRestore(); + elementSpy.mockRestore(); + unmount(); + }); + + it('should return false when allowGlobal=false (default) and ref.current is null', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { lock, isLocked } = result.current; + + let returnValue = true; + + act(() => { + returnValue = lock(); + }); + + expect(returnValue).toBe(false); + expect(isLocked()).toBe(false); + + unmount(); + }); + + it('should do nothing on unlock when allowGlobal=false and ref.current is null', () => { + const { result, unmount } = renderHook(() => useBlokr()); + const { unlock, isLocked } = result.current; + + expect(() => { + act(() => { + unlock(); + }); + }).not.toThrow(); + + expect(isLocked()).toBe(false); + + unmount(); + }); + + it('should recreate functions when allowGlobal value changes', () => { + const { result, rerender, unmount } = renderHook( + ({ allowGlobal }) => useBlokr(allowGlobal), + { initialProps: { allowGlobal: false } } + ); + + const { lock: lock1, unlock: unlock1, isLocked: isLocked1 } = result.current; + + rerender({ allowGlobal: true }); + + const { lock: lock2, unlock: unlock2, isLocked: isLocked2 } = result.current; + + // Functions should be recreated when allowGlobal changes + expect(lock1).not.toBe(lock2); + expect(unlock1).not.toBe(unlock2); + expect(isLocked1).not.toBe(isLocked2); + + unmount(); + }); + + it('should switch behavior when allowGlobal changes from true to false', () => { + const { result, rerender, unmount } = renderHook( + ({ allowGlobal }) => useBlokr(allowGlobal), + { initialProps: { allowGlobal: true } } + ); + + let returnValue = false; + + // With allowGlobal=true, should work even if ref.current is null + act(() => { + returnValue = result.current.lock(); + }); + + expect(returnValue).toBe(true); + + act(() => { + result.current.unlock(); + }); + + // Change to allowGlobal=false + rerender({ allowGlobal: false }); + + // With allowGlobal=false, should return false if ref.current is null + act(() => { + returnValue = result.current.lock(); + }); + + expect(returnValue).toBe(false); + + unmount(); + }); + + it('should not affect other instances when using global instance', () => { + const { result: result1, unmount: unmount1 } = renderHook(() => useBlokr(true)); + const { result: result2, unmount: unmount2 } = renderHook(() => useBlokr(true)); + + const { lock: lock1, unlock: unlock1, isLocked: isLocked1 } = result1.current; + const { isLocked: isLocked2 } = result2.current; + + // Both should use the same global instance + act(() => { + lock1(); + }); + + // Both should report as locked since they share the same global instance + expect(isLocked1()).toBe(true); + expect(isLocked2()).toBe(true); + + act(() => { + unlock1(); + }); + + // Both should report as unlocked + expect(isLocked1()).toBe(false); + expect(isLocked2()).toBe(false); + + unmount1(); + unmount2(); + }); + }); +}); From 69f5c1d6337f2b53de975279204a3090e338b3df Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:42:58 +0900 Subject: [PATCH 5/9] test: update existing tests for improved accuracy - Remove afterEach cleanup logic (unnecessary with Vitest browser mode auto-reset) - Remove redundant test cases and improve test names in blokr.test.ts - Change event-blocking.test.ts from mousedown to click event tests - Reflect core improvements from previous commits --- tests/blokr.test.ts | 29 +++++------------------------ tests/event-blocking.test.ts | 16 ++++------------ 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/tests/blokr.test.ts b/tests/blokr.test.ts index 4ef49a8..02ab0cb 100644 --- a/tests/blokr.test.ts +++ b/tests/blokr.test.ts @@ -1,15 +1,7 @@ -import { describe, it, expect, afterEach, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import blokr from '../src/blokr.ts'; describe('Blokr Factory Function', () => { - afterEach(() => { - // Clean up any locks after each test - const globalInstance = blokr(); - if (globalInstance.isLocked()) { - globalInstance.unlock(); - } - }); - describe('Factory Behavior', () => { it('should return a Blokr instance', () => { const instance = blokr(); @@ -196,18 +188,6 @@ describe('Blokr Factory Function', () => { vi.useRealTimers(); }); - it('should not set timeout when timeout is 0', () => { - const instance = blokr(); - const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout'); - - instance.lock({ timeout: 0 }); - - expect(setTimeoutSpy).not.toHaveBeenCalled(); - - setTimeoutSpy.mockRestore(); - instance.unlock(); - }); - it('should clear timeout on manual unlock', () => { vi.useFakeTimers(); @@ -282,11 +262,12 @@ describe('Blokr Factory Function', () => { instance.unlock(); }); - it('should use default scope "inside" for global instance', () => { + it('should block all events for global instance regardless of scope', () => { const instance = blokr(); - // Global instance with no target should still work (no filtering) - const result = instance.lock(); + // Global instance blocks all events, scope parameter is ignored + const result = instance.lock({ scope: 'outside' }); expect(result).toBe(true); + expect(instance.isLocked()).toBe(true); instance.unlock(); }); }); diff --git a/tests/event-blocking.test.ts b/tests/event-blocking.test.ts index b879116..fca787a 100644 --- a/tests/event-blocking.test.ts +++ b/tests/event-blocking.test.ts @@ -1,26 +1,18 @@ -import { describe, it, expect, afterEach, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import blokr from '../src/blokr.ts'; describe('Event Blocking Integration', () => { - afterEach(() => { - // Clean up any locks after each test - const globalInstance = blokr(); - if (globalInstance.isLocked()) { - globalInstance.unlock(); - } - }); - describe('Global Event Blocking', () => { - it('should block mousedown events when locked globally', () => { + it('should block click events when locked globally', () => { const handler = vi.fn(); const element = document.createElement('button'); - element.addEventListener('mousedown', handler); + element.addEventListener('click', handler); document.body.appendChild(element); const instance = blokr(); instance.lock(); - const event = new MouseEvent('mousedown', { + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); From 9355230acbbce5b0064e39141e2be51278278e63 Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:43:10 +0900 Subject: [PATCH 6/9] build: configure build system for React Hook support - Change to multi-entry configuration (index and react entry points) - Remove UMD format (ES modules only) - Configure React as external dependency - Change exports from 'default' to 'auto' - Add @vitejs/plugin-react plugin for React support --- vite.config.ts | 16 +++++++++++----- vitest.config.ts | 2 ++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index a43ea70..3b6f5bc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,20 +1,25 @@ import { defineConfig } from 'vite'; +import { resolve } from 'path'; import dts from 'vite-plugin-dts'; import terser from '@rollup/plugin-terser'; import license from 'rollup-plugin-license'; +import react from '@vitejs/plugin-react'; export default defineConfig({ build: { minify: false, lib: { - entry: 'src/index.ts', - name: 'blokr', - formats: ['es', 'umd'], - fileName: format => format === 'es' ? 'index.js' : 'blokr.js', + entry: { + index: resolve(__dirname, 'src/index.ts'), + react: resolve(__dirname, 'src/react.ts') + }, + formats: ['es'] }, rollupOptions: { + external: ['react'], output: { - exports: 'default', + entryFileNames: '[name].js', + exports: 'auto', plugins: [ license({ banner: '@license\nCopyright 2025 KNOWLEDGECODE\nSPDX-License-Identifier: MIT' @@ -25,6 +30,7 @@ export default defineConfig({ } }, plugins: [ + react(), dts({ include: ['src/**/*'] }) diff --git a/vitest.config.ts b/vitest.config.ts index 62a360d..5aeaddc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vitest/config'; import { playwright } from '@vitest/browser-playwright'; +import react from '@vitejs/plugin-react'; export default defineConfig({ + plugins: [react()], test: { browser: { enabled: true, From add05de679be460c361f2981adb826bd10d054c9 Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:43:24 +0900 Subject: [PATCH 7/9] chore: update ESLint configuration for React support - Add eslint-plugin-react-hooks for React Hook linting - Remove unused TypeScript rules - Add new rule: @typescript-eslint/no-empty-function: 'off' --- eslint.config.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 098189c..3ecdbd4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,7 @@ import { defineConfig } from 'eslint/config'; import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; import stylistic from '@stylistic/eslint-plugin'; +import reactHooks from 'eslint-plugin-react-hooks'; export default defineConfig( { @@ -16,6 +17,7 @@ export default defineConfig( eslint.configs.recommended, tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked, + reactHooks.configs.flat.recommended, { plugins: { '@stylistic': stylistic @@ -29,10 +31,7 @@ export default defineConfig( }, 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/restrict-template-expressions': ['error', { allowNever: true }], - '@typescript-eslint/unified-signatures': ['error', { ignoreDifferentlyNamedParameters: true }], + '@typescript-eslint/no-empty-function': 'off', 'accessor-pairs': 'error', 'array-callback-return': 'error', From 144ece8320f2a034c625dbcc2a9ae0704e0c8a4c Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:43:50 +0900 Subject: [PATCH 8/9] chore: update package configuration for v0.4.0 release - Bump version from 0.3.0 to 0.4.0 - Add "sideEffects": false for tree-shaking support - Add ./react entry point to exports field - Add React 18/19 as optional peer dependencies - Add clean command to build scripts - Add new dev dependencies for React support - Update multiple package versions --- package-lock.json | 1180 ++++++++++++++++++++++++++++++++++++++------- package.json | 38 +- 2 files changed, 1034 insertions(+), 184 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc3a209..6b612b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,218 @@ { "name": "blokr", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blokr", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.39.2", "@rollup/plugin-terser": "^0.4.4", - "@stylistic/eslint-plugin": "^5.6.1", - "@vitest/browser": "^4.0.16", - "@vitest/browser-playwright": "^4.0.16", + "@stylistic/eslint-plugin": "^5.7.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^25.1.0", + "@types/react": "^19.2.10", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/browser-playwright": "^4.0.18", "eslint": "^9.39.2", - "playwright": "^1.57.0", + "eslint-plugin-react-hooks": "^7.0.1", + "playwright": "^1.58.0", + "react": "^19.2.3", "rollup-plugin-license": "^3.6.0", - "typescript-eslint": "^8.51.0", - "vite": "^7.3.0", + "typescript-eslint": "^8.54.0", + "vite": "^7.3.1", "vite-plugin-dts": "^4.5.4", - "vitest": "^4.0.16" + "vitest": "^4.0.18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { @@ -33,23 +226,47 @@ } }, "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -58,15 +275,91 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -515,9 +808,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -547,9 +840,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -758,6 +1051,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -931,6 +1235,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", @@ -1421,14 +1732,14 @@ "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==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.7.1.tgz", + "integrity": "sha512-zjTUwIsEfT+k9BmXwq1QEFYsb4afBlsI1AXFyWQBgggMzwBFOuu92pGrE5OFx90IOjNl+lUbQoTG7f8S0PkOdg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.0", - "@typescript-eslint/types": "^8.47.0", + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.53.1", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", @@ -1441,6 +1752,55 @@ "eslint": ">=9.0.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@types/argparse": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", @@ -1448,6 +1808,59 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1480,21 +1893,41 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz", + "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "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==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.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", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1504,7 +1937,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.51.0", + "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1520,17 +1953,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", - "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1545,15 +1978,15 @@ } }, "node_modules/@typescript-eslint/project-service": { - "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==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.51.0", - "@typescript-eslint/types": "^8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1567,14 +2000,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", - "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0" + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1585,9 +2018,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "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==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", "dev": true, "license": "MIT", "engines": { @@ -1602,17 +2035,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", - "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", "dev": true, "license": "MIT", "dependencies": { - "@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.2.0" + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1627,9 +2060,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", - "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", "dev": true, "license": "MIT", "engines": { @@ -1641,21 +2074,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", - "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", "dev": true, "license": "MIT", "dependencies": { - "@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", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1695,16 +2128,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", - "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1719,13 +2152,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "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==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1736,15 +2169,36 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@vitest/browser": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.16.tgz", - "integrity": "sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz", + "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/mocker": "4.0.16", - "@vitest/utils": "4.0.16", + "@vitest/mocker": "4.0.18", + "@vitest/utils": "4.0.18", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", @@ -1756,18 +2210,18 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.16" + "vitest": "4.0.18" } }, "node_modules/@vitest/browser-playwright": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.16.tgz", - "integrity": "sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz", + "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/browser": "4.0.16", - "@vitest/mocker": "4.0.16", + "@vitest/browser": "4.0.18", + "@vitest/mocker": "4.0.18", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1775,7 +2229,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "4.0.16" + "vitest": "4.0.18" }, "peerDependenciesMeta": { "playwright": { @@ -1784,16 +2238,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", - "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -1802,13 +2256,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", - "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.16", + "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1829,9 +2283,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", - "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { @@ -1842,13 +2296,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", - "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.16", + "@vitest/utils": "4.0.18", "pathe": "^2.0.3" }, "funding": { @@ -1856,13 +2310,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", - "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", + "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1871,9 +2325,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", - "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, "license": "MIT", "funding": { @@ -1881,13 +2335,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", - "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", + "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" }, "funding": { @@ -2113,6 +2567,17 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2136,6 +2601,17 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -2163,6 +2639,16 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2174,6 +2660,40 @@ "concat-map": "0.0.1" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2191,6 +2711,27 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2273,6 +2814,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2288,6 +2836,13 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -2320,6 +2875,32 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.278", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz", + "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==", + "dev": true, + "license": "ISC" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2382,6 +2963,16 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2455,6 +3046,26 @@ } } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -2723,6 +3334,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2789,6 +3410,23 @@ "he": "bin/he" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2889,6 +3527,13 @@ "dev": true, "license": "MIT" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2902,6 +3547,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2923,6 +3581,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -3002,9 +3673,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -3028,6 +3699,17 @@ "node": ">=10" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3143,6 +3825,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -3317,13 +4006,13 @@ } }, "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", + "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0" + "playwright-core": "1.58.0" }, "bin": { "playwright": "cli.js" @@ -3336,9 +4025,9 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3397,6 +4086,36 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3434,6 +4153,48 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3559,6 +4320,14 @@ ], "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -3875,9 +4644,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", - "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -3916,16 +4685,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", - "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3946,6 +4715,13 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -3956,6 +4732,37 @@ "node": ">= 10.0.0" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3967,9 +4774,9 @@ } }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { @@ -4084,19 +4891,19 @@ } }, "node_modules/vitest": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", - "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", "dependencies": { - "@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", + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", @@ -4124,10 +4931,10 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.16", - "@vitest/browser-preview": "4.0.16", - "@vitest/browser-webdriverio": "4.0.16", - "@vitest/ui": "4.0.16", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, @@ -4212,9 +5019,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": { @@ -4252,6 +5059,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index d3412da..1a69d65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "blokr", - "version": "0.3.0", + "version": "0.4.0", + "sideEffects": false, "description": "Lightweight library to block user interactions in browsers", "keywords": [ "event", @@ -34,6 +35,10 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./react": { + "types": "./dist/react.d.ts", + "import": "./dist/react.js" } }, "files": [ @@ -42,23 +47,38 @@ "README.md" ], "scripts": { - "build": "vite build", + "build": "npm run clean && vite build", + "clean": "rm -rf dist", "dev": "vite", "lint": "eslint", "test": "vitest run", "prepublishOnly": "npm run build" }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + }, "devDependencies": { + "@eslint/js": "^9.39.2", "@rollup/plugin-terser": "^0.4.4", - "@stylistic/eslint-plugin": "^5.6.1", - "@vitest/browser": "^4.0.16", - "@vitest/browser-playwright": "^4.0.16", + "@stylistic/eslint-plugin": "^5.7.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^25.1.0", + "@types/react": "^19.2.10", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/browser-playwright": "^4.0.18", "eslint": "^9.39.2", - "playwright": "^1.57.0", + "eslint-plugin-react-hooks": "^7.0.1", + "playwright": "^1.58.0", + "react": "^19.2.3", "rollup-plugin-license": "^3.6.0", - "typescript-eslint": "^8.51.0", - "vite": "^7.3.0", + "typescript-eslint": "^8.54.0", + "vite": "^7.3.1", "vite-plugin-dts": "^4.5.4", - "vitest": "^4.0.16" + "vitest": "^4.0.18" } } From e61f9622604c1daf821bb725c367e5f3dcdc8cda Mon Sep 17 00:00:00 2001 From: knowledgecode Date: Thu, 29 Jan 2026 20:44:08 +0900 Subject: [PATCH 9/9] docs: update documentation for v0.4.0 - Add v0.4.0 new features section (React Hook support) - Update Breaking Changes section (UMD format removal) - Expand "Why Blokr?" section with comparison table of alternative solutions - Add React Hook section with usage examples, API, and allowGlobal parameter - Remove CDN UMD examples - Simplify "Limitations" section --- README.md | 218 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 163 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 95af95a..28209c0 100644 --- a/README.md +++ b/README.md @@ -5,50 +5,63 @@ 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 -- **All interaction types**: Blocks mouse, keyboard, touch, and wheel events +- **Scope filtering**: Control which events to block (`inside`, `outside`, `self`) - **Per-lock timeout**: Optional automatic unlock after specified time -- **No dependencies**: Zero external dependencies - **TypeScript**: Full type support included +- **React Hook**: Built-in `useBlokr()` hook for React components ## Why Blokr? -### Problems with CSS-based Solutions +### Comparison with Alternative Solutions + +Blokr provides a unique approach to blocking user interactions. Here's how it compares with other techniques: + +#### The `inert` Attribute + +The HTML5 `inert` attribute marks an element as "inert," preventing user interactions including keyboard navigation. + +#### CSS `pointer-events: none` + +CSS `pointer-events: none` disables mouse and touch events on elements, but cannot block keyboard events or prevent tab navigation. + +#### The `` Element + +The HTML5 `` element creates a modal dialog but adds a DOM element and provides limited scope flexibility for non-modal use cases. + +#### Comparison Summary + +| Feature | Blokr | inert | pointer-events | dialog | +|---------|-------|-------|----------------|--------| +| Blocks keyboard events | ✅ | ✅ | ❌ | ✅ | +| Global interaction lock | ✅ | ❌ | ❌ | ❌ | +| Inside/outside scope | ✅ | ❌ | ❌ | ❌ | +| Timeout protection | ✅ | ❌ | ❌ | ❌ | +| No DOM overlay | ✅ | ✅ | ✅ | ❌ | +| No DOM modifications | ✅ | ❌ | ✅ | ❌ | + +**Key differentiators:** + +- **Global interaction lock**: Blokr can block interactions across the entire page, not just within specific elements +- **Inside/outside scope**: Unique ability to selectively block events inside or outside a target element +- **Timeout protection**: Automatic unlock prevents permanent locks due to errors or forgotten cleanup +- **No DOM modifications**: Works purely via event listeners without modifying DOM structure or attributes -While CSS `pointer-events: none` can disable interactions, it has several limitations: +## What's New in v0.4.0 -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 +- **React Hook support**: New `useBlokr()` hook for React applications (React 18+ required) -### How Blokr Solves These Problems +## ⚠️ Breaking Changes in v0.4.0 -- ✅ **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 +- **UMD format removed**: CDN usage now requires ES modules only (`blokr/dist/index.js`) +- **No breaking changes to core API**: All v0.3.0 JavaScript APIs remain unchanged + +For changes from v0.2.x, see the [Migration from v0.2.x](#migration-from-v02x) section below. + +**Note:** This library is under active development. Future versions may introduce additional breaking changes. Please refer to the changelog before upgrading. ## Installation @@ -56,15 +69,26 @@ While CSS `pointer-events: none` can disable interactions, it has several limita npm install blokr ``` -## Usage +### React Hook Support + +The `useBlokr()` React Hook is included in the same package. React 18.0+ or React 19.0+ is required to use the hook: + +```bash +npm install blokr react +``` + +The `react` package is an optional peer dependency. If you don't use React, you can ignore this requirement. + +## Usage (Vanilla) -### Basic Usage (ES Modules) +### Basic Usage ```typescript import blokr from 'blokr'; // Global lock - blocks all user interactions const instance = blokr(); + instance.lock(); // Check if locked @@ -90,7 +114,7 @@ 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 +instance.lock({ scope: 'self' }); // Block events on the container only ``` ### Auto-timeout @@ -107,21 +131,6 @@ instance.lock({ timeout: 5000 }); instance.lock({ timeout: 0 }); ``` -### CDN Usage (UMD) - -```html - - -``` - ### CDN Usage (ES Modules) ```html @@ -166,9 +175,9 @@ Locks user interactions. Returns `true` if lock was applied, `false` if already **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 + - `'inside'`: Block events inside the target element (default) + - `'outside'`: Block events outside the target element + - `'self'`: Block events on the target element only **Returns:** `true` if lock was applied, `false` if already locked @@ -320,6 +329,107 @@ async function loadData() { } ``` +## React Hook + +The `useBlokr()` hook provides a React-friendly way to manage user interaction blocking. It works seamlessly with the factory-based API and manages refs automatically. + +### Import + +```typescript +import { useBlokr } from 'blokr/react'; +``` + +### Basic Usage + +```tsx +import { useBlokr } from 'blokr/react'; + +export function PageWithLinks() { + const { target, lock, unlock, isLocked } = useBlokr(); + + const handleLock = () => { + lock({ timeout: 5000 }); // Auto-unlock after 5 seconds + }; + + return ( + <> + + + + + ); +} +``` + +### Options + +The `lock()` function accepts the same options as the core API: + +```tsx +const { target, lock, unlock } = useBlokr(); + +// With timeout (auto-unlock after 5 seconds) +lock({ timeout: 5000 }); + +// With scope +lock({ scope: 'inside' }); // Block inside the element +lock({ scope: 'outside' }); // Block outside the element +lock({ scope: 'self' }); // Block on the element only + +// With both options +lock({ scope: 'inside', timeout: 5000 }); +``` + +### Hook API + +#### `useBlokr(allowGlobal?: boolean): { target: RefObject; lock: (options?: Options) => boolean; unlock: () => void; isLocked: () => boolean }` + +Returns an object containing a ref and three control functions for managing user interaction blocking. + +**Type Parameters:** +- `T` (optional): The DOM element type. Default: `Element` + +**Parameters:** +- `allowGlobal` (optional): If `true`, enables global lock mode that blocks interactions across the entire page instead of a specific element. When using global lock, the `target` ref is not needed. Default: `false` + +**Returns:** An object with: +- `target`: A React ref to assign to the target element (`RefObject`) +- `lock`: Function to lock user interactions on the element (`(options?: Options) => boolean`) +- `unlock`: Function to unlock user interactions (`() => void`) +- `isLocked`: Function to check if currently locked (`() => boolean`) + +**Parameters (lock function):** +- `options.timeout` (optional): Auto-unlock timeout in milliseconds +- `options.scope` (optional): Event blocking scope (`'inside'`, `'outside'`, or `'self'`) + +**Returns (lock function):** `true` if lock was applied, `false` if already locked or if the ref is not set (when using element-specific lock) + +### allowGlobal Parameter + +The `allowGlobal` parameter enables global lock mode, which blocks user interactions across the entire page instead of scoping to a specific element. + +**Global Lock (`allowGlobal=true`):** +```tsx +// No need to destructure 'target' since we're not using element-specific locking +const { lock, unlock, isLocked } = useBlokr(true); + +// Locks all interactions across the entire page +lock(); // Blocks all user interactions globally +``` + +**Element-Specific Lock (Default: `allowGlobal=false`):** +```tsx +const { target, lock, unlock, isLocked } = useBlokr(); + +// Attach target to an element +
Content
+ +// Lock only affects this specific element (by default, scope='inside') +lock(); // Blocks interactions inside the div +``` + ## Migration from v0.2.x ### API Changes @@ -374,9 +484,7 @@ instance.lock({ scope: 'self' }); ## Limitations -- **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). +- **Event listener priority**: Event listeners are registered at the capture phase. May not work correctly when used with event delegation libraries. ## License