From 29f5f3b8fa67f337e21c7c8461e277beb12635ac Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Mon, 17 Aug 2026 04:51:42 +0100 Subject: [PATCH 01/26] feat: Add gesture support to lightbox --- src/ImageGallery.test.tsx | 193 ++++++++++++++++++++- src/ImageGallery.tsx | 295 +++----------------------------- src/hooks/useCarouselGesture.ts | 162 ++++++++++++++++++ src/lightbox/Caption.tsx | 26 +++ src/lightbox/CarouselTrack.tsx | 222 ++++++++++++++++++++++++ src/lightbox/Lightbox.tsx | 157 +++++++++++++++++ src/lightbox/Navigation.tsx | 43 +++++ src/lightbox/ThumbnailStrip.tsx | 60 +++++++ src/lightbox/Toolbar.tsx | 83 +++++++++ src/lightbox/index.ts | 1 + src/style.css | 205 +++++++++++++++++++--- src/utils/clamp.ts | 3 + 12 files changed, 1151 insertions(+), 299 deletions(-) create mode 100644 src/hooks/useCarouselGesture.ts create mode 100644 src/lightbox/Caption.tsx create mode 100644 src/lightbox/CarouselTrack.tsx create mode 100644 src/lightbox/Lightbox.tsx create mode 100644 src/lightbox/Navigation.tsx create mode 100644 src/lightbox/ThumbnailStrip.tsx create mode 100644 src/lightbox/Toolbar.tsx create mode 100644 src/lightbox/index.ts create mode 100644 src/utils/clamp.ts diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index fd90595..fcbe598 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -1,7 +1,78 @@ -import { render } from "@testing-library/react"; -import { test } from "@jest/globals"; +import { expect, test, beforeAll, jest } from "@jest/globals"; +import { act, fireEvent, render, screen } from "@testing-library/react"; import { ImageGallery } from "./ImageGallery"; +beforeAll(() => { + HTMLDialogElement.prototype.showModal = function () { + this.setAttribute("open", ""); + }; + HTMLDialogElement.prototype.close = function () { + this.removeAttribute("open"); + }; + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: true, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + + Element.prototype.scrollIntoView = jest.fn(); + Element.prototype.setPointerCapture = jest.fn(); + Element.prototype.releasePointerCapture = jest.fn(); + + class MockPointerEvent extends Event { + clientX: number; + clientY: number; + isPrimary: boolean; + pointerId: number; + constructor(type: string, props: any) { + super(type, props); + this.clientX = props?.clientX || 0; + this.clientY = props?.clientY || 0; + this.isPrimary = props?.isPrimary || false; + this.pointerId = props?.pointerId || 0; + } + } + window.PointerEvent = MockPointerEvent as any; +}); + +beforeAll(() => { + HTMLDialogElement.prototype.showModal = jest.fn(function mock( + this: HTMLDialogElement + ) { + this.open = true; + this.setAttribute('open', ''); + }); + HTMLDialogElement.prototype.close = jest.fn(function mock( + this: HTMLDialogElement + ) { + this.open = false; + this.removeAttribute('open'); + }); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: query === '(prefers-reduced-motion: reduce)', + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); +}); + const imagesArray = [ { id: crypto.randomUUID(), @@ -181,3 +252,121 @@ test("customizing image click action with the built-in imageData and index param />, ); }); + +test("lightbox navigation updates counter and loops correctly", async () => { + render(); + + // Open lightbox on image 9 + const img9 = screen.getAllByAltText("Image9's alt text")[0]; + fireEvent.click(img9); + + // Initial state should be 9 / 13 + expect(await screen.findByText("9 / 13")).toBeTruthy(); + + // Click next button + const nextBtn = screen.getByTitle("Next image"); + act(() => { + fireEvent.click(nextBtn); + }); + expect(await screen.findByText("10 / 13")).toBeTruthy(); + + // Click previous button twice to go to 8 + const prevBtn = screen.getByTitle("Previous image"); + act(() => { + fireEvent.click(prevBtn); + }); + act(() => { + fireEvent.click(prevBtn); + }); + expect(await screen.findByText("8 / 13")).toBeTruthy(); +}); + +test("lightbox looping boundary 13 -> 1 and 1 -> 13", async () => { + render(); + + // Open lightbox on last image + const img13 = screen.getAllByAltText("Image13's alt text")[0]; + fireEvent.click(img13); + expect(await screen.findByText("13 / 13")).toBeTruthy(); + + // Click next -> should loop to 1 + const nextBtn = screen.getByTitle("Next image"); + act(() => { + fireEvent.click(nextBtn); + }); + expect(screen.getByText("1 / 13")).toBeTruthy(); + + // Click prev -> should loop back to 13 + const prevBtn = screen.getByTitle("Previous image"); + act(() => { + fireEvent.click(prevBtn); + }); + expect(screen.getByText("13 / 13")).toBeTruthy(); +}); + +test("lightbox gesture navigation threshold and state update", async () => { + render(); + + // Open lightbox on image 1 + const img1 = screen.getAllByAltText("Image1's alt text")[0]; + fireEvent.click(img1); + expect(await screen.findByText("1 / 13")).toBeTruthy(); + + // Find the track container + const track = document.querySelector(".cs-rigg-carousel-track") as HTMLElement; + if (!track) throw new Error("Track not found"); + + // Mock getBoundingClientRect for trackWidth + track.getBoundingClientRect = () => ({ width: 1000 } as any); + + const originalNow = performance.now; + let currentTime = 0; + jest.spyOn(performance, 'now').mockImplementation(() => currentTime); + + // 1. Short slow drag (should cancel) - deltaX < 200, duration > 250 + act(() => { + currentTime = 0; + fireEvent.pointerDown(track, { isPrimary: true, clientX: 500, clientY: 500, pointerId: 1 }); + }); + act(() => { + currentTime = 500; + fireEvent.pointerMove(track, { isPrimary: true, clientX: 450, clientY: 500, pointerId: 1 }); // deltaX = -50 + }); + act(() => { + currentTime = 510; + fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); + }); + expect(await screen.findByText("1 / 13")).toBeTruthy(); // Cancelled + + // 2. Long slow drag (should commit next) - deltaX < -200, duration > 250 + act(() => { + currentTime = 1000; + fireEvent.pointerDown(track, { isPrimary: true, clientX: 500, clientY: 500, pointerId: 1 }); + }); + act(() => { + currentTime = 1500; + fireEvent.pointerMove(track, { isPrimary: true, clientX: 200, clientY: 500, pointerId: 1 }); // deltaX = -300 + }); + act(() => { + currentTime = 1510; + fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); + }); + expect(await screen.findByText("2 / 13")).toBeTruthy(); + + // 3. Short fast flick (should commit next) - deltaX < 200, duration < 250 + act(() => { + currentTime = 2000; + fireEvent.pointerDown(track, { isPrimary: true, clientX: 500, clientY: 500, pointerId: 1 }); + }); + act(() => { + currentTime = 2050; // 50ms duration + fireEvent.pointerMove(track, { isPrimary: true, clientX: 450, clientY: 500, pointerId: 1 }); // deltaX = -50 + }); + act(() => { + currentTime = 2060; + fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); + }); + expect(await screen.findByText("3 / 13")).toBeTruthy(); + + performance.now = originalNow; +}); diff --git a/src/ImageGallery.tsx b/src/ImageGallery.tsx index 6fc0be2..5598a7b 100644 --- a/src/ImageGallery.tsx +++ b/src/ImageGallery.tsx @@ -1,10 +1,7 @@ -import { useRef, useState, useEffect } from "react"; -import { flushSync } from "react-dom"; -import { SvgElement, updateCaptionOpacity } from "./helpers.ts"; -import { - ImageGalleryPropsType, - ImgSrcInfoType, -} from "./ImageGallery.types.tsx"; +import { useState } from "react"; +import { updateCaptionOpacity } from "./helpers.ts"; +import { ImageGalleryPropsType } from "./ImageGallery.types.tsx"; +import { Lightbox } from "./lightbox"; export function ImageGallery({ columnCount = "auto", @@ -18,49 +15,20 @@ export function ImageGallery({ lazyFromIndex = 6, thumbnailBorder = "3px solid #fff", }: ImageGalleryPropsType) { - const [imgSrcInfo, setImgSrcInfo] = useState(null); + const [lightboxOpen, setLightboxOpen] = useState(false); const [slideNumber, setSlideNumber] = useState(1); - const [showModalControls, setShowModalControls] = useState(false); const [showThumbnails, setShowThumbnails] = useState(false); - const [fullscreen, setFullscreen] = useState(false); - const dialogRef = useRef(null); - const lightboxRef = useRef(null); - const activeThumbImgRef = useRef(null); - function switchFullScreen(on: boolean) { - if (on) { - lightboxRef.current?.requestFullscreen().catch((error) => { - alert( - `Error while attempting to switch into fullscreen mode: ${error.message} (${error.name})`, - ); - }); - } - if (!on) { - document.exitFullscreen().catch((error) => console.error(error)); - } - } - - function openLightboxOnSlide( - number: number, - src: string, - srcSet?: string, - mediaSizes?: string, - ) { - setImgSrcInfo({ src, srcSet, mediaSizes }); + function openLightboxOnSlide(number: number) { setSlideNumber(number); - dialogRef.current?.showModal(); + setLightboxOpen(true); } function showImageCards() { - const imageElementsArray = imagesData.map((imageData, index) => { + return imagesData.map((imageData, index) => { function handleImageClick() { enableDefaultLightbox - ? openLightboxOnSlide( - index + 1, - imageData.src, - imageData.srcSet, - imageData.mediaSizes, - ) + ? openLightboxOnSlide(index + 1) : customizeImageClickAction(imageData, index); } if (imageData.id) { @@ -108,244 +76,14 @@ export function ImageGallery({ ); } return ( -
+
Error: Each item in the `imagesArray` needs a unique `id`
); }); - return imageElementsArray; - } - - function changeSlide(thumbClick: boolean, step: number) { - const totalImages = imagesData.length; - let newSlideNumber = thumbClick ? step + 1 : slideNumber + step; - - newSlideNumber < 1 && (newSlideNumber = totalImages); - newSlideNumber > totalImages && (newSlideNumber = 1); - - if (newSlideNumber <= totalImages && newSlideNumber > 0) { - const imageData = imagesData[newSlideNumber - 1]; - setSlideNumber(newSlideNumber); - setImgSrcInfo({ - src: imageData.src, - srcSet: imageData.srcSet, - mediaSizes: imageData.mediaSizes, - }); - } - } - - function scrollImage( - thumbClick: boolean, - direction: number, - imgIndex: number, - ) { - const step = thumbClick ? imgIndex : direction; - flushSync(() => changeSlide(thumbClick, step)); - activeThumbImgRef.current?.scrollIntoView({ - behavior: "smooth", - block: "nearest", - inline: "center", - }); - } - - function handleKeyDownOnModal(e: React.KeyboardEvent) { - e.key === "ArrowLeft" && scrollImage(false, -1, 0); - e.key === "ArrowRight" && scrollImage(false, 1, 0); - e.key === "f" && fullscreen && switchFullScreen(false); - e.key === "f" && !fullscreen && switchFullScreen(true); - } - - function exitFullScreenAndDialog() { - fullscreen && switchFullScreen(false); - dialogRef.current?.close(); - } - - function showLightBox() { - const imageData = imagesData[slideNumber - 1]; - return ( - -
handleKeyDownOnModal(e)} - onMouseEnter={() => setShowModalControls(true)} - onMouseLeave={() => setShowModalControls(false)} - onClick={(e) => - (e.target as HTMLElement).tagName === "SECTION" && - exitFullScreenAndDialog() - } - > - {`${slideNumber} / ${imagesData.length}`} - - - - - - -
- -
- fixedCaption ? undefined : updateCaptionOpacity(e, "1") - } - onMouseLeave={(e) => - fixedCaption ? undefined : updateCaptionOpacity(e, "0") - } - > - {imageData.alt} - {imageData.caption ? ( -
-
{imageData.caption}
- {imageData?.cta?.href && imageData?.cta?.text && ( - - )} -
- ) : ( - "" - )} -
- -
-
-
- {imagesData.map((imageData, index) => ( - {imageData.alt} scrollImage(true, 0, index)} - /> - ))} -
-
-
-
- ); } - useEffect(() => { - function handleFullscreenChange() { - setFullscreen(Boolean(document.fullscreenElement)); - lightboxRef.current?.focus(); - } - document.addEventListener("fullscreenchange", handleFullscreenChange); - return () => - document.removeEventListener("fullscreenchange", handleFullscreenChange); - }, []); - - useEffect(() => { - dialogRef.current?.open && - (document.documentElement.style.overflow = "hidden"); - !dialogRef.current?.open && (document.documentElement.style.overflow = ""); - }); - return (
{showImageCards()} - {enableDefaultLightbox && showLightBox()} + {enableDefaultLightbox && ( + setLightboxOpen(false)} + showThumbnails={showThumbnails} + setShowThumbnails={setShowThumbnails} + thumbnailBorder={thumbnailBorder} + lazy={lazy} + /> + )}
); } diff --git a/src/hooks/useCarouselGesture.ts b/src/hooks/useCarouselGesture.ts new file mode 100644 index 0000000..e4135aa --- /dev/null +++ b/src/hooks/useCarouselGesture.ts @@ -0,0 +1,162 @@ +import { useRef, useCallback, useEffect } from 'react'; + +export interface DragResult { + action: "next" | "prev" | "cancel"; +} + +interface UseCarouselGestureProps { + trackRef: React.RefObject; + onDragEnd: (result: DragResult) => void; + canNavigatePrev: boolean; + canNavigateNext: boolean; +} + +export function useCarouselGesture({ + trackRef, + onDragEnd, + canNavigatePrev, + canNavigateNext, +}: UseCarouselGestureProps) { + const isDragging = useRef(false); + const startX = useRef(0); + const startY = useRef(0); + const currentX = useRef(0); + const startTime = useRef(0); + const isHorizontalIntent = useRef(null); + const rafId = useRef(null); + const trackWidth = useRef(0); + + const resetState = useCallback(() => { + isDragging.current = false; + isHorizontalIntent.current = null; + currentX.current = 0; + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current); + rafId.current = null; + } + }, []); + + const updateTransform = useCallback((x: number) => { + if (trackRef.current) { + trackRef.current.style.transform = `translate3d(${x}px, 0, 0)`; + } + }, [trackRef]); + + const handlePointerDown = useCallback((e: React.PointerEvent) => { + if (!e.isPrimary) return; + + if (trackRef.current) { + trackRef.current.style.transition = ''; + trackWidth.current = trackRef.current.getBoundingClientRect().width; + if (typeof (e.target as HTMLElement).setPointerCapture === "function") { + (e.target as HTMLElement).setPointerCapture(e.pointerId); + } + } + + isDragging.current = true; + startX.current = e.clientX; + startY.current = e.clientY; + currentX.current = 0; + startTime.current = performance.now(); + isHorizontalIntent.current = null; + + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current); + } + }, [trackRef]); + + const handlePointerMove = useCallback((e: React.PointerEvent) => { + if (!isDragging.current || !e.isPrimary) return; + + const deltaX = e.clientX - startX.current; + const deltaY = e.clientY - startY.current; + + if (isHorizontalIntent.current === null) { + if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { + isHorizontalIntent.current = Math.abs(deltaX) > Math.abs(deltaY); + } + } + + if (isHorizontalIntent.current === false) { + return; + } + + if (isHorizontalIntent.current) { + e.preventDefault(); + + let newX = deltaX; + + if (newX > 0 && !canNavigatePrev) { + newX = newX * 0.25; + } else if (newX < 0 && !canNavigateNext) { + newX = newX * 0.25; + } + + currentX.current = newX; + + if (rafId.current === null) { + rafId.current = requestAnimationFrame(() => { + updateTransform(currentX.current); + rafId.current = null; + }); + } + } + }, [canNavigateNext, canNavigatePrev, updateTransform]); + + const handlePointerUp = useCallback((e: React.PointerEvent) => { + if (!isDragging.current || !e.isPrimary) return; + + const duration = performance.now() - startTime.current; + const deltaX = currentX.current; + const absDeltaX = Math.abs(deltaX); + const velocity = absDeltaX / duration; + + const threshold = trackWidth.current * 0.2; + const isFastFlick = velocity > 0.5 && duration < 250; + const passedThreshold = absDeltaX > threshold; + + const wasHorizontal = isHorizontalIntent.current; + + resetState(); + + if (trackRef.current) { + (e.target as HTMLElement).releasePointerCapture(e.pointerId); + } + + if (wasHorizontal) { + let action: "next" | "prev" | "cancel" = "cancel"; + if ((passedThreshold || isFastFlick) && deltaX > 0 && canNavigatePrev) { + action = "prev"; + } else if ((passedThreshold || isFastFlick) && deltaX < 0 && canNavigateNext) { + action = "next"; + } + onDragEnd({ action }); + } + }, [canNavigateNext, canNavigatePrev, onDragEnd, resetState, trackRef]); + + const handlePointerCancel = useCallback((e: React.PointerEvent) => { + if (!isDragging.current || !e.isPrimary) return; + + resetState(); + if (trackRef.current) { + (e.target as HTMLElement).releasePointerCapture(e.pointerId); + } + onDragEnd({ action: "cancel" }); + }, [onDragEnd, resetState, trackRef]); + + useEffect(() => { + return () => { + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current); + } + }; + }, []); + + return { + onPointerDown: handlePointerDown, + onPointerMove: handlePointerMove, + onPointerUp: handlePointerUp, + onPointerCancel: handlePointerCancel, + onLostPointerCapture: handlePointerCancel, + }; +} diff --git a/src/lightbox/Caption.tsx b/src/lightbox/Caption.tsx new file mode 100644 index 0000000..dc31f90 --- /dev/null +++ b/src/lightbox/Caption.tsx @@ -0,0 +1,26 @@ +import { ImageDataType } from "../ImageGallery.types"; + +interface CaptionProps { + imageData: ImageDataType; +} + +export function Caption({ imageData }: CaptionProps) { + if (!imageData.caption) return null; + + return ( +
+
{imageData.caption}
+ {imageData.cta?.href && imageData.cta?.text && ( + + )} +
+ ); +} diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx new file mode 100644 index 0000000..852fad7 --- /dev/null +++ b/src/lightbox/CarouselTrack.tsx @@ -0,0 +1,222 @@ +import { useRef, useImperativeHandle, forwardRef, useLayoutEffect, useState, useEffect } from "react"; +import { ImageDataType } from "../ImageGallery.types"; +import { useCarouselGesture } from "../hooks/useCarouselGesture"; + +import { Caption } from "./Caption"; + +export interface CarouselTrackRef { + navigate: (direction: -1 | 1) => void; + goToSlide: (newSlideNumber: number) => void; +} + +interface CarouselTrackProps { + imagesData: Array; + slideNumber: number; // 1-indexed + onSlideChange: (newSlideNumber: number) => void; + showThumbnails: boolean; + lazy: boolean; +} + +export const CarouselTrack = forwardRef( + ({ imagesData, slideNumber, onSlideChange, showThumbnails, lazy }, ref) => { + const trackRef = useRef(null); + const isAnimating = useRef(false); + const targetSlide = useRef(slideNumber); + const [currentLogicalIndex, setCurrentLogicalIndex] = useState(slideNumber - 1); + + // Keep targetSlide in sync with external changes (e.g. thumbnails) + useEffect(() => { + targetSlide.current = slideNumber; + setCurrentLogicalIndex(slideNumber - 1); + }, [slideNumber]); + + const total = imagesData.length; + const currentIndex = currentLogicalIndex; + + // Determine the indices for prev and next slides, looping + const prevIndex = currentIndex === 0 ? total - 1 : currentIndex - 1; + const nextIndex = currentIndex === total - 1 ? 0 : currentIndex + 1; + + // Reset track position instantly after a logical slide change + useLayoutEffect(() => { + if (trackRef.current) { + trackRef.current.style.transition = ""; + trackRef.current.style.transform = "translate3d(0px, 0, 0)"; + + // Force layout recalculation + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + trackRef.current.offsetHeight; + + isAnimating.current = false; + } + }, [slideNumber]); + + const executeNavigation = (direction: -1 | 1 | 0, fromX: number = 0) => { + if (!trackRef.current) return; + isAnimating.current = true; + + const track = trackRef.current; + const trackWidth = track.getBoundingClientRect().width; + const targetX = direction === 1 ? -trackWidth : (direction === -1 ? trackWidth : 0); + + // If we are cancelling a drag, targetX is 0. + // Ensure we start from currentX if provided (for smooth cancel) + if (fromX !== 0 && track.style.transform !== `translate3d(${fromX}px, 0px, 0px)`) { + track.style.transition = ""; + track.style.transform = `translate3d(${fromX}px, 0, 0)`; + // Force layout + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + track.offsetHeight; + } + + // Check if reduced motion is enabled + const isReducedMotion = typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + if (isReducedMotion) { + // No animation, just update logical state immediately + finishTransition(direction); + return; + } + + // Enable transition + track.style.transition = "transform var(--cs-lightbox-transition-duration, 300ms) var(--cs-lightbox-transition-easing, cubic-bezier(0.22, 1, 0.36, 1))"; + track.style.transform = `translate3d(${targetX}px, 0, 0)`; + + let transitionFired = false; + const onTransitionEnd = (e: TransitionEvent) => { + if (e.target === track && e.propertyName === "transform") { + transitionFired = true; + track.removeEventListener("transitionend", onTransitionEnd); + clearTimeout(fallbackTimeout); + finishTransition(direction); + } + }; + + track.addEventListener("transitionend", onTransitionEnd); + + // Fallback for interrupted transitions + const fallbackTimeout = setTimeout(() => { + if (!transitionFired) { + track.removeEventListener("transitionend", onTransitionEnd); + finishTransition(direction); + } + }, 400); + }; + + const finishTransition = (direction: -1 | 1 | 0) => { + if (direction === 0) { + // Just a cancel, reset animation state + isAnimating.current = false; + return; + } + + let newSlideNumber = targetSlide.current; + if (direction === 1) { + newSlideNumber += 1; + } else if (direction === -1) { + newSlideNumber -= 1; + } + + if (newSlideNumber > total) newSlideNumber = 1; + if (newSlideNumber < 1) newSlideNumber = total; + + targetSlide.current = newSlideNumber; + setCurrentLogicalIndex(newSlideNumber - 1); + // Updating the state will trigger the useLayoutEffect which resets the track + onSlideChange(newSlideNumber); + }; + + const navigate = (direction: -1 | 1) => { + if (isAnimating.current || !trackRef.current || total <= 1) return; + executeNavigation(direction); + }; + + const goToSlide = (newSlideNumber: number) => { + if (isAnimating.current || newSlideNumber === slideNumber || total <= 1) return; + + const diff = newSlideNumber - slideNumber; + let direction: -1 | 1 = diff > 0 ? 1 : -1; + + // Take the shortest path for looping carousels + if (Math.abs(diff) > total / 2) { + direction = direction === 1 ? -1 : 1; + } + + executeNavigation(direction); + }; + + const handleDragEnd = (result: { action: "next" | "prev" | "cancel" }) => { + if (!trackRef.current || total <= 1) return; + + // Get current transform X from the track style to know where to animate from + const style = trackRef.current.style.transform; + const match = style.match(/translate3d\(([-\d.]+)px/); + const currentX = match ? parseFloat(match[1]) : 0; + + if (result.action === "next") { + executeNavigation(1, currentX); + } else if (result.action === "prev") { + executeNavigation(-1, currentX); + } else { + executeNavigation(0, currentX); // cancel / snap back + } + }; + + const { onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture } = useCarouselGesture({ + trackRef, + onDragEnd: handleDragEnd, + canNavigatePrev: total > 1, // Currently looping is always enabled if > 1 slide + canNavigateNext: total > 1, + }); + + const renderSlide = (index: number, position: "prev" | "curr" | "next") => { + const imageData = imagesData[index]; + if (!imageData) return null; + + const imageLoading = position === "curr" ? (lazy ? "lazy" : "eager") : "eager"; + + // Stable keys based on imageData.id + return ( +
+
+ {imageData.alt} + +
+
+ ); + }; + + useImperativeHandle(ref, () => ({ + navigate, + goToSlide, + })); + + return ( +
+
+ {total > 1 && renderSlide(prevIndex, "prev")} + {total > 0 && renderSlide(currentIndex, "curr")} + {total > 1 && renderSlide(nextIndex, "next")} +
+
+ ); + } +); diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx new file mode 100644 index 0000000..e421802 --- /dev/null +++ b/src/lightbox/Lightbox.tsx @@ -0,0 +1,157 @@ +import { useRef, useEffect, useState } from "react"; +import { ImageDataType } from "../ImageGallery.types"; +import { Toolbar } from "./Toolbar"; +import { Navigation } from "./Navigation"; +import { CarouselTrack, CarouselTrackRef } from "./CarouselTrack"; +import { ThumbnailStrip } from "./ThumbnailStrip"; + +interface LightboxProps { + isOpen: boolean; + imagesData: Array; + initialSlideNumber: number; + onClose: () => void; + showThumbnails: boolean; + setShowThumbnails: (show: boolean) => void; + thumbnailBorder: string; + lazy: boolean; +} + +export function Lightbox({ + isOpen, + imagesData, + initialSlideNumber, + onClose, + showThumbnails, + setShowThumbnails, + thumbnailBorder, + lazy, +}: LightboxProps) { + const [slideNumber, setSlideNumber] = useState(initialSlideNumber); + const [showModalControls, setShowModalControls] = useState(false); + const [fullscreen, setFullscreen] = useState(false); + + const dialogRef = useRef(null); + const lightboxRef = useRef(null); + const carouselRef = useRef(null); + + // Sync initial slide + useEffect(() => { + setSlideNumber(initialSlideNumber); + }, [initialSlideNumber]); + + // Handle open/close + useEffect(() => { + if (isOpen) { + if (dialogRef.current && !dialogRef.current.open) { + dialogRef.current.showModal(); + document.documentElement.style.overflow = "hidden"; + } + } else { + if (dialogRef.current && dialogRef.current.open) { + dialogRef.current.close(); + document.documentElement.style.overflow = ""; + } + } + }, [isOpen]); + + function switchFullScreen(on: boolean) { + if (on) { + lightboxRef.current?.requestFullscreen().catch((error) => { + alert(`Error while attempting to switch into fullscreen mode: ${error.message}`); + }); + } else { + document.exitFullscreen().catch((error) => console.error(error)); + } + } + + useEffect(() => { + function handleFullscreenChange() { + setFullscreen(Boolean(document.fullscreenElement)); + lightboxRef.current?.focus(); + } + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); + }, []); + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "ArrowLeft") { + carouselRef.current?.navigate(-1); + } else if (e.key === "ArrowRight") { + carouselRef.current?.navigate(1); + } else if (e.key === "f") { + switchFullScreen(!fullscreen); + } else if (e.key === "Escape") { + e.preventDefault(); // Prevent default dialog closing so we can run our cleanup + exitFullScreenAndDialog(); + } + } + + function exitFullScreenAndDialog() { + if (fullscreen) { + switchFullScreen(false); + } + dialogRef.current?.close(); + onClose(); + } + + function handleThumbnailClick(index: number) { + carouselRef.current?.goToSlide(index + 1); + } + + return ( + { + e.preventDefault(); + exitFullScreenAndDialog(); + }}> +
setShowModalControls(true)} + onMouseLeave={() => setShowModalControls(false)} + // We use pointer down to close when clicking the backdrop + onPointerDown={(e) => { + if ((e.target as HTMLElement).tagName === "ARTICLE") { + exitFullScreenAndDialog(); + } + }} + > + setShowThumbnails(!showThumbnails)} + fullscreen={fullscreen} + onToggleFullscreen={switchFullScreen} + onClose={exitFullScreenAndDialog} + /> + +
+ + carouselRef.current?.navigate(dir)} + showControls={showModalControls} + /> +
+ + +
+
+ ); +} diff --git a/src/lightbox/Navigation.tsx b/src/lightbox/Navigation.tsx new file mode 100644 index 0000000..55a03d0 --- /dev/null +++ b/src/lightbox/Navigation.tsx @@ -0,0 +1,43 @@ +import { SvgElement } from "../helpers"; + +interface NavigationProps { + onNavigate: (direction: -1 | 1) => void; + showControls: boolean; +} + +export function Navigation({ onNavigate, showControls }: NavigationProps) { + return ( + <> + + + + ); +} diff --git a/src/lightbox/ThumbnailStrip.tsx b/src/lightbox/ThumbnailStrip.tsx new file mode 100644 index 0000000..affbfcf --- /dev/null +++ b/src/lightbox/ThumbnailStrip.tsx @@ -0,0 +1,60 @@ +import { useEffect, useRef } from "react"; +import { ImageDataType } from "../ImageGallery.types"; + +interface ThumbnailStripProps { + imagesData: Array; + slideNumber: number; + showThumbnails: boolean; + onThumbnailClick: (index: number) => void; + thumbnailBorder: string; + lazy: boolean; +} + +export function ThumbnailStrip({ + imagesData, + slideNumber, + showThumbnails, + onThumbnailClick, + thumbnailBorder, + lazy, +}: ThumbnailStripProps) { + const activeThumbImgRef = useRef(null); + + useEffect(() => { + if (showThumbnails && activeThumbImgRef.current) { + activeThumbImgRef.current.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + }, [slideNumber, showThumbnails]); + + return ( +
+
+ {imagesData.map((imageData, index) => ( + {imageData.alt} onThumbnailClick(index)} + /> + ))} +
+
+ ); +} diff --git a/src/lightbox/Toolbar.tsx b/src/lightbox/Toolbar.tsx new file mode 100644 index 0000000..3f38f63 --- /dev/null +++ b/src/lightbox/Toolbar.tsx @@ -0,0 +1,83 @@ +import { SvgElement } from "../helpers"; + +interface ToolbarProps { + showControls: boolean; + slideNumber: number; + totalImages: number; + onToggleThumbnails: () => void; + fullscreen: boolean; + onToggleFullscreen: (on: boolean) => void; + onClose: () => void; +} + +export function Toolbar({ + showControls, + slideNumber, + totalImages, + onToggleThumbnails, + fullscreen, + onToggleFullscreen, + onClose, +}: ToolbarProps) { + return ( + <> + + {`${slideNumber} / ${totalImages}`} + + + + + + + + + ); +} diff --git a/src/lightbox/index.ts b/src/lightbox/index.ts new file mode 100644 index 0000000..5e08f35 --- /dev/null +++ b/src/lightbox/index.ts @@ -0,0 +1 @@ +export { Lightbox } from "./Lightbox"; diff --git a/src/style.css b/src/style.css index c50c44c..f417096 100644 --- a/src/style.css +++ b/src/style.css @@ -33,6 +33,13 @@ .cs-rigg-dialog { margin: auto; + border: none; + padding: 0; + background: transparent; +} + +.cs-rigg-dialog::backdrop { + background: transparent; } .cs-rigg-modal-container { @@ -42,42 +49,71 @@ left: 0; width: 100vw; height: 100vh; - background-color: rgba(0, 0, 0, 0.73); - -webkit-backdrop-filter: blur(5px); - backdrop-filter: blur(5px); + background-color: rgba(18, 18, 18, 0.85); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + display: flex; + flex-direction: column; } .cs-rigg-modal-slide-number { - z-index: 1; + z-index: 100; position: absolute; - left: 0; - padding: 13px; - background-color: rgba(35, 35, 35, 0.73); + left: 16px; + top: 16px; + padding: 8px 16px; + background-color: rgba(18, 18, 18, 0.55); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 999px; color: #fff; font-size: 0.93rem; - transition: opacity 1s ease-in-out; + transition: opacity 0.3s ease-in-out; user-select: none; -webkit-user-select: none; + -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(16px); } .cs-rigg-modal-toolbar { - z-index: 1; + z-index: 100; display: flex; position: absolute; - right: 0; + right: 16px; + top: 16px; align-items: center; - background-color: rgba(35, 35, 35, 0.73); - transition: opacity 1s ease-in-out; + background-color: rgba(18, 18, 18, 0.55); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 999px; + transition: opacity 0.3s ease-in-out; cursor: pointer; + padding: 4px; + -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(16px); } .cs-rigg-modal-toolbar-btn { margin: 0; border: none; background: none; - padding: 13px; + padding: 10px; + width: 44px; + height: 44px; + border-radius: 50%; color: #fff; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.cs-rigg-modal-toolbar-btn:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.cs-rigg-modal-toolbar-btn:focus-visible { + outline: 2px solid #fff; + outline-offset: -2px; } .cs-rigg-modal-slide-show-section { @@ -85,38 +121,159 @@ display: flex; align-items: center; justify-content: center; - width: inherit; - transition: height 0.7s linear; + width: 100%; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.cs-rigg-carousel-track-container { + overflow: hidden; + width: 100%; + height: 100%; + position: relative; + touch-action: pan-y; +} + +.cs-rigg-carousel-track { + display: flex; + width: 100%; + height: 100%; + position: relative; + will-change: transform; +} + +.cs-rigg-carousel-slide { + position: absolute; + top: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + user-select: none; +} + +.cs-rigg-carousel-slide figure { + margin: 0; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.cs-rigg-carousel-slide[data-position="prev"] { + left: -100%; +} + +.cs-rigg-carousel-slide[data-position="curr"] { + left: 0; +} + +.cs-rigg-carousel-slide[data-position="next"] { + left: 100%; } .cs-rigg-modal-slide-btn { position: absolute; + z-index: 50; border: none; - margin-inline: 7px; - padding: 10px 15px; - background-color: rgba(35, 35, 35, 0.73); + margin-inline: 16px; + width: 44px; + height: 44px; + border-radius: 50%; + background-color: rgba(18, 18, 18, 0.55); + border: 1px solid rgba(255, 255, 255, 0.08); color: #fff; - transition: opacity 1s ease-in-out; + transition: opacity 0.3s ease-in-out, background-color 0.2s ease, transform 0.2s ease; cursor: pointer; user-select: none; -webkit-user-select: none; + -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(16px); + display: flex; + align-items: center; + justify-content: center; +} + +.cs-rigg-modal-slide-btn:hover { + background-color: rgba(255, 255, 255, 0.1); + transform: scale(1.05); +} + +.cs-rigg-modal-slide-btn:focus-visible { + outline: 2px solid #fff; + outline-offset: -2px; +} + +.cs-rigg-nav-prev { + left: 0; +} + +.cs-rigg-nav-next { + right: 0; } .cs-rigg-modal-image { margin: auto; max-width: 100vw; - transition: height 0.7s linear; + max-height: 100vh; + object-fit: contain; + transition: max-height 0.4s cubic-bezier(0.22, 1, 0.36, 1); + user-select: none; + -webkit-user-drag: none; } .cs-rigg-modal-thumbnail-section { - overflow: hidden; + overflow-x: auto; + overflow-y: hidden; height: 20vh; text-align: center; + transition: opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1); + background-color: rgba(0, 0, 0, 0.3); + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* Internet Explorer 10+ */ +} + +.cs-rigg-modal-thumbnail-section::-webkit-scrollbar { + display: none; /* Safari and Chrome */ } .cs-rigg-modal-thumb-imgs-pod { display: inline-flex; - height: inherit; - padding-block: 12px; - column-gap: 7px; + height: 100%; + padding: 16px; + column-gap: 12px; + align-items: center; +} + +.cs-rigg-modal-thumb-imgs-pod img { + height: 100%; + max-height: 100px; + object-fit: cover; + border-radius: 8px; + transition: transform 0.2s ease, opacity 0.2s ease; + opacity: 0.6; +} + +.cs-rigg-modal-thumb-imgs-pod img[style*="border"] { + opacity: 1; + transform: scale(1.05); +} + +.cs-rigg-modal-thumb-imgs-pod img:hover { + opacity: 1; +} + +@media (prefers-reduced-motion: reduce) { + .cs-rigg-modal-slide-number, + .cs-rigg-modal-toolbar, + .cs-rigg-modal-slide-btn, + .cs-rigg-modal-thumbnail-section, + .cs-rigg-modal-image, + .cs-rigg-modal-thumb-imgs-pod img { + transition: none !important; + } } diff --git a/src/utils/clamp.ts b/src/utils/clamp.ts new file mode 100644 index 0000000..0210f95 --- /dev/null +++ b/src/utils/clamp.ts @@ -0,0 +1,3 @@ +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} From 5c189292cbca9041f7e03ed65b6bc487bdc6c99a Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 19 Aug 2026 04:42:19 +0100 Subject: [PATCH 02/26] fix: Resolve the persistent scroll locks when the lightbox closes --- src/ImageGallery.test.tsx | 106 +++++++++++++++++++++++++++++++++ src/lightbox/CarouselTrack.tsx | 21 ++++--- src/lightbox/Lightbox.tsx | 35 +++++++---- 3 files changed, 141 insertions(+), 21 deletions(-) diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index fcbe598..a27c51e 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -370,3 +370,109 @@ test("lightbox gesture navigation threshold and state update", async () => { performance.now = originalNow; }); + +test("direct thumbnail navigation completely resets state", async () => { + render(); + + // Open lightbox on image 1 + const img1 = screen.getAllByAltText("Image1's alt text")[0]; + fireEvent.click(img1); + expect(await screen.findByText("1 / 13")).toBeTruthy(); + + // Show thumbnails and click thumbnail for image 4 + const toggleThumbBtn = screen.getByTitle("Show thumbnails"); + fireEvent.click(toggleThumbBtn); + + const thumbs = document.querySelectorAll(".cs-rigg-modal-thumb-imgs-pod img"); + expect(thumbs.length).toBe(13); + + act(() => { + fireEvent.click(thumbs[3]); // index 3 is image 4 + }); + + // Counter should instantly be 4 / 13 + expect(await screen.findByText("4 / 13")).toBeTruthy(); + + // Ensure Next button goes to 5, meaning state is perfectly synced + const nextBtn = screen.getByTitle("Next image"); + act(() => { + fireEvent.click(nextBtn); + }); + expect(await screen.findByText("5 / 13")).toBeTruthy(); + + // Click thumbnail 10 + act(() => { + fireEvent.click(thumbs[9]); // index 9 is image 10 + }); + expect(await screen.findByText("10 / 13")).toBeTruthy(); + + // Previous button goes to 9 + const prevBtn = screen.getByTitle("Previous image"); + act(() => { + fireEvent.click(prevBtn); + }); + expect(await screen.findByText("9 / 13")).toBeTruthy(); +}); + +test("backdrop click closes lightbox, but content click does not", async () => { + render(); + + // Open lightbox + fireEvent.click(screen.getAllByAltText("Image1's alt text")[0]); + expect(await screen.findByText("1 / 13")).toBeTruthy(); + + // Click the image - should not close + const modalImg = document.querySelector(".cs-rigg-modal-image") as HTMLElement; + fireEvent.click(modalImg); + expect(screen.queryByText("1 / 13")).toBeTruthy(); // still open + + // Click toolbar button - should not close + const nextBtn = screen.getByTitle("Next image"); + fireEvent.click(nextBtn); + expect(screen.queryByText("2 / 13")).toBeTruthy(); // still open, just moved to slide 2 + + // Click the background area (track container which has data-backdrop="true") + const trackContainer = document.querySelector(".cs-rigg-carousel-track-container") as HTMLElement; + act(() => { + fireEvent.pointerDown(trackContainer); + }); + + // Should close + const dialog = document.querySelector(".cs-rigg-dialog") as HTMLDialogElement; + expect(dialog.open).toBe(false); +}); + +test("scroll lock lifecycle correctly restores exactly previous styles", async () => { + // Set initial overflow to something custom + document.documentElement.style.overflow = "scroll"; + + const { unmount } = render(); + + expect(document.documentElement.style.overflow).toBe("scroll"); + + // Open lightbox + fireEvent.click(screen.getAllByAltText("Image1's alt text")[0]); + expect(await screen.findByText("1 / 13")).toBeTruthy(); + + // Should be locked + expect(document.documentElement.style.overflow).toBe("hidden"); + + // Close via escape + act(() => { + const dialog = document.querySelector(".cs-rigg-dialog") as HTMLDialogElement; + fireEvent(dialog, new Event('cancel')); + }); + + expect((document.querySelector(".cs-rigg-dialog") as HTMLDialogElement).open).toBe(false); + + // Should exact restore previous style + expect(document.documentElement.style.overflow).toBe("scroll"); + + // Test unmount cleanup + fireEvent.click(screen.getAllByAltText("Image1's alt text")[0]); + expect(await screen.findByText("1 / 13")).toBeTruthy(); + expect(document.documentElement.style.overflow).toBe("hidden"); + + unmount(); + expect(document.documentElement.style.overflow).toBe("scroll"); +}); diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx index 852fad7..5d64d01 100644 --- a/src/lightbox/CarouselTrack.tsx +++ b/src/lightbox/CarouselTrack.tsx @@ -134,17 +134,19 @@ export const CarouselTrack = forwardRef( }; const goToSlide = (newSlideNumber: number) => { - if (isAnimating.current || newSlideNumber === slideNumber || total <= 1) return; + if (newSlideNumber === slideNumber || total <= 1) return; - const diff = newSlideNumber - slideNumber; - let direction: -1 | 1 = diff > 0 ? 1 : -1; + // Direct navigation: completely reset visual state and instantly jump + isAnimating.current = false; + targetSlide.current = newSlideNumber; + setCurrentLogicalIndex(newSlideNumber - 1); - // Take the shortest path for looping carousels - if (Math.abs(diff) > total / 2) { - direction = direction === 1 ? -1 : 1; + if (trackRef.current) { + trackRef.current.style.transition = ""; + trackRef.current.style.transform = "translate3d(0px, 0, 0)"; } - executeNavigation(direction); + onSlideChange(newSlideNumber); }; const handleDragEnd = (result: { action: "next" | "prev" | "cancel" }) => { @@ -179,7 +181,7 @@ export const CarouselTrack = forwardRef( // Stable keys based on imageData.id return ( -
+
( return (
-
+
{total > 1 && renderSlide(prevIndex, "prev")} {total > 0 && renderSlide(currentIndex, "curr")} {total > 1 && renderSlide(nextIndex, "next")} diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx index e421802..757f83e 100644 --- a/src/lightbox/Lightbox.tsx +++ b/src/lightbox/Lightbox.tsx @@ -39,19 +39,27 @@ export function Lightbox({ setSlideNumber(initialSlideNumber); }, [initialSlideNumber]); - // Handle open/close + // Handle open/close and scroll lock useEffect(() => { - if (isOpen) { - if (dialogRef.current && !dialogRef.current.open) { - dialogRef.current.showModal(); - document.documentElement.style.overflow = "hidden"; - } - } else { - if (dialogRef.current && dialogRef.current.open) { + if (!isOpen) { + if (dialogRef.current?.open) { dialogRef.current.close(); - document.documentElement.style.overflow = ""; } + return; + } + + if (dialogRef.current && !dialogRef.current.open) { + dialogRef.current.showModal(); } + + const html = document.documentElement; + const originalHtmlOverflow = html.style.overflow; + + html.style.overflow = "hidden"; + + return () => { + html.style.overflow = originalHtmlOverflow; + }; }, [isOpen]); function switchFullScreen(on: boolean) { @@ -65,13 +73,15 @@ export function Lightbox({ } useEffect(() => { + if (!isOpen) return; + function handleFullscreenChange() { setFullscreen(Boolean(document.fullscreenElement)); lightboxRef.current?.focus(); } document.addEventListener("fullscreenchange", handleFullscreenChange); return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); - }, []); + }, [isOpen]); function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "ArrowLeft") { @@ -108,12 +118,13 @@ export function Lightbox({ tabIndex={-1} ref={lightboxRef} className="cs-rigg-modal-container" + data-backdrop="true" onKeyDown={handleKeyDown} onMouseEnter={() => setShowModalControls(true)} onMouseLeave={() => setShowModalControls(false)} // We use pointer down to close when clicking the backdrop onPointerDown={(e) => { - if ((e.target as HTMLElement).tagName === "ARTICLE") { + if ((e.target as HTMLElement).getAttribute("data-backdrop") === "true") { exitFullScreenAndDialog(); } }} @@ -128,7 +139,7 @@ export function Lightbox({ onClose={exitFullScreenAndDialog} /> -
+
Date: Thu, 20 Aug 2026 04:56:53 +0100 Subject: [PATCH 03/26] chore: Move interfaces to the types file --- src/ImageGallery.types.tsx | 11 ++ src/hooks/useCarouselGesture.ts | 217 +++++++++++++++++--------------- 2 files changed, 124 insertions(+), 104 deletions(-) diff --git a/src/ImageGallery.types.tsx b/src/ImageGallery.types.tsx index 98e4b04..4d36a7f 100644 --- a/src/ImageGallery.types.tsx +++ b/src/ImageGallery.types.tsx @@ -1,3 +1,7 @@ +export interface DragResult { + action: "next" | "prev" | "cancel"; +} + export interface ImageDataType { id: string | number; alt: string; @@ -36,3 +40,10 @@ export interface ImgSrcInfoType { src: string; srcSet: string | undefined; } + +export interface UseCarouselGestureProps { + trackRef: React.RefObject; + onDragEnd: (result: DragResult) => void; + canNavigatePrev: boolean; + canNavigateNext: boolean; +} diff --git a/src/hooks/useCarouselGesture.ts b/src/hooks/useCarouselGesture.ts index e4135aa..0185811 100644 --- a/src/hooks/useCarouselGesture.ts +++ b/src/hooks/useCarouselGesture.ts @@ -1,15 +1,5 @@ -import { useRef, useCallback, useEffect } from 'react'; - -export interface DragResult { - action: "next" | "prev" | "cancel"; -} - -interface UseCarouselGestureProps { - trackRef: React.RefObject; - onDragEnd: (result: DragResult) => void; - canNavigatePrev: boolean; - canNavigateNext: boolean; -} +import { useCallback, useEffect, useRef } from "react"; +import type { UseCarouselGestureProps } from "../ImageGallery.types"; export function useCarouselGesture({ trackRef, @@ -26,7 +16,7 @@ export function useCarouselGesture({ const rafId = useRef(null); const trackWidth = useRef(0); - const resetState = useCallback(() => { + const resetRefs = useCallback(() => { isDragging.current = false; isHorizontalIntent.current = null; currentX.current = 0; @@ -36,113 +26,132 @@ export function useCarouselGesture({ } }, []); - const updateTransform = useCallback((x: number) => { - if (trackRef.current) { - trackRef.current.style.transform = `translate3d(${x}px, 0, 0)`; - } - }, [trackRef]); + const updateTransform = useCallback( + (x: number) => { + if (trackRef.current) { + trackRef.current.style.transform = `translate3d(${x}px, 0, 0)`; + } + }, + [trackRef], + ); + + const handlePointerDown = useCallback( + (e: React.PointerEvent) => { + if (!e.isPrimary) return; + + if (trackRef.current) { + trackRef.current.style.transition = ""; + trackWidth.current = trackRef.current.getBoundingClientRect().width; + if (typeof (e.target as HTMLElement).setPointerCapture === "function") { + (e.target as HTMLElement).setPointerCapture(e.pointerId); + } + } - const handlePointerDown = useCallback((e: React.PointerEvent) => { - if (!e.isPrimary) return; + isDragging.current = true; + startX.current = e.clientX; + startY.current = e.clientY; + currentX.current = 0; + startTime.current = performance.now(); + isHorizontalIntent.current = null; - if (trackRef.current) { - trackRef.current.style.transition = ''; - trackWidth.current = trackRef.current.getBoundingClientRect().width; - if (typeof (e.target as HTMLElement).setPointerCapture === "function") { - (e.target as HTMLElement).setPointerCapture(e.pointerId); + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current); } - } - - isDragging.current = true; - startX.current = e.clientX; - startY.current = e.clientY; - currentX.current = 0; - startTime.current = performance.now(); - isHorizontalIntent.current = null; + }, + [trackRef], + ); - if (rafId.current !== null) { - cancelAnimationFrame(rafId.current); - } - }, [trackRef]); + const handlePointerMove = useCallback( + (e: React.PointerEvent) => { + if (!isDragging.current || !e.isPrimary) return; - const handlePointerMove = useCallback((e: React.PointerEvent) => { - if (!isDragging.current || !e.isPrimary) return; + const deltaX = e.clientX - startX.current; + const deltaY = e.clientY - startY.current; - const deltaX = e.clientX - startX.current; - const deltaY = e.clientY - startY.current; + if (isHorizontalIntent.current === null) { + if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { + isHorizontalIntent.current = Math.abs(deltaX) > Math.abs(deltaY); + } + } - if (isHorizontalIntent.current === null) { - if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { - isHorizontalIntent.current = Math.abs(deltaX) > Math.abs(deltaY); + if (isHorizontalIntent.current === false) { + return; } - } - if (isHorizontalIntent.current === false) { - return; - } + if (isHorizontalIntent.current) { + e.preventDefault(); + + let newX = deltaX; + + if (newX > 0 && !canNavigatePrev) { + newX = newX * 0.25; + } else if (newX < 0 && !canNavigateNext) { + newX = newX * 0.25; + } - if (isHorizontalIntent.current) { - e.preventDefault(); - - let newX = deltaX; - - if (newX > 0 && !canNavigatePrev) { - newX = newX * 0.25; - } else if (newX < 0 && !canNavigateNext) { - newX = newX * 0.25; + currentX.current = newX; + + if (rafId.current === null) { + rafId.current = requestAnimationFrame(() => { + updateTransform(currentX.current); + rafId.current = null; + }); + } } + }, + [canNavigateNext, canNavigatePrev, updateTransform], + ); + + const handlePointerUp = useCallback( + (e: React.PointerEvent) => { + if (!isDragging.current || !e.isPrimary) return; + + const duration = performance.now() - startTime.current; + const deltaX = currentX.current; + const absDeltaX = Math.abs(deltaX); + const velocity = absDeltaX / duration; + + const threshold = trackWidth.current * 0.2; + const isFastFlick = velocity > 0.5 && duration < 250; + const passedThreshold = absDeltaX > threshold; - currentX.current = newX; + const wasHorizontal = isHorizontalIntent.current; - if (rafId.current === null) { - rafId.current = requestAnimationFrame(() => { - updateTransform(currentX.current); - rafId.current = null; - }); + resetRefs(); + + if (trackRef.current) { + (e.target as HTMLElement).releasePointerCapture(e.pointerId); } - } - }, [canNavigateNext, canNavigatePrev, updateTransform]); - - const handlePointerUp = useCallback((e: React.PointerEvent) => { - if (!isDragging.current || !e.isPrimary) return; - - const duration = performance.now() - startTime.current; - const deltaX = currentX.current; - const absDeltaX = Math.abs(deltaX); - const velocity = absDeltaX / duration; - - const threshold = trackWidth.current * 0.2; - const isFastFlick = velocity > 0.5 && duration < 250; - const passedThreshold = absDeltaX > threshold; - - const wasHorizontal = isHorizontalIntent.current; - - resetState(); - - if (trackRef.current) { - (e.target as HTMLElement).releasePointerCapture(e.pointerId); - } - if (wasHorizontal) { - let action: "next" | "prev" | "cancel" = "cancel"; - if ((passedThreshold || isFastFlick) && deltaX > 0 && canNavigatePrev) { - action = "prev"; - } else if ((passedThreshold || isFastFlick) && deltaX < 0 && canNavigateNext) { - action = "next"; + if (wasHorizontal) { + let action: "next" | "prev" | "cancel" = "cancel"; + if ((passedThreshold || isFastFlick) && deltaX > 0 && canNavigatePrev) { + action = "prev"; + } else if ( + (passedThreshold || isFastFlick) && + deltaX < 0 && + canNavigateNext + ) { + action = "next"; + } + onDragEnd({ action }); } - onDragEnd({ action }); - } - }, [canNavigateNext, canNavigatePrev, onDragEnd, resetState, trackRef]); - - const handlePointerCancel = useCallback((e: React.PointerEvent) => { - if (!isDragging.current || !e.isPrimary) return; - - resetState(); - if (trackRef.current) { - (e.target as HTMLElement).releasePointerCapture(e.pointerId); - } - onDragEnd({ action: "cancel" }); - }, [onDragEnd, resetState, trackRef]); + }, + [canNavigateNext, canNavigatePrev, onDragEnd, resetRefs, trackRef], + ); + + const handlePointerCancel = useCallback( + (e: React.PointerEvent) => { + if (!isDragging.current || !e.isPrimary) return; + + resetRefs(); + if (trackRef.current) { + (e.target as HTMLElement).releasePointerCapture(e.pointerId); + } + onDragEnd({ action: "cancel" }); + }, + [onDragEnd, resetRefs, trackRef], + ); useEffect(() => { return () => { From 2dc0572e493b0654acdb5cf6fce70f1f205e0ada Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Thu, 20 Aug 2026 05:09:17 +0100 Subject: [PATCH 04/26] refactor: Renamed useCallback handlers to names that describe their intent --- src/hooks/useCarouselGesture.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/hooks/useCarouselGesture.ts b/src/hooks/useCarouselGesture.ts index 0185811..551e47b 100644 --- a/src/hooks/useCarouselGesture.ts +++ b/src/hooks/useCarouselGesture.ts @@ -35,7 +35,7 @@ export function useCarouselGesture({ [trackRef], ); - const handlePointerDown = useCallback( + const trackGesture = useCallback( (e: React.PointerEvent) => { if (!e.isPrimary) return; @@ -61,7 +61,7 @@ export function useCarouselGesture({ [trackRef], ); - const handlePointerMove = useCallback( + const trackDragOffset = useCallback( (e: React.PointerEvent) => { if (!isDragging.current || !e.isPrimary) return; @@ -102,7 +102,7 @@ export function useCarouselGesture({ [canNavigateNext, canNavigatePrev, updateTransform], ); - const handlePointerUp = useCallback( + const evaluateAndCommitSwipe = useCallback( (e: React.PointerEvent) => { if (!isDragging.current || !e.isPrimary) return; @@ -140,7 +140,7 @@ export function useCarouselGesture({ [canNavigateNext, canNavigatePrev, onDragEnd, resetRefs, trackRef], ); - const handlePointerCancel = useCallback( + const cancelGesture = useCallback( (e: React.PointerEvent) => { if (!isDragging.current || !e.isPrimary) return; @@ -162,10 +162,10 @@ export function useCarouselGesture({ }, []); return { - onPointerDown: handlePointerDown, - onPointerMove: handlePointerMove, - onPointerUp: handlePointerUp, - onPointerCancel: handlePointerCancel, - onLostPointerCapture: handlePointerCancel, + onPointerDown: trackGesture, + onPointerMove: trackDragOffset, + onPointerUp: evaluateAndCommitSwipe, + onPointerCancel: cancelGesture, + onLostPointerCapture: cancelGesture, }; } From 1f79fd83bf9b96c0e7801bdb744791ba133aac36 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Fri, 21 Aug 2026 16:05:55 +0100 Subject: [PATCH 05/26] refactor: Rename handleDragEnd to executeSwipeNavigation --- src/lightbox/CarouselTrack.tsx | 165 ++++++++++++++++++++------------- 1 file changed, 99 insertions(+), 66 deletions(-) diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx index 5d64d01..0c6bad1 100644 --- a/src/lightbox/CarouselTrack.tsx +++ b/src/lightbox/CarouselTrack.tsx @@ -1,8 +1,14 @@ -import { useRef, useImperativeHandle, forwardRef, useLayoutEffect, useState, useEffect } from "react"; -import { ImageDataType } from "../ImageGallery.types"; -import { useCarouselGesture } from "../hooks/useCarouselGesture"; - +import { + forwardRef, + useEffect, + useImperativeHandle, + useLayoutEffect, + useRef, + useState, +} from "react"; import { Caption } from "./Caption"; +import { useCarouselGesture } from "../hooks/useCarouselGesture"; +import { ImageDataType } from "../ImageGallery.types"; export interface CarouselTrackRef { navigate: (direction: -1 | 1) => void; @@ -22,17 +28,19 @@ export const CarouselTrack = forwardRef( const trackRef = useRef(null); const isAnimating = useRef(false); const targetSlide = useRef(slideNumber); - const [currentLogicalIndex, setCurrentLogicalIndex] = useState(slideNumber - 1); - + const [currentLogicalIndex, setCurrentLogicalIndex] = useState( + slideNumber - 1, + ); + // Keep targetSlide in sync with external changes (e.g. thumbnails) useEffect(() => { targetSlide.current = slideNumber; setCurrentLogicalIndex(slideNumber - 1); }, [slideNumber]); - + const total = imagesData.length; const currentIndex = currentLogicalIndex; - + // Determine the indices for prev and next slides, looping const prevIndex = currentIndex === 0 ? total - 1 : currentIndex - 1; const nextIndex = currentIndex === total - 1 ? 0 : currentIndex + 1; @@ -42,26 +50,30 @@ export const CarouselTrack = forwardRef( if (trackRef.current) { trackRef.current.style.transition = ""; trackRef.current.style.transform = "translate3d(0px, 0, 0)"; - + // Force layout recalculation // eslint-disable-next-line @typescript-eslint/no-unused-expressions - trackRef.current.offsetHeight; - + trackRef.current.offsetHeight; + isAnimating.current = false; } }, [slideNumber]); - const executeNavigation = (direction: -1 | 1 | 0, fromX: number = 0) => { + function executeNavigation(direction: -1 | 1 | 0, fromX: number = 0) { if (!trackRef.current) return; isAnimating.current = true; const track = trackRef.current; const trackWidth = track.getBoundingClientRect().width; - const targetX = direction === 1 ? -trackWidth : (direction === -1 ? trackWidth : 0); - - // If we are cancelling a drag, targetX is 0. + const targetX = + direction === 1 ? -trackWidth : direction === -1 ? trackWidth : 0; + + // If we are cancelling a drag, targetX is 0. // Ensure we start from currentX if provided (for smooth cancel) - if (fromX !== 0 && track.style.transform !== `translate3d(${fromX}px, 0px, 0px)`) { + if ( + fromX !== 0 && + track.style.transform !== `translate3d(${fromX}px, 0px, 0px)` + ) { track.style.transition = ""; track.style.transform = `translate3d(${fromX}px, 0, 0)`; // Force layout @@ -69,9 +81,33 @@ export const CarouselTrack = forwardRef( track.offsetHeight; } + function finishTransition(direction: -1 | 1 | 0) { + if (direction === 0) { + // Just a cancel, reset animation state + isAnimating.current = false; + return; + } + + let newSlideNumber = targetSlide.current; + if (direction === 1) { + newSlideNumber += 1; + } else if (direction === -1) { + newSlideNumber -= 1; + } + + if (newSlideNumber > total) newSlideNumber = 1; + if (newSlideNumber < 1) newSlideNumber = total; + + targetSlide.current = newSlideNumber; + setCurrentLogicalIndex(newSlideNumber - 1); + // Updating the state will trigger the useLayoutEffect which resets the track + onSlideChange(newSlideNumber); + } + // Check if reduced motion is enabled - const isReducedMotion = typeof window !== 'undefined' && - typeof window.matchMedia === 'function' && + const isReducedMotion = + typeof window !== "undefined" && + typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (isReducedMotion) { @@ -81,77 +117,58 @@ export const CarouselTrack = forwardRef( } // Enable transition - track.style.transition = "transform var(--cs-lightbox-transition-duration, 300ms) var(--cs-lightbox-transition-easing, cubic-bezier(0.22, 1, 0.36, 1))"; + track.style.transition = + "transform var(--cs-lightbox-transition-duration, 300ms) var(--cs-lightbox-transition-easing, cubic-bezier(0.22, 1, 0.36, 1))"; track.style.transform = `translate3d(${targetX}px, 0, 0)`; let transitionFired = false; - const onTransitionEnd = (e: TransitionEvent) => { + + function onTransitionEnd(e: TransitionEvent) { if (e.target === track && e.propertyName === "transform") { transitionFired = true; track.removeEventListener("transitionend", onTransitionEnd); clearTimeout(fallbackTimeout); finishTransition(direction); } - }; + } track.addEventListener("transitionend", onTransitionEnd); - + // Fallback for interrupted transitions const fallbackTimeout = setTimeout(() => { if (!transitionFired) { track.removeEventListener("transitionend", onTransitionEnd); finishTransition(direction); } - }, 400); - }; - - const finishTransition = (direction: -1 | 1 | 0) => { - if (direction === 0) { - // Just a cancel, reset animation state - isAnimating.current = false; - return; - } - - let newSlideNumber = targetSlide.current; - if (direction === 1) { - newSlideNumber += 1; - } else if (direction === -1) { - newSlideNumber -= 1; - } + }, 400); + } - if (newSlideNumber > total) newSlideNumber = 1; - if (newSlideNumber < 1) newSlideNumber = total; - - targetSlide.current = newSlideNumber; - setCurrentLogicalIndex(newSlideNumber - 1); - // Updating the state will trigger the useLayoutEffect which resets the track - onSlideChange(newSlideNumber); - }; - - const navigate = (direction: -1 | 1) => { + function navigate(direction: -1 | 1) { if (isAnimating.current || !trackRef.current || total <= 1) return; executeNavigation(direction); - }; + } - const goToSlide = (newSlideNumber: number) => { + function goToSlide(newSlideNumber: number) { if (newSlideNumber === slideNumber || total <= 1) return; - + // Direct navigation: completely reset visual state and instantly jump isAnimating.current = false; targetSlide.current = newSlideNumber; setCurrentLogicalIndex(newSlideNumber - 1); - + if (trackRef.current) { trackRef.current.style.transition = ""; trackRef.current.style.transform = "translate3d(0px, 0, 0)"; } - + onSlideChange(newSlideNumber); - }; + } - const handleDragEnd = (result: { action: "next" | "prev" | "cancel" }) => { + function executeSwipeNavigation(result: { + action: "next" | "prev" | "cancel"; + }) { if (!trackRef.current || total <= 1) return; - + // Get current transform X from the track style to know where to animate from const style = trackRef.current.style.transform; const match = style.match(/translate3d\(([-\d.]+)px/); @@ -164,24 +181,36 @@ export const CarouselTrack = forwardRef( } else { executeNavigation(0, currentX); // cancel / snap back } - }; + } - const { onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture } = useCarouselGesture({ + const { + onPointerDown, + onPointerMove, + onPointerUp, + onPointerCancel, + onLostPointerCapture, + } = useCarouselGesture({ trackRef, - onDragEnd: handleDragEnd, + onDragEnd: executeSwipeNavigation, canNavigatePrev: total > 1, // Currently looping is always enabled if > 1 slide canNavigateNext: total > 1, }); - const renderSlide = (index: number, position: "prev" | "curr" | "next") => { + function renderSlide(index: number, position: "prev" | "curr" | "next") { const imageData = imagesData[index]; if (!imageData) return null; - const imageLoading = position === "curr" ? (lazy ? "lazy" : "eager") : "eager"; + const imageLoading = + position === "curr" ? (lazy ? "lazy" : "eager") : "eager"; // Stable keys based on imageData.id return ( -
+
(
); - }; + } useImperativeHandle(ref, () => ({ navigate, @@ -205,7 +234,7 @@ export const CarouselTrack = forwardRef( })); return ( -
( onPointerCancel={onPointerCancel} onLostPointerCapture={onLostPointerCapture} > -
+
{total > 1 && renderSlide(prevIndex, "prev")} {total > 0 && renderSlide(currentIndex, "curr")} {total > 1 && renderSlide(nextIndex, "next")}
); - } + }, ); From ac3ab42804548ada2ff618593044be9d864c4266 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Fri, 21 Aug 2026 16:28:10 +0100 Subject: [PATCH 06/26] refactor: Remove forwardRef and access ref as a prop instead --- src/lightbox/CarouselTrack.tsx | 415 +++++++++++++++++---------------- 1 file changed, 210 insertions(+), 205 deletions(-) diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx index 0c6bad1..ad4fe68 100644 --- a/src/lightbox/CarouselTrack.tsx +++ b/src/lightbox/CarouselTrack.tsx @@ -1,5 +1,4 @@ import { - forwardRef, useEffect, useImperativeHandle, useLayoutEffect, @@ -21,238 +20,244 @@ interface CarouselTrackProps { onSlideChange: (newSlideNumber: number) => void; showThumbnails: boolean; lazy: boolean; + ref: React.Ref; } -export const CarouselTrack = forwardRef( - ({ imagesData, slideNumber, onSlideChange, showThumbnails, lazy }, ref) => { - const trackRef = useRef(null); - const isAnimating = useRef(false); - const targetSlide = useRef(slideNumber); - const [currentLogicalIndex, setCurrentLogicalIndex] = useState( - slideNumber - 1, - ); - - // Keep targetSlide in sync with external changes (e.g. thumbnails) - useEffect(() => { - targetSlide.current = slideNumber; - setCurrentLogicalIndex(slideNumber - 1); - }, [slideNumber]); - - const total = imagesData.length; - const currentIndex = currentLogicalIndex; - - // Determine the indices for prev and next slides, looping - const prevIndex = currentIndex === 0 ? total - 1 : currentIndex - 1; - const nextIndex = currentIndex === total - 1 ? 0 : currentIndex + 1; - - // Reset track position instantly after a logical slide change - useLayoutEffect(() => { - if (trackRef.current) { - trackRef.current.style.transition = ""; - trackRef.current.style.transform = "translate3d(0px, 0, 0)"; +export function CarouselTrack({ + imagesData, + slideNumber, + onSlideChange, + showThumbnails, + lazy, + ref, +}: CarouselTrackProps) { + const trackRef = useRef(null); + const isAnimating = useRef(false); + const targetSlide = useRef(slideNumber); + const [currentLogicalIndex, setCurrentLogicalIndex] = useState( + slideNumber - 1, + ); + + // Keep targetSlide in sync with external changes (e.g. thumbnails) + useEffect(() => { + targetSlide.current = slideNumber; + setCurrentLogicalIndex(slideNumber - 1); + }, [slideNumber]); + + const total = imagesData.length; + const currentIndex = currentLogicalIndex; + + // Determine the indices for prev and next slides, looping + const prevIndex = currentIndex === 0 ? total - 1 : currentIndex - 1; + const nextIndex = currentIndex === total - 1 ? 0 : currentIndex + 1; + + // Reset track position instantly after a logical slide change + useLayoutEffect(() => { + if (trackRef.current) { + trackRef.current.style.transition = ""; + trackRef.current.style.transform = "translate3d(0px, 0, 0)"; + + // Force layout recalculation + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + trackRef.current.offsetHeight; - // Force layout recalculation - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - trackRef.current.offsetHeight; + isAnimating.current = false; + } + }, [slideNumber]); + + function executeNavigation(direction: -1 | 1 | 0, fromX: number = 0) { + if (!trackRef.current) return; + isAnimating.current = true; + + const track = trackRef.current; + const trackWidth = track.getBoundingClientRect().width; + const targetX = + direction === 1 ? -trackWidth : direction === -1 ? trackWidth : 0; + + // If we are cancelling a drag, targetX is 0. + // Ensure we start from currentX if provided (for smooth cancel) + if ( + fromX !== 0 && + track.style.transform !== `translate3d(${fromX}px, 0px, 0px)` + ) { + track.style.transition = ""; + track.style.transform = `translate3d(${fromX}px, 0, 0)`; + // Force layout + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + track.offsetHeight; + } + function finishTransition(direction: -1 | 1 | 0) { + if (direction === 0) { + // Just a cancel, reset animation state isAnimating.current = false; - } - }, [slideNumber]); - - function executeNavigation(direction: -1 | 1 | 0, fromX: number = 0) { - if (!trackRef.current) return; - isAnimating.current = true; - - const track = trackRef.current; - const trackWidth = track.getBoundingClientRect().width; - const targetX = - direction === 1 ? -trackWidth : direction === -1 ? trackWidth : 0; - - // If we are cancelling a drag, targetX is 0. - // Ensure we start from currentX if provided (for smooth cancel) - if ( - fromX !== 0 && - track.style.transform !== `translate3d(${fromX}px, 0px, 0px)` - ) { - track.style.transition = ""; - track.style.transform = `translate3d(${fromX}px, 0, 0)`; - // Force layout - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - track.offsetHeight; + return; } - function finishTransition(direction: -1 | 1 | 0) { - if (direction === 0) { - // Just a cancel, reset animation state - isAnimating.current = false; - return; - } - - let newSlideNumber = targetSlide.current; - if (direction === 1) { - newSlideNumber += 1; - } else if (direction === -1) { - newSlideNumber -= 1; - } - - if (newSlideNumber > total) newSlideNumber = 1; - if (newSlideNumber < 1) newSlideNumber = total; - - targetSlide.current = newSlideNumber; - setCurrentLogicalIndex(newSlideNumber - 1); - // Updating the state will trigger the useLayoutEffect which resets the track - onSlideChange(newSlideNumber); + let newSlideNumber = targetSlide.current; + if (direction === 1) { + newSlideNumber += 1; + } else if (direction === -1) { + newSlideNumber -= 1; } - // Check if reduced motion is enabled - const isReducedMotion = - typeof window !== "undefined" && - typeof window.matchMedia === "function" && - window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (newSlideNumber > total) newSlideNumber = 1; + if (newSlideNumber < 1) newSlideNumber = total; - if (isReducedMotion) { - // No animation, just update logical state immediately - finishTransition(direction); - return; - } + targetSlide.current = newSlideNumber; + setCurrentLogicalIndex(newSlideNumber - 1); + // Updating the state will trigger the useLayoutEffect which resets the track + onSlideChange(newSlideNumber); + } - // Enable transition - track.style.transition = - "transform var(--cs-lightbox-transition-duration, 300ms) var(--cs-lightbox-transition-easing, cubic-bezier(0.22, 1, 0.36, 1))"; - track.style.transform = `translate3d(${targetX}px, 0, 0)`; + // Check if reduced motion is enabled + const isReducedMotion = + typeof window !== "undefined" && + typeof window.matchMedia === "function" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; - let transitionFired = false; + if (isReducedMotion) { + // No animation, just update logical state immediately + finishTransition(direction); + return; + } - function onTransitionEnd(e: TransitionEvent) { - if (e.target === track && e.propertyName === "transform") { - transitionFired = true; - track.removeEventListener("transitionend", onTransitionEnd); - clearTimeout(fallbackTimeout); - finishTransition(direction); - } - } + // Enable transition + track.style.transition = + "transform var(--cs-lightbox-transition-duration, 300ms) var(--cs-lightbox-transition-easing, cubic-bezier(0.22, 1, 0.36, 1))"; + track.style.transform = `translate3d(${targetX}px, 0, 0)`; - track.addEventListener("transitionend", onTransitionEnd); + let transitionFired = false; - // Fallback for interrupted transitions - const fallbackTimeout = setTimeout(() => { - if (!transitionFired) { - track.removeEventListener("transitionend", onTransitionEnd); - finishTransition(direction); - } - }, 400); + function onTransitionEnd(e: TransitionEvent) { + if (e.target === track && e.propertyName === "transform") { + transitionFired = true; + track.removeEventListener("transitionend", onTransitionEnd); + clearTimeout(fallbackTimeout); + finishTransition(direction); + } } - function navigate(direction: -1 | 1) { - if (isAnimating.current || !trackRef.current || total <= 1) return; - executeNavigation(direction); - } + track.addEventListener("transitionend", onTransitionEnd); - function goToSlide(newSlideNumber: number) { - if (newSlideNumber === slideNumber || total <= 1) return; + // Fallback for interrupted transitions + const fallbackTimeout = setTimeout(() => { + if (!transitionFired) { + track.removeEventListener("transitionend", onTransitionEnd); + finishTransition(direction); + } + }, 400); + } - // Direct navigation: completely reset visual state and instantly jump - isAnimating.current = false; - targetSlide.current = newSlideNumber; - setCurrentLogicalIndex(newSlideNumber - 1); + function navigate(direction: -1 | 1) { + if (isAnimating.current || !trackRef.current || total <= 1) return; + executeNavigation(direction); + } - if (trackRef.current) { - trackRef.current.style.transition = ""; - trackRef.current.style.transform = "translate3d(0px, 0, 0)"; - } + function goToSlide(newSlideNumber: number) { + if (newSlideNumber === slideNumber || total <= 1) return; - onSlideChange(newSlideNumber); - } + // Direct navigation: completely reset visual state and instantly jump + isAnimating.current = false; + targetSlide.current = newSlideNumber; + setCurrentLogicalIndex(newSlideNumber - 1); - function executeSwipeNavigation(result: { - action: "next" | "prev" | "cancel"; - }) { - if (!trackRef.current || total <= 1) return; - - // Get current transform X from the track style to know where to animate from - const style = trackRef.current.style.transform; - const match = style.match(/translate3d\(([-\d.]+)px/); - const currentX = match ? parseFloat(match[1]) : 0; - - if (result.action === "next") { - executeNavigation(1, currentX); - } else if (result.action === "prev") { - executeNavigation(-1, currentX); - } else { - executeNavigation(0, currentX); // cancel / snap back - } + if (trackRef.current) { + trackRef.current.style.transition = ""; + trackRef.current.style.transform = "translate3d(0px, 0, 0)"; } - const { - onPointerDown, - onPointerMove, - onPointerUp, - onPointerCancel, - onLostPointerCapture, - } = useCarouselGesture({ - trackRef, - onDragEnd: executeSwipeNavigation, - canNavigatePrev: total > 1, // Currently looping is always enabled if > 1 slide - canNavigateNext: total > 1, - }); - - function renderSlide(index: number, position: "prev" | "curr" | "next") { - const imageData = imagesData[index]; - if (!imageData) return null; - - const imageLoading = - position === "curr" ? (lazy ? "lazy" : "eager") : "eager"; - - // Stable keys based on imageData.id - return ( -
-
- {imageData.alt} - -
-
- ); + onSlideChange(newSlideNumber); + } + + function executeSwipeNavigation(result: { + action: "next" | "prev" | "cancel"; + }) { + if (!trackRef.current || total <= 1) return; + + // Get current transform X from the track style to know where to animate from + const style = trackRef.current.style.transform; + const match = style.match(/translate3d\(([-\d.]+)px/); + const currentX = match ? parseFloat(match[1]) : 0; + + if (result.action === "next") { + executeNavigation(1, currentX); + } else if (result.action === "prev") { + executeNavigation(-1, currentX); + } else { + executeNavigation(0, currentX); // cancel / snap back } - - useImperativeHandle(ref, () => ({ - navigate, - goToSlide, - })); - + } + + const { + onPointerDown, + onPointerMove, + onPointerUp, + onPointerCancel, + onLostPointerCapture, + } = useCarouselGesture({ + trackRef, + onDragEnd: executeSwipeNavigation, + canNavigatePrev: total > 1, // Currently looping is always enabled if > 1 slide + canNavigateNext: total > 1, + }); + + function renderSlide(index: number, position: "prev" | "curr" | "next") { + const imageData = imagesData[index]; + if (!imageData) return null; + + const imageLoading = + position === "curr" ? (lazy ? "lazy" : "eager") : "eager"; + + // Stable keys based on imageData.id return (
-
- {total > 1 && renderSlide(prevIndex, "prev")} - {total > 0 && renderSlide(currentIndex, "curr")} - {total > 1 && renderSlide(nextIndex, "next")} -
+
+ {imageData.alt} + +
); - }, -); + } + + useImperativeHandle(ref, () => ({ + navigate, + goToSlide, + })); + + return ( +
+
+ {total > 1 && renderSlide(prevIndex, "prev")} + {total > 0 && renderSlide(currentIndex, "curr")} + {total > 1 && renderSlide(nextIndex, "next")} +
+
+ ); +} From 1920bdd9d69366e33f52ae61804a593b9b2524dc Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Fri, 21 Aug 2026 21:37:53 +0100 Subject: [PATCH 07/26] refactor: Rename currentLogicalIndex to currentIndex --- src/lightbox/CarouselTrack.tsx | 81 ++++++++++++++++------------------ 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx index ad4fe68..55277ff 100644 --- a/src/lightbox/CarouselTrack.tsx +++ b/src/lightbox/CarouselTrack.tsx @@ -34,36 +34,13 @@ export function CarouselTrack({ const trackRef = useRef(null); const isAnimating = useRef(false); const targetSlide = useRef(slideNumber); - const [currentLogicalIndex, setCurrentLogicalIndex] = useState( - slideNumber - 1, - ); - - // Keep targetSlide in sync with external changes (e.g. thumbnails) - useEffect(() => { - targetSlide.current = slideNumber; - setCurrentLogicalIndex(slideNumber - 1); - }, [slideNumber]); + const [currentIndex, setCurrentIndex] = useState(slideNumber - 1); - const total = imagesData.length; - const currentIndex = currentLogicalIndex; + const totalImages = imagesData.length; // Determine the indices for prev and next slides, looping - const prevIndex = currentIndex === 0 ? total - 1 : currentIndex - 1; - const nextIndex = currentIndex === total - 1 ? 0 : currentIndex + 1; - - // Reset track position instantly after a logical slide change - useLayoutEffect(() => { - if (trackRef.current) { - trackRef.current.style.transition = ""; - trackRef.current.style.transform = "translate3d(0px, 0, 0)"; - - // Force layout recalculation - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - trackRef.current.offsetHeight; - - isAnimating.current = false; - } - }, [slideNumber]); + const prevIndex = currentIndex === 0 ? totalImages - 1 : currentIndex - 1; + const nextIndex = currentIndex === totalImages - 1 ? 0 : currentIndex + 1; function executeNavigation(direction: -1 | 1 | 0, fromX: number = 0) { if (!trackRef.current) return; @@ -101,11 +78,11 @@ export function CarouselTrack({ newSlideNumber -= 1; } - if (newSlideNumber > total) newSlideNumber = 1; - if (newSlideNumber < 1) newSlideNumber = total; + if (newSlideNumber > totalImages) newSlideNumber = 1; + if (newSlideNumber < 1) newSlideNumber = totalImages; targetSlide.current = newSlideNumber; - setCurrentLogicalIndex(newSlideNumber - 1); + setCurrentIndex(newSlideNumber - 1); // Updating the state will trigger the useLayoutEffect which resets the track onSlideChange(newSlideNumber); } @@ -150,17 +127,17 @@ export function CarouselTrack({ } function navigate(direction: -1 | 1) { - if (isAnimating.current || !trackRef.current || total <= 1) return; + if (isAnimating.current || !trackRef.current || totalImages <= 1) return; executeNavigation(direction); } function goToSlide(newSlideNumber: number) { - if (newSlideNumber === slideNumber || total <= 1) return; + if (newSlideNumber === slideNumber || totalImages <= 1) return; // Direct navigation: completely reset visual state and instantly jump isAnimating.current = false; targetSlide.current = newSlideNumber; - setCurrentLogicalIndex(newSlideNumber - 1); + setCurrentIndex(newSlideNumber - 1); if (trackRef.current) { trackRef.current.style.transition = ""; @@ -173,7 +150,7 @@ export function CarouselTrack({ function executeSwipeNavigation(result: { action: "next" | "prev" | "cancel"; }) { - if (!trackRef.current || total <= 1) return; + if (!trackRef.current || totalImages <= 1) return; // Get current transform X from the track style to know where to animate from const style = trackRef.current.style.transform; @@ -198,8 +175,8 @@ export function CarouselTrack({ } = useCarouselGesture({ trackRef, onDragEnd: executeSwipeNavigation, - canNavigatePrev: total > 1, // Currently looping is always enabled if > 1 slide - canNavigateNext: total > 1, + canNavigatePrev: totalImages > 1, // Currently looping is always enabled if > 1 slide + canNavigateNext: totalImages > 1, }); function renderSlide(index: number, position: "prev" | "curr" | "next") { @@ -239,24 +216,44 @@ export function CarouselTrack({ goToSlide, })); + // Keep targetSlide in sync with external changes (e.g. thumbnails) + useEffect(() => { + targetSlide.current = slideNumber; + setCurrentIndex(slideNumber - 1); + }, [slideNumber]); + + // Reset track position instantly after a logical slide change + useLayoutEffect(() => { + if (trackRef.current) { + trackRef.current.style.transition = ""; + trackRef.current.style.transform = "translate3d(0px, 0, 0)"; + + // Force layout recalculation + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + trackRef.current.offsetHeight; + + isAnimating.current = false; + } + }, [slideNumber]); + return (
- {total > 1 && renderSlide(prevIndex, "prev")} - {total > 0 && renderSlide(currentIndex, "curr")} - {total > 1 && renderSlide(nextIndex, "next")} + {totalImages > 1 && renderSlide(prevIndex, "prev")} + {totalImages > 0 && renderSlide(currentIndex, "curr")} + {totalImages > 1 && renderSlide(nextIndex, "next")}
); From b7fb2a9fecfdd49f857b2d5d65e091209c5cd4a7 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Sat, 22 Aug 2026 04:54:54 +0100 Subject: [PATCH 08/26] refactor: Remove barrel index.ts file and import Lightbox directly --- src/ImageGallery.tsx | 2 +- src/lightbox/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/lightbox/index.ts diff --git a/src/ImageGallery.tsx b/src/ImageGallery.tsx index 5598a7b..b2350da 100644 --- a/src/ImageGallery.tsx +++ b/src/ImageGallery.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { updateCaptionOpacity } from "./helpers.ts"; import { ImageGalleryPropsType } from "./ImageGallery.types.tsx"; -import { Lightbox } from "./lightbox"; +import { Lightbox } from "./lightbox/Lightbox.tsx"; export function ImageGallery({ columnCount = "auto", diff --git a/src/lightbox/index.ts b/src/lightbox/index.ts deleted file mode 100644 index 5e08f35..0000000 --- a/src/lightbox/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Lightbox } from "./Lightbox"; From 215b6a0c3528aa6b9d34b05d17e880a2d77f8686 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Sat, 22 Aug 2026 05:50:14 +0100 Subject: [PATCH 09/26] refactor: Rename handleThumbnailClick to goToClickedThumbnail --- src/lightbox/Lightbox.tsx | 152 ++++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 73 deletions(-) diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx index 757f83e..06dc125 100644 --- a/src/lightbox/Lightbox.tsx +++ b/src/lightbox/Lightbox.tsx @@ -1,89 +1,52 @@ import { useRef, useEffect, useState } from "react"; -import { ImageDataType } from "../ImageGallery.types"; -import { Toolbar } from "./Toolbar"; -import { Navigation } from "./Navigation"; import { CarouselTrack, CarouselTrackRef } from "./CarouselTrack"; +import { Navigation } from "./Navigation"; import { ThumbnailStrip } from "./ThumbnailStrip"; +import { Toolbar } from "./Toolbar"; +import { ImageDataType } from "../ImageGallery.types"; interface LightboxProps { - isOpen: boolean; imagesData: Array; initialSlideNumber: number; + isOpen: boolean; + lazy: boolean; onClose: () => void; - showThumbnails: boolean; setShowThumbnails: (show: boolean) => void; + showThumbnails: boolean; thumbnailBorder: string; - lazy: boolean; } export function Lightbox({ - isOpen, imagesData, initialSlideNumber, + isOpen, + lazy, onClose, - showThumbnails, setShowThumbnails, + showThumbnails, thumbnailBorder, - lazy, }: LightboxProps) { - const [slideNumber, setSlideNumber] = useState(initialSlideNumber); - const [showModalControls, setShowModalControls] = useState(false); const [fullscreen, setFullscreen] = useState(false); - + const [showModalControls, setShowModalControls] = useState(false); + const [slideNumber, setSlideNumber] = useState(initialSlideNumber); + + const carouselRef = useRef(null); const dialogRef = useRef(null); const lightboxRef = useRef(null); - const carouselRef = useRef(null); - - // Sync initial slide - useEffect(() => { - setSlideNumber(initialSlideNumber); - }, [initialSlideNumber]); - - // Handle open/close and scroll lock - useEffect(() => { - if (!isOpen) { - if (dialogRef.current?.open) { - dialogRef.current.close(); - } - return; - } - - if (dialogRef.current && !dialogRef.current.open) { - dialogRef.current.showModal(); - } - - const html = document.documentElement; - const originalHtmlOverflow = html.style.overflow; - - html.style.overflow = "hidden"; - - return () => { - html.style.overflow = originalHtmlOverflow; - }; - }, [isOpen]); function switchFullScreen(on: boolean) { if (on) { lightboxRef.current?.requestFullscreen().catch((error) => { - alert(`Error while attempting to switch into fullscreen mode: ${error.message}`); + alert( + `Error while attempting to switch into fullscreen mode: ${error.message}`, + ); }); } else { document.exitFullscreen().catch((error) => console.error(error)); } } - useEffect(() => { - if (!isOpen) return; - - function handleFullscreenChange() { - setFullscreen(Boolean(document.fullscreenElement)); - lightboxRef.current?.focus(); - } - document.addEventListener("fullscreenchange", handleFullscreenChange); - return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); - }, [isOpen]); - - function handleKeyDown(e: React.KeyboardEvent) { + function handleKeyboardShortcut(e: React.KeyboardEvent) { if (e.key === "ArrowLeft") { carouselRef.current?.navigate(-1); } else if (e.key === "ArrowRight") { @@ -97,37 +60,79 @@ export function Lightbox({ } function exitFullScreenAndDialog() { - if (fullscreen) { - switchFullScreen(false); - } + if (fullscreen) switchFullScreen(false); dialogRef.current?.close(); onClose(); } - function handleThumbnailClick(index: number) { + function goToClickedThumbnail(index: number) { carouselRef.current?.goToSlide(index + 1); } + // Sync initial slide + useEffect(() => { + setSlideNumber(initialSlideNumber); + }, [initialSlideNumber]); + + // Handle open/close and scroll lock + useEffect(() => { + if (!isOpen) { + if (dialogRef.current?.open) dialogRef.current.close(); + return; + } + + if (dialogRef.current && !dialogRef.current.open) + dialogRef.current.showModal(); + + const html = document.documentElement; + const originalHtmlOverflow = html.style.overflow; + + html.style.overflow = "hidden"; + return () => { + html.style.overflow = originalHtmlOverflow; + }; + }, [isOpen]); + + // Keep track of fullscreen state + useEffect(() => { + if (!isOpen) return; + + function syncFullscreenState() { + setFullscreen(Boolean(document.fullscreenElement)); + lightboxRef.current?.focus(); + } + + document.addEventListener("fullscreenchange", syncFullscreenState); + return () => + document.removeEventListener("fullscreenchange", syncFullscreenState); + }, [isOpen]); + return ( - { - e.preventDefault(); - exitFullScreenAndDialog(); - }}> + { + e.preventDefault(); + exitFullScreenAndDialog(); + }} + >
setShowModalControls(true)} onMouseLeave={() => setShowModalControls(false)} // We use pointer down to close when clicking the backdrop onPointerDown={(e) => { - if ((e.target as HTMLElement).getAttribute("data-backdrop") === "true") { + if ( + (e.target as HTMLElement).getAttribute("data-backdrop") === "true" + ) { exitFullScreenAndDialog(); } }} + ref={lightboxRef} + tabIndex={-1} > - -
+
- carouselRef.current?.navigate(dir)} - showControls={showModalControls} + carouselRef.current?.navigate(dir)} + showControls={showModalControls} />
- From 40002f625841c06aaac9342fa6676eca3622cdff Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Sat, 22 Aug 2026 06:58:27 +0100 Subject: [PATCH 10/26] chore: Create closeLightbox logic to close lightbox on backdrop click --- src/ImageGallery.test.tsx | 130 +++++++++++++++++++++------------ src/lightbox/CarouselTrack.tsx | 10 +-- src/lightbox/Lightbox.tsx | 40 +++++----- src/style.css | 20 +++-- 4 files changed, 121 insertions(+), 79 deletions(-) diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index a27c51e..cf8561f 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -10,9 +10,9 @@ beforeAll(() => { this.removeAttribute("open"); }; - Object.defineProperty(window, 'matchMedia', { + Object.defineProperty(window, "matchMedia", { writable: true, - value: jest.fn().mockImplementation(query => ({ + value: jest.fn().mockImplementation((query) => ({ matches: true, media: query, onchange: null, @@ -46,22 +46,22 @@ beforeAll(() => { beforeAll(() => { HTMLDialogElement.prototype.showModal = jest.fn(function mock( - this: HTMLDialogElement + this: HTMLDialogElement, ) { this.open = true; - this.setAttribute('open', ''); + this.setAttribute("open", ""); }); HTMLDialogElement.prototype.close = jest.fn(function mock( - this: HTMLDialogElement + this: HTMLDialogElement, ) { this.open = false; - this.removeAttribute('open'); + this.removeAttribute("open"); }); - Object.defineProperty(window, 'matchMedia', { + Object.defineProperty(window, "matchMedia", { writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: query === '(prefers-reduced-motion: reduce)', + value: jest.fn().mockImplementation((query) => ({ + matches: query === "(prefers-reduced-motion: reduce)", media: query, onchange: null, addListener: jest.fn(), // Deprecated @@ -313,24 +313,36 @@ test("lightbox gesture navigation threshold and state update", async () => { expect(await screen.findByText("1 / 13")).toBeTruthy(); // Find the track container - const track = document.querySelector(".cs-rigg-carousel-track") as HTMLElement; + const track = document.querySelector( + ".cs-rigg-carousel-track", + ) as HTMLElement; if (!track) throw new Error("Track not found"); // Mock getBoundingClientRect for trackWidth - track.getBoundingClientRect = () => ({ width: 1000 } as any); + track.getBoundingClientRect = () => ({ width: 1000 }) as any; const originalNow = performance.now; let currentTime = 0; - jest.spyOn(performance, 'now').mockImplementation(() => currentTime); + jest.spyOn(performance, "now").mockImplementation(() => currentTime); // 1. Short slow drag (should cancel) - deltaX < 200, duration > 250 act(() => { currentTime = 0; - fireEvent.pointerDown(track, { isPrimary: true, clientX: 500, clientY: 500, pointerId: 1 }); + fireEvent.pointerDown(track, { + isPrimary: true, + clientX: 500, + clientY: 500, + pointerId: 1, + }); }); act(() => { currentTime = 500; - fireEvent.pointerMove(track, { isPrimary: true, clientX: 450, clientY: 500, pointerId: 1 }); // deltaX = -50 + fireEvent.pointerMove(track, { + isPrimary: true, + clientX: 450, + clientY: 500, + pointerId: 1, + }); // deltaX = -50 }); act(() => { currentTime = 510; @@ -341,11 +353,21 @@ test("lightbox gesture navigation threshold and state update", async () => { // 2. Long slow drag (should commit next) - deltaX < -200, duration > 250 act(() => { currentTime = 1000; - fireEvent.pointerDown(track, { isPrimary: true, clientX: 500, clientY: 500, pointerId: 1 }); + fireEvent.pointerDown(track, { + isPrimary: true, + clientX: 500, + clientY: 500, + pointerId: 1, + }); }); act(() => { currentTime = 1500; - fireEvent.pointerMove(track, { isPrimary: true, clientX: 200, clientY: 500, pointerId: 1 }); // deltaX = -300 + fireEvent.pointerMove(track, { + isPrimary: true, + clientX: 200, + clientY: 500, + pointerId: 1, + }); // deltaX = -300 }); act(() => { currentTime = 1510; @@ -356,11 +378,21 @@ test("lightbox gesture navigation threshold and state update", async () => { // 3. Short fast flick (should commit next) - deltaX < 200, duration < 250 act(() => { currentTime = 2000; - fireEvent.pointerDown(track, { isPrimary: true, clientX: 500, clientY: 500, pointerId: 1 }); + fireEvent.pointerDown(track, { + isPrimary: true, + clientX: 500, + clientY: 500, + pointerId: 1, + }); }); act(() => { currentTime = 2050; // 50ms duration - fireEvent.pointerMove(track, { isPrimary: true, clientX: 450, clientY: 500, pointerId: 1 }); // deltaX = -50 + fireEvent.pointerMove(track, { + isPrimary: true, + clientX: 450, + clientY: 500, + pointerId: 1, + }); // deltaX = -50 }); act(() => { currentTime = 2060; @@ -382,30 +414,30 @@ test("direct thumbnail navigation completely resets state", async () => { // Show thumbnails and click thumbnail for image 4 const toggleThumbBtn = screen.getByTitle("Show thumbnails"); fireEvent.click(toggleThumbBtn); - + const thumbs = document.querySelectorAll(".cs-rigg-modal-thumb-imgs-pod img"); expect(thumbs.length).toBe(13); - + act(() => { fireEvent.click(thumbs[3]); // index 3 is image 4 }); - + // Counter should instantly be 4 / 13 expect(await screen.findByText("4 / 13")).toBeTruthy(); - + // Ensure Next button goes to 5, meaning state is perfectly synced const nextBtn = screen.getByTitle("Next image"); act(() => { fireEvent.click(nextBtn); }); expect(await screen.findByText("5 / 13")).toBeTruthy(); - + // Click thumbnail 10 act(() => { fireEvent.click(thumbs[9]); // index 9 is image 10 }); expect(await screen.findByText("10 / 13")).toBeTruthy(); - + // Previous button goes to 9 const prevBtn = screen.getByTitle("Previous image"); act(() => { @@ -416,27 +448,31 @@ test("direct thumbnail navigation completely resets state", async () => { test("backdrop click closes lightbox, but content click does not", async () => { render(); - + // Open lightbox fireEvent.click(screen.getAllByAltText("Image1's alt text")[0]); expect(await screen.findByText("1 / 13")).toBeTruthy(); - + // Click the image - should not close - const modalImg = document.querySelector(".cs-rigg-modal-image") as HTMLElement; + const modalImg = document.querySelector( + ".cs-rigg-modal-image", + ) as HTMLElement; fireEvent.click(modalImg); expect(screen.queryByText("1 / 13")).toBeTruthy(); // still open - + // Click toolbar button - should not close const nextBtn = screen.getByTitle("Next image"); fireEvent.click(nextBtn); expect(screen.queryByText("2 / 13")).toBeTruthy(); // still open, just moved to slide 2 - - // Click the background area (track container which has data-backdrop="true") - const trackContainer = document.querySelector(".cs-rigg-carousel-track-container") as HTMLElement; + + // Click the backdrop area (the track container empty space) + const backdrop = document.querySelector( + ".cs-rigg-carousel-track-container", + ) as HTMLElement; act(() => { - fireEvent.pointerDown(trackContainer); + fireEvent.click(backdrop); }); - + // Should close const dialog = document.querySelector(".cs-rigg-dialog") as HTMLDialogElement; expect(dialog.open).toBe(false); @@ -445,34 +481,38 @@ test("backdrop click closes lightbox, but content click does not", async () => { test("scroll lock lifecycle correctly restores exactly previous styles", async () => { // Set initial overflow to something custom document.documentElement.style.overflow = "scroll"; - + const { unmount } = render(); - + expect(document.documentElement.style.overflow).toBe("scroll"); - + // Open lightbox fireEvent.click(screen.getAllByAltText("Image1's alt text")[0]); expect(await screen.findByText("1 / 13")).toBeTruthy(); - + // Should be locked expect(document.documentElement.style.overflow).toBe("hidden"); - + // Close via escape act(() => { - const dialog = document.querySelector(".cs-rigg-dialog") as HTMLDialogElement; - fireEvent(dialog, new Event('cancel')); + const dialog = document.querySelector( + ".cs-rigg-dialog", + ) as HTMLDialogElement; + fireEvent(dialog, new Event("cancel")); }); - - expect((document.querySelector(".cs-rigg-dialog") as HTMLDialogElement).open).toBe(false); - + + expect( + (document.querySelector(".cs-rigg-dialog") as HTMLDialogElement).open, + ).toBe(false); + // Should exact restore previous style expect(document.documentElement.style.overflow).toBe("scroll"); - + // Test unmount cleanup fireEvent.click(screen.getAllByAltText("Image1's alt text")[0]); expect(await screen.findByText("1 / 13")).toBeTruthy(); expect(document.documentElement.style.overflow).toBe("hidden"); - + unmount(); expect(document.documentElement.style.overflow).toBe("scroll"); }); diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx index 55277ff..f19df0d 100644 --- a/src/lightbox/CarouselTrack.tsx +++ b/src/lightbox/CarouselTrack.tsx @@ -192,7 +192,6 @@ export function CarouselTrack({ key={imageData.id} className={`cs-rigg-carousel-slide`} data-position={position} - data-backdrop="true" >
-
+
{totalImages > 1 && renderSlide(prevIndex, "prev")} {totalImages > 0 && renderSlide(currentIndex, "curr")} {totalImages > 1 && renderSlide(nextIndex, "next")} diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx index 06dc125..8a33960 100644 --- a/src/lightbox/Lightbox.tsx +++ b/src/lightbox/Lightbox.tsx @@ -46,6 +46,25 @@ export function Lightbox({ } } + function exitFullScreenAndDialog() { + if (fullscreen) switchFullScreen(false); + dialogRef.current?.close(); + onClose(); + } + + function closeLightbox(e: React.MouseEvent) { + const target = e.target as HTMLElement; + const isBackdropClick = + target.tagName === "SECTION" || + target.tagName === "ARTICLE" || + target.tagName === "FIGURE" || + target.classList.contains("cs-rigg-carousel-slide") || + target.classList.contains("cs-rigg-carousel-track-container") || + target.classList.contains("cs-rigg-carousel-track"); + + if (isBackdropClick) exitFullScreenAndDialog(); + } + function handleKeyboardShortcut(e: React.KeyboardEvent) { if (e.key === "ArrowLeft") { carouselRef.current?.navigate(-1); @@ -59,12 +78,6 @@ export function Lightbox({ } } - function exitFullScreenAndDialog() { - if (fullscreen) switchFullScreen(false); - dialogRef.current?.close(); - onClose(); - } - function goToClickedThumbnail(index: number) { carouselRef.current?.goToSlide(index + 1); } @@ -119,18 +132,10 @@ export function Lightbox({
setShowModalControls(true)} onMouseLeave={() => setShowModalControls(false)} - // We use pointer down to close when clicking the backdrop - onPointerDown={(e) => { - if ( - (e.target as HTMLElement).getAttribute("data-backdrop") === "true" - ) { - exitFullScreenAndDialog(); - } - }} ref={lightboxRef} tabIndex={-1} > @@ -143,10 +148,7 @@ export function Lightbox({ onToggleFullscreen={switchFullScreen} onClose={exitFullScreenAndDialog} /> -
+
Date: Sun, 23 Aug 2026 21:55:14 +0100 Subject: [PATCH 11/26] refactor: Rename onNavigate to changeSlide --- src/lightbox/Lightbox.tsx | 2 +- src/lightbox/Navigation.tsx | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx index 8a33960..78b70a7 100644 --- a/src/lightbox/Lightbox.tsx +++ b/src/lightbox/Lightbox.tsx @@ -158,7 +158,7 @@ export function Lightbox({ lazy={lazy} /> carouselRef.current?.navigate(dir)} + changeSlide={(dir) => carouselRef.current?.navigate(dir)} showControls={showModalControls} />
diff --git a/src/lightbox/Navigation.tsx b/src/lightbox/Navigation.tsx index 55a03d0..f80ff07 100644 --- a/src/lightbox/Navigation.tsx +++ b/src/lightbox/Navigation.tsx @@ -1,40 +1,40 @@ import { SvgElement } from "../helpers"; interface NavigationProps { - onNavigate: (direction: -1 | 1) => void; + changeSlide: (direction: -1 | 1) => void; showControls: boolean; } -export function Navigation({ onNavigate, showControls }: NavigationProps) { +export function Navigation({ changeSlide, showControls }: NavigationProps) { return ( <> From 092ddfd7574360dd8f319f10e17f8a43952229a7 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 14:00:01 +0100 Subject: [PATCH 12/26] refactor: Rename onThumbnailClick to goToClickedThumbnail --- src/lightbox/CarouselTrack.tsx | 8 ++++---- src/lightbox/Lightbox.tsx | 14 +++++++------- src/lightbox/ThumbnailStrip.tsx | 14 +++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx index f19df0d..e9bc224 100644 --- a/src/lightbox/CarouselTrack.tsx +++ b/src/lightbox/CarouselTrack.tsx @@ -17,7 +17,7 @@ export interface CarouselTrackRef { interface CarouselTrackProps { imagesData: Array; slideNumber: number; // 1-indexed - onSlideChange: (newSlideNumber: number) => void; + changeSlide: (newSlideNumber: number) => void; showThumbnails: boolean; lazy: boolean; ref: React.Ref; @@ -26,7 +26,7 @@ interface CarouselTrackProps { export function CarouselTrack({ imagesData, slideNumber, - onSlideChange, + changeSlide, showThumbnails, lazy, ref, @@ -84,7 +84,7 @@ export function CarouselTrack({ targetSlide.current = newSlideNumber; setCurrentIndex(newSlideNumber - 1); // Updating the state will trigger the useLayoutEffect which resets the track - onSlideChange(newSlideNumber); + changeSlide(newSlideNumber); } // Check if reduced motion is enabled @@ -144,7 +144,7 @@ export function CarouselTrack({ trackRef.current.style.transform = "translate3d(0px, 0, 0)"; } - onSlideChange(newSlideNumber); + changeSlide(newSlideNumber); } function executeSwipeNavigation(result: { diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx index 78b70a7..57d610a 100644 --- a/src/lightbox/Lightbox.tsx +++ b/src/lightbox/Lightbox.tsx @@ -150,12 +150,12 @@ export function Lightbox({ />
carouselRef.current?.navigate(dir)} @@ -163,12 +163,12 @@ export function Lightbox({ />
diff --git a/src/lightbox/ThumbnailStrip.tsx b/src/lightbox/ThumbnailStrip.tsx index affbfcf..105d9c7 100644 --- a/src/lightbox/ThumbnailStrip.tsx +++ b/src/lightbox/ThumbnailStrip.tsx @@ -2,21 +2,21 @@ import { useEffect, useRef } from "react"; import { ImageDataType } from "../ImageGallery.types"; interface ThumbnailStripProps { + goToClickedThumbnail: (index: number) => void; imagesData: Array; - slideNumber: number; + lazy: boolean; showThumbnails: boolean; - onThumbnailClick: (index: number) => void; + slideNumber: number; thumbnailBorder: string; - lazy: boolean; } export function ThumbnailStrip({ + goToClickedThumbnail, imagesData, - slideNumber, + lazy, showThumbnails, - onThumbnailClick, + slideNumber, thumbnailBorder, - lazy, }: ThumbnailStripProps) { const activeThumbImgRef = useRef(null); @@ -51,7 +51,7 @@ export function ThumbnailStrip({ key={imageData.id} src={imageData.thumbSrc || imageData.src} alt={imageData.alt} - onClick={() => onThumbnailClick(index)} + onClick={() => goToClickedThumbnail(index)} /> ))}
From 87eedcb9732f20a9dffacad8536ab4757f12c74f Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 14:32:24 +0100 Subject: [PATCH 13/26] refactor: Rename onClose to exitFullScreenAndDialog --- src/ImageGallery.tsx | 8 ++++---- src/lightbox/Lightbox.tsx | 14 +++++++------- src/lightbox/Toolbar.tsx | 24 ++++++++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/ImageGallery.tsx b/src/ImageGallery.tsx index b2350da..08dea12 100644 --- a/src/ImageGallery.tsx +++ b/src/ImageGallery.tsx @@ -95,14 +95,14 @@ export function ImageGallery({ {showImageCards()} {enableDefaultLightbox && ( setLightboxOpen(false)} imagesData={imagesData} initialSlideNumber={slideNumber} - onClose={() => setLightboxOpen(false)} - showThumbnails={showThumbnails} + isOpen={lightboxOpen} + lazy={lazy} setShowThumbnails={setShowThumbnails} + showThumbnails={showThumbnails} thumbnailBorder={thumbnailBorder} - lazy={lazy} /> )}
diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx index 57d610a..f69d2d3 100644 --- a/src/lightbox/Lightbox.tsx +++ b/src/lightbox/Lightbox.tsx @@ -6,22 +6,22 @@ import { Toolbar } from "./Toolbar"; import { ImageDataType } from "../ImageGallery.types"; interface LightboxProps { + exitLightbox: () => void; imagesData: Array; initialSlideNumber: number; isOpen: boolean; lazy: boolean; - onClose: () => void; setShowThumbnails: (show: boolean) => void; showThumbnails: boolean; thumbnailBorder: string; } export function Lightbox({ + exitLightbox, imagesData, initialSlideNumber, isOpen, lazy, - onClose, setShowThumbnails, showThumbnails, thumbnailBorder, @@ -49,7 +49,7 @@ export function Lightbox({ function exitFullScreenAndDialog() { if (fullscreen) switchFullScreen(false); dialogRef.current?.close(); - onClose(); + exitLightbox(); } function closeLightbox(e: React.MouseEvent) { @@ -140,13 +140,13 @@ export function Lightbox({ tabIndex={-1} > setShowThumbnails(!showThumbnails)} totalImages={imagesData.length} - onToggleThumbnails={() => setShowThumbnails(!showThumbnails)} - fullscreen={fullscreen} - onToggleFullscreen={switchFullScreen} - onClose={exitFullScreenAndDialog} + exitFullScreenAndDialog={exitFullScreenAndDialog} />
void; + fullscreen: boolean; showControls: boolean; slideNumber: number; + toggleFullscreen: (on: boolean) => void; + toggleThumbnails: () => void; totalImages: number; - onToggleThumbnails: () => void; - fullscreen: boolean; - onToggleFullscreen: (on: boolean) => void; - onClose: () => void; } export function Toolbar({ + exitFullScreenAndDialog, + fullscreen, showControls, slideNumber, + toggleFullscreen, + toggleThumbnails, totalImages, - onToggleThumbnails, - fullscreen, - onToggleFullscreen, - onClose, }: ToolbarProps) { return ( <> @@ -36,7 +36,7 @@ export function Toolbar({ aria-label="Show thumbnails" className="cs-rigg-modal-toolbar-btn" title="Show thumbnails" - onClick={onToggleThumbnails} + onClick={toggleThumbnails} > {SvgElement( , @@ -48,7 +48,7 @@ export function Toolbar({ className="cs-rigg-modal-toolbar-btn" style={{ display: fullscreen ? "none" : "block" }} title="Go full screen (f)" - onClick={() => onToggleFullscreen(true)} + onClick={() => toggleFullscreen(true)} > {SvgElement( , @@ -60,7 +60,7 @@ export function Toolbar({ className="cs-rigg-modal-toolbar-btn" style={{ display: fullscreen ? "block" : "none" }} title="Exit full screen" - onClick={() => onToggleFullscreen(false)} + onClick={() => toggleFullscreen(false)} > {SvgElement( , @@ -71,7 +71,7 @@ export function Toolbar({ aria-label="Close lightbox" className="cs-rigg-modal-toolbar-btn" title="Close lightbox" - onClick={onClose} + onClick={exitFullScreenAndDialog} > {SvgElement( , From 89e96b0545d9b2bcf953f900ccd76fb4766e5222 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 14:34:52 +0100 Subject: [PATCH 14/26] chore: Delete the redundant clamp utility --- src/utils/clamp.ts | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 src/utils/clamp.ts diff --git a/src/utils/clamp.ts b/src/utils/clamp.ts deleted file mode 100644 index 0210f95..0000000 --- a/src/utils/clamp.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max); -} From 0cefc315c03498003c6d586410496f113e88d63c Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 14:48:58 +0100 Subject: [PATCH 15/26] chore: Move ImageGalleryPropsType into ImageGallery.tsx --- src/ImageGallery.tsx | 17 ++++++++++++++++- src/ImageGallery.types.tsx | 16 ---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/ImageGallery.tsx b/src/ImageGallery.tsx index 08dea12..faa7c21 100644 --- a/src/ImageGallery.tsx +++ b/src/ImageGallery.tsx @@ -1,8 +1,23 @@ import { useState } from "react"; import { updateCaptionOpacity } from "./helpers.ts"; -import { ImageGalleryPropsType } from "./ImageGallery.types.tsx"; import { Lightbox } from "./lightbox/Lightbox.tsx"; +interface ImageGalleryPropsType { + columnCount?: string | number; + columnWidth?: string | number; + customizeImageClickAction?: ( + imageData?: ImageDataType, + index?: number, + ) => void; + enableDefaultLightbox?: boolean; + fixedCaption?: boolean; + gapSize?: number; + imagesData: Array; + lazy?: boolean; + lazyFromIndex?: number; + thumbnailBorder?: string; +} + export function ImageGallery({ columnCount = "auto", columnWidth = 230, diff --git a/src/ImageGallery.types.tsx b/src/ImageGallery.types.tsx index 4d36a7f..20d8ffa 100644 --- a/src/ImageGallery.types.tsx +++ b/src/ImageGallery.types.tsx @@ -19,22 +19,6 @@ export interface ImageDataType { mediaSizes?: string; } -export interface ImageGalleryPropsType { - columnCount?: string | number; - columnWidth?: string | number; - customizeImageClickAction?: ( - imageData?: ImageDataType, - index?: number, - ) => void; - enableDefaultLightbox?: boolean; - fixedCaption?: boolean; - gapSize?: number; - imagesData: Array; - lazy?: boolean; - lazyFromIndex?: number; - thumbnailBorder?: string; -} - export interface ImgSrcInfoType { mediaSizes: string | undefined; src: string; From 0ee456053aac13759cb72ba627689fdd18728f80 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 14:55:16 +0100 Subject: [PATCH 16/26] chore: Delete the redundant ImgSrcInfoType interface --- src/ImageGallery.types.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ImageGallery.types.tsx b/src/ImageGallery.types.tsx index 20d8ffa..c440073 100644 --- a/src/ImageGallery.types.tsx +++ b/src/ImageGallery.types.tsx @@ -19,12 +19,6 @@ export interface ImageDataType { mediaSizes?: string; } -export interface ImgSrcInfoType { - mediaSizes: string | undefined; - src: string; - srcSet: string | undefined; -} - export interface UseCarouselGestureProps { trackRef: React.RefObject; onDragEnd: (result: DragResult) => void; From 252affb6aa58f34a30b5dc02363511bdcc505ceb Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 14:59:06 +0100 Subject: [PATCH 17/26] chore: Move UseCarouselGestureProps into useCarouselGesture.ts hook --- src/ImageGallery.types.tsx | 11 ----------- src/hooks/useCarouselGesture.ts | 12 +++++++++++- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ImageGallery.types.tsx b/src/ImageGallery.types.tsx index c440073..af8e573 100644 --- a/src/ImageGallery.types.tsx +++ b/src/ImageGallery.types.tsx @@ -1,7 +1,3 @@ -export interface DragResult { - action: "next" | "prev" | "cancel"; -} - export interface ImageDataType { id: string | number; alt: string; @@ -18,10 +14,3 @@ export interface ImageDataType { srcSet?: string; mediaSizes?: string; } - -export interface UseCarouselGestureProps { - trackRef: React.RefObject; - onDragEnd: (result: DragResult) => void; - canNavigatePrev: boolean; - canNavigateNext: boolean; -} diff --git a/src/hooks/useCarouselGesture.ts b/src/hooks/useCarouselGesture.ts index 551e47b..b639d75 100644 --- a/src/hooks/useCarouselGesture.ts +++ b/src/hooks/useCarouselGesture.ts @@ -1,5 +1,15 @@ import { useCallback, useEffect, useRef } from "react"; -import type { UseCarouselGestureProps } from "../ImageGallery.types"; + +interface DragResult { + action: "next" | "prev" | "cancel"; +} + +interface UseCarouselGestureProps { + trackRef: React.RefObject; + onDragEnd: (result: DragResult) => void; + canNavigatePrev: boolean; + canNavigateNext: boolean; +} export function useCarouselGesture({ trackRef, From 76706d574e5e57a35dfeedc87916c9b5cb04e660 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 15:06:32 +0100 Subject: [PATCH 18/26] chore: Import ImageDataType --- src/ImageGallery.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ImageGallery.tsx b/src/ImageGallery.tsx index faa7c21..cdb4e5b 100644 --- a/src/ImageGallery.tsx +++ b/src/ImageGallery.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { updateCaptionOpacity } from "./helpers.ts"; import { Lightbox } from "./lightbox/Lightbox.tsx"; +import { ImageDataType } from "./ImageGallery.types"; interface ImageGalleryPropsType { columnCount?: string | number; From 94456e6b24f59a0e4269e1a839a4d6d2d221840a Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 20:29:40 +0100 Subject: [PATCH 19/26] test: Merge the two beforeAll blocks into one --- src/ImageGallery.test.tsx | 49 +++++++++++---------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index cf8561f..7789581 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -3,21 +3,27 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { ImageGallery } from "./ImageGallery"; beforeAll(() => { - HTMLDialogElement.prototype.showModal = function () { + HTMLDialogElement.prototype.showModal = jest.fn(function mock( + this: HTMLDialogElement, + ) { + this.open = true; this.setAttribute("open", ""); - }; - HTMLDialogElement.prototype.close = function () { + }); + HTMLDialogElement.prototype.close = jest.fn(function mock( + this: HTMLDialogElement, + ) { + this.open = false; this.removeAttribute("open"); - }; + }); Object.defineProperty(window, "matchMedia", { writable: true, value: jest.fn().mockImplementation((query) => ({ - matches: true, + matches: query === "(prefers-reduced-motion: reduce)", media: query, onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), @@ -44,35 +50,6 @@ beforeAll(() => { window.PointerEvent = MockPointerEvent as any; }); -beforeAll(() => { - HTMLDialogElement.prototype.showModal = jest.fn(function mock( - this: HTMLDialogElement, - ) { - this.open = true; - this.setAttribute("open", ""); - }); - HTMLDialogElement.prototype.close = jest.fn(function mock( - this: HTMLDialogElement, - ) { - this.open = false; - this.removeAttribute("open"); - }); - - Object.defineProperty(window, "matchMedia", { - writable: true, - value: jest.fn().mockImplementation((query) => ({ - matches: query === "(prefers-reduced-motion: reduce)", - media: query, - onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), - }); -}); - const imagesArray = [ { id: crypto.randomUUID(), From 289690549b3713d80ce5a5947c74cca8910c3b62 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 21:23:58 +0100 Subject: [PATCH 20/26] test: Remove deprecated mock methods --- src/ImageGallery.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index 7789581..3a299fb 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -22,8 +22,6 @@ beforeAll(() => { matches: query === "(prefers-reduced-motion: reduce)", media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), From e1981680f65874f4f7dc31fb1aacac6a0a231472 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 21:37:45 +0100 Subject: [PATCH 21/26] test: Remove the redundant act() wrappers --- src/ImageGallery.test.tsx | 164 +++++++++++++++----------------------- 1 file changed, 64 insertions(+), 100 deletions(-) diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index 3a299fb..d0fe5a6 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -1,5 +1,5 @@ import { expect, test, beforeAll, jest } from "@jest/globals"; -import { act, fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { ImageGallery } from "./ImageGallery"; beforeAll(() => { @@ -240,19 +240,13 @@ test("lightbox navigation updates counter and loops correctly", async () => { // Click next button const nextBtn = screen.getByTitle("Next image"); - act(() => { - fireEvent.click(nextBtn); - }); + fireEvent.click(nextBtn); expect(await screen.findByText("10 / 13")).toBeTruthy(); // Click previous button twice to go to 8 const prevBtn = screen.getByTitle("Previous image"); - act(() => { - fireEvent.click(prevBtn); - }); - act(() => { - fireEvent.click(prevBtn); - }); + fireEvent.click(prevBtn); + fireEvent.click(prevBtn); expect(await screen.findByText("8 / 13")).toBeTruthy(); }); @@ -266,16 +260,12 @@ test("lightbox looping boundary 13 -> 1 and 1 -> 13", async () => { // Click next -> should loop to 1 const nextBtn = screen.getByTitle("Next image"); - act(() => { - fireEvent.click(nextBtn); - }); + fireEvent.click(nextBtn); expect(screen.getByText("1 / 13")).toBeTruthy(); // Click prev -> should loop back to 13 const prevBtn = screen.getByTitle("Previous image"); - act(() => { - fireEvent.click(prevBtn); - }); + fireEvent.click(prevBtn); expect(screen.getByText("13 / 13")).toBeTruthy(); }); @@ -301,78 +291,66 @@ test("lightbox gesture navigation threshold and state update", async () => { jest.spyOn(performance, "now").mockImplementation(() => currentTime); // 1. Short slow drag (should cancel) - deltaX < 200, duration > 250 - act(() => { - currentTime = 0; - fireEvent.pointerDown(track, { - isPrimary: true, - clientX: 500, - clientY: 500, - pointerId: 1, - }); - }); - act(() => { - currentTime = 500; - fireEvent.pointerMove(track, { - isPrimary: true, - clientX: 450, - clientY: 500, - pointerId: 1, - }); // deltaX = -50 - }); - act(() => { - currentTime = 510; - fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); + currentTime = 0; + fireEvent.pointerDown(track, { + isPrimary: true, + clientX: 500, + clientY: 500, + pointerId: 1, }); + + currentTime = 500; + fireEvent.pointerMove(track, { + isPrimary: true, + clientX: 450, + clientY: 500, + pointerId: 1, + }); // deltaX = -50 + + currentTime = 510; + fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); expect(await screen.findByText("1 / 13")).toBeTruthy(); // Cancelled // 2. Long slow drag (should commit next) - deltaX < -200, duration > 250 - act(() => { - currentTime = 1000; - fireEvent.pointerDown(track, { - isPrimary: true, - clientX: 500, - clientY: 500, - pointerId: 1, - }); - }); - act(() => { - currentTime = 1500; - fireEvent.pointerMove(track, { - isPrimary: true, - clientX: 200, - clientY: 500, - pointerId: 1, - }); // deltaX = -300 - }); - act(() => { - currentTime = 1510; - fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); + currentTime = 1000; + fireEvent.pointerDown(track, { + isPrimary: true, + clientX: 500, + clientY: 500, + pointerId: 1, }); + + currentTime = 1500; + fireEvent.pointerMove(track, { + isPrimary: true, + clientX: 200, + clientY: 500, + pointerId: 1, + }); // deltaX = -300 + + currentTime = 1510; + fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); expect(await screen.findByText("2 / 13")).toBeTruthy(); // 3. Short fast flick (should commit next) - deltaX < 200, duration < 250 - act(() => { - currentTime = 2000; - fireEvent.pointerDown(track, { - isPrimary: true, - clientX: 500, - clientY: 500, - pointerId: 1, - }); - }); - act(() => { - currentTime = 2050; // 50ms duration - fireEvent.pointerMove(track, { - isPrimary: true, - clientX: 450, - clientY: 500, - pointerId: 1, - }); // deltaX = -50 - }); - act(() => { - currentTime = 2060; - fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); + currentTime = 2000; + fireEvent.pointerDown(track, { + isPrimary: true, + clientX: 500, + clientY: 500, + pointerId: 1, }); + + currentTime = 2050; // 50ms duration + fireEvent.pointerMove(track, { + isPrimary: true, + clientX: 450, + clientY: 500, + pointerId: 1, + }); // deltaX = -50 + + currentTime = 2060; + fireEvent.pointerUp(track, { isPrimary: true, pointerId: 1 }); expect(await screen.findByText("3 / 13")).toBeTruthy(); performance.now = originalNow; @@ -393,31 +371,23 @@ test("direct thumbnail navigation completely resets state", async () => { const thumbs = document.querySelectorAll(".cs-rigg-modal-thumb-imgs-pod img"); expect(thumbs.length).toBe(13); - act(() => { - fireEvent.click(thumbs[3]); // index 3 is image 4 - }); + fireEvent.click(thumbs[3]); // index 3 is image 4 // Counter should instantly be 4 / 13 expect(await screen.findByText("4 / 13")).toBeTruthy(); // Ensure Next button goes to 5, meaning state is perfectly synced const nextBtn = screen.getByTitle("Next image"); - act(() => { - fireEvent.click(nextBtn); - }); + fireEvent.click(nextBtn); expect(await screen.findByText("5 / 13")).toBeTruthy(); // Click thumbnail 10 - act(() => { - fireEvent.click(thumbs[9]); // index 9 is image 10 - }); + fireEvent.click(thumbs[9]); // index 9 is image 10 expect(await screen.findByText("10 / 13")).toBeTruthy(); // Previous button goes to 9 const prevBtn = screen.getByTitle("Previous image"); - act(() => { - fireEvent.click(prevBtn); - }); + fireEvent.click(prevBtn); expect(await screen.findByText("9 / 13")).toBeTruthy(); }); @@ -444,9 +414,7 @@ test("backdrop click closes lightbox, but content click does not", async () => { const backdrop = document.querySelector( ".cs-rigg-carousel-track-container", ) as HTMLElement; - act(() => { - fireEvent.click(backdrop); - }); + fireEvent.click(backdrop); // Should close const dialog = document.querySelector(".cs-rigg-dialog") as HTMLDialogElement; @@ -469,12 +437,8 @@ test("scroll lock lifecycle correctly restores exactly previous styles", async ( expect(document.documentElement.style.overflow).toBe("hidden"); // Close via escape - act(() => { - const dialog = document.querySelector( - ".cs-rigg-dialog", - ) as HTMLDialogElement; - fireEvent(dialog, new Event("cancel")); - }); + const dialog = document.querySelector(".cs-rigg-dialog") as HTMLDialogElement; + fireEvent(dialog, new Event("cancel")); expect( (document.querySelector(".cs-rigg-dialog") as HTMLDialogElement).open, From e9f4c50ea01675eb10d1af30699eba5ac30e0923 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 21:46:56 +0100 Subject: [PATCH 22/26] docs: Explain what each section in the beforeAll block does --- src/ImageGallery.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index d0fe5a6..5f8bf19 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -3,6 +3,8 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { ImageGallery } from "./ImageGallery"; beforeAll(() => { + // JSDOM doesn't implement showModal/close, so we polyfill them. + // Setting this.open keeps dialog.open readable in assertions. HTMLDialogElement.prototype.showModal = jest.fn(function mock( this: HTMLDialogElement, ) { @@ -16,6 +18,8 @@ beforeAll(() => { this.removeAttribute("open"); }); + // JSDOM doesn't implement matchMedia. The carousel reads this to disable + // CSS transitions when the user prefers reduced motion. Object.defineProperty(window, "matchMedia", { writable: true, value: jest.fn().mockImplementation((query) => ({ @@ -28,10 +32,14 @@ beforeAll(() => { })), }); + // JSDOM stubs: scrollIntoView is called by the thumbnail strip; pointer + // capture methods are called by the gesture hook on drag start/end. Element.prototype.scrollIntoView = jest.fn(); Element.prototype.setPointerCapture = jest.fn(); Element.prototype.releasePointerCapture = jest.fn(); + // JSDOM doesn't implement PointerEvent. This minimal mock adds the fields + // (clientX, clientY, isPrimary, pointerId) that the gesture hook reads. class MockPointerEvent extends Event { clientX: number; clientY: number; From d2b6257234f8991c0caa83ebf74079cf35531457 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 22:27:59 +0100 Subject: [PATCH 23/26] build: Bump commitlint/cli from v20.5.2 to v21.2.2 --- package-lock.json | 2063 +++++++++++++++++++++++---------------------- package.json | 16 +- 2 files changed, 1074 insertions(+), 1005 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d280dd..c5df16e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,16 @@ "version": "0.0.0-development", "license": "MIT", "devDependencies": { - "@commitlint/cli": "^20.5.2", - "@commitlint/config-conventional": "^20.5.0", + "@commitlint/cli": "^21.2.2", + "@commitlint/config-conventional": "^21.2.2", "@testing-library/react": "^16.3.2", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", "husky": "^9.1.7", - "jest": "^30.3.0", - "jest-environment-jsdom": "^30.3.0", - "react": "^19.2.4", - "ts-jest": "^29.4.9", + "jest": "^30.4.2", + "jest-environment-jsdom": "^30.4.1", + "react": "^19.2.5", + "ts-jest": "^29.4.12", "typescript": "^6.0.3" }, "funding": { @@ -51,13 +51,13 @@ "license": "ISC" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -66,9 +66,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -76,21 +76,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -117,14 +117,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -134,14 +134,14 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -161,9 +161,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -171,29 +171,29 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -203,9 +203,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -213,9 +213,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -223,9 +223,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -233,9 +233,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -243,27 +243,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -370,13 +370,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -496,13 +496,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -522,33 +522,33 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -556,14 +556,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -577,134 +577,135 @@ "license": "MIT" }, "node_modules/@commitlint/cli": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.3.tgz", - "integrity": "sha512-OJdL0EXWD5y9LPa0nr/geOwzaS8BsdaybKkcloB0JgsguGxNv2R+hC2FTPqrAcprg35zF33KOQerY0x8W1aesA==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.2.2.tgz", + "integrity": "sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/format": "^20.5.0", - "@commitlint/lint": "^20.5.3", - "@commitlint/load": "^20.5.3", - "@commitlint/read": "^20.5.0", - "@commitlint/types": "^20.5.0", + "@commitlint/config-conventional": "^21.2.2", + "@commitlint/format": "^21.2.2", + "@commitlint/lint": "^21.2.2", + "@commitlint/load": "^21.2.2", + "@commitlint/read": "^21.2.1", + "@commitlint/types": "^21.2.0", "tinyexec": "^1.0.0", - "yargs": "^17.0.0" + "yargs": "^18.0.0" }, "bin": { "commitlint": "cli.js" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/config-conventional": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.3.tgz", - "integrity": "sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.2.2.tgz", + "integrity": "sha512-NxA37SZviusFUEYOQZ5hNnZ1h7O/KiemPkxjOlpzKJNnWxThiwc6/SaZhaPa8fyLvfRBAywhQhJJk8XESHWlpQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", - "conventional-changelog-conventionalcommits": "^9.2.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-conventionalcommits": "^10.0.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/config-validator": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", - "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.2.0.tgz", + "integrity": "sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", + "@commitlint/types": "^21.2.0", "ajv": "^8.11.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/ensure": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.3.tgz", - "integrity": "sha512-4i4AgNvH62owG9MwSiWKrle7HGNpBHHdLnWFIp5fTsHUYe5kRuh15t08L/0pdbbrRk8JKXQxxN4hZQcn+szkrw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.2.0.tgz", + "integrity": "sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/execute-rule": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", - "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz", + "integrity": "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==", "dev": true, "license": "MIT", "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/format": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", - "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.2.2.tgz", + "integrity": "sha512-v6fvxZSc/AvVMROlr3H34+1766bZSYApRUSCAMjWamStPjKMvZ8GdvVA5YW/VQNgbFTmcMz6OYmSTJEvIjPrfA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", + "@commitlint/types": "^21.2.0", "picocolors": "^1.1.1" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/is-ignored": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", - "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.2.2.tgz", + "integrity": "sha512-9UoKNgfFE3LU7FrzierCvk3CdDfMDeVGC86qZiT/n0TIjfq/dmZ9MHuXd45OTNRa26ZanmJRxEtmiXk/lEJihg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", + "@commitlint/types": "^21.2.0", "semver": "^7.6.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/lint": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.3.tgz", - "integrity": "sha512-M7JbWBNr2gXKaPc4i/KipsuW1gkDHpj35KPjWtKy3Z+2AQw5wu1gBi1LIO0uoaij67CqY4K8PxPZSGens4evCw==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.2.2.tgz", + "integrity": "sha512-Fy8JxEBzdmsYWFude/61GxXu5O+wEymwiRK2z9GL9R8mCsXphCoGxAFc5iHn5mjlfcSrhiiONE+ksf4KOjnaPg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/is-ignored": "^20.5.0", - "@commitlint/parse": "^20.5.0", - "@commitlint/rules": "^20.5.3", - "@commitlint/types": "^20.5.0" + "@commitlint/is-ignored": "^21.2.2", + "@commitlint/parse": "^21.2.2", + "@commitlint/rules": "^21.2.2", + "@commitlint/types": "^21.2.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/load": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.3.tgz", - "integrity": "sha512-1FDZWuKyu98Myb8i7Tp31jPU2rZpOwAdYRyJcy2KoGg7Xk2A+bgHN8smhMaaNSNkmE8fwt53BokywZq8Gv/5XQ==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.2.2.tgz", + "integrity": "sha512-0Tt6wDPX167cjKC5D4zhm0+20wJJG+TN/TKovMOspfSe78rOnKX+MNzlVNiu6HyQPZChPJ8QBH31MVt6Bb8fCg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^20.5.0", - "@commitlint/execute-rule": "^20.0.0", - "@commitlint/resolve-extends": "^20.5.3", - "@commitlint/types": "^20.5.0", + "@commitlint/config-validator": "^21.2.0", + "@commitlint/execute-rule": "^21.0.1", + "@commitlint/resolve-extends": "^21.2.2", + "@commitlint/types": "^21.2.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", @@ -712,139 +713,137 @@ "picocolors": "^1.1.1" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/message": { - "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz", - "integrity": "sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.2.0.tgz", + "integrity": "sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==", "dev": true, "license": "MIT", "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/parse": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz", - "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.2.2.tgz", + "integrity": "sha512-MEkobPfvRp+z06Wro8HMG1BDGHzZmj82A1LH1nWeG3ipHpg/x4m6v3wEDvMBIKjRFUnfR3nBeFs3MVCr7UdAmg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-angular": "^9.0.0", + "conventional-commits-parser": "^7.0.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/read": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz", - "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.2.1.tgz", + "integrity": "sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/top-level": "^20.4.3", - "@commitlint/types": "^20.5.0", - "git-raw-commits": "^5.0.0", - "minimist": "^1.2.8", + "@commitlint/top-level": "^21.2.0", + "@commitlint/types": "^21.2.0", + "@conventional-changelog/git-client": "^3.0.0", "tinyexec": "^1.0.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/resolve-extends": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.3.tgz", - "integrity": "sha512-+ogW9v/u9JqpvAgTrLra/YTFo0KkjU6iNblF89pPsj4NebNc+DAWctsludwezI8YnsjBmfHpApSwcXprN/f/ew==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.2.2.tgz", + "integrity": "sha512-RPkJ/IFi7sMUUVbZLqwWFtWw/zRDcfFsmrPSiTMrt5wb7AdxOr86EGQFvmGzef5QKV5IPBWWCujqVTw1RWX44A==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^20.5.0", - "@commitlint/types": "^20.5.0", + "@commitlint/config-validator": "^21.2.0", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", - "import-meta-resolve": "^4.0.0", "resolve-from": "^5.0.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/rules": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.3.tgz", - "integrity": "sha512-MPlMnb9D3wbszYMp+1hPtuhtPJndRo6I6yfkZVA4+jR8w7Kqp0u2u/Y+gzbaItx5Lltq5rw7FSZQWJMoXUC4NQ==", + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.2.2.tgz", + "integrity": "sha512-eplQzyYkBjYB1HyyRj8hkcK11Y9DU9nuBz7uOKEd6NpE9NGDytLFCAnlRE+OoiK/5sHEJsaz2RGhuWBvYzIbNA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^20.5.3", - "@commitlint/message": "^20.4.3", - "@commitlint/to-lines": "^20.0.0", - "@commitlint/types": "^20.5.0" + "@commitlint/ensure": "^21.2.0", + "@commitlint/message": "^21.2.0", + "@commitlint/to-lines": "^21.0.1", + "@commitlint/types": "^21.2.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/to-lines": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", - "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-21.0.1.tgz", + "integrity": "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==", "dev": true, "license": "MIT", "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/top-level": { - "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.3.tgz", - "integrity": "sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.2.0.tgz", + "integrity": "sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==", "dev": true, "license": "MIT", "dependencies": { "escalade": "^3.2.0" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@commitlint/types": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz", - "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.2.0.tgz", + "integrity": "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==", "dev": true, "license": "MIT", "dependencies": { - "conventional-commits-parser": "^6.3.0", + "conventional-commits-parser": "^7.0.0", "picocolors": "^1.1.1" }, "engines": { - "node": ">=v18" + "node": ">=22.12.0" } }, "node_modules/@conventional-changelog/git-client": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", - "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.1.2.tgz", + "integrity": "sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.2.0", + "@simple-libs/child-process-utils": "^2.0.0", + "@simple-libs/stream-utils": "^2.0.0", "semver": "^7.5.2" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.4.0" + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.1.2" }, "peerDependenciesMeta": { "conventional-commits-filter": { @@ -855,6 +854,16 @@ } } }, + "node_modules/@conventional-changelog/template": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.4.0.tgz", + "integrity": "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1050,9 +1059,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { @@ -1074,17 +1083,17 @@ } }, "node_modules/@jest/console": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", - "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", "slash": "^3.0.0" }, "engines": { @@ -1092,38 +1101,39 @@ } }, "node_modules/@jest/core": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", - "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.3.0", - "jest-config": "30.3.0", - "jest-haste-map": "30.3.0", - "jest-message-util": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-resolve-dependencies": "30.3.0", - "jest-runner": "30.3.0", - "jest-runtime": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", - "jest-watcher": "30.3.0", - "pretty-format": "30.3.0", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", "slash": "^3.0.0" }, "engines": { @@ -1152,31 +1162,25 @@ } }, "node_modules/@jest/core/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/core/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/@jest/diff-sequences": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", - "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", "dev": true, "license": "MIT", "engines": { @@ -1184,35 +1188,35 @@ } }, "node_modules/@jest/environment": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", - "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.3.0", - "@jest/types": "30.3.0", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", - "jest-mock": "30.3.0" + "jest-mock": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", - "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", + "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/fake-timers": "30.3.0", - "@jest/types": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1228,23 +1232,23 @@ } }, "node_modules/@jest/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.3.0", - "jest-snapshot": "30.3.0" + "expect": "30.4.1", + "jest-snapshot": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", - "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1255,18 +1259,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", - "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", - "@sinonjs/fake-timers": "^15.0.0", + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", "@types/node": "*", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1283,47 +1287,47 @@ } }, "node_modules/@jest/globals": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", - "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/expect": "30.3.0", - "@jest/types": "30.3.0", - "jest-mock": "30.3.0" + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "jest-regex-util": "30.0.1" + "jest-regex-util": "30.4.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/reporters": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", - "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -1336,9 +1340,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "jest-worker": "30.3.0", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -1356,9 +1360,9 @@ } }, "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1369,13 +1373,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", - "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -1400,14 +1404,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", - "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -1416,15 +1420,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", - "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.3.0", + "@jest/test-result": "30.4.1", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", + "jest-haste-map": "30.4.1", "slash": "^3.0.0" }, "engines": { @@ -1432,23 +1436,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", - "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.3.0", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -1458,14 +1462,14 @@ } }, "node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", @@ -1527,16 +1531,25 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@pkgjs/parseargs": { @@ -1551,51 +1564,51 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/pkgr" } }, "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz", + "integrity": "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0" + "@simple-libs/stream-utils": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://ko-fi.com/dangreen" } }, "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://ko-fi.com/dangreen" } }, "node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", "dev": true, "license": "MIT" }, @@ -1610,9 +1623,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", - "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1669,9 +1682,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -1782,9 +1795,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "dev": true, "license": "MIT", "dependencies": { @@ -1792,9 +1805,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1840,9 +1853,9 @@ "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", "cpu": [ "arm" ], @@ -1854,9 +1867,9 @@ ] }, "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", "cpu": [ "arm64" ], @@ -1868,9 +1881,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", "cpu": [ "arm64" ], @@ -1882,9 +1895,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", "cpu": [ "x64" ], @@ -1896,9 +1909,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", "cpu": [ "x64" ], @@ -1910,9 +1923,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", "cpu": [ "arm" ], @@ -1924,9 +1937,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", "cpu": [ "arm" ], @@ -1938,9 +1951,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", "cpu": [ "arm64" ], @@ -1955,9 +1968,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", "cpu": [ "arm64" ], @@ -1971,10 +1984,44 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", "cpu": [ "ppc64" ], @@ -1989,9 +2036,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", "cpu": [ "riscv64" ], @@ -2006,9 +2053,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", "cpu": [ "riscv64" ], @@ -2023,9 +2070,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", "cpu": [ "s390x" ], @@ -2040,9 +2087,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", "cpu": [ "x64" ], @@ -2057,9 +2104,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", "cpu": [ "x64" ], @@ -2073,10 +2120,24 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", "cpu": [ "wasm32" ], @@ -2084,16 +2145,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", "cpu": [ "arm64" ], @@ -2105,9 +2168,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", "cpu": [ "ia32" ], @@ -2119,9 +2182,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", "cpu": [ "x64" ], @@ -2235,6 +2298,19 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/argue-cli": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/argue-cli/-/argue-cli-3.1.0.tgz", + "integrity": "sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -2246,24 +2322,17 @@ "dequal": "^2.0.3" } }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, "node_modules/babel-jest": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", - "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.3.0", + "@jest/transform": "30.4.1", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.3.0", + "babel-preset-jest": "30.4.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -2296,9 +2365,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", - "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", "dev": true, "license": "MIT", "dependencies": { @@ -2336,13 +2405,13 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", - "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.3.0", + "babel-plugin-jest-hoist": "30.4.0", "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { @@ -2360,9 +2429,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.24", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", - "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", + "version": "2.11.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz", + "integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2373,9 +2442,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2383,9 +2452,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -2403,11 +2472,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -2467,9 +2536,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001791", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", - "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -2531,75 +2600,78 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", "dev": true, "license": "MIT" }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -2643,17 +2715,6 @@ "dev": true, "license": "MIT" }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2662,46 +2723,46 @@ "license": "MIT" }, "node_modules/conventional-changelog-angular": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", - "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.4.0.tgz", + "integrity": "sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-changelog-conventionalcommits": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", - "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.4.0.tgz", + "integrity": "sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-commits-parser": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", - "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz", + "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "meow": "^13.0.0" + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/convert-source-map": { @@ -2712,9 +2773,9 @@ "license": "MIT" }, "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", "dependencies": { @@ -2885,19 +2946,6 @@ "license": "MIT", "peer": true }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2906,9 +2954,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.345", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.345.tgz", - "integrity": "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg==", + "version": "1.5.415", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz", + "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==", "dev": true, "license": "ISC" }, @@ -2966,14 +3014,16 @@ } }, "node_modules/es-toolkit": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", - "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", + "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==", "dev": true, "license": "MIT", "workspaces": [ "docs", - "benchmarks" + "benchmarks", + "tests/types", + "tests/browser-compat" ] }, "node_modules/escalade": { @@ -3052,18 +3102,18 @@ } }, "node_modules/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.3.0", + "@jest/expect-utils": "30.4.1", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3084,9 +3134,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "dev": true, "funding": [ { @@ -3183,6 +3233,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3206,23 +3269,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/git-raw-commits": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", - "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@conventional-changelog/git-client": "^2.6.0", - "meow": "^13.0.0" - }, - "bin": { - "git-raw-commits": "src/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -3434,17 +3480,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3511,16 +3546,6 @@ "node": ">=6" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3649,16 +3674,16 @@ } }, "node_modules/jest": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", - "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.3.0", - "@jest/types": "30.3.0", + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", "import-local": "^3.2.0", - "jest-cli": "30.3.0" + "jest-cli": "30.4.2" }, "bin": { "jest": "bin/jest.js" @@ -3676,14 +3701,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", - "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.3.0", + "jest-util": "30.4.1", "p-limit": "^3.1.0" }, "engines": { @@ -3691,29 +3716,29 @@ } }, "node_modules/jest-circus": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", - "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/expect": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.3.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-runtime": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", "p-limit": "^3.1.0", - "pretty-format": "30.3.0", + "pretty-format": "30.4.1", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -3736,43 +3761,37 @@ } }, "node_modules/jest-circus/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-circus/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-cli": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", - "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", "yargs": "^17.7.2" }, "bin": { @@ -3790,34 +3809,121 @@ } } }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jest-config": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", - "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.3.0", - "@jest/types": "30.3.0", - "babel-jest": "30.3.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-circus": "30.3.0", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-runner": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", "parse-json": "^5.2.0", - "pretty-format": "30.3.0", + "pretty-format": "30.4.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3855,38 +3961,32 @@ } }, "node_modules/jest-config/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-config/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-diff": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", + "@jest/diff-sequences": "30.4.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3906,31 +4006,25 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-docblock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", - "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", "dev": true, "license": "MIT", "dependencies": { @@ -3941,17 +4035,17 @@ } }, "node_modules/jest-each": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", - "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "chalk": "^4.1.2", - "jest-util": "30.3.0", - "pretty-format": "30.3.0" + "jest-util": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3971,36 +4065,30 @@ } }, "node_modules/jest-each/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-each/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-environment-jsdom": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", - "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", + "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/environment-jsdom-abstract": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/environment-jsdom-abstract": "30.4.1", "jsdom": "^26.1.0" }, "engines": { @@ -4016,39 +4104,39 @@ } }, "node_modules/jest-environment-node": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", - "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/fake-timers": "30.3.0", - "@jest/types": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", - "jest-mock": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0" + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", - "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.3.0", - "jest-worker": "30.3.0", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", "picomatch": "^4.0.3", "walker": "^1.0.8" }, @@ -4060,14 +4148,14 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", - "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4087,38 +4175,32 @@ } }, "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-leak-detector/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-matcher-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", - "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.3.0", - "pretty-format": "30.3.0" + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4138,41 +4220,36 @@ } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-message-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", - "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", "picomatch": "^4.0.3", - "pretty-format": "30.3.0", + "pretty-format": "30.4.1", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -4194,37 +4271,31 @@ } }, "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-mock": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", - "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", - "jest-util": "30.3.0" + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4249,9 +4320,9 @@ } }, "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", "dev": true, "license": "MIT", "engines": { @@ -4259,18 +4330,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", - "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", + "jest-haste-map": "30.4.1", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -4279,46 +4350,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", - "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", "dev": true, "license": "MIT", "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.3.0" + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", - "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.3.0", - "@jest/environment": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.3.0", - "jest-haste-map": "30.3.0", - "jest-leak-detector": "30.3.0", - "jest-message-util": "30.3.0", - "jest-resolve": "30.3.0", - "jest-runtime": "30.3.0", - "jest-util": "30.3.0", - "jest-watcher": "30.3.0", - "jest-worker": "30.3.0", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4327,32 +4398,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", - "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/fake-timers": "30.3.0", - "@jest/globals": "30.3.0", + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4361,9 +4432,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", - "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", "dev": true, "license": "MIT", "dependencies": { @@ -4372,20 +4443,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.3.0", + "@jest/expect-utils": "30.4.1", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.3.0", + "expect": "30.4.1", "graceful-fs": "^4.2.11", - "jest-diff": "30.3.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "pretty-format": "30.3.0", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -4407,35 +4478,29 @@ } }, "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -4447,18 +4512,18 @@ } }, "node_modules/jest-validate": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", - "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4491,41 +4556,35 @@ } }, "node_modules/jest-validate/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-validate/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-watcher": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", - "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.3.0", + "jest-util": "30.4.1", "string-length": "^4.0.2" }, "engines": { @@ -4533,15 +4592,15 @@ } }, "node_modules/jest-worker": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", - "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.3.0", + "jest-util": "30.4.1", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -4583,10 +4642,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4766,19 +4835,6 @@ "tmpl": "1.0.5" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4877,11 +4933,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -4907,9 +4966,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, "license": "MIT" }, @@ -5232,6 +5291,22 @@ "license": "MIT", "peer": true }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "dev": true, + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5311,9 +5386,9 @@ "peer": true }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -5522,9 +5597,9 @@ } }, "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { @@ -5588,13 +5663,13 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.9" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -5619,9 +5694,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -5665,9 +5740,9 @@ } }, "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -5728,9 +5803,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.9", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", - "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", "dev": true, "license": "MIT", "dependencies": { @@ -5740,7 +5815,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.4", + "semver": "^7.8.5", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -5860,44 +5935,47 @@ "license": "MIT" }, "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "napi-postinstall": "^0.3.0" + "napi-postinstall": "^0.3.4" }, "funding": { "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, "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==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -6141,9 +6219,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { @@ -6197,22 +6275,21 @@ "license": "ISC" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { @@ -6225,39 +6302,31 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "license": "ISC", "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yocto-queue": { diff --git a/package.json b/package.json index ab4dbe4..3ceb31e 100644 --- a/package.json +++ b/package.json @@ -51,16 +51,16 @@ "email": "contact@codesweetly.com" }, "devDependencies": { - "@commitlint/cli": "^20.5.2", - "@commitlint/config-conventional": "^20.5.0", + "@commitlint/cli": "^21.2.2", + "@commitlint/config-conventional": "^21.2.2", "@testing-library/react": "^16.3.2", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", "husky": "^9.1.7", - "jest": "^30.3.0", - "jest-environment-jsdom": "^30.3.0", - "react": "^19.2.4", - "ts-jest": "^29.4.9", + "jest": "^30.4.2", + "jest-environment-jsdom": "^30.4.1", + "react": "^19.2.5", + "ts-jest": "^29.4.12", "typescript": "^6.0.3" }, "peerDependencies": { From 2612530369e4a7f9f9a0412d0219a9b778088bef Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Wed, 26 Aug 2026 23:08:51 +0100 Subject: [PATCH 24/26] refactor: Show the navigation buttons and caption when users hover over the dialog --- src/ImageGallery.tsx | 1 + src/lightbox/Caption.tsx | 17 +++++++++++++++-- src/lightbox/CarouselTrack.tsx | 30 ++++++++++++++++++++++-------- src/lightbox/Lightbox.tsx | 5 +++++ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/ImageGallery.tsx b/src/ImageGallery.tsx index cdb4e5b..4926406 100644 --- a/src/ImageGallery.tsx +++ b/src/ImageGallery.tsx @@ -112,6 +112,7 @@ export function ImageGallery({ {enableDefaultLightbox && ( setLightboxOpen(false)} + fixedCaption={fixedCaption} imagesData={imagesData} initialSlideNumber={slideNumber} isOpen={lightboxOpen} diff --git a/src/lightbox/Caption.tsx b/src/lightbox/Caption.tsx index dc31f90..0f7fe12 100644 --- a/src/lightbox/Caption.tsx +++ b/src/lightbox/Caption.tsx @@ -1,14 +1,27 @@ import { ImageDataType } from "../ImageGallery.types"; interface CaptionProps { + fixedCaption?: boolean; imageData: ImageDataType; + showControls: boolean; } -export function Caption({ imageData }: CaptionProps) { +export function Caption({ + fixedCaption, + imageData, + showControls, +}: CaptionProps) { if (!imageData.caption) return null; return ( -
+
{imageData.caption}
{imageData.cta?.href && imageData.cta?.text && (
diff --git a/src/lightbox/CarouselTrack.tsx b/src/lightbox/CarouselTrack.tsx index e9bc224..2f4e7ac 100644 --- a/src/lightbox/CarouselTrack.tsx +++ b/src/lightbox/CarouselTrack.tsx @@ -10,26 +10,32 @@ import { useCarouselGesture } from "../hooks/useCarouselGesture"; import { ImageDataType } from "../ImageGallery.types"; export interface CarouselTrackRef { - navigate: (direction: -1 | 1) => void; goToSlide: (newSlideNumber: number) => void; + navigate: (direction: -1 | 1) => void; } interface CarouselTrackProps { - imagesData: Array; - slideNumber: number; // 1-indexed changeSlide: (newSlideNumber: number) => void; - showThumbnails: boolean; + fixedCaption?: boolean; + imagesData: Array; lazy: boolean; ref: React.Ref; + showControls: boolean; + showThumbnails: boolean; + slideNumber: number; // 1-indexed + toggleControls: () => void; } export function CarouselTrack({ - imagesData, - slideNumber, changeSlide, - showThumbnails, + fixedCaption, + imagesData, lazy, ref, + showControls, + showThumbnails, + slideNumber, + toggleControls, }: CarouselTrackProps) { const trackRef = useRef(null); const isAnimating = useRef(false); @@ -203,8 +209,16 @@ export function CarouselTrack({ className="cs-rigg-modal-image" style={{ maxHeight: showThumbnails ? "80vh" : "100vh" }} draggable={false} + onClick={(e) => { + e.stopPropagation(); + toggleControls(); + }} + /> + -
); diff --git a/src/lightbox/Lightbox.tsx b/src/lightbox/Lightbox.tsx index f69d2d3..7128230 100644 --- a/src/lightbox/Lightbox.tsx +++ b/src/lightbox/Lightbox.tsx @@ -7,6 +7,7 @@ import { ImageDataType } from "../ImageGallery.types"; interface LightboxProps { exitLightbox: () => void; + fixedCaption?: boolean; imagesData: Array; initialSlideNumber: number; isOpen: boolean; @@ -18,6 +19,7 @@ interface LightboxProps { export function Lightbox({ exitLightbox, + fixedCaption, imagesData, initialSlideNumber, isOpen, @@ -151,10 +153,13 @@ export function Lightbox({
setShowModalControls((prev) => !prev)} ref={carouselRef} /> Date: Thu, 27 Aug 2026 03:52:58 +0100 Subject: [PATCH 25/26] chore: Rename imagesArray to imagesData --- README.md | 75 ++++++++++++++++++++++----------------- src/ImageGallery.test.tsx | 30 ++++++++-------- src/ImageGallery.tsx | 6 ++-- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 969e1c7..6718574 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ # React Image Grid Gallery -A responsive React image gallery component with an optional lightbox for displaying image grids in React applications. +A responsive React image gallery component with an optional lightbox carousel for displaying image grids in React applications. -![npm](https://img.shields.io/npm/v/react-image-grid-gallery) ![NPM](https://img.shields.io/npm/l/react-image-grid-gallery) +[![npm](https://img.shields.io/npm/v/react-image-grid-gallery)](https://www.npmjs.com/package/react-image-grid-gallery) [![NPM](https://img.shields.io/npm/l/react-image-grid-gallery)](https://github.com/codesweetly/react-image-grid-gallery/tree/main?tab=MIT-1-ov-file) [![Release](https://github.com/codesweetly/react-image-grid-gallery/actions/workflows/release.yml/badge.svg)](https://github.com/codesweetly/react-image-grid-gallery/actions/workflows/release.yml) ## Features - SEO friendly -- Fullscreen support +- Full-screen support - Keyboard accessible +- Keyboard navigation - Mobile responsive +- Swipe and gesture navigation +- Carousel-style lightbox - Lightbox with translucent background -- Thumbnails +- Thumbnail navigation - Image captions - Lazy loading support - Set column numbers dynamically or manually @@ -54,7 +57,7 @@ Import the library and its stylesheet, then use it like this: import { ImageGallery } from "react-image-grid-gallery"; import "react-image-grid-gallery/style.css"; -const imagesArray = [ +const imagesData = [ { id: "uniqueid111", alt: "Image1's alt text", @@ -88,7 +91,7 @@ const imagesArray = [ ]; function App() { - return ; + return ; } ``` @@ -96,6 +99,21 @@ function App() { The components rely on predefined CSS classes for styling. Importing the stylesheet once ensures consistent styling across the gallery components. +## Lightbox Navigation + +When the default lightbox is enabled, the gallery provides a carousel-style image viewing experience. + +Users can navigate between images using: + +- Previous and next controls +- Thumbnail navigation +- Keyboard arrow keys +- Swipe and gesture navigation on supported devices + +The current image position is displayed in the lightbox, and the active thumbnail remains synchronized with the displayed image. + +Swipe navigation is built into the default lightbox and requires no additional configuration. + ## Props @@ -170,7 +188,7 @@ The `customizeImageClickAction` function receives two arguments, `imageData` and ```jsx { console.log("Image clicked!"); @@ -182,7 +200,7 @@ The `customizeImageClickAction` function receives two arguments, `imageData` and ```jsx { console.log("Image clicked:", imageData, index); @@ -288,36 +306,27 @@ The `"use client"` directive tells NextJS to consider all modules imported into The `ImageGallery` package works _only_ as a Client Component because it uses React's State and Lifecycle effects, such as `useState()` and `useEffect()`. -## Note for Docusaurus users +## Browser-Only APIs and Static Rendering -Did you get a `ReferenceError: crypto is not defined` error during the build step? If so, this note is for you. +If your `imagesData` uses browser-only APIs such as the Web Crypto API (for example, `crypto.randomUUID()` to generate image IDs), you may need to ensure that the code generating those values runs only in the browser when using a framework or static site generator that performs server-side rendering (SSR) or static rendering (SSG). -Wrap the `ImageGallery` component in [``](https://docusaurus.io/docs/next/advanced/ssg#browseronly) if you get a `ReferenceError: crypto is not defined` error during your build step. +**Example:** ```jsx -import BrowserOnly from "@docusaurus/BrowserOnly"; - -function YourComponent() { - return ( - Loading...}> - {() => { - const ImageGallery = require("react-image-grid-gallery").ImageGallery; - require("react-image-grid-gallery/style.css"); - return ( - - ); - }} - - ); -} +const imagesData = [ + { + id: crypto.randomUUID(), + alt: "A beautiful landscape", + src: "/images/landscape.jpg", + }, +]; ``` -This process is essential if your `imagesArray` uses the Web Crypto API. The `` component tells Docusaurus to render the `ImageGallery` library only in the browser. It ensures that the Crypto API runs only in CSR (Client-Side Rendering) rather than during build or SSR (Server-Side Rendering). +If your framework attempts to evaluate browser-only APIs during server-side rendering or the build process, you may encounter errors such as `ReferenceError: crypto is not defined`. + +In such cases, follow your framework's recommended approach for deferring browser-only code to client-side execution. + +**Note:** `react-image-grid-gallery` does not require the Web Crypto API. This consideration applies only when your application uses browser-only APIs while creating or processing the data passed to the gallery. ## Build @@ -341,7 +350,7 @@ For larger changes, it is helpful to open an issue first so we can discuss the p Need help integrating this package, customizing the gallery, or solving a frontend implementation issue? -See [Work with me on CodeSweetly](https://codesweetly.com/work-with-me). +See [Let's build on CodeSweetly](https://codesweetly.com/work-with-me). ## Support diff --git a/src/ImageGallery.test.tsx b/src/ImageGallery.test.tsx index 5f8bf19..3c03bef 100644 --- a/src/ImageGallery.test.tsx +++ b/src/ImageGallery.test.tsx @@ -56,7 +56,7 @@ beforeAll(() => { window.PointerEvent = MockPointerEvent as any; }); -const imagesArray = [ +const imagesData = [ { id: crypto.randomUUID(), alt: "Image1's alt text", @@ -169,7 +169,7 @@ const imagesArray = [ test("image gallery renders correctly", () => { render( { }); test("image gallery works with only the imagesData prop", () => { - render(); + render(); }); test("image gallery works with fixed caption", () => { render( { test("image gallery works with custom thumbnail border", () => { render( , ); @@ -204,20 +204,20 @@ test("image gallery works with custom thumbnail border", () => { test("image gallery works with lazy loading", () => { render( - , + , ); }); test("image gallery works without lightbox", () => { render( - , + , ); }); test("customizing image click action without using the built-in imageData and index parameters works", () => { render( console.log("You clicked an image!")} />, @@ -227,7 +227,7 @@ test("customizing image click action without using the built-in imageData and in test("customizing image click action with the built-in imageData and index parameters works", () => { render( console.log("You clicked an image!", imageData, index) @@ -237,7 +237,7 @@ test("customizing image click action with the built-in imageData and index param }); test("lightbox navigation updates counter and loops correctly", async () => { - render(); + render(); // Open lightbox on image 9 const img9 = screen.getAllByAltText("Image9's alt text")[0]; @@ -259,7 +259,7 @@ test("lightbox navigation updates counter and loops correctly", async () => { }); test("lightbox looping boundary 13 -> 1 and 1 -> 13", async () => { - render(); + render(); // Open lightbox on last image const img13 = screen.getAllByAltText("Image13's alt text")[0]; @@ -278,7 +278,7 @@ test("lightbox looping boundary 13 -> 1 and 1 -> 13", async () => { }); test("lightbox gesture navigation threshold and state update", async () => { - render(); + render(); // Open lightbox on image 1 const img1 = screen.getAllByAltText("Image1's alt text")[0]; @@ -365,7 +365,7 @@ test("lightbox gesture navigation threshold and state update", async () => { }); test("direct thumbnail navigation completely resets state", async () => { - render(); + render(); // Open lightbox on image 1 const img1 = screen.getAllByAltText("Image1's alt text")[0]; @@ -400,7 +400,7 @@ test("direct thumbnail navigation completely resets state", async () => { }); test("backdrop click closes lightbox, but content click does not", async () => { - render(); + render(); // Open lightbox fireEvent.click(screen.getAllByAltText("Image1's alt text")[0]); @@ -433,7 +433,7 @@ test("scroll lock lifecycle correctly restores exactly previous styles", async ( // Set initial overflow to something custom document.documentElement.style.overflow = "scroll"; - const { unmount } = render(); + const { unmount } = render(); expect(document.documentElement.style.overflow).toBe("scroll"); diff --git a/src/ImageGallery.tsx b/src/ImageGallery.tsx index 4926406..d7694c6 100644 --- a/src/ImageGallery.tsx +++ b/src/ImageGallery.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { updateCaptionOpacity } from "./helpers.ts"; -import { Lightbox } from "./lightbox/Lightbox.tsx"; import { ImageDataType } from "./ImageGallery.types"; +import { Lightbox } from "./lightbox/Lightbox.tsx"; interface ImageGalleryPropsType { columnCount?: string | number; @@ -32,8 +32,8 @@ export function ImageGallery({ thumbnailBorder = "3px solid #fff", }: ImageGalleryPropsType) { const [lightboxOpen, setLightboxOpen] = useState(false); - const [slideNumber, setSlideNumber] = useState(1); const [showThumbnails, setShowThumbnails] = useState(false); + const [slideNumber, setSlideNumber] = useState(1); function openLightboxOnSlide(number: number) { setSlideNumber(number); @@ -93,7 +93,7 @@ export function ImageGallery({ } return (
- Error: Each item in the `imagesArray` needs a unique + Error: Each item in the `imagesData` needs a unique `id`
); From 78a1550b6e04281a5f9fe20dddf11c99eb545072 Mon Sep 17 00:00:00 2001 From: Oluwatobi Sofela Date: Thu, 27 Aug 2026 04:05:19 +0100 Subject: [PATCH 26/26] chore: Update test-publish script It fixes you must specify a tag using --tag when publishing a prerelease version' npm error --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ceb31e..1a0e8a7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build:cjs": "tsc -p tsconfig.cjs.json && node scripts/create-package-json.js --cjs", "copy-files": "node scripts/copy-css.js", "test": "jest --config jest.config.cjs", - "test-publish": "npm publish --dry-run", + "test-publish": "npm publish --dry-run --tag next", "watch": "tsc -p tsconfig.esm.json --watch", "prepare": "husky" },