From b0ca7b8cc1f0490781b33576fe9f5acecfbd2c5c Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 24 Apr 2026 20:49:04 -0700 Subject: [PATCH 1/9] Switch from GWT to React --- .../PlateTemplateDesigner.scss | 608 ++++++++++++++++++ .../PlateTemplateDesigner.tsx | 393 +++++++++++ .../src/client/PlateTemplateDesigner/app.tsx | 20 + .../components/GroupTypesPanel.tsx | 280 ++++++++ .../components/ShiftPanel.tsx | 28 + .../components/StatusBar.tsx | 32 + .../components/TemplateGrid.tsx | 125 ++++ .../components/WarningPanel.tsx | 31 + .../components/WellGroupProperties.tsx | 107 +++ .../src/client/PlateTemplateDesigner/dev.tsx | 21 + .../client/PlateTemplateDesigner/models.ts | 67 ++ .../PlateTemplateDesigner/typings/main.d.ts | 14 + assay/src/client/entryPoints.js | 8 + .../src/org/labkey/assay/PlateController.java | 375 ++++++++++- .../src/org/labkey/assay/plate/PlateImpl.java | 2 +- 15 files changed, 2107 insertions(+), 4 deletions(-) create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/app.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/dev.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/models.ts create mode 100644 assay/src/client/PlateTemplateDesigner/typings/main.d.ts diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss new file mode 100644 index 00000000000..3a790af9729 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -0,0 +1,608 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +.plate-template-designer { + padding: 12px 16px; + font-family: Arial, Helvetica, sans-serif; + font-size: 13px; + + &__error { + color: #c00; + padding: 16px; + } + + &__loading { + padding: 16px; + color: #666; + } + + &__header { + margin: 10px 0; + } + + &__name-label { + font-weight: bold; + } + + &__name-input { + margin-left: 8px; + padding: 4px 6px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 13px; + width: 280px; + } + + &__body { + display: flex; + gap: 16px; + align-items: flex-start; + } + + &__left { + flex: 0 0 auto; + } + + &__right { + flex: 1; + min-width: 200px; + } +} + +// StatusBar +.status-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0 10px; + border-bottom: 1px solid #ddd; + margin-bottom: 8px; + + &__btn { + padding: 5px 14px; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + font-size: 13px; + + &:hover:not(:disabled) { + background: #e8e8e8; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + + &--primary { + background: #337ab7; + border-color: #2e6da4; + color: #fff; + + &:hover:not(:disabled) { + background: #286090; + } + } + } + + &__dirty { + color: #c80; + font-style: italic; + } + + &__status { + color: #555; + } +} + +// GroupTypesPanel +.group-types-panel { + border: 1px solid #ccc; + border-radius: 3px; + + &__tabs { + display: flex; + flex-wrap: wrap; + border-bottom: 1px solid #ccc; + background: #f0f0f0; + } + + &__tab { + padding: 5px 10px; + border: none; + background: transparent; + cursor: pointer; + font-size: 12px; + border-right: 1px solid #ddd; + + &:hover { + background: #e0e0e0; + } + + &--active { + background: #fff; + font-weight: bold; + border-bottom: 2px solid #337ab7; + } + } + + &__tab-body { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px; + } + + &__groups { + flex: 0 0 160px; + min-width: 280px; + min-height: 60px; + } + + &__group { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px; + margin-bottom: 3px; + border-radius: 3px; + cursor: pointer; + border: 1px solid transparent; + min-width: 0; + + &:hover { + background: #f0f0f0; + } + + &--active { + border-color: #337ab7; + background: #e8f0fb; + } + } + + &__color-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + border: 1px solid rgba(0, 0, 0, 0.2); + flex-shrink: 0; + } + + &__group-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + } + + &__rename-input { + flex: 1; + padding: 1px 4px; + border: 1px solid #337ab7; + border-radius: 3px; + font-size: 12px; + min-width: 0; + } + + &__group-actions { + display: flex; + gap: 2px; + margin-left: auto; + flex-shrink: 0; + } + + &__action-btn { + padding: 2px 5px; + border: 1px solid #ccc; + border-radius: 2px; + background: transparent; + cursor: pointer; + font-size: 11px; + line-height: 1; + color: #555; + + &:hover { + background: #e0e0e0; + } + + &--delete { + color: #c00; + + &:hover { + background: #ffe0e0; + } + } + } + + &__create-row { + display: flex; + align-items: center; + gap: 4px; + margin-top: 6px; + } + + &__new-name-input { + flex: 1; + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + min-width: 0; + } + + &__add-btn { + padding: 3px 8px; + font-size: 12px; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + color: #333; + white-space: nowrap; + + &:hover:not(:disabled) { + background: #e8e8e8; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + + &--primary { + background: #337ab7; + border-color: #2e6da4; + color: #fff; + + &:hover:not(:disabled) { + background: #286090; + } + } + } +} + +// Multi-create dialog +.multi-create-dialog { + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + padding: 20px; + min-width: 300px; + + &__overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + &__title { + font-weight: bold; + font-size: 14px; + margin-bottom: 14px; + } + + &__table { + width: 100%; + border-collapse: collapse; + + td { + padding-bottom: 10px; + } + } + + &__label { + padding: 6px 16px 6px 0; + white-space: nowrap; + font-size: 13px; + vertical-align: middle; + } + + &__input { + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 13px; + width: 100%; + box-sizing: border-box; + + &--count { + width: 80px; + } + } + + &__error { + color: #c00; + font-size: 12px; + margin-top: 2px; + } + + &__buttons { + padding-top: 10px; + display: flex; + gap: 6px; + justify-content: flex-end; + } +} + +// Wrapper for grid + shift panel +.plate-grid-area { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +// ShiftPanel +.shift-panel { + &__grid { + display: grid; + grid-template-columns: repeat(3, 26px); + grid-template-rows: repeat(3, 26px); + gap: 2px; + align-items: center; + justify-items: center; + } + + &__btn { + width: 26px; + height: 26px; + padding: 0; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + font-size: 14px; + line-height: 1; + + &:hover { + background: #e8e8e8; + } + } + + &__label { + font-size: 10px; + color: #666; + text-align: center; + line-height: 1; + } +} + +// TemplateGrid +.template-grid { + user-select: none; + + &__table { + border-collapse: collapse; + border: 1px solid #aaa; + } + + &__corner { + width: 24px; + height: 24px; + } + + &__col-header { + text-align: center; + font-size: 11px; + font-weight: bold; + width: 28px; + height: 22px; + background: #f0f0f0; + border: 1px solid #ccc; + } + + &__row-header { + text-align: center; + font-size: 11px; + font-weight: bold; + width: 22px; + background: #f0f0f0; + border: 1px solid #ccc; + } + + &__cell { + width: 28px; + height: 28px; + border: 1px solid #ccc; + cursor: crosshair; + transition: filter 0.1s; + + &:hover { + filter: brightness(0.88); + } + + &--active { + outline: 2px solid #333; + outline-offset: -2px; + } + } +} + +// Right panel tabs +.right-panel-tabs { + display: flex; + border-bottom: 1px solid #ccc; + margin-bottom: 8px; + + &__tab { + padding: 5px 10px; + border: none; + background: transparent; + cursor: pointer; + font-size: 12px; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + + &:hover { + background: #f0f0f0; + } + + &--active { + font-weight: bold; + border-bottom-color: #337ab7; + background: #fff; + } + + &--warn { + color: #c80; + } + + &--active#{&}--warn { + border-bottom-color: #c80; + } + } +} + +// WellGroupProperties +.well-group-properties { + border: 1px solid #ccc; + border-radius: 3px; + padding: 10px; + min-height: 60px; + + &--empty { + color: #888; + font-style: italic; + } + + &__title { + font-weight: bold; + margin-bottom: 8px; + } + + &__no-props { + color: #888; + font-style: italic; + font-size: 12px; + } + + &__table { + width: 100%; + border-collapse: collapse; + } + + &__key { + font-weight: bold; + padding: 3px 6px 3px 0; + white-space: nowrap; + width: 40%; + } + + &__value-cell { + padding: 2px 4px 2px 0; + } + + &__action-cell { + padding: 2px 0 2px 6px; + white-space: nowrap; + width: 1%; + } + + &__value { + padding: 3px 4px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + width: 100%; + box-sizing: border-box; + } + + &__delete-btn { + padding: 2px 5px; + border: 1px solid #ccc; + border-radius: 2px; + background: transparent; + cursor: pointer; + font-size: 11px; + line-height: 1; + color: #c00; + + &:hover { + background: #ffe0e0; + } + } + + &__add-row td { + border-top: 1px solid #eee; + padding-top: 6px; + } + + &__new-key { + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + width: 100%; + box-sizing: border-box; + } + + &__new-value { + padding: 3px 5px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + width: 100%; + box-sizing: border-box; + } + + &__add-btn { + padding: 3px 8px; + border: 1px solid #aaa; + border-radius: 3px; + background: #f5f5f5; + cursor: pointer; + font-size: 12px; + white-space: nowrap; + + &:hover:not(:disabled) { + background: #e8e8e8; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + } +} + +// WarningPanel +.warning-panel { + border: 1px solid #e8a000; + border-radius: 3px; + padding: 10px; + background: #fffbe6; + + &__title { + font-weight: bold; + color: #c80; + margin-bottom: 6px; + } + + &__none { + color: #888; + font-style: italic; + font-size: 12px; + } + + &__list { + margin: 0; + padding-left: 18px; + } + + &__item { + color: #7a5800; + font-size: 12px; + margin-bottom: 3px; + } +} diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx new file mode 100644 index 00000000000..16297b36f3e --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ActionURL, Ajax, Utils } from '@labkey/api'; + +import { PlateTemplate, WellGroup, computeWarnings } from './models'; +import { StatusBar } from './components/StatusBar'; +import { GroupTypesPanel } from './components/GroupTypesPanel'; +import { ShiftPanel } from './components/ShiftPanel'; +import { TemplateGrid } from './components/TemplateGrid'; +import { WellGroupProperties } from './components/WellGroupProperties'; +import { WarningPanel } from './components/WarningPanel'; + +import './PlateTemplateDesigner.scss'; + +const COLORS = [ + '#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', + '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac', + '#6ba3be', '#ffbe7d', '#ff9d9a', '#86bcb6', '#8cd17d', + '#f1ce63', '#d4a6c8', '#ffb7c5', '#c7a97e', '#d7d5cf', +]; + +function assignColors(groups: WellGroup[]): Map { + const map = new Map(); + groups.forEach((g, i) => { + map.set(g.rowId, COLORS[i % COLORS.length]); + }); + return map; +} + +export function PlateTemplateDesigner(): JSX.Element { + const [plate, setPlate] = useState(null); + const [activeGroup, setActiveGroup] = useState(null); + const [activeTab, setActiveTab] = useState(''); + const [rightTab, setRightTab] = useState<'properties' | 'warnings'>('properties'); + const [isDirty, setIsDirty] = useState(false); + const [status, setStatus] = useState(''); + const [colorMap, setColorMap] = useState>(new Map()); + const [error, setError] = useState(null); + const plateNameRef = useRef(''); + const statusTimerRef = useRef | null>(null); + const nextGroupIdRef = useRef(-1); + + useEffect(() => { + const templateName = ActionURL.getParameter('templateName'); + const plateIdStr = ActionURL.getParameter('plateId'); + const assayType = ActionURL.getParameter('assayType'); + const templateType = ActionURL.getParameter('templateType'); + const rowCountStr = ActionURL.getParameter('rowCount'); + const colCountStr = ActionURL.getParameter('colCount'); + const copy = ActionURL.getParameter('copy') === 'true' || ActionURL.getParameter('copyTemplate') === 'true'; + + const params: Record = {}; + if (templateName) params.templateName = templateName; + if (plateIdStr) params.plateId = parseInt(plateIdStr, 10); + if (assayType) params.assayType = assayType; + if (templateType) params.templateType = templateType; + if (rowCountStr) params.rowCount = parseInt(rowCountStr, 10); + if (colCountStr) params.colCount = parseInt(colCountStr, 10); + params.copy = copy; + + Ajax.request({ + url: ActionURL.buildURL('plate', 'getTemplateDefinition.api'), + method: 'GET', + params, + success: Utils.getCallbackWrapper((response: { data: PlateTemplate }) => { + const plate = response.data; + plateNameRef.current = plate.defaultPlateName || plate.name || ''; + setPlate({ ...plate, name: plateNameRef.current }); + setColorMap(assignColors(plate.groups)); + setActiveTab(plate.groupTypes[0] ?? ''); + if (plate.copyMode) setIsDirty(true); + }), + failure: Utils.getCallbackWrapper((response: any) => { + setError(response?.exception ?? 'Failed to load plate template.'); + }, null, true), + }); + }, []); + + const handleNameChange = useCallback((name: string) => { + plateNameRef.current = name; + setPlate(prev => prev ? { ...prev, name } : null); + setIsDirty(true); + }, []); + + const handleGroupSelect = useCallback((group: WellGroup) => { + setActiveGroup(group); + }, []); + + const handleCellAssign = useCallback((row: number, col: number) => { + if (!activeGroup || !plate) return; + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => { + if (g.rowId === activeGroup.rowId) { + const alreadyHas = g.positions.some(p => p.row === row && p.col === col); + if (alreadyHas) return g; + return { ...g, positions: [...g.positions, { row, col }] }; + } + if (g.type === activeGroup.type) { + // Remove from other groups of the same type to avoid conflicts + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return g; + }); + return { ...prev, groups: updatedGroups }; + }); + setIsDirty(true); + }, [activeGroup, plate]); + + const handleCellToggle = useCallback((row: number, col: number) => { + if (!activeGroup || !plate) return; + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => { + if (g.rowId === activeGroup.rowId) { + const hasCell = g.positions.some(p => p.row === row && p.col === col); + if (hasCell) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return { ...g, positions: [...g.positions, { row, col }] }; + } + if (g.type === activeGroup.type) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return g; + }); + return { ...prev, groups: updatedGroups }; + }); + setIsDirty(true); + }, [activeGroup, plate]); + + const handleAddGroup = useCallback((type: string, name: string) => { + if (!plate) return; + const rowId = nextGroupIdRef.current--; + const newGroup: WellGroup = { + rowId, + type, + name, + positions: [], + properties: {}, + allowNewGroups: plate.canCreateGroupsByType?.[type] ?? false, + }; + setPlate(prev => prev ? { ...prev, groups: [...prev.groups, newGroup] } : null); + setColorMap(prev => { + const next = new Map(prev); + next.set(rowId, COLORS[prev.size % COLORS.length]); + return next; + }); + setActiveGroup(newGroup); + setIsDirty(true); + }, [plate]); + + const handleShift = useCallback((verticalShift: number, horizontalShift: number) => { + setPlate(prev => { + if (!prev) return null; + const { rows, cols } = prev; + const updatedGroups = prev.groups.map(g => { + if (g.type !== activeTab) return g; + return { + ...g, + positions: g.positions.map(p => ({ + row: ((p.row - verticalShift) % rows + rows) % rows, + col: ((p.col - horizontalShift) % cols + cols) % cols, + })), + }; + }); + return { ...prev, groups: updatedGroups }; + }); + setIsDirty(true); + }, [activeTab]); + + const handleDeleteGroup = useCallback((rowId: number) => { + setPlate(prev => prev ? { ...prev, groups: prev.groups.filter(g => g.rowId !== rowId) } : null); + setColorMap(prev => { const next = new Map(prev); next.delete(rowId); return next; }); + setActiveGroup(prev => prev?.rowId === rowId ? null : prev); + setIsDirty(true); + }, []); + + const handleRenameGroup = useCallback((rowId: number, newName: string) => { + setPlate(prev => prev ? { ...prev, groups: prev.groups.map(g => g.rowId === rowId ? { ...g, name: newName } : g) } : null); + setActiveGroup(prev => prev?.rowId === rowId ? { ...prev, name: newName } : prev); + setIsDirty(true); + }, []); + + const handlePropertyChange = useCallback((groupRowId: number, key: string, value: string) => { + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => + g.rowId === groupRowId ? { ...g, properties: { ...g.properties, [key]: value } } : g + ); + return { ...prev, groups: updatedGroups }; + }); + setActiveGroup(prev => prev?.rowId === groupRowId ? { ...prev, properties: { ...prev.properties, [key]: value } } : prev); + setIsDirty(true); + }, []); + + const handleDeleteProperty = useCallback((groupRowId: number, key: string) => { + setPlate(prev => { + if (!prev) return null; + const updatedGroups = prev.groups.map(g => { + if (g.rowId !== groupRowId) return g; + const { [key]: _removed, ...rest } = g.properties; + return { ...g, properties: rest }; + }); + return { ...prev, groups: updatedGroups }; + }); + setActiveGroup(prev => { + if (prev?.rowId !== groupRowId) return prev; + const { [key]: _removed, ...rest } = prev.properties; + return { ...prev, properties: rest }; + }); + setIsDirty(true); + }, []); + + const warningCount = useMemo(() => { + if (!plate?.showWarningPanel) return 0; + return computeWarnings(plate).length; + }, [plate]); + + const navigateAway = useCallback(() => { + const returnURL = ActionURL.getParameter('returnURL') || ActionURL.getParameter('returnUrl'); + const isSameOrigin = (url: string) => { + try { + return new URL(url, window.location.origin).origin === window.location.origin; + } catch { + return false; + } + }; + window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); + }, []); + + const handleSave = useCallback(() => { + if (!plate) return; + setStatus('Saving...'); + Ajax.request({ + url: ActionURL.buildURL('plate', 'saveTemplate.api'), + method: 'POST', + jsonData: plate, + success: Utils.getCallbackWrapper((response: { data: { rowId: number } }) => { + const rowId = response.data.rowId; + setIsDirty(false); + setPlate(prev => prev ? { ...prev, rowId } : null); + // Update URL to canonical form so a refresh reloads this plate + const url = new URL(window.location.href); + url.search = ''; + url.searchParams.set('templateName', plate.name); + url.searchParams.set('plateId', String(rowId)); + window.history.replaceState(null, '', url.toString()); + setStatus('Saved.'); + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + statusTimerRef.current = setTimeout(() => setStatus(''), 5000); + }), + failure: Utils.getCallbackWrapper((response: any) => { + setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); + }, null, true), + }); + }, [plate]); + + const handleSaveAndClose = useCallback(() => { + if (!plate) return; + if (!isDirty) { + navigateAway(); + return; + } + setStatus('Saving...'); + Ajax.request({ + url: ActionURL.buildURL('plate', 'saveTemplate.api'), + method: 'POST', + jsonData: plate, + success: Utils.getCallbackWrapper(() => { + setIsDirty(false); + navigateAway(); + }), + failure: Utils.getCallbackWrapper((response: any) => { + setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); + }, null, true), + }); + }, [plate, isDirty, navigateAway]); + + const handleCancel = useCallback(() => { + navigateAway(); + }, [navigateAway]); + + // Warn on unsaved navigation + useEffect(() => { + const handler = (e: BeforeUnloadEvent) => { + if (isDirty) { + e.preventDefault(); + e.returnValue = ''; + } + }; + window.addEventListener('beforeunload', handler); + return () => window.removeEventListener('beforeunload', handler); + }, [isDirty]); + + // Keep activeGroup in sync when plate changes + useEffect(() => { + if (activeGroup && plate) { + const updated = plate.groups.find(g => g.rowId === activeGroup.rowId); + if (updated) setActiveGroup(updated); + } + }, [plate]); // eslint-disable-line react-hooks/exhaustive-deps + + if (error) { + return
{error}
; + } + + if (!plate) { + return
Loading...
; + } + + return ( +
+ +
+ +
+
+
+ { setActiveTab(tab); setActiveGroup(null); }} + onAddGroup={handleAddGroup} + onDeleteGroup={handleDeleteGroup} + onRenameGroup={handleRenameGroup} + > +
+ + +
+
+
+
+ {plate.showWarningPanel && ( +
+ + +
+ )} + {(!plate.showWarningPanel || rightTab === 'properties') && ( + + )} + {plate.showWarningPanel && rightTab === 'warnings' && ( + + )} +
+
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/app.tsx b/assay/src/client/PlateTemplateDesigner/app.tsx new file mode 100644 index 00000000000..03276e83aa1 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/app.tsx @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { getServerContext } from '@labkey/api'; +import { ServerContextProvider, withAppUser } from '@labkey/components'; + +import { PlateTemplateDesigner } from './PlateTemplateDesigner'; + +// Need to wait for container element to be available in labkey wrapper before render +window.addEventListener('DOMContentLoaded', () => { + createRoot(document.getElementById('app')).render( + + + + ); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx new file mode 100644 index 00000000000..fa4d0a21275 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +import { PlateTemplate, WellGroup } from '../models'; + +interface Props { + plate: PlateTemplate; + activeGroup: WellGroup | null; + activeTab: string; + colorMap: Map; + onGroupSelect: (group: WellGroup) => void; + onTabChange: (tab: string) => void; + onAddGroup: (type: string, name: string) => void; + onDeleteGroup: (rowId: number) => void; + onRenameGroup: (rowId: number, newName: string) => void; + children?: React.ReactNode; +} + +export function GroupTypesPanel({ + plate, + activeGroup, + activeTab, + colorMap, + onGroupSelect, + onTabChange, + onAddGroup, + onDeleteGroup, + onRenameGroup, + children, +}: Props): JSX.Element { + const [newGroupName, setNewGroupName] = useState(''); + const [renamingId, setRenamingId] = useState(null); + const [renameValue, setRenameValue] = useState(''); + const [multiCreateOpen, setMultiCreateOpen] = useState(false); + const [multiBaseName, setMultiBaseName] = useState(''); + const [multiCount, setMultiCount] = useState('2'); + const [multiCountError, setMultiCountError] = useState(''); + const multiBaseNameRef = useRef(null); + + const groupsOfType = plate.groups.filter(g => g.type === activeTab); + const canAdd = plate.canCreateGroupsByType?.[activeTab] ?? false; + + const unusedDefaults = useMemo(() => { + const defaults = plate.typesToDefaultGroups[activeTab] ?? []; + return defaults.filter(d => !groupsOfType.some(g => g.name === d)); + }, [plate, activeTab, groupsOfType]); + + // Reset create-input when tab changes + useEffect(() => { + setNewGroupName(unusedDefaults[0] ?? ''); + setRenamingId(null); + }, [activeTab]); // eslint-disable-line react-hooks/exhaustive-deps + + // Advance to next unused default when the current one gets used + useEffect(() => { + if (unusedDefaults.length > 0 && !unusedDefaults.includes(newGroupName)) { + setNewGroupName(unusedDefaults[0]); + } + }, [unusedDefaults]); // eslint-disable-line react-hooks/exhaustive-deps + + const handleCreate = () => { + const trimmed = newGroupName.trim(); + if (!trimmed) return; + onAddGroup(activeTab, trimmed); + }; + + const openMultiCreate = () => { + setMultiBaseName(newGroupName.trim()); + setMultiCount('2'); + setMultiCountError(''); + setMultiCreateOpen(true); + // Focus the base name input after the modal renders + setTimeout(() => multiBaseNameRef.current?.select(), 0); + }; + + const handleMultiCreate = () => { + const count = parseInt(multiCount, 10); + if (isNaN(count) || count < 1) { + setMultiCountError(`"${multiCount}" is not a valid count.`); + return; + } + const baseName = multiBaseName.trim(); + if (!baseName) return; + for (let i = 1; i <= count; i++) { + onAddGroup(activeTab, `${baseName} ${i}`); + } + setMultiCreateOpen(false); + }; + + const handleDeleteClick = (e: React.MouseEvent, group: WellGroup) => { + e.stopPropagation(); + if (window.confirm(`Delete well group "${group.name}"?`)) { + onDeleteGroup(group.rowId); + } + }; + + const handleRenameClick = (e: React.MouseEvent, group: WellGroup) => { + e.stopPropagation(); + setRenamingId(group.rowId); + setRenameValue(group.name); + }; + + const handleRenameCommit = (rowId: number) => { + const trimmed = renameValue.trim(); + if (trimmed) onRenameGroup(rowId, trimmed); + setRenamingId(null); + }; + + return ( +
+
+ {plate.groupTypes.map(type => ( + + ))} +
+
+
+ {groupsOfType.map(group => { + const color = colorMap.get(group.rowId); + const isActive = activeGroup?.rowId === group.rowId; + const isRenaming = renamingId === group.rowId; + return ( +
{ if (!isRenaming) onGroupSelect(group); }} + > + + {isRenaming ? ( + setRenameValue(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') handleRenameCommit(group.rowId); + if (e.key === 'Escape') setRenamingId(null); + }} + onBlur={() => handleRenameCommit(group.rowId)} + onClick={e => e.stopPropagation()} + /> + ) : ( + {group.name} + )} + {isActive && !isRenaming && group.allowNewGroups && ( + + + + + )} +
+ ); + })} + {canAdd && ( +
+ {unusedDefaults.length > 0 ? ( + + ) : ( + setNewGroupName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newGroupName.trim()) handleCreate(); }} + /> + )} + + +
+ )} +
+ {children} +
+ {multiCreateOpen && ( +
setMultiCreateOpen(false)}> +
e.stopPropagation()}> +
Create Multiple Groups
+ + + + + + + + + + + + + + +
Base Name + setMultiBaseName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} + /> +
Count + { setMultiCount(e.target.value); setMultiCountError(''); }} + onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} + /> + {multiCountError &&
{multiCountError}
} +
+ + + +
+
+
+ )} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx new file mode 100644 index 00000000000..889c24f7127 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; + +interface Props { + onShift: (verticalShift: number, horizontalShift: number) => void; +} + +export function ShiftPanel({ onShift }: Props): JSX.Element { + return ( +
+
+ + + + + Shift + + + + +
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx new file mode 100644 index 00000000000..c68b54e9684 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; + +interface Props { + isDirty: boolean; + status: string; + onSaveAndClose: () => void; + onSave: () => void; + onCancel: () => void; +} + +export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: Props): JSX.Element { + return ( +
+ + + + {isDirty && Unsaved changes} + {status && {status}} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx new file mode 100644 index 00000000000..b4cf732fece --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useCallback, useRef } from 'react'; + +import { PlateTemplate, WellGroup } from '../models'; + +interface Props { + plate: PlateTemplate; + activeGroup: WellGroup | null; + activeTab: string; + colorMap: Map; + onCellAssign: (row: number, col: number) => void; + onCellToggle: (row: number, col: number) => void; +} + +function getRowLabel(row: number): string { + return String.fromCharCode(65 + row); +} + +function getCellColor(row: number, col: number, activeTab: string, plate: PlateTemplate, colorMap: Map): string | undefined { + // Only color cells belonging to groups of the currently active tab type, + // matching the GWT behavior of showing one type's layout at a time. + let color: string | undefined; + for (const group of plate.groups) { + if (group.type === activeTab && group.positions.some(p => p.row === row && p.col === col)) { + color = colorMap.get(group.rowId); + } + } + return color; +} + +export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAssign, onCellToggle }: Props): JSX.Element { + const isDragging = useRef(false); + const hasMoved = useRef(false); + const startCell = useRef<{ row: number; col: number } | null>(null); + const dragCells = useRef>(new Set()); + + const handleMouseDown = useCallback((row: number, col: number, e: React.MouseEvent) => { + if (e.button !== 0) return; + isDragging.current = true; + hasMoved.current = false; + startCell.current = { row, col }; + dragCells.current = new Set([`${row},${col}`]); + e.preventDefault(); + }, []); + + const handleMouseEnter = useCallback((row: number, col: number) => { + if (!isDragging.current) return; + if (!hasMoved.current) { + hasMoved.current = true; + // Deferred: assign the mousedown cell now that we know it's a drag + if (startCell.current) { + onCellAssign(startCell.current.row, startCell.current.col); + } + } + const key = `${row},${col}`; + if (!dragCells.current.has(key)) { + dragCells.current.add(key); + onCellAssign(row, col); + } + }, [onCellAssign]); + + // Called on mouseup over a specific cell — handles click-toggle + const handleCellMouseUp = useCallback((row: number, col: number) => { + if (isDragging.current && !hasMoved.current) { + onCellToggle(row, col); + } + }, [onCellToggle]); + + // Called on the wrapper div — cleans up drag state + const handleDragEnd = useCallback(() => { + isDragging.current = false; + hasMoved.current = false; + startCell.current = null; + dragCells.current = new Set(); + }, []); + + return ( +
+ + + + + ))} + + + + {Array.from({ length: plate.rows }, (_, row) => ( + + + {Array.from({ length: plate.cols }, (_, col) => { + const color = getCellColor(row, col, activeTab, plate, colorMap); + const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); + const location = `${getRowLabel(row)}${col + 1}`; + const groupForCell = plate.groups.find( + g => g.type === activeTab && g.positions.some(p => p.row === row && p.col === col) + ); + const tooltip = groupForCell ? `${location}: ${groupForCell.name}` : location; + return ( + + ))} + +
+ {Array.from({ length: plate.cols }, (_, col) => ( + {col + 1}
{getRowLabel(row)} handleMouseDown(row, col, e)} + onMouseEnter={() => handleMouseEnter(row, col)} + onMouseUp={() => handleCellMouseUp(row, col)} + /> + ); + })} +
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx new file mode 100644 index 00000000000..d297bc1b125 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; + +import { PlateTemplate, computeWarnings } from '../models'; + +interface Props { + plate: PlateTemplate; +} + +export function WarningPanel({ plate }: Props): JSX.Element { + const warnings = computeWarnings(plate); + + return ( +
+
Warnings
+ {warnings.length === 0 ? ( +
No warnings.
+ ) : ( +
    + {warnings.map((w, i) => ( +
  • {w}
  • + ))} +
+ )} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx new file mode 100644 index 00000000000..e31158345b7 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useState } from 'react'; + +import { WellGroup } from '../models'; + +interface Props { + activeGroup: WellGroup | null; + onPropertyChange: (groupRowId: number, key: string, value: string) => void; + onDeleteProperty: (groupRowId: number, key: string) => void; +} + +export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: Props): JSX.Element { + const [newKey, setNewKey] = useState(''); + const [newValue, setNewValue] = useState(''); + + if (!activeGroup) { + return ( +
+ Select a well group to view its properties. +
+ ); + } + + const propEntries = Object.entries(activeGroup.properties); + + const handleAdd = () => { + const key = newKey.trim(); + if (!key) return; + onPropertyChange(activeGroup.rowId, key, newValue); + setNewKey(''); + setNewValue(''); + }; + + return ( +
+
{activeGroup.name}
+ + + {propEntries.length === 0 && ( + + + + )} + {propEntries.map(([key, value]) => ( + + + + + + ))} + + + + + + + + +
No properties defined.
{key} + onPropertyChange(activeGroup.rowId, key, e.target.value)} + /> + + +
+ setNewKey(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} + /> + + setNewValue(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} + /> + + +
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/dev.tsx b/assay/src/client/PlateTemplateDesigner/dev.tsx new file mode 100644 index 00000000000..e0208c5e08a --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/dev.tsx @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { getServerContext } from '@labkey/api'; +import { ServerContextProvider, withAppUser } from '@labkey/components'; + +import { PlateTemplateDesigner } from './PlateTemplateDesigner'; + +const render = () => { + createRoot(document.getElementById('app')).render( + + + + ); +}; + +render(); diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts new file mode 100644 index 00000000000..94e157b72e5 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface Position { + row: number; + col: number; +} + +export interface WellGroup { + rowId: number; + type: string; + name: string; + positions: Position[]; + properties: Record; + allowNewGroups: boolean; +} + +export interface PlateTemplate { + rowId: number; + name: string; + type: string; + rows: number; + cols: number; + groupTypes: string[]; + canCreateGroupsByType: Record; + groups: WellGroup[]; + plateProperties: Record; + typesToDefaultGroups: Record; + showWarningPanel: boolean; + existingTemplateNames: string[]; + copyMode: boolean; + defaultPlateName: string; +} + +export interface SaveTemplateResponse { + rowId: number; +} + +// Matches GWT TemplateGridCell.getWarnings() logic exactly. +export function computeWarnings(plate: PlateTemplate): string[] { + const cellTypes = new Map>(); + for (const group of plate.groups) { + for (const pos of group.positions) { + const key = `${pos.row},${pos.col}`; + if (!cellTypes.has(key)) cellTypes.set(key, new Set()); + cellTypes.get(key).add(group.type); + } + } + const warnings: string[] = []; + for (const [key, types] of cellTypes.entries()) { + const [row, col] = key.split(',').map(Number); + const cellLabel = `${String.fromCharCode(65 + row)}${col + 1}`; + const hasReplicate = types.has('REPLICATE'); + const hasSpecimen = types.has('SPECIMEN'); + const hasControl = types.has('CONTROL'); + if (hasReplicate && !(hasSpecimen || hasControl)) { + warnings.push(`${cellLabel}: Well is a replicate, but is not part of a specimen or control group.`); + } + if (hasControl && hasSpecimen) { + warnings.push(`${cellLabel}: Well is in both a specimen and a control group.`); + } + } + return warnings; +} diff --git a/assay/src/client/PlateTemplateDesigner/typings/main.d.ts b/assay/src/client/PlateTemplateDesigner/typings/main.d.ts new file mode 100644 index 00000000000..b8a4a9133e6 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/typings/main.d.ts @@ -0,0 +1,14 @@ +/** + * @deprecated Use getServerContext() from @labkey/api instead + */ +declare const LABKEY: import('@labkey/api').LabKey; + +/** + * Needed so we can use process.env.NODE_ENV, which is injected by webpack, but not included in the types declared in + * the browser environments. + */ +declare const process: { + env: { + NODE_ENV: string; + }; +}; diff --git a/assay/src/client/entryPoints.js b/assay/src/client/entryPoints.js index b11f0af9722..e0522e96d3c 100644 --- a/assay/src/client/entryPoints.js +++ b/assay/src/client/entryPoints.js @@ -9,5 +9,13 @@ module.exports = { title: 'New Assay Design', permissionClasses: ['org.labkey.api.assay.security.DesignAssayPermission'], path: './src/client/AssayTypeSelect' + }, { + name: 'plateTemplateDesigner', + title: 'Plate Template Designer', + permissionClasses: [ + 'org.labkey.api.security.permissions.InsertPermission', + 'org.labkey.api.assay.security.DesignAssayPermission' + ], + path: './src/client/PlateTemplateDesigner' }] }; \ No newline at end of file diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index e33db16cdd4..d35a2655dc3 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -15,11 +15,15 @@ */ package org.labkey.assay; +import org.apache.commons.io.input.BoundedInputStream; import org.apache.logging.log4j.Logger; import org.json.JSONArray; import org.json.JSONObject; import org.labkey.api.action.ApiJsonForm; +import org.labkey.api.action.ApiUsageException; +import org.labkey.api.action.BaseApiAction; import org.labkey.api.action.FormHandlerAction; +import org.labkey.api.action.NullSafeBindException; import org.labkey.api.action.FormViewAction; import org.labkey.api.action.GWTServiceAction; import org.labkey.api.action.Marshal; @@ -32,9 +36,12 @@ import org.labkey.api.action.SpringActionController; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateCustomField; +import org.labkey.api.assay.plate.PlateLayoutHandler; import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.plate.PlateSet; import org.labkey.api.assay.plate.PlateType; +import org.labkey.api.assay.plate.Position; +import org.labkey.api.assay.plate.WellGroup; import org.labkey.api.assay.security.DesignAssayPermission; import org.labkey.api.collections.RowMapFactory; import org.labkey.api.data.Container; @@ -62,6 +69,8 @@ import org.labkey.api.util.JsonUtil; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.URLHelper; +import org.labkey.api.module.ModuleHtmlView; +import org.labkey.api.module.ModuleLoader; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ActionURL; import org.labkey.api.view.DataViewSnapshotSelectionForm; @@ -76,6 +85,7 @@ import org.labkey.assay.plate.PlateSetExport; import org.labkey.assay.plate.PlateUrls; import org.labkey.assay.plate.TsvPlateLayoutHandler; +import org.labkey.assay.plate.WellGroupImpl; import org.labkey.assay.plate.model.CreatePlateSetOptions; import org.labkey.assay.plate.model.ReformatOptions; import org.labkey.assay.view.AssayGWTView; @@ -91,6 +101,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -122,7 +133,7 @@ public ActionURL getPlateDetailsURL(Container c) } @RequiresPermission(ReadPermission.class) - public static class BeginAction extends SimpleRedirectAction + public static class BeginAction extends SimpleRedirectAction { @Override public ActionURL getRedirectURL(Object o) @@ -211,7 +222,365 @@ public ActionURL getRedirectURL(RowIdForm form) } @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public class DesignerAction extends SimpleViewAction + public class GetTemplateDefinitionAction extends ReadOnlyApiAction + { + @Override + public Object execute(DesignerForm form, BindException errors) throws Exception + { + String templateName = form.getTemplateName(); + Long plateId = form.getPlateId(); + boolean copyTemplate = form.isCopy(); + + if (templateName == null && plateId != null) + { + Plate plate = PlateManager.get().getPlate(getContainer(), plateId); + if (plate != null) + { + templateName = plate.getName(); + } + } + + Plate template; + PlateLayoutHandler handler; + + if (templateName != null) + { + if (plateId == null) + throw new Exception("plateId is required when templateName is specified."); + template = PlateService.get().getPlate(getContainer(), plateId); + if (template == null) + throw new NotFoundException("Plate '" + templateName + "' does not exist."); + handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); + if (handler == null) + throw new Exception("Plate template type '" + template.getAssayType() + "' does not exist."); + } + else + { + String assayTypeName = form.getAssayType(); + String templateTypeName = form.getTemplateType(); + int rowCount = form.getRowCount(); + int colCount = form.getColCount(); + + handler = PlateManager.get().getPlateLayoutHandler(assayTypeName); + if (handler == null) + throw new Exception("Plate template type '" + assayTypeName + "' does not exist."); + PlateType plateType = PlateService.get().getPlateType(rowCount, colCount); + if (plateType == null) + throw new Exception("The plate type (" + rowCount + " x " + colCount + ") does not exist."); + template = handler.createPlate(templateTypeName, getContainer(), plateType); + } + + // Build groups list + List groups = template.getWellGroups(); + List> groupList = new ArrayList<>(); + for (int i = 0; i < groups.size(); i++) + { + WellGroup group = groups.get(i); + List> positions = new ArrayList<>(); + for (Position position : group.getPositions()) + { + Map pos = new HashMap<>(); + pos.put("row", position.getRow()); + pos.put("col", position.getColumn()); + positions.add(pos); + } + Map groupProps = new HashMap<>(); + for (String propName : group.getPropertyNames()) + { + Object propValue = group.getProperty(propName); + groupProps.put(propName, (propValue == null || propValue == JSONObject.NULL) ? null : propValue.toString()); + } + + int wellGroupId = copyTemplate || group.getRowId() == null ? -1 * (i + 1) : group.getRowId(); + Map g = new HashMap<>(); + g.put("rowId", wellGroupId); + g.put("type", group.getType().name()); + g.put("name", group.getName()); + g.put("positions", positions); + g.put("properties", groupProps); + g.put("allowNewGroups", handler.canCreateNewGroups(group.getType())); + groupList.add(g); + } + + Map templateProperties = new HashMap<>(); + for (String propName : template.getPropertyNames()) + templateProperties.put(propName, template.getProperty(propName) == null ? null : template.getProperty(propName).toString()); + + // Build type list + List typeList = new ArrayList<>(); + for (WellGroup.Type type : handler.getWellGroupTypes()) + typeList.add(type.name()); + + // Build canCreateGroupsByType + Map canCreateGroupsByType = new LinkedHashMap<>(); + for (WellGroup.Type type : handler.getWellGroupTypes()) + canCreateGroupsByType.put(type.name(), handler.canCreateNewGroups(type)); + + // Build typesToDefaultGroups + Map> typesToDefaultGroups = handler.getDefaultGroupsForTypes(); + + // Existing template names in container + List existingTemplateNames = new ArrayList<>(); + for (Plate p : PlateService.get().getPlates(getContainer())) + existingTemplateNames.add(p.getName()); + + long responseRowId = copyTemplate || template.getRowId() == null ? -1 : template.getRowId(); + String defaultPlateName; + if (templateName != null) + { + defaultPlateName = copyTemplate ? getUniqueName(getContainer(), templateName) : templateName; + } + else + { + defaultPlateName = ""; + } + + Map result = new HashMap<>(); + result.put("rowId", responseRowId); + result.put("name", template.getName()); + result.put("type", template.getAssayType()); + result.put("rows", template.getRows()); + result.put("cols", template.getColumns()); + result.put("groupTypes", typeList); + result.put("canCreateGroupsByType", canCreateGroupsByType); + result.put("groups", groupList); + result.put("plateProperties", templateProperties); + result.put("typesToDefaultGroups", typesToDefaultGroups); + result.put("showWarningPanel", handler.showEditorWarningPanel()); + result.put("existingTemplateNames", existingTemplateNames); + result.put("copyMode", copyTemplate); + result.put("defaultPlateName", defaultPlateName); + + return success(result); + } + } + + public static class SaveTemplateForm implements ApiJsonForm + { + private JSONObject _json; + + @Override + public void bindJson(JSONObject json) + { + _json = json; + } + + public JSONObject getJson() + { + return _json != null ? _json : new JSONObject(); + } + } + + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + public static class SaveTemplateAction extends MutatingApiAction + { + private static final int MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB + + @Override + protected BaseApiAction.FormAndErrors populateJacksonForm() throws Exception + { + byte[] bytes; + try (BoundedInputStream bounded = BoundedInputStream.builder() + .setInputStream(getViewContext().getRequest().getInputStream()) + .setMaxCount((long) MAX_BODY_BYTES + 1) + .get()) + { + bytes = bounded.readAllBytes(); + } + if (bytes.length > MAX_BODY_BYTES) + throw new ApiUsageException("Request body exceeds maximum allowed size of 10 MB."); + String body = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + JSONObject jsonObj = body.isEmpty() ? new JSONObject() : new JSONObject(body); + SaveTemplateForm form = new SaveTemplateForm(); + form.bindJson(jsonObj); + return new BaseApiAction.FormAndErrors<>(form, new NullSafeBindException(form, "form")); + } + + @Override + public Object execute(SaveTemplateForm form, BindException errors) throws Exception + { + JSONObject json = form.getJson(); + + long rowId = json.optLong("rowId", -1); + String name = json.getString("name"); + String type = json.getString("type"); + int rows = json.getInt("rows"); + int cols = json.getInt("cols"); + JSONArray groupsJson = json.optJSONArray("groups"); + JSONObject platePropsJson = json.optJSONObject("plateProperties"); + + Map plateProperties = new HashMap<>(); + if (platePropsJson != null) + { + for (String key : platePropsJson.keySet()) + plateProperties.put(key, platePropsJson.get(key)); + } + + boolean updateExisting = false; + Plate plate; + if (rowId > 0) + { + plate = PlateManager.get().getPlate(getContainer(), rowId); + if (plate == null) + throw new NotFoundException("Plate template not found: " + rowId); + // Check for a conflicting name from a different plate + Plate conflict = PlateManager.get().getPlateByName(getContainer(), name); + if (conflict != null && !conflict.getRowId().equals(plate.getRowId())) + throw new ApiUsageException("A plate template with name '" + name + "' already exists."); + if (!plate.getAssayType().equals(type)) + throw new ApiUsageException("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + name + "'"); + if (plate.getRows() != rows || plate.getColumns() != cols) + throw new ApiUsageException("Plate template dimensions cannot be changed for '" + name + "'"); + updateExisting = true; + } + else + { + if (PlateManager.get().getPlateByName(getContainer(), name) != null) + throw new ApiUsageException("A plate template with name '" + name + "' already exists."); + PlateType plateType = PlateService.get().getPlateType(rows, cols); + if (plateType == null) + throw new NotFoundException("The plate type (" + rows + " x " + cols + ") does not exist."); + plate = PlateManager.get().createPlate(getContainer(), type, plateType); + } + + plate.setName(name); + plate.setProperties(plateProperties); + + // Parse groups from JSON + List> submittedGroups = new ArrayList<>(); + Set submittedGroupIds = new HashSet<>(); + if (groupsJson != null) + { + for (int i = 0; i < groupsJson.length(); i++) + { + JSONObject g = groupsJson.getJSONObject(i); + Map gm = new HashMap<>(); + gm.put("rowId", g.optInt("rowId", -1)); + gm.put("type", g.getString("type")); + gm.put("name", g.getString("name")); + JSONArray posArr = g.optJSONArray("positions"); + List positions = new ArrayList<>(); + if (posArr != null) + { + for (int j = 0; j < posArr.length(); j++) + { + JSONObject p = posArr.getJSONObject(j); + positions.add(new int[]{p.getInt("row"), p.getInt("col")}); + } + } + gm.put("positions", positions); + JSONObject propsObj = g.optJSONObject("properties"); + Map props = new HashMap<>(); + if (propsObj != null) + { + for (String key : propsObj.keySet()) + { + Object val = propsObj.get(key); + props.put(key, val == JSONObject.NULL ? null : val); + } + } + gm.put("properties", props); + submittedGroups.add(gm); + if ((int) gm.get("rowId") > 0) + submittedGroupIds.add((int) gm.get("rowId")); + } + } + + // Mark well groups not in submission for deletion + List existingWellGroups = plate.getWellGroups(); + for (WellGroup existingGroup : existingWellGroups) + { + if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) + ((PlateImpl) plate).markWellGroupForDeletion(existingGroup); + } + + // Update or create well groups + for (Map gm : submittedGroups) + { + int gRowId = (int) gm.get("rowId"); + String groupTypeName = (String) gm.get("type"); + WellGroup.Type groupType; + try + { + groupType = WellGroup.Type.valueOf(groupTypeName); + } + catch (IllegalArgumentException e) + { + throw new ApiUsageException("Unknown well group type: '" + groupTypeName + "'"); + } + @SuppressWarnings("unchecked") + List posList = (List) gm.get("positions"); + List positions = new ArrayList<>(); + for (int[] p : posList) + positions.add(plate.getPosition(p[0], p[1])); + + @SuppressWarnings("unchecked") + Map props = (Map) gm.get("properties"); + + WellGroupImpl group; + if (updateExisting && gRowId > 0) + { + WellGroupImpl existing = findExistingWellGroup(existingWellGroups, gRowId); + if (existing == null) + throw new Exception("Well group " + gRowId + " was not found."); + if (existing.getType() != groupType) + throw new Exception("Well group type cannot be changed: " + gm.get("name")); + existing.setName((String) gm.get("name")); + existing.setPositions(positions); + ((PlateImpl) plate).storeWellGroup(existing); + group = existing; + } + else + { + group = (WellGroupImpl) plate.addWellGroup((String) gm.get("name"), groupType, positions); + } + group.setProperties(props); + } + + PlateLayoutHandler plateLayoutHandler = PlateManager.get().getPlateLayoutHandler(plate.getAssayType()); + + if (plateLayoutHandler == null) + { + throw new NotFoundException("Invalid assay type"); + } + plateLayoutHandler.validatePlate(getContainer(), getUser(), plate); + long savedRowId = PlateService.get().save(getContainer(), getUser(), plate); + return success(Map.of("rowId", savedRowId)); + } + + private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) + { + for (WellGroup wg : wellGroups) + { + if (wg.getRowId() != null && wg.getRowId() == rowId) + return (WellGroupImpl) wg; + } + return null; + } + } + + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + public static class DesignerAction extends SimpleViewAction + { + @Override + public ModelAndView getView(DesignerForm form, BindException errors) + { + return ModuleHtmlView.get( + ModuleLoader.getInstance().getModule("assay"), + ModuleHtmlView.getGeneratedViewPath("plateTemplateDesigner") + ); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("editPlateTemplate"); + root.addChild("Plate Editor"); + } + } + + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + public class DesignerGwtAction extends SimpleViewAction { @Override public ModelAndView getView(DesignerForm form, BindException errors) @@ -262,7 +631,7 @@ else if (form.getPlateId() != null) public void addNavTrail(NavTree root) { setHelpTopic("editPlateTemplate"); - root.addChild("Plate Editor"); + root.addChild("Plate Editor (GWT)"); } } diff --git a/assay/src/org/labkey/assay/plate/PlateImpl.java b/assay/src/org/labkey/assay/plate/PlateImpl.java index 5a255cf82ca..9934c2921de 100644 --- a/assay/src/org/labkey/assay/plate/PlateImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateImpl.java @@ -237,7 +237,7 @@ public WellGroup addWellGroup(WellGroupImpl group) } @JsonIgnore - protected WellGroupImpl storeWellGroup(WellGroupImpl group) + public WellGroupImpl storeWellGroup(WellGroupImpl group) { group.setPlate(this); From ca99f5ed0e8ac3cfa68f890d1216aacd71d188e9 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 25 Apr 2026 08:46:49 -0700 Subject: [PATCH 2/9] Auto code review and comments --- .../PlateTemplateDesigner.scss | 100 +++++++++-- .../PlateTemplateDesigner.tsx | 162 +++++++++++++----- .../components/GroupTypesPanel.tsx | 148 +++++++++++++--- .../components/ShiftPanel.tsx | 22 ++- .../components/StatusBar.tsx | 19 +- .../components/TemplateGrid.tsx | 87 +++++++--- .../components/WarningPanel.tsx | 16 +- .../components/WellGroupProperties.tsx | 20 ++- .../client/PlateTemplateDesigner/models.ts | 32 +++- 9 files changed, 479 insertions(+), 127 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 3a790af9729..073aa16695b 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -4,6 +4,34 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ +/* + * ────────────────────────────────────────────────────────────────────────────── + * Overall layout (vertical stack): + * + * ┌──────────────────────────────────────────────────────────┐ + * │ .status-bar (Save & Close | Save | Cancel | status) │ + * ├──────────────────────────────────────────────────────────┤ + * │ .plate-template-designer__header (Plate Name input) │ + * ├──────────────────────────────────────────────────────────┤ + * │ .plate-template-designer__body (horizontal flex) │ + * │ ┌──────────────────────────────┐ ┌───────────────────┐ │ + * │ │ __left (flex: 0 0 auto) │ │ __right (flex:1) │ │ + * │ │ .group-types-panel │ │ right-panel-tabs │ │ + * │ │ ├─ tab strip │ │ + WellGroupProps │ │ + * │ │ └─ tab-body (flex row) │ │ or WarningPanel│ │ + * │ │ ├─ group list │ │ │ │ + * │ │ └─ .plate-grid-area │ │ │ │ + * │ │ ├─ TemplateGrid │ │ │ │ + * │ │ └─ ShiftPanel │ │ │ │ + * │ └──────────────────────────────┘ └───────────────────┘ │ + * └──────────────────────────────────────────────────────────┘ + * + * The left column is sized to its content (fixed width); the right column grows + * to fill remaining space with a minimum width so it doesn't collapse. + * ────────────────────────────────────────────────────────────────────────────── + */ + +// Root container .plate-template-designer { padding: 12px 16px; font-family: Arial, Helvetica, sans-serif; @@ -36,23 +64,27 @@ width: 280px; } + // Horizontal flex: left (group panel + grid) | right (properties / warnings) &__body { display: flex; gap: 16px; align-items: flex-start; } + // Shrinks/grows to fit GroupTypesPanel content; does not flex &__left { flex: 0 0 auto; } + // Fills remaining width; min-width prevents collapse when the window is narrow &__right { flex: 1; min-width: 200px; } } -// StatusBar +// ── StatusBar ───────────────────────────────────────────────────────────────── +// Pinned above the plate header; uses flex to space buttons and status text. .status-bar { display: flex; align-items: center; @@ -89,17 +121,22 @@ } } + // Unsaved-changes indicator — uses role="status" in JSX for screen reader announcements &__dirty { - color: #c80; + color: #7a5800; font-style: italic; } + // Transient save status ("Saving…", "Saved.") — auto-clears after 5 s &__status { color: #555; } } -// GroupTypesPanel +// ── GroupTypesPanel ─────────────────────────────────────────────────────────── +// Outer panel with a tab strip and a two-column flex body: +// left column: scrollable group list + create controls +// right column: TemplateGrid + ShiftPanel (passed as children) .group-types-panel { border: 1px solid #ccc; border-radius: 3px; @@ -130,6 +167,7 @@ } } + // Flex row: [group list] [grid area] — keeps the grid visually docked to the panel &__tab-body { display: flex; align-items: flex-start; @@ -137,12 +175,14 @@ padding: 8px; } + // Group list column — fixed width with a minimum so short lists don't collapse &__groups { flex: 0 0 160px; min-width: 280px; min-height: 60px; } + // Individual group row — clickable to select; keyboard-accessible via tabIndex + onKeyDown &__group { display: flex; align-items: center; @@ -164,6 +204,7 @@ } } + // Colour indicator matching the group's colour on the grid &__color-swatch { display: inline-block; width: 12px; @@ -181,6 +222,7 @@ min-width: 0; } + // In-place rename input (replaces group-name span when renaming is active) &__rename-input { flex: 1; padding: 1px 4px; @@ -190,6 +232,7 @@ min-width: 0; } + // Rename + delete buttons, visible only for the active group row &__group-actions { display: flex; gap: 2px; @@ -220,6 +263,7 @@ } } + // Row that holds the new-group name input + Create / Create multiple buttons &__create-row { display: flex; align-items: center; @@ -227,6 +271,7 @@ margin-top: 6px; } + // Shared by both the (free-text) variants &__new-name-input { flex: 1; padding: 3px 5px; @@ -267,7 +312,10 @@ } } -// Multi-create dialog +// ── Multi-create dialog ─────────────────────────────────────────────────────── +// Modal overlay + dialog for batch-creating N numbered groups. +// The overlay captures click-outside-to-close; the inner dialog stops propagation. +// Focus is trapped inside the dialog while open (see GroupTypesPanel focus-trap effect). .multi-create-dialog { background: #fff; border: 1px solid #ccc; @@ -301,6 +349,7 @@ } } + // Label cells carry id attributes and are referenced via aria-labelledby on the inputs &__label { padding: 6px 16px 6px 0; white-space: nowrap; @@ -335,7 +384,8 @@ } } -// Wrapper for grid + shift panel +// ── Plate grid area ─────────────────────────────────────────────────────────── +// Centres the TemplateGrid and ShiftPanel as a vertical column inside GroupTypesPanel. .plate-grid-area { display: flex; flex-direction: column; @@ -343,7 +393,9 @@ gap: 8px; } -// ShiftPanel +// ── ShiftPanel ──────────────────────────────────────────────────────────────── +// 3×3 CSS grid with arrow buttons at compass positions and a label in the centre. +// Empty corners use placeholders to maintain grid alignment. .shift-panel { &__grid { display: grid; @@ -378,9 +430,12 @@ } } -// TemplateGrid +// ── TemplateGrid ────────────────────────────────────────────────────────────── +// HTML table where each is a well. The user paints groups by clicking or dragging. +// Background colour comes from the colorMap for the active tab's groups (inline style). +// The --active modifier draws an outline around wells belonging to the selected group. .template-grid { - user-select: none; + user-select: none; // Prevents text selection during drag painting &__table { border-collapse: collapse; @@ -422,6 +477,7 @@ filter: brightness(0.88); } + // Indicates cells belonging to the currently active group &--active { outline: 2px solid #333; outline-offset: -2px; @@ -429,7 +485,9 @@ } } -// Right panel tabs +// ── Right panel tabs ────────────────────────────────────────────────────────── +// Tab strip shown in the right column when showWarningPanel is true. +// --warn colours the Warnings tab amber; --active+--warn shifts the indicator colour too. .right-panel-tabs { display: flex; border-bottom: 1px solid #ccc; @@ -455,16 +513,18 @@ } &--warn { - color: #c80; + color: #7a5800; } &--active#{&}--warn { - border-bottom-color: #c80; + border-bottom-color: #7a5800; } } } -// WellGroupProperties +// ── WellGroupProperties ─────────────────────────────────────────────────────── +// Table-based key/value editor for the active group's property bag. +// The --empty modifier styles the "no group selected" placeholder. .well-group-properties { border: 1px solid #ccc; border-radius: 3px; @@ -472,7 +532,7 @@ min-height: 60px; &--empty { - color: #888; + color: #767676; font-style: italic; } @@ -482,7 +542,7 @@ } &__no-props { - color: #888; + color: #767676; font-style: italic; font-size: 12px; } @@ -492,6 +552,7 @@ border-collapse: collapse; } + // Key column — fixed at 40% width, non-wrapping &__key { font-weight: bold; padding: 3px 6px 3px 0; @@ -506,7 +567,7 @@ &__action-cell { padding: 2px 0 2px 6px; white-space: nowrap; - width: 1%; + width: 1%; // Shrink-wraps to button content } &__value { @@ -533,6 +594,7 @@ } } + // Footer row — Add new property; separated visually from existing entries &__add-row td { border-top: 1px solid #eee; padding-top: 6px; @@ -576,7 +638,9 @@ } } -// WarningPanel +// ── WarningPanel ────────────────────────────────────────────────────────────── +// Amber-tinted panel listing validation warnings from computeWarnings(). +// Only rendered when plate.showWarningPanel is true (assay-type controlled). .warning-panel { border: 1px solid #e8a000; border-radius: 3px; @@ -585,12 +649,12 @@ &__title { font-weight: bold; - color: #c80; + color: #7a5800; margin-bottom: 6px; } &__none { - color: #888; + color: #767676; font-style: italic; font-size: 12px; } diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 16297b36f3e..9840731dbb4 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -4,6 +4,7 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; import { ActionURL, Ajax, Utils } from '@labkey/api'; import { PlateTemplate, WellGroup, computeWarnings } from './models'; @@ -16,6 +17,47 @@ import { WarningPanel } from './components/WarningPanel'; import './PlateTemplateDesigner.scss'; +/** + * Root component of the Plate Template Designer. + * + * ─── User workflow ────────────────────────────────────────────────────────────── + * 1. On mount, URL parameters are read (templateName, plateId, assayType, rowCount, + * colCount, copy) and the plate definition is fetched from the server. + * 2. The user selects a group type tab (e.g. CONTROL, SPECIMEN, REPLICATE). + * 3. Within that type, the user selects or creates a named group. + * 4. The user clicks or drags wells on the grid to paint them onto the active group. + * 5. The user optionally edits well group properties in the right panel. + * 6. "Save" persists without leaving; "Save & Close" saves then navigates to returnURL + * (or the plate list). "Cancel" navigates away without saving. + * + * ─── State architecture ───────────────────────────────────────────────────────── + * `plate` is the single source of truth for all template data. All mutations go + * through `setPlate` with functional updaters to avoid stale-closure bugs. + * + * `activeGroup` is a denormalized mirror of the currently selected group, kept in + * sync with `plate` via the sync effect below. It exists separately because: + * - Callbacks that use `setPlate(prev => ...)` don't have access to the current + * group data inside the updater; they use `activeGroup` from their closure. + * - Components that show the active group (WellGroupProperties, TemplateGrid + * cell highlighting) need a stable reference that doesn't require traversing + * `plate.groups` on every access. + * + * ─── ID conventions ───────────────────────────────────────────────────────────── + * Server-assigned group IDs are positive integers. Client-side created groups + * receive temporary negative IDs (nextGroupIdRef counts down from -1). This ensures + * new groups never collide with existing ones before the first save. The server + * replaces all IDs with permanent values on save; the client does not update + * individual group IDs — only the top-level `plate.rowId` is updated after save. + * + * ─── Cell interaction ─────────────────────────────────────────────────────────── + * Two cell callbacks are distinguished: + * `handleCellAssign` — idempotent add; also evicts the cell from any other group + * of the same type (one cell can only belong to one group per type). Used during + * drag operations. + * `handleCellToggle` — pure on/off; does not steal from siblings. Used for + * single-click (no drag movement). + */ + const COLORS = [ '#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac', @@ -40,9 +82,9 @@ export function PlateTemplateDesigner(): JSX.Element { const [status, setStatus] = useState(''); const [colorMap, setColorMap] = useState>(new Map()); const [error, setError] = useState(null); - const plateNameRef = useRef(''); + const plateNameRef = useRef(''); // Mirrors plate.name; used in save-success to update URL without stale closure const statusTimerRef = useRef | null>(null); - const nextGroupIdRef = useRef(-1); + const nextGroupIdRef = useRef(-1); // Temporary negative IDs for client-created groups (see ID conventions above) useEffect(() => { const templateName = ActionURL.getParameter('templateName'); @@ -233,32 +275,42 @@ export function PlateTemplateDesigner(): JSX.Element { window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); }, []); - const handleSave = useCallback(() => { - if (!plate) return; + /** + * Shared Ajax save logic. Takes the plate snapshot and a success callback to avoid + * duplicating the request setup and failure handler in handleSave / handleSaveAndClose. + * The plate is passed as a parameter (rather than closed over) so callers can pass the + * latest snapshot without worrying about stale state. + */ + const requestSave = useCallback((currentPlate: PlateTemplate, onSuccess: (response: { data: { rowId: number } }) => void) => { setStatus('Saving...'); Ajax.request({ url: ActionURL.buildURL('plate', 'saveTemplate.api'), method: 'POST', - jsonData: plate, - success: Utils.getCallbackWrapper((response: { data: { rowId: number } }) => { - const rowId = response.data.rowId; - setIsDirty(false); - setPlate(prev => prev ? { ...prev, rowId } : null); - // Update URL to canonical form so a refresh reloads this plate - const url = new URL(window.location.href); - url.search = ''; - url.searchParams.set('templateName', plate.name); - url.searchParams.set('plateId', String(rowId)); - window.history.replaceState(null, '', url.toString()); - setStatus('Saved.'); - if (statusTimerRef.current) clearTimeout(statusTimerRef.current); - statusTimerRef.current = setTimeout(() => setStatus(''), 5000); - }), + jsonData: currentPlate, + success: Utils.getCallbackWrapper(onSuccess), failure: Utils.getCallbackWrapper((response: any) => { setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); }, null, true), }); - }, [plate]); + }, []); + + const handleSave = useCallback(() => { + if (!plate) return; + requestSave(plate, (response) => { + const rowId = response.data.rowId; + setIsDirty(false); + setPlate(prev => prev ? { ...prev, rowId } : null); + // Update URL to canonical form so a refresh reloads this plate + const url = new URL(window.location.href); + url.search = ''; + url.searchParams.set('templateName', plateNameRef.current); + url.searchParams.set('plateId', String(rowId)); + window.history.replaceState(null, '', url.toString()); + setStatus('Saved.'); + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + statusTimerRef.current = setTimeout(() => setStatus(''), 5000); + }); + }, [plate, requestSave]); const handleSaveAndClose = useCallback(() => { if (!plate) return; @@ -266,20 +318,11 @@ export function PlateTemplateDesigner(): JSX.Element { navigateAway(); return; } - setStatus('Saving...'); - Ajax.request({ - url: ActionURL.buildURL('plate', 'saveTemplate.api'), - method: 'POST', - jsonData: plate, - success: Utils.getCallbackWrapper(() => { - setIsDirty(false); - navigateAway(); - }), - failure: Utils.getCallbackWrapper((response: any) => { - setStatus('Save failed: ' + (response?.exception ?? 'unknown error')); - }, null, true), + requestSave(plate, () => { + setIsDirty(false); + navigateAway(); }); - }, [plate, isDirty, navigateAway]); + }, [plate, isDirty, navigateAway, requestSave]); const handleCancel = useCallback(() => { navigateAway(); @@ -297,7 +340,17 @@ export function PlateTemplateDesigner(): JSX.Element { return () => window.removeEventListener('beforeunload', handler); }, [isDirty]); - // Keep activeGroup in sync when plate changes + // Keep activeGroup in sync when plate changes. + // + // Most plate mutations go through setPlate(prev => ...) updaters which don't have + // access to the current activeGroup. After each plate update, this effect finds the + // matching group by rowId and refreshes activeGroup so downstream components (e.g. + // WellGroupProperties, TemplateGrid cell highlight) see the latest data. + // + // activeGroup is intentionally excluded from the deps array: adding it would cause + // an infinite loop (effect sets activeGroup → triggers effect → sets activeGroup …). + // handleDeleteGroup handles the "group no longer exists" case by explicitly setting + // activeGroup to null before this effect can run. useEffect(() => { if (activeGroup && plate) { const updated = plate.groups.find(g => g.rowId === activeGroup.rowId); @@ -346,6 +399,8 @@ export function PlateTemplateDesigner(): JSX.Element { onDeleteGroup={handleDeleteGroup} onRenameGroup={handleRenameGroup} > + {/* The grid and shift panel are passed as children so they render + inside GroupTypesPanel's flex row, visually adjacent to the group list. */}
+ {/* Right panel: WellGroupProperties and (if enabled) a Warnings tab. + The tab strip only renders when showWarningPanel is true; otherwise + WellGroupProperties fills the full right column without tabs. */}
{plate.showWarningPanel && ( -
+
)} {(!plate.showWarningPanel || rightTab === 'properties') && ( - +
+ +
)} {plate.showWarningPanel && rightTab === 'warnings' && ( - +
+ +
)}
diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index fa4d0a21275..19f05b1ea36 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -4,6 +4,7 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React, { useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; @@ -20,6 +21,48 @@ interface Props { children?: React.ReactNode; } +/** + * Left-hand panel that manages group types (tabs) and individual well groups. + * + * ─── Layout ──────────────────────────────────────────────────────────────────── + * The panel is split into two side-by-side areas via a flex row: + * Left column – the group list + create controls (fixed width) + * Right area – children (the TemplateGrid + ShiftPanel), passed in from the parent + * + * This composition pattern keeps the grid visually anchored inside the panel boundary + * while letting the tab strip and group list scroll independently. + * + * ─── Tab switching ───────────────────────────────────────────────────────────── + * Each tab corresponds to a group type key (e.g. "CONTROL", "SPECIMEN", "REPLICATE"). + * Switching tabs: + * - Clears the active group selection (the parent sets activeGroup to null). + * - Updates the grid to show only that type's colour layout. + * - Resets the create-name input to the first unused default for the new type. + * + * ─── Group selection ─────────────────────────────────────────────────────────── + * Clicking a group row makes it the "active group". Once active, clicking or + * dragging cells on the TemplateGrid paints them onto that group. The active group + * is highlighted with a blue border and shows inline rename/delete actions. + * + * ─── Creating groups ─────────────────────────────────────────────────────────── + * Some group types come with predefined slot names (`typesToDefaultGroups`), e.g. + * "Virus" and "Cell Control" for certain assay types. While unused defaults remain, + * a + * appears for custom names. + * + * "Create multiple…" opens a modal dialog that batch-creates N numbered groups + * (e.g. "Sample 1" through "Sample 8") from a base name and count. Useful for + * assays with many specimens or replicates. + * + * ─── Renaming ────────────────────────────────────────────────────────────────── + * The pencil button activates an inline rename input in place of the group name. + * Blur or Enter commits the change; Escape discards it. + * + * ─── Modal focus trap ────────────────────────────────────────────────────────── + * When the multi-create dialog opens, a useEffect traps Tab/Shift-Tab focus inside + * the dialog and moves initial focus to the first focusable element. Escape closes + * the dialog from anywhere within it. + */ export function GroupTypesPanel({ plate, activeGroup, @@ -40,10 +83,14 @@ export function GroupTypesPanel({ const [multiCount, setMultiCount] = useState('2'); const [multiCountError, setMultiCountError] = useState(''); const multiBaseNameRef = useRef(null); + const dialogRef = useRef(null); - const groupsOfType = plate.groups.filter(g => g.type === activeTab); + // Stable derived list — memoized so useMemo and useEffect deps are stable. + const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); const canAdd = plate.canCreateGroupsByType?.[activeTab] ?? false; + // Predefined slot names not yet occupied by an existing group of this type. + // Drives the toggle in the create row. const unusedDefaults = useMemo(() => { const defaults = plate.typesToDefaultGroups[activeTab] ?? []; return defaults.filter(d => !groupsOfType.some(g => g.name === d)); @@ -62,6 +109,34 @@ export function GroupTypesPanel({ } }, [unusedDefaults]); // eslint-disable-line react-hooks/exhaustive-deps + // Focus trap for multi-create dialog + useEffect(() => { + if (!multiCreateOpen || !dialogRef.current) return; + const dialog = dialogRef.current; + const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setMultiCreateOpen(false); + return; + } + if (e.key !== 'Tab') return; + const focusable = getFocusable(); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last?.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first?.focus(); } + } + }; + + dialog.addEventListener('keydown', handleKeyDown); + getFocusable()[0]?.focus(); + return () => dialog.removeEventListener('keydown', handleKeyDown); + }, [multiCreateOpen]); + const handleCreate = () => { const trimmed = newGroupName.trim(); if (!trimmed) return; @@ -73,8 +148,7 @@ export function GroupTypesPanel({ setMultiCount('2'); setMultiCountError(''); setMultiCreateOpen(true); - // Focus the base name input after the modal renders - setTimeout(() => multiBaseNameRef.current?.select(), 0); + // Focus is handled by the focus-trap effect above }; const handleMultiCreate = () => { @@ -112,21 +186,27 @@ export function GroupTypesPanel({ return (
-
+
{plate.groupTypes.map(type => ( ))}
-
+
{groupsOfType.map(group => { const color = colorMap.get(group.rowId); @@ -135,11 +215,17 @@ export function GroupTypesPanel({ return (
{ if (!isRenaming) onGroupSelect(group); }} + onKeyDown={e => { + if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onGroupSelect(group); + } + }} > setRenameValue(e.target.value)} @@ -161,21 +248,24 @@ export function GroupTypesPanel({ ) : ( {group.name} )} + {/* Rename/delete actions appear only on the active group row */} {isActive && !isRenaming && group.allowNewGroups && ( )} @@ -184,8 +274,14 @@ export function GroupTypesPanel({ })} {canAdd && (
+ {/* + * Show a once all + * defaults are consumed or if there are none defined for this type. + */} {unusedDefaults.length > 0 ? ( {multiCreateOpen && (
setMultiCreateOpen(false)}> -
e.stopPropagation()}> -
Create Multiple Groups
+
e.stopPropagation()} + > +
Create Multiple Groups
- + - + diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx index 889c24f7127..43c3d3cb2a0 100644 --- a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx @@ -9,18 +9,32 @@ interface Props { onShift: (verticalShift: number, horizontalShift: number) => void; } +/** + * A compass-rose control that shifts all wells of the currently active group type one step in any + * cardinal direction. The shift wraps around plate edges (toroidal), so wells that fall off the + * bottom reappear at the top, etc. + * + * Sign convention (matches the modular arithmetic in PlateTemplateDesigner.handleShift): + * verticalShift > 0 → cells move UP (row index decreases: row = (row - shift + rows) % rows) + * verticalShift < 0 → cells move DOWN + * horizontalShift > 0 → cells move LEFT (col index decreases) + * horizontalShift < 0 → cells move RIGHT + * + * Shifts apply to every group of the active type simultaneously, preserving relative layout + * between groups. Only the active tab's type is affected; other types are unchanged. + */ export function ShiftPanel({ onShift }: Props): JSX.Element { return (
- + - + Shift - + - +
diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx index c68b54e9684..9afc2600133 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -13,6 +13,21 @@ interface Props { onCancel: () => void; } +/** + * Persistent action bar pinned to the top of the designer. + * + * Button behavior: + * - "Save & Close": saves if dirty, then navigates to the returnURL (or plate list). + * Always enabled so users can leave even when clean. + * - "Save": persists the current state and updates the page URL to the canonical + * ?templateName=...&plateId=... form so a browser refresh reloads the same plate. + * Disabled when the plate is clean to prevent redundant requests. + * - "Cancel": navigates away without saving. The browser's beforeunload handler + * will prompt if there are unsaved changes. + * + * The "Unsaved changes" indicator and transient status text ("Saving…", "Saved.") + * use `role="status"` so screen readers announce them as they appear. + */ export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: Props): JSX.Element { return (
@@ -25,8 +40,8 @@ export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: - {isDirty && Unsaved changes} - {status && {status}} + {isDirty ? 'Unsaved changes' : ''} + {status}
); } diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index b4cf732fece..49f03c5e388 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -3,7 +3,8 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; +import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; @@ -20,24 +21,61 @@ function getRowLabel(row: number): string { return String.fromCharCode(65 + row); } -function getCellColor(row: number, col: number, activeTab: string, plate: PlateTemplate, colorMap: Map): string | undefined { - // Only color cells belonging to groups of the currently active tab type, - // matching the GWT behavior of showing one type's layout at a time. - let color: string | undefined; - for (const group of plate.groups) { - if (group.type === activeTab && group.positions.some(p => p.row === row && p.col === col)) { - color = colorMap.get(group.rowId); - } - } - return color; -} - +/** + * A scrollable well grid that lets the user paint cells onto the active well group. + * + * ─── Coloring ────────────────────────────────────────────────────────────────── + * Only wells belonging to groups of the *active tab type* are coloured. Wells from + * other types are invisible in the current view. This matches the original GWT + * behaviour of presenting one group type at a time. + * + * ─── Drag / click interaction ────────────────────────────────────────────────── + * Cell assignment uses a three-phase state machine tracked entirely via refs + * (no re-renders on drag): + * + * Phase 1 – mousedown on a cell: + * Enter drag mode. Record the start cell. Do NOT assign it yet — we first need + * to know whether the user is clicking (toggle) or dragging (assign-only). + * + * Phase 2 – mouseenter a *different* cell while dragging: + * We now know it's a drag. Retroactively assign the original start cell + * (deferred assign), then assign each subsequently entered cell. + * `dragCells` deduplicates entries so fast mouse movement can't assign the + * same cell twice. + * + * Phase 3 – mouseup: + * If the pointer never left the start cell (hasMoved === false), treat the + * interaction as a click and toggle that cell (add if absent, remove if present). + * Either way, reset all drag state. + * + * Drag state is also cleaned up on mouseleave of the outer div, preventing stuck + * drag state when the pointer exits the grid. + * + * `onCellAssign` is idempotent (ignores duplicates) and also removes the assigned + * cell from any other group of the same type, enforcing the one-group-per-cell-per- + * type constraint. `onCellToggle` does a pure add/remove with no stealing. + */ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAssign, onCellToggle }: Props): JSX.Element { const isDragging = useRef(false); const hasMoved = useRef(false); const startCell = useRef<{ row: number; col: number } | null>(null); const dragCells = useRef>(new Set()); + // Pre-compute a "row,col" → {color, groupName} map for the active tab type. + // This lets each cell do an O(1) lookup rather than scanning all groups and + // positions on every render (which would be O(groups × positions) per cell). + const positionMap = useMemo(() => { + const map = new Map(); + for (const group of plate.groups) { + if (group.type !== activeTab) continue; + const color = colorMap.get(group.rowId) ?? '#f5f5f5'; + for (const p of group.positions) { + map.set(`${p.row},${p.col}`, { color, groupName: group.name }); + } + } + return map; + }, [plate, activeTab, colorMap]); + const handleMouseDown = useCallback((row: number, col: number, e: React.MouseEvent) => { if (e.button !== 0) return; isDragging.current = true; @@ -51,7 +89,8 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs if (!isDragging.current) return; if (!hasMoved.current) { hasMoved.current = true; - // Deferred: assign the mousedown cell now that we know it's a drag + // Deferred assign: now that we know this is a drag, assign the cell + // the user originally pressed down on. if (startCell.current) { onCellAssign(startCell.current.row, startCell.current.col); } @@ -85,30 +124,26 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs + ))} {Array.from({ length: plate.rows }, (_, row) => ( - + {Array.from({ length: plate.cols }, (_, col) => { - const color = getCellColor(row, col, activeTab, plate, colorMap); + const entry = positionMap.get(`${row},${col}`); const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); const location = `${getRowLabel(row)}${col + 1}`; - const groupForCell = plate.groups.find( - g => g.type === activeTab && g.positions.some(p => p.row === row && p.col === col) - ); - const tooltip = groupForCell ? `${location}: ${groupForCell.name}` : location; + const tooltip = entry ? `${location}: ${entry.groupName}` : location; return ( @@ -75,6 +91,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro className="well-group-properties__new-key" type="text" placeholder="Property name" + aria-label="Property name" value={newKey} onChange={e => setNewKey(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} @@ -85,6 +102,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro className="well-group-properties__new-value" type="text" placeholder="Value" + aria-label="Property value" value={newValue} onChange={e => setNewValue(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && newKey.trim()) handleAdd(); }} diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts index 94e157b72e5..71036b33950 100644 --- a/assay/src/client/PlateTemplateDesigner/models.ts +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -10,12 +10,12 @@ export interface Position { } export interface WellGroup { - rowId: number; - type: string; + rowId: number; // Positive = server-assigned; negative = client-side temp ID (see nextGroupIdRef) + type: string; // Group type key, e.g. "CONTROL", "SPECIMEN", "REPLICATE" name: string; positions: Position[]; properties: Record; - allowNewGroups: boolean; + allowNewGroups: boolean; // Whether the user can create/rename/delete groups of this type } export interface PlateTemplate { @@ -24,14 +24,14 @@ export interface PlateTemplate { type: string; rows: number; cols: number; - groupTypes: string[]; - canCreateGroupsByType: Record; + groupTypes: string[]; // Ordered list of type keys; drives the tab strip + canCreateGroupsByType: Record; // Which types expose the create-group UI groups: WellGroup[]; plateProperties: Record; - typesToDefaultGroups: Record; - showWarningPanel: boolean; + typesToDefaultGroups: Record; // Predefined slot names per type (e.g. "Virus", "Cell Control") + showWarningPanel: boolean; // Set by the server based on assay type config existingTemplateNames: string[]; - copyMode: boolean; + copyMode: boolean; // True when the plate was loaded as a copy; starts the editor in dirty state defaultPlateName: string; } @@ -39,8 +39,22 @@ export interface SaveTemplateResponse { rowId: number; } -// Matches GWT TemplateGridCell.getWarnings() logic exactly. +/** + * Replicates the GWT TemplateGridCell.getWarnings() logic exactly. + * + * Two conditions produce warnings: + * 1. A REPLICATE well that belongs to neither a SPECIMEN nor a CONTROL group is almost certainly + * a configuration error — replicates are only meaningful relative to a specimen or control. + * 2. A well assigned to both a SPECIMEN and a CONTROL group is contradictory; those roles are + * mutually exclusive in LabKey assay semantics. + * + * Notes: + * - Warnings are per-cell, not per-group. + * - A cell can appear in multiple groups of different types (e.g. SPECIMEN + REPLICATE together is fine). + * - Cell labels use spreadsheet notation: row → letter (A=0, B=1, …), col → 1-based number. + */ export function computeWarnings(plate: PlateTemplate): string[] { + // Build a map from cell position → set of group types that include it. const cellTypes = new Map>(); for (const group of plate.groups) { for (const pos of group.positions) { From a4b284f3b5a96c6243434389a43e9cf905bbc175 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 25 Apr 2026 12:06:16 -0700 Subject: [PATCH 3/9] Test fixes --- .../PlateTemplateDesigner.scss | 11 + .../PlateTemplateDesigner.tsx | 90 +++++-- .../components/GroupTypesPanel.tsx | 236 ++++++++++-------- .../components/TemplateGrid.tsx | 52 ++-- 4 files changed, 234 insertions(+), 155 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 073aa16695b..5de546210cf 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -230,6 +230,17 @@ border-radius: 3px; font-size: 12px; min-width: 0; + + &--error { + border-color: #c00; + } + } + + // Validation error shown below the create row or the rename input + &__name-error { + color: #c00; + font-size: 12px; + margin-top: 2px; } // Rename + delete buttons, visible only for the active group row diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 9840731dbb4..8d6ad0ffe8c 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames'; import { ActionURL, Ajax, Utils } from '@labkey/api'; -import { PlateTemplate, WellGroup, computeWarnings } from './models'; +import { PlateTemplate, Position, WellGroup, computeWarnings } from './models'; import { StatusBar } from './components/StatusBar'; import { GroupTypesPanel } from './components/GroupTypesPanel'; import { ShiftPanel } from './components/ShiftPanel'; @@ -85,6 +85,10 @@ export function PlateTemplateDesigner(): JSX.Element { const plateNameRef = useRef(''); // Mirrors plate.name; used in save-success to update URL without stale closure const statusTimerRef = useRef | null>(null); const nextGroupIdRef = useRef(-1); // Temporary negative IDs for client-created groups (see ID conventions above) + // Always-current ref so callbacks can read the latest activeGroup without stale-closure bugs. + const activeGroupRef = useRef(null); + activeGroupRef.current = activeGroup; + const nextColorIndexRef = useRef(0); // Monotonically increasing; never decrements on delete so colors stay unique useEffect(() => { const templateName = ActionURL.getParameter('templateName'); @@ -113,6 +117,11 @@ export function PlateTemplateDesigner(): JSX.Element { plateNameRef.current = plate.defaultPlateName || plate.name || ''; setPlate({ ...plate, name: plateNameRef.current }); setColorMap(assignColors(plate.groups)); + nextColorIndexRef.current = plate.groups.length; + // Initialize below the minimum server rowId to avoid collisions. + // Server IDs should be positive, but guard against zero or negative values. + const minRowId = plate.groups.reduce((min, g) => Math.min(min, g.rowId), 0); + nextGroupIdRef.current = Math.min(-1, minRowId - 1); setActiveTab(plate.groupTypes[0] ?? ''); if (plate.copyMode) setIsDirty(true); }), @@ -132,48 +141,75 @@ export function PlateTemplateDesigner(): JSX.Element { setActiveGroup(group); }, []); - const handleCellAssign = useCallback((row: number, col: number) => { - if (!activeGroup || !plate) return; + // Called on every mouseenter during a drag with the rectangle defined by the + // mousedown cell and the current cell. preDragPositions is the snapshot of the + // active group's positions taken at mousedown (in TemplateGrid), before any drag + // events can modify state. + // + // Select mode (drag started on an empty cell): adds the rectangle to the group's + // pre-drag positions, so existing wells outside the rectangle are preserved. + // Also evicts rectangle cells from sibling groups of the same type. + // + // Unselect mode (drag started on a cell already in the group): removes all + // rectangle cells from the pre-drag positions without affecting other groups. + const handleDragRect = useCallback((r1: number, c1: number, r2: number, c2: number, isUnselect: boolean, preDragPositions: Position[]) => { + const activeGroup = activeGroupRef.current; + if (!activeGroup) return; + const minRow = Math.min(r1, r2); + const maxRow = Math.max(r1, r2); + const minCol = Math.min(c1, c2); + const maxCol = Math.max(c1, c2); + const rectPositions: Position[] = []; + for (let r = minRow; r <= maxRow; r++) { + for (let c = minCol; c <= maxCol; c++) { + rectPositions.push({ row: r, col: c }); + } + } + const rectKeys = new Set(rectPositions.map(p => `${p.row},${p.col}`)); setPlate(prev => { if (!prev) return null; + // Look up the active group's current type from prev to avoid stale-closure issues. + const currentType = prev.groups.find(g => g.rowId === activeGroup.rowId)?.type; const updatedGroups = prev.groups.map(g => { if (g.rowId === activeGroup.rowId) { - const alreadyHas = g.positions.some(p => p.row === row && p.col === col); - if (alreadyHas) return g; - return { ...g, positions: [...g.positions, { row, col }] }; + if (isUnselect) { + // Remove rect from pre-drag snapshot + return { ...g, positions: preDragPositions.filter(p => !rectKeys.has(`${p.row},${p.col}`)) }; + } + // Add rect to pre-drag snapshot (union, deduped) + const preDragKeys = new Set(preDragPositions.map(p => `${p.row},${p.col}`)); + const added = rectPositions.filter(p => !preDragKeys.has(`${p.row},${p.col}`)); + return { ...g, positions: [...preDragPositions, ...added] }; } - if (g.type === activeGroup.type) { - // Remove from other groups of the same type to avoid conflicts - return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + if (!isUnselect && currentType !== undefined && g.type === currentType) { + // Evict rectangle cells from sibling groups of the same type + return { ...g, positions: g.positions.filter(p => !rectKeys.has(`${p.row},${p.col}`)) }; } return g; }); return { ...prev, groups: updatedGroups }; }); setIsDirty(true); - }, [activeGroup, plate]); + }, []); + // Pure toggle: add the cell if absent, remove it if present const handleCellToggle = useCallback((row: number, col: number) => { - if (!activeGroup || !plate) return; + const activeGroup = activeGroupRef.current; + if (!activeGroup) return; setPlate(prev => { if (!prev) return null; const updatedGroups = prev.groups.map(g => { - if (g.rowId === activeGroup.rowId) { - const hasCell = g.positions.some(p => p.row === row && p.col === col); - if (hasCell) { - return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; - } - return { ...g, positions: [...g.positions, { row, col }] }; - } - if (g.type === activeGroup.type) { + if (g.rowId !== activeGroup.rowId) return g; + const hasCell = g.positions.some(p => p.row === row && p.col === col); + if (hasCell) { return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; } - return g; + return { ...g, positions: [...g.positions, { row, col }] }; }); return { ...prev, groups: updatedGroups }; }); setIsDirty(true); - }, [activeGroup, plate]); + }, []); const handleAddGroup = useCallback((type: string, name: string) => { if (!plate) return; @@ -187,9 +223,10 @@ export function PlateTemplateDesigner(): JSX.Element { allowNewGroups: plate.canCreateGroupsByType?.[type] ?? false, }; setPlate(prev => prev ? { ...prev, groups: [...prev.groups, newGroup] } : null); + const colorIndex = nextColorIndexRef.current++; setColorMap(prev => { const next = new Map(prev); - next.set(rowId, COLORS[prev.size % COLORS.length]); + next.set(rowId, COLORS[colorIndex % COLORS.length]); return next; }); setActiveGroup(newGroup); @@ -352,9 +389,12 @@ export function PlateTemplateDesigner(): JSX.Element { // handleDeleteGroup handles the "group no longer exists" case by explicitly setting // activeGroup to null before this effect can run. useEffect(() => { - if (activeGroup && plate) { + if (!plate) return; + if (activeGroup) { const updated = plate.groups.find(g => g.rowId === activeGroup.rowId); - if (updated) setActiveGroup(updated); + if (updated) { + setActiveGroup(updated); + } } }, [plate]); // eslint-disable-line react-hooks/exhaustive-deps @@ -407,7 +447,7 @@ export function PlateTemplateDesigner(): JSX.Element { activeGroup={activeGroup} activeTab={activeTab} colorMap={colorMap} - onCellAssign={handleCellAssign} + onDragRect={handleDragRect} onCellToggle={handleCellToggle} /> diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 19f05b1ea36..8b3aae7ae00 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -78,6 +78,7 @@ export function GroupTypesPanel({ const [newGroupName, setNewGroupName] = useState(''); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); + const [renameError, setRenameError] = useState(null); const [multiCreateOpen, setMultiCreateOpen] = useState(false); const [multiBaseName, setMultiBaseName] = useState(''); const [multiCount, setMultiCount] = useState('2'); @@ -89,6 +90,9 @@ export function GroupTypesPanel({ const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); const canAdd = plate.canCreateGroupsByType?.[activeTab] ?? false; + // True when the current create-input value is already taken by a group of this type. + const createNameConflicts = newGroupName.trim() !== '' && groupsOfType.some(g => g.name === newGroupName.trim()); + // Predefined slot names not yet occupied by an existing group of this type. // Drives the toggle in the create row. const unusedDefaults = useMemo(() => { @@ -139,8 +143,9 @@ export function GroupTypesPanel({ const handleCreate = () => { const trimmed = newGroupName.trim(); - if (!trimmed) return; + if (!trimmed || createNameConflicts) return; onAddGroup(activeTab, trimmed); + setNewGroupName(''); }; const openMultiCreate = () => { @@ -159,9 +164,14 @@ export function GroupTypesPanel({ } const baseName = multiBaseName.trim(); if (!baseName) return; - for (let i = 1; i <= count; i++) { - onAddGroup(activeTab, `${baseName} ${i}`); + const existingNames = new Set(groupsOfType.map(g => g.name)); + const namesToCreate = Array.from({ length: count }, (_, i) => `${baseName} ${i + 1}`) + .filter(name => !existingNames.has(name)); + if (namesToCreate.length === 0) { + setMultiCountError(`All ${count} generated name${count === 1 ? '' : 's'} already exist in this type.`); + return; } + namesToCreate.forEach(name => onAddGroup(activeTab, name)); setMultiCreateOpen(false); }; @@ -176,12 +186,25 @@ export function GroupTypesPanel({ e.stopPropagation(); setRenamingId(group.rowId); setRenameValue(group.name); + setRenameError(null); }; - const handleRenameCommit = (rowId: number) => { + // revertOnConflict=true: silently discard (used on blur so moving focus away doesn't leave the input frozen). + // revertOnConflict=false: show an inline error and keep the input open (used on Enter so the user sees feedback). + const handleRenameCommit = (rowId: number, revertOnConflict: boolean) => { const trimmed = renameValue.trim(); + if (trimmed && groupsOfType.some(g => g.rowId !== rowId && g.name === trimmed)) { + if (revertOnConflict) { + setRenamingId(null); + setRenameError(null); + } else { + setRenameError(`"${trimmed}" is already used by another group of this type.`); + } + return; + } if (trimmed) onRenameGroup(rowId, trimmed); setRenamingId(null); + setRenameError(null); }; return ( @@ -213,108 +236,125 @@ export function GroupTypesPanel({ const isActive = activeGroup?.rowId === group.rowId; const isRenaming = renamingId === group.rowId; return ( -
{ if (!isRenaming) onGroupSelect(group); }} - onKeyDown={e => { - if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault(); - onGroupSelect(group); - } - }} - > - - {isRenaming ? ( - setRenameValue(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') handleRenameCommit(group.rowId); - if (e.key === 'Escape') setRenamingId(null); - }} - onBlur={() => handleRenameCommit(group.rowId)} - onClick={e => e.stopPropagation()} + +
{ if (!isRenaming) onGroupSelect(group); }} + onKeyDown={e => { + if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onGroupSelect(group); + } + }} + > + - ) : ( - {group.name} - )} - {/* Rename/delete actions appear only on the active group row */} - {isActive && !isRenaming && group.allowNewGroups && ( - - - - + aria-describedby={renameError ? 'rename-error' : undefined} + aria-invalid={!!renameError} + className={classNames('group-types-panel__rename-input', { + 'group-types-panel__rename-input--error': !!renameError, + })} + value={renameValue} + onChange={e => { setRenameValue(e.target.value); setRenameError(null); }} + onKeyDown={e => { + if (e.key === 'Enter') handleRenameCommit(group.rowId, false); + if (e.key === 'Escape') { setRenamingId(null); setRenameError(null); } + }} + onBlur={() => handleRenameCommit(group.rowId, true)} + onClick={e => e.stopPropagation()} + /> + ) : ( + {group.name} + )} + {/* Rename/delete actions appear only on the active group row */} + {isActive && !isRenaming && group.allowNewGroups && ( + + + + + )} +
+ {isRenaming && renameError && ( +
{renameError}
)} -
+ ); })} {canAdd && ( -
- {/* - * Show a once all - * defaults are consumed or if there are none defined for this type. - */} - {unusedDefaults.length > 0 ? ( - while predefined defaults remain (prevents typos and + * ensures canonical names). Switch to a free-text once all + * defaults are consumed or if there are none defined for this type. + */} + {unusedDefaults.length > 0 ? ( + + ) : ( + setNewGroupName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter' && newGroupName.trim() && !createNameConflicts) handleCreate(); }} + /> + )} + + +
+ {createNameConflicts && ( +
+ A group named "{newGroupName.trim()}" already exists in this type. +
)} - - - + )} {children} diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index 49f03c5e388..6056bd1f949 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -6,14 +6,14 @@ import React, { useCallback, useMemo, useRef } from 'react'; import classNames from 'classnames'; -import { PlateTemplate, WellGroup } from '../models'; +import { PlateTemplate, Position, WellGroup } from '../models'; interface Props { plate: PlateTemplate; activeGroup: WellGroup | null; activeTab: string; colorMap: Map; - onCellAssign: (row: number, col: number) => void; + onDragRect: (r1: number, c1: number, r2: number, c2: number, isUnselect: boolean, preDragPositions: Position[]) => void; onCellToggle: (row: number, col: number) => void; } @@ -34,14 +34,14 @@ function getRowLabel(row: number): string { * (no re-renders on drag): * * Phase 1 – mousedown on a cell: - * Enter drag mode. Record the start cell. Do NOT assign it yet — we first need - * to know whether the user is clicking (toggle) or dragging (assign-only). + * Enter drag mode. Record the start cell. Do NOT assign anything yet — we + * first need to know whether the user is clicking (toggle) or dragging (rect). * * Phase 2 – mouseenter a *different* cell while dragging: - * We now know it's a drag. Retroactively assign the original start cell - * (deferred assign), then assign each subsequently entered cell. - * `dragCells` deduplicates entries so fast mouse movement can't assign the - * same cell twice. + * We now know it's a drag. Call onDragRect with the axis-aligned rectangle + * defined by the mousedown cell and the current cell, plus the drag mode + * (select vs unselect) determined at mousedown. The parent replaces or removes + * cells on every call, so the selection dynamically resizes as the mouse moves. * * Phase 3 – mouseup: * If the pointer never left the start cell (hasMoved === false), treat the @@ -50,16 +50,13 @@ function getRowLabel(row: number): string { * * Drag state is also cleaned up on mouseleave of the outer div, preventing stuck * drag state when the pointer exits the grid. - * - * `onCellAssign` is idempotent (ignores duplicates) and also removes the assigned - * cell from any other group of the same type, enforcing the one-group-per-cell-per- - * type constraint. `onCellToggle` does a pure add/remove with no stealing. */ -export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAssign, onCellToggle }: Props): JSX.Element { +export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRect, onCellToggle }: Props): JSX.Element { const isDragging = useRef(false); const hasMoved = useRef(false); const startCell = useRef<{ row: number; col: number } | null>(null); - const dragCells = useRef>(new Set()); + const dragIsUnselect = useRef(false); // true when the drag started on a cell already in the active group + const preDragPositions = useRef([]); // snapshot of activeGroup.positions at mousedown // Pre-compute a "row,col" → {color, groupName} map for the active tab type. // This lets each cell do an O(1) lookup rather than scanning all groups and @@ -81,26 +78,17 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs isDragging.current = true; hasMoved.current = false; startCell.current = { row, col }; - dragCells.current = new Set([`${row},${col}`]); + dragIsUnselect.current = activeGroup?.positions.some(p => p.row === row && p.col === col) ?? false; + // Snapshot the current positions NOW, from the prop, before any drag events can modify state. + preDragPositions.current = activeGroup?.positions ?? []; e.preventDefault(); - }, []); + }, [activeGroup]); const handleMouseEnter = useCallback((row: number, col: number) => { - if (!isDragging.current) return; - if (!hasMoved.current) { - hasMoved.current = true; - // Deferred assign: now that we know this is a drag, assign the cell - // the user originally pressed down on. - if (startCell.current) { - onCellAssign(startCell.current.row, startCell.current.col); - } - } - const key = `${row},${col}`; - if (!dragCells.current.has(key)) { - dragCells.current.add(key); - onCellAssign(row, col); - } - }, [onCellAssign]); + if (!isDragging.current || !startCell.current) return; + hasMoved.current = true; + onDragRect(startCell.current.row, startCell.current.col, row, col, dragIsUnselect.current, preDragPositions.current); + }, [onDragRect]); // Called on mouseup over a specific cell — handles click-toggle const handleCellMouseUp = useCallback((row: number, col: number) => { @@ -114,7 +102,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onCellAs isDragging.current = false; hasMoved.current = false; startCell.current = null; - dragCells.current = new Set(); + dragIsUnselect.current = false; }, []); return ( From 062240583f630f9c20257bd175495b2a32c9a5a7 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 25 Apr 2026 13:14:45 -0700 Subject: [PATCH 4/9] Other cleanup --- .../PlateTemplateDesigner.scss | 1 - .../PlateTemplateDesigner.tsx | 5 +- .../components/GroupTypesPanel.tsx | 5 +- .../components/TemplateGrid.tsx | 61 +++++++++++++++++-- .../client/PlateTemplateDesigner/models.ts | 6 -- 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 5de546210cf..46162a3fde6 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -34,7 +34,6 @@ // Root container .plate-template-designer { padding: 12px 16px; - font-family: Arial, Helvetica, sans-serif; font-size: 13px; &__error { diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 8d6ad0ffe8c..d9459e6a23e 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -463,6 +463,7 @@ export function PlateTemplateDesigner(): JSX.Element {
Base NameBase Name setMultiBaseName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} @@ -242,17 +347,20 @@ export function GroupTypesPanel({
CountCount { setMultiCount(e.target.value); setMultiCountError(''); }} onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} /> - {multiCountError &&
{multiCountError}
} + {multiCountError &&
{multiCountError}
}
{Array.from({ length: plate.cols }, (_, col) => ( - {col + 1}{col + 1}
{getRowLabel(row)}{getRowLabel(row)} handleMouseDown(row, col, e)} onMouseEnter={() => handleMouseEnter(row, col)} diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx index d297bc1b125..ca49a6ed61c 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx @@ -3,7 +3,7 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { PlateTemplate, computeWarnings } from '../models'; @@ -11,8 +11,16 @@ interface Props { plate: PlateTemplate; } +/** + * Displays the list of validation warnings for the current plate layout. + * + * Warnings are recomputed synchronously from the latest plate state on each render. + * The panel is only shown when `plate.showWarningPanel` is true, which is controlled by the + * server-side assay type configuration (not all assay types use the REPLICATE/SPECIMEN/CONTROL + * group semantics that produce warnings). + */ export function WarningPanel({ plate }: Props): JSX.Element { - const warnings = computeWarnings(plate); + const warnings = useMemo(() => computeWarnings(plate), [plate]); return (
@@ -21,8 +29,8 @@ export function WarningPanel({ plate }: Props): JSX.Element {
No warnings.
) : (
    - {warnings.map((w, i) => ( -
  • {w}
  • + {warnings.map((w) => ( +
  • {w}
  • ))}
)} diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx index e31158345b7..67e242a2d68 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -13,6 +13,20 @@ interface Props { onDeleteProperty: (groupRowId: number, key: string) => void; } +/** + * Shows and edits the key/value property bag for the currently selected well group. + * + * Properties are assay-type-specific metadata attached to a group (e.g. concentration, + * dilution factor, sample ID). They are stored as plain strings and round-tripped through + * the server without interpretation by the designer. + * + * Interaction pattern: + * - Existing properties: each row has an inline text input for the value; changes propagate + * immediately to the parent (no separate submit step) via onPropertyChange. + * - Deleting: the trash button removes a property key entirely. + * - Adding: the footer row accepts a new key + value; "Add" (or Enter) commits the pair. + * The new-key input is the gate — the Add button stays disabled until a key is typed. + */ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: Props): JSX.Element { const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); @@ -52,6 +66,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro onPropertyChange(activeGroup.rowId, key, e.target.value)} /> @@ -60,9 +75,10 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro
([]); // snapshot of activeGroup.positions at mousedown + // Roving-tabindex state: tracks which cell holds tabIndex=0. Null means no cell has been + // focused yet, in which case (0,0) is the tab entry point. + const [focusedCell, setFocusedCell] = useState<{ row: number; col: number } | null>(null); + const cellRefs = useRef>(new Map()); + // Pre-compute a "row,col" → {color, groupName} map for the active tab type. // This lets each cell do an O(1) lookup rather than scanning all groups and // positions on every render (which would be O(groups × positions) per cell). @@ -105,9 +109,44 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe dragIsUnselect.current = false; }, []); + const handleCellFocus = useCallback((row: number, col: number) => { + setFocusedCell({ row, col }); + }, []); + + // Keyboard interaction for grid cells: + // Space / Enter → toggle the cell (same as a click with no drag) + // Arrow keys → move focus to the adjacent cell (wraps are intentionally prevented + // at plate edges to avoid confusing wrap-around focus jumps) + const handleCellKeyDown = useCallback((row: number, col: number, e: React.KeyboardEvent) => { + const moveFocus = (r: number, c: number) => { + e.preventDefault(); + setFocusedCell({ row: r, col: c }); + cellRefs.current.get(`${r},${c}`)?.focus(); + }; + switch (e.key) { + case ' ': + case 'Enter': + e.preventDefault(); + onCellToggle(row, col); + break; + case 'ArrowUp': + if (row > 0) moveFocus(row - 1, col); + break; + case 'ArrowDown': + if (row < plate.rows - 1) moveFocus(row + 1, col); + break; + case 'ArrowLeft': + if (col > 0) moveFocus(row, col - 1); + break; + case 'ArrowRight': + if (col < plate.cols - 1) moveFocus(row, col + 1); + break; + } + }, [onCellToggle, plate.rows, plate.cols]); + return (
- +
@@ -125,17 +164,29 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); const location = `${getRowLabel(row)}${col + 1}`; const tooltip = entry ? `${location}: ${entry.groupName}` : location; + const isTabStop = focusedCell + ? focusedCell.row === row && focusedCell.col === col + : row === 0 && col === 0; return ( { + const key = `${row},${col}`; + if (el) cellRefs.current.set(key, el); + else cellRefs.current.delete(key); + }} + tabIndex={isTabStop ? 0 : -1} className={classNames('template-grid__cell', { 'template-grid__cell--active': isActiveGroupCell, })} style={{ backgroundColor: entry?.color ?? '#f5f5f5' }} title={tooltip} + aria-label={tooltip} onMouseDown={e => handleMouseDown(row, col, e)} onMouseEnter={() => handleMouseEnter(row, col)} onMouseUp={() => handleCellMouseUp(row, col)} + onFocus={() => handleCellFocus(row, col)} + onKeyDown={e => handleCellKeyDown(row, col, e)} /> ); })} diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts index 71036b33950..7d8dd6c431f 100644 --- a/assay/src/client/PlateTemplateDesigner/models.ts +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -35,13 +35,7 @@ export interface PlateTemplate { defaultPlateName: string; } -export interface SaveTemplateResponse { - rowId: number; -} - /** - * Replicates the GWT TemplateGridCell.getWarnings() logic exactly. - * * Two conditions produce warnings: * 1. A REPLICATE well that belongs to neither a SPECIMEN nor a CONTROL group is almost certainly * a configuration error — replicates are only meaningful relative to a specimen or control. From 19e61e82ae6f321686338ae22f6872841c9d007d Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sun, 26 Apr 2026 19:08:34 -0700 Subject: [PATCH 5/9] Improve generics for plate code --- .../labkey/api/assay/plate/PlateService.java | 2 +- .../org/labkey/api/assay/plate/PlateSet.java | 2 +- .../src/org/labkey/assay/PlateController.java | 113 ++++++++++-------- .../plate/AssayPlateMetadataServiceImpl.java | 20 ++-- .../org/labkey/assay/plate/PlateCache.java | 24 ++-- .../org/labkey/assay/plate/PlateManager.java | 48 ++++---- .../labkey/assay/plate/PlateManagerTest.java | 60 +++++----- .../org/labkey/assay/plate/PlateSetImpl.java | 2 +- .../assay/plate/layout/LayoutEngine.java | 4 +- .../assay/plate/layout/LayoutOperation.java | 2 +- .../assay/plate/query/PlateSetTable.java | 4 +- 11 files changed, 147 insertions(+), 134 deletions(-) diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateService.java b/assay/api-src/org/labkey/api/assay/plate/PlateService.java index 5021f606c33..b55634aa859 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateService.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateService.java @@ -156,7 +156,7 @@ static PlateService get() */ @Nullable Plate getPlate(ContainerFilter cf, Long plateSetId, Object plateIdentifier); - @NotNull List getPlates(Container container); + @NotNull List getPlates(Container container); /** * Gets the plate set by ID diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateSet.java b/assay/api-src/org/labkey/api/assay/plate/PlateSet.java index 8e06e1c4c77..8dc659afa4d 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateSet.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateSet.java @@ -26,7 +26,7 @@ public interface PlateSet extends Identifiable boolean isTemplate(); - List getPlates(); + List getPlates(); PlateSetType getType(); diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index d35a2655dc3..dffcee52849 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -112,6 +112,45 @@ public class PlateController extends SpringActionController private static final SpringActionController.DefaultActionResolver _actionResolver = new DefaultActionResolver(PlateController.class); private static final Logger LOG = LogHelper.getLogger(PlateController.class, "Controller for plate related actions"); + record SubmittedGroup(int rowId, String type, String name, List positions, Map properties) + { + public static SubmittedGroup from(JSONObject g) + { + int rowId = g.optInt("rowId", -1); + String type = g.getString("type"); + String name = g.getString("name"); + JSONArray posArr = g.optJSONArray("positions"); + List positions = new ArrayList<>(); + if (posArr != null) + { + for (int j = 0; j < posArr.length(); j++) + { + JSONObject p = posArr.getJSONObject(j); + positions.add(PlatePosition.from(p)); + } + } + JSONObject propsObj = g.optJSONObject("properties"); + Map props = new HashMap<>(); + if (propsObj != null) + { + for (String key : propsObj.keySet()) + { + Object val = propsObj.get(key); + props.put(key, val == JSONObject.NULL ? null : val); + } + } + return new SubmittedGroup(rowId, type, name, positions, props); + } + } + + record PlatePosition(int row, int col) + { + public static PlatePosition from(JSONObject p) + { + return new PlatePosition(p.getInt("row"), p.getInt("col")); + } + } + public PlateController() { setActionResolver(_actionResolver); @@ -165,7 +204,7 @@ public static class PlateListAction extends SimpleViewAction public ModelAndView getView(ReturnUrlForm form, BindException errors) { setHelpTopic("editPlateTemplate"); - List plateTemplates = PlateService.get().getPlates(getContainer()) + List plateTemplates = PlateService.get().getPlates(getContainer()) .stream() .filter(p -> !TsvPlateLayoutHandler.TYPE.equalsIgnoreCase(p.getAssayType())) .toList(); @@ -180,6 +219,7 @@ public void addNavTrail(NavTree root) } } + /** Delete soon! */ @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) public static class DesignerServiceAction extends GWTServiceAction { @@ -417,7 +457,7 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except } boolean updateExisting = false; - Plate plate; + PlateImpl plate; if (rowId > 0) { plate = PlateManager.get().getPlate(getContainer(), rowId); @@ -447,42 +487,16 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except plate.setProperties(plateProperties); // Parse groups from JSON - List> submittedGroups = new ArrayList<>(); + List submittedGroups = new ArrayList<>(); Set submittedGroupIds = new HashSet<>(); if (groupsJson != null) { for (int i = 0; i < groupsJson.length(); i++) { - JSONObject g = groupsJson.getJSONObject(i); - Map gm = new HashMap<>(); - gm.put("rowId", g.optInt("rowId", -1)); - gm.put("type", g.getString("type")); - gm.put("name", g.getString("name")); - JSONArray posArr = g.optJSONArray("positions"); - List positions = new ArrayList<>(); - if (posArr != null) - { - for (int j = 0; j < posArr.length(); j++) - { - JSONObject p = posArr.getJSONObject(j); - positions.add(new int[]{p.getInt("row"), p.getInt("col")}); - } - } - gm.put("positions", positions); - JSONObject propsObj = g.optJSONObject("properties"); - Map props = new HashMap<>(); - if (propsObj != null) - { - for (String key : propsObj.keySet()) - { - Object val = propsObj.get(key); - props.put(key, val == JSONObject.NULL ? null : val); - } - } - gm.put("properties", props); - submittedGroups.add(gm); - if ((int) gm.get("rowId") > 0) - submittedGroupIds.add((int) gm.get("rowId")); + SubmittedGroup g = SubmittedGroup.from(groupsJson.getJSONObject(i)); + submittedGroups.add(g); + if (g.rowId > 0) + submittedGroupIds.add(g.rowId); } } @@ -491,14 +505,14 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except for (WellGroup existingGroup : existingWellGroups) { if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) - ((PlateImpl) plate).markWellGroupForDeletion(existingGroup); + plate.markWellGroupForDeletion(existingGroup); } // Update or create well groups - for (Map gm : submittedGroups) + for (SubmittedGroup gm : submittedGroups) { - int gRowId = (int) gm.get("rowId"); - String groupTypeName = (String) gm.get("type"); + int gRowId = gm.rowId(); + String groupTypeName = gm.type(); WellGroup.Type groupType; try { @@ -508,14 +522,12 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except { throw new ApiUsageException("Unknown well group type: '" + groupTypeName + "'"); } - @SuppressWarnings("unchecked") - List posList = (List) gm.get("positions"); + List posList = gm.positions(); List positions = new ArrayList<>(); - for (int[] p : posList) - positions.add(plate.getPosition(p[0], p[1])); + for (PlatePosition p : posList) + positions.add(plate.getPosition(p.row, p.col)); - @SuppressWarnings("unchecked") - Map props = (Map) gm.get("properties"); + Map props = gm.properties(); WellGroupImpl group; if (updateExisting && gRowId > 0) @@ -524,15 +536,15 @@ public Object execute(SaveTemplateForm form, BindException errors) throws Except if (existing == null) throw new Exception("Well group " + gRowId + " was not found."); if (existing.getType() != groupType) - throw new Exception("Well group type cannot be changed: " + gm.get("name")); - existing.setName((String) gm.get("name")); + throw new Exception("Well group type cannot be changed: " + gm.name()); + existing.setName(gm.name); existing.setPositions(positions); - ((PlateImpl) plate).storeWellGroup(existing); + plate.storeWellGroup(existing); group = existing; } else { - group = (WellGroupImpl) plate.addWellGroup((String) gm.get("name"), groupType, positions); + group = plate.addWellGroup(gm.name, groupType, positions); } group.setProperties(props); } @@ -579,6 +591,7 @@ public void addNavTrail(NavTree root) } } + /** Delete soon! */ @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) public class DesignerGwtAction extends SimpleViewAction { @@ -664,7 +677,7 @@ public static class CopyTemplateBean private HtmlString _treeHtml; private Plate _plate; private String _selectedDestination; - private List _destinationTemplates; + private List _destinationTemplates; public CopyTemplateBean(final Container container, final User user, final Integer plateId, final String selectedDestination) { @@ -1083,7 +1096,7 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti PlateImpl newPlate = new PlateImpl(getContainer(), form.getName(), form.getBarcode(), form.getAssayType(), _plateType); if (form.getData() == null && form.getTemplateId() != null && TsvPlateLayoutHandler.TYPE.equalsIgnoreCase(newPlate.getAssayType())) { - newPlate = (PlateImpl) PlateManager.get().copyPlate( + newPlate = PlateManager.get().copyPlate( getContainer(), getUser(), form.getTemplateId(), @@ -1104,7 +1117,7 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti if (form.isTemplate() && data == null) data = PlateManager.get().prepareEmptyPlateTemplateData(getContainer(), _plateType); - newPlate = (PlateImpl) PlateManager.get().createAndSavePlate(getContainer(), getUser(), newPlate, form.getPlateSetId(), data); + newPlate = PlateManager.get().createAndSavePlate(getContainer(), getUser(), newPlate, form.getPlateSetId(), data); } return success(newPlate); diff --git a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java index b4d7635f972..1dca54939e2 100644 --- a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java +++ b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java @@ -238,7 +238,7 @@ public Map apply(Map row) }); } - private List getPlatesForPlateSet( + private List getPlatesForPlateSet( Container container, User user, Long plateSetId, @@ -270,7 +270,7 @@ public DataIteratorBuilder parsePlateData( ) throws ExperimentException { // get the ordered list of plates for the plate set - List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); + List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); if (plates.isEmpty()) throw new ExperimentException("No plates were found for the plate set (" + plateSetId + ")."); PlateSet plateSet = plates.get(0).getPlateSet(); @@ -297,7 +297,7 @@ private List> _parsePlateData( AssayProvider provider, ExpProtocol protocol, PlateSet plateSet, - List plates, + List plates, FileLike dataFile, DataLoaderSettings settings ) throws ExperimentException @@ -356,7 +356,7 @@ public DataIteratorBuilder mergeReRunData( ) throws ExperimentException { Long plateSetId = getPlateSetId(context, provider, protocol); - List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); + List plates = getPlatesForPlateSet(container, user, plateSetId, protocol); if (plates.isEmpty()) throw new ExperimentException("No plates were found for the plate set (" + plateSetId + ")."); @@ -540,7 +540,7 @@ private boolean isGridFormat(List> data) private List> parsePlateRows( AssayProvider provider, ExpProtocol protocol, - List plates, + List plates, List> data ) throws ExperimentException { @@ -604,7 +604,7 @@ private List> parsePlateRows( } // Resolves a pre-calculated "plateIdField" to a plate rowId and furnishes new "data" rows with the plate rowId. - private List> resolvePlateIdentifier(List plates, List> data, String plateIdField) + private List> resolvePlateIdentifier(List plates, List> data, String plateIdField) { var newData = new ArrayList>(); var plateIdentifiers = new HashMap(); @@ -664,7 +664,7 @@ public PlateGridInfo(PlateUtils.GridInfo info, PlateSet plateSet, Set me // locate the plate in the plate set this grid is associated with plus an optional // measure name - List plates = PlateManager.get().getPlatesForPlateSet(plateSet); + List plates = PlateManager.get().getPlatesForPlateSet(plateSet); List annotations = getAnnotations(); // if the plate set only has one plate, then treat a single annotation as the measure @@ -694,7 +694,7 @@ public PlateGridInfo(PlateUtils.GridInfo info, PlateSet plateSet, Set me } } - private @NotNull Plate getPlateForId(String annotation, List platesetPlates) throws ExperimentException + private @NotNull Plate getPlateForId(String annotation, List platesetPlates) throws ExperimentException { Plate plate = platesetPlates.stream().filter(p -> p.isIdentifierMatch(annotation)).findFirst().orElse(null); if (plate == null) @@ -734,7 +734,7 @@ private List> parsePlateGrids( AssayProvider provider, ExpProtocol protocol, PlateSet plateSet, - List plates, + List plates, FileLike dataFile ) throws ExperimentException { @@ -1754,7 +1754,7 @@ public void testGridAnnotations() throws Exception ); PlateSet plateSet = PlateManager.get().createPlateSet(container, user, new PlateSetImpl(), plates, null, null); - List plateSetPlates = PlateManager.get().getPlatesForPlateSet(plateSet); + List plateSetPlates = PlateManager.get().getPlatesForPlateSet(plateSet); assertEquals("Expected two plates to be created.", 2, plateSetPlates.size()); Plate plate = plateSetPlates.get(0); diff --git a/assay/src/org/labkey/assay/plate/PlateCache.java b/assay/src/org/labkey/assay/plate/PlateCache.java index 9827c7cfb5c..828a9674ccf 100644 --- a/assay/src/org/labkey/assay/plate/PlateCache.java +++ b/assay/src/org/labkey/assay/plate/PlateCache.java @@ -32,15 +32,15 @@ public class PlateCache { private static final PlateLoader _loader = new PlateLoader(); - private static final Cache PLATE_CACHE = CacheManager.getBlockingStringKeyCache(CacheManager.UNLIMITED, CacheManager.DAY, "Plate Cache", _loader); + private static final Cache PLATE_CACHE = CacheManager.getBlockingStringKeyCache(CacheManager.UNLIMITED, CacheManager.DAY, "Plate Cache", _loader); private static final Logger LOG = LogManager.getLogger(PlateCache.class); - private static class PlateLoader implements CacheLoader + private static class PlateLoader implements CacheLoader { private final Map> _containerPlateMap = new HashMap<>(); // internal collection to help un-cache all plates for a container @Override - public Plate load(@NotNull String key, @Nullable Object argument) + public PlateImpl load(@NotNull String key, @Nullable Object argument) { // parse the cache key PlateCacheKey cacheKey = new PlateCacheKey(key); @@ -55,7 +55,7 @@ public Plate load(@NotNull String key, @Nullable Object argument) { PlateBean bean = plates.get(0); - Plate plate = PlateManager.get().populatePlate(bean); + PlateImpl plate = PlateManager.get().populatePlate(bean); LOG.debug(String.format("Caching plate \"%s\" for folder %s", plate.getName(), cacheKey._container.getPath())); // add all cache keys for this plate @@ -65,7 +65,7 @@ public Plate load(@NotNull String key, @Nullable Object argument) return null; } - private void addCacheKeys(PlateCacheKey cacheKey, Plate plate) + private void addCacheKeys(PlateCacheKey cacheKey, PlateImpl plate) { if (plate != null) { @@ -84,14 +84,14 @@ private void addCacheKeys(PlateCacheKey cacheKey, Plate plate) if (cacheKey._type != PlateCacheKey.Type.plateId) PLATE_CACHE.put(PlateCacheKey.getCacheKey(plate.getContainer(), plate.getPlateId()), plate); - _containerPlateMap.computeIfAbsent(cacheKey._container, k -> new HashSet<>()).add(plate.getRowId()); + _containerPlateMap.computeIfAbsent(cacheKey._container, _ -> new HashSet<>()).add(plate.getRowId()); } } } - public static @Nullable Plate getPlate(Container c, long rowId) + public static @Nullable PlateImpl getPlate(Container c, long rowId) { - Plate plate = PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, rowId)); + PlateImpl plate = PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, rowId)); // We allow plates to be mutated, return a copy of the cached object which still references the // original wells and well groups return plate != null ? plate.copy() : null; @@ -150,23 +150,23 @@ private void addCacheKeys(PlateCacheKey cacheKey, Plate plate) ).getArrayList(Long.class); } - private static @NotNull List getPlates(Container c, @Nullable SimpleFilter filter) + private static @NotNull List getPlates(Container c, @Nullable SimpleFilter filter) { List ids = getPlateIDs(c, filter); return ids.stream().map(id -> PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, id))).toList(); } - public static @NotNull List getPlates(Container c) + public static @NotNull List getPlates(Container c) { return getPlates(c, null); } - public static @NotNull List getPlatesForPlateSet(Container c, Long plateSetRowId) + public static @NotNull List getPlatesForPlateSet(Container c, Long plateSetRowId) { return getPlates(c, new SimpleFilter(FieldKey.fromParts(PlateTable.Column.PlateSet.name()), plateSetRowId)); } - public static @NotNull List getPlateTemplates(Container c) + public static @NotNull List getPlateTemplates(Container c) { return getPlates(c, new SimpleFilter(FieldKey.fromParts(PlateTable.Column.Template.name()), true)); } diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 2512cdb4ea9..c12203b1708 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -300,15 +300,15 @@ public List getWellGroupTypes() } @Override - public @NotNull Plate createPlate(Container container, String assayType, @NotNull PlateType plateType) + public @NotNull PlateImpl createPlate(Container container, String assayType, @NotNull PlateType plateType) { return new PlateImpl(container, null, null, assayType, plateType); } - public @NotNull Plate createAndSavePlate( + public @NotNull PlateImpl createAndSavePlate( @NotNull Container container, @NotNull User user, - @NotNull Plate plate, + @NotNull PlateImpl plate, @Nullable Long plateSetId, @Nullable List> data ) throws Exception @@ -316,10 +316,10 @@ public List getWellGroupTypes() return createAndSavePlate(container, user, plate, plateSetId, data, false); } - private @NotNull Plate createAndSavePlate( + private @NotNull PlateImpl createAndSavePlate( @NotNull Container container, @NotNull User user, - @NotNull Plate plate, + @NotNull PlateImpl plate, @Nullable Long plateSetId, @Nullable List> data, boolean skipAudit @@ -346,7 +346,7 @@ public List getWellGroupTypes() throw new ValidationException(String.format("Failed to create plate. Plate set \"%s\" is not a template plate set.", plateSet.getName())); if (!plate.isTemplate() && plateSet.isTemplate()) throw new ValidationException(String.format("Failed to create plate. Plate set \"%s\" is a template plate set.", plateSet.getName())); - ((PlateImpl) plate).setPlateSet(plateSet); + plate.setPlateSet(plateSet); } // Intentionally passing skipAudit=true, and not the passed in value for skipAudit, @@ -479,7 +479,7 @@ public Position createPosition(Container container, int row, int column) List plates = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), filter, null).getArrayList(PlateBean.class); // this should be 1 or 0, but don't blow up if there are more than one if (!plates.isEmpty()) - return populatePlate(plates.get(0)); + return populatePlate(plates.getFirst()); return null; } @@ -512,7 +512,7 @@ public List getMetadataColumns(@NotNull PlateSet plateSet, Container c } @NotNull - public List getPlateTemplates(Container container) + public List getPlateTemplates(Container container) { return PlateCache.getPlateTemplates(container); } @@ -557,7 +557,7 @@ public int getRunCountUsingPlate(@NotNull Container c, @NotNull User user, @NotN * @return A map of plate rowId to total number of runs across all plate-based assay runs in the * container/user scope for the specified plates. */ - public Map getPlateRunCounts(@NotNull Container c, @NotNull User user, @NotNull Collection plates) + public Map getPlateRunCounts(@NotNull Container c, @NotNull User user, @NotNull Collection plates) { if (plates.isEmpty()) return emptyMap(); @@ -742,7 +742,7 @@ private int getRunCountUsingPlateInResults(@NotNull Container c, @NotNull User u } @Override - public @Nullable Plate getPlate(Container container, long rowId) + public @Nullable PlateImpl getPlate(Container container, long rowId) { return PlateCache.getPlate(container, rowId); } @@ -786,10 +786,10 @@ private int getRunCountUsingPlateInResults(@NotNull Container c, @NotNull User u Plate plate = null; if (plateIdentifier != null) { - List plates = getPlatesForPlateSet(plateSet); - List matchingPlates = plates.stream().filter(p -> p.isIdentifierMatch(plateIdentifier.toString())).toList(); + List plates = getPlatesForPlateSet(plateSet); + List matchingPlates = plates.stream().filter(p -> p.isIdentifierMatch(plateIdentifier.toString())).toList(); if (matchingPlates.size() == 1) - plate = matchingPlates.get(0); + plate = matchingPlates.getFirst(); else if (matchingPlates.isEmpty()) throw new IllegalArgumentException("The plate identifier \"" + plateIdentifier + "\" does not match any plate in the plate set \"" + plateSet.getName() + "\"."); else @@ -820,7 +820,7 @@ else if (matchingPlates.isEmpty()) throw new IllegalStateException("More than one " + tableInfo.getName() + " found that matches the filter."); if (containers.size() == 1) - return ContainerManager.getForId(containers.get(0)); + return ContainerManager.getForId(containers.getFirst()); return null; } @@ -952,7 +952,7 @@ public boolean isDuplicatePlateTemplateName(Container container, String name) } @Override - public @NotNull List getPlates(Container c) + public @NotNull List getPlates(Container c) { return PlateCache.getPlates(c); } @@ -962,7 +962,7 @@ public boolean isDuplicatePlateTemplateName(Container container, String name) return PlateSetCache.getPlateSets(c); } - public List getPlatesForPlateSet(PlateSet plateSet) + public List getPlatesForPlateSet(PlateSet plateSet) { return PlateCache.getPlatesForPlateSet(plateSet.getContainer(), plateSet.getRowId()); } @@ -1028,7 +1028,7 @@ private long save(Container container, User user, Plate plate, @Nullable List wellGroupIds = wellToWellGroups.computeIfAbsent(wellId, k -> new HashSet<>()); + Set wellGroupIds = wellToWellGroups.computeIfAbsent(wellId, _ -> new HashSet<>()); wellGroupIds.add(wellGroupId); } @@ -1070,7 +1070,7 @@ protected Plate populatePlate(PlateBean bean) { for (Integer wellGroupId : wellGroupIds) { - List groupPositions = groupIdToPositions.computeIfAbsent(wellGroupId, k -> new ArrayList<>()); + List groupPositions = groupIdToPositions.computeIfAbsent(wellGroupId, _ -> new ArrayList<>()); groupPositions.add(well); } } @@ -1274,7 +1274,7 @@ private long savePlateImpl( if (wellDataMap.containsKey(position.getDescription())) { wellDataMap.get(position.getDescription()).forEach( - (key, value) -> wellRow.merge(key, value, (v1, v2) -> v1) + (key, value) -> wellRow.merge(key, value, (v1, _) -> v1) ); } @@ -1956,7 +1956,7 @@ private void copyWellGroups(@NotNull Plate source, @NotNull Plate copy) } } - public Plate copyPlate( + public PlateImpl copyPlate( Container container, User user, Long sourcePlateRowId, @@ -4541,7 +4541,7 @@ public record ReformatResult( Long plateSetRowId; String plateSetName; - List newPlates; + List newPlates; if (targetPlateSet.isNew()) { @@ -4846,7 +4846,7 @@ else if (!Objects.equals(sourcePlateSet.getRowId(), plateSet.getRowId())) return Pair.of(sourcePlateSet, sourcePlates); } - private @NotNull List getReformatTargetPlates(@NotNull PlateSetImpl targetPlateSet) + private @NotNull List getReformatTargetPlates(@NotNull PlateSetImpl targetPlateSet) { if (targetPlateSet.isNew()) return emptyList(); @@ -5079,7 +5079,7 @@ private record HydratedResult(List plateData, @Nullable Integer plate List sourcedWells = Arrays.stream(wellLayout.getWells()).filter(well -> well != null && well.sourcePlateId() > 0).toList(); if (!sourcedWells.isEmpty()) { - Long sourcePlateId = sourcedWells.get(0).sourcePlateId(); + Long sourcePlateId = sourcedWells.getFirst().sourcePlateId(); if (sourcedWells.stream().allMatch(w -> sourcePlateId.equals(w.sourcePlateId()))) templateId = sourcePlateId; } diff --git a/assay/src/org/labkey/assay/plate/PlateManagerTest.java b/assay/src/org/labkey/assay/plate/PlateManagerTest.java index b0df652dbb9..a60e5aacb43 100644 --- a/assay/src/org/labkey/assay/plate/PlateManagerTest.java +++ b/assay/src/org/labkey/assay/plate/PlateManagerTest.java @@ -239,7 +239,7 @@ public void testCreatePlateTemplate() throws Exception List sampleWellGroups = savedTemplate.getWellGroups(WellGroup.Type.SAMPLE); assertEquals(1, sampleWellGroups.size()); - WellGroup savedWg1 = sampleWellGroups.get(0); + WellGroup savedWg1 = sampleWellGroups.getFirst(); assertEquals("wg1", savedWg1.getName()); assertEquals("100", savedWg1.getProperty("score")); @@ -296,7 +296,7 @@ public void testCreatePlateTemplate() throws Exception assertEquals(1, updatedControlWellGroups.size()); // verify added positions - assertEquals(2, updatedControlWellGroups.get(0).getPositions().size()); + assertEquals(2, updatedControlWellGroups.getFirst().getPositions().size()); // verify plate type information assertEquals(plateType.getRows().intValue(), updatedTemplate.getRows()); @@ -353,7 +353,7 @@ public void testAccessPlateByIdentifiers() throws Exception // Assert assertTrue("Expected plateSet to have been persisted and provided with a rowId", plateSet.getRowId() > 0); - List plates = plateSet.getPlates(); + List plates = plateSet.getPlates(); assertEquals("Expected plateSet to have 3 plates", 3, plates.size()); // verify access via plate rowId @@ -394,7 +394,7 @@ public void testCreatePlateTemplates() throws Exception createPlate(PLATE_TYPE_96_WELLS); // Verify only plate templates are returned - List templates = PlateManager.get().getPlateTemplates(container); + List templates = PlateManager.get().getPlateTemplates(container); assertFalse("Expected there to be a plate template", templates.isEmpty()); for (Plate t : templates) assertTrue("Expected saved plate to have the template field set to true", t.isTemplate()); @@ -704,7 +704,7 @@ public void testGetWorklistSingleSampleManyToMany() throws Exception { // Arrange ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(container, user); - ExpMaterial sample = createSamples(1).get(0); + ExpMaterial sample = createSamples(1).getFirst(); List> rows1 = List.of( wellWithMetdata(createWellRow("A1", "SAMPLE", sample.getRowId()), 2.25, "B1234"), @@ -732,7 +732,7 @@ public void testGetWorklistSingleSampleOneToOne() throws Exception { // Arrange ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(container, user); - ExpMaterial sample = createSamples(3).get(0); + ExpMaterial sample = createSamples(3).getFirst(); List> rows1 = List.of( wellWithMetdata(createWellRow("A1", "SAMPLE", sample.getRowId()), 2.25, "B1234"), @@ -769,7 +769,7 @@ public void testGetWorklistSingleSampleOneToMany() throws Exception { // Arrange ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(container, user); - ExpMaterial sample = createSamples(3).get(0); + ExpMaterial sample = createSamples(3).getFirst(); List> rows1 = List.of( wellWithMetdata(createWellRow("A1", "SAMPLE", sample.getRowId()), 2.25, "B1234") @@ -927,7 +927,7 @@ public void testReformatQuadrant() throws Exception assertNotNull(result.previewData()); assertEquals("Expected quadrant operation on 3 plates to generate 1 plate.", 1, result.previewData().size()); - var previewPlate = result.previewData().get(0); + var previewPlate = result.previewData().getFirst(); var wellData = previewPlate.data(); assertEquals("Expected 12 wells to have data", 12, wellData.size()); @@ -961,7 +961,7 @@ public void testReformatQuadrant() throws Exception assertTrue("Expected a new plate set to be created", result.plateSetRowId() > 0); assertEquals(1, result.plateRowIds().size()); - var newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + var newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_384_WELLS, newPlate.getPlateType()); @@ -1087,7 +1087,7 @@ public void testReformatCompressByColumn() throws Exception assertNotNull(result.previewData()); assertEquals("Expected column compress operation on a 384-well plate to generate 1 12-well plates.", 1, result.previewData().size()); - List> plateData = result.previewData().get(0).data(); + List> plateData = result.previewData().getFirst().data(); assertEquals("Expected well P12 to be dropped as it does not include a sample.", sourcePlateData.size() - 1, plateData.size()); assertEquals(sampleRowIds.get(0), plateData.get(0).get("sampleId")); @@ -1111,7 +1111,7 @@ public void testReformatCompressByColumn() throws Exception assertEquals("Expected target plate set to be used", targetPlateSetId, result.plateSetRowId()); assertEquals(1, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); @@ -1168,7 +1168,7 @@ public void testReformatCompressByRow() throws Exception assertNotNull(result.previewData()); assertEquals("Expected row compress operation on a 384-well plate to generate 1 12-well plates.", 1, result.previewData().size()); - List> plateData = result.previewData().get(0).data(); + List> plateData = result.previewData().getFirst().data(); assertEquals("Expected well P12 to be dropped as it does not include a sample.", sourcePlateData.size() - 1, plateData.size()); assertEquals(sampleRowIds.get(0), plateData.get(0).get("sampleId")); @@ -1192,7 +1192,7 @@ public void testReformatCompressByRow() throws Exception assertEquals("Expected target plate set to be used", targetPlateSetId, result.plateSetRowId()); assertEquals(1, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); @@ -1302,7 +1302,7 @@ public void testReformatArrayByColumn() throws Exception assertEquals("Expected target plate set to be used", context.targetPlateSetId, result.plateSetRowId()); assertEquals(2, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); List sampleRowIds = context.sampleRowIds; @@ -1368,7 +1368,7 @@ public void testReformatArrayByRow() throws Exception assertEquals("Expected target plate set to be used", context.targetPlateSetId, result.plateSetRowId()); assertEquals(2, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); List sampleRowIds = context.sampleRowIds; @@ -1454,7 +1454,7 @@ public void testReformatArrayFromTemplate() throws Exception assertEquals("Expected target plate set to be used", context.targetPlateSetId, result.plateSetRowId()); assertEquals(3, result.plateRowIds().size()); - Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().get(0)); + Plate newPlate = PlateManager.get().getPlate(container, result.plateRowIds().getFirst()); assertNotNull(newPlate); assertEquals(PLATE_TYPE_12_WELLS, newPlate.getPlateType()); List sampleRowIds = context.sampleRowIds; @@ -1505,7 +1505,7 @@ public void testReformatArrayFromTemplate() throws Exception switch (wellPosition) { - case "A1" -> assertEquals(sampleRowIds.get(0).intValue(), sampleId); // Group "S1" + case "A1" -> assertEquals(sampleRowIds.getFirst().intValue(), sampleId); // Group "S1" case "A2" -> assertEquals(sampleRowIds.get(11).intValue(), sampleId); case "A3" -> assertEquals(sampleRowIds.get(12).intValue(), sampleId); case "A4" -> assertEquals(0, sampleId); @@ -1516,7 +1516,7 @@ public void testReformatArrayFromTemplate() throws Exception case "C1" -> assertEquals(0, sampleId); // Control case "C2" -> assertEquals(0, sampleId); case "C3" -> assertEquals(0, sampleId); // Control - case "C4" -> assertEquals(sampleRowIds.get(0).intValue(), sampleId); // Group "S1" + case "C4" -> assertEquals(sampleRowIds.getFirst().intValue(), sampleId); // Group "S1" } var barcode = r.getString(FieldKey.fromParts(PlateMetadataFields.barcode.name())); @@ -1612,7 +1612,7 @@ public void testReplicateWellValidation() throws Exception assertCreatePlateThrows(expectedMessage, PLATE_TYPE_96_WELLS, plateName, null, sourcePlateData); // Fixup rows by making all rows the same and resubmit - sourcePlateData.forEach(row -> row.put("sampleId", sampleRowIds.get(0))); + sourcePlateData.forEach(row -> row.put("sampleId", sampleRowIds.getFirst())); // Act var newPlate = createPlate(PLATE_TYPE_96_WELLS, plateName, null, sourcePlateData); @@ -1668,8 +1668,8 @@ public void testReplicateCrossPlateValidation() throws Exception List sampleRowIds = createSamples(2).stream().map(ExpObject::getRowId).sorted().toList(); List> plate1Data = new ArrayList<>(); - plate1Data.add(createWellRow("A1", "SAMPLE", sampleRowIds.get(0), null, "R1")); - plate1Data.add(createWellRow("A2", "SAMPLE", sampleRowIds.get(0), null, "R1")); + plate1Data.add(createWellRow("A1", "SAMPLE", sampleRowIds.getFirst(), null, "R1")); + plate1Data.add(createWellRow("A2", "SAMPLE", sampleRowIds.getFirst(), null, "R1")); plate1Data.add(createWellRow("A3", "SAMPLE", sampleRowIds.get(0), null, "R1")); List> plate2Data = new ArrayList<>(); @@ -1679,8 +1679,8 @@ public void testReplicateCrossPlateValidation() throws Exception List> plate3Data = new ArrayList<>(); plate2Data.add(createWellRow("C1", "SAMPLE", sampleRowIds.get(0), null, "R2")); - plate2Data.add(createWellRow("C2", "SAMPLE", sampleRowIds.get(0), null, "R2")); - plate2Data.add(createWellRow("C3", "SAMPLE", sampleRowIds.get(0), null, "R2")); + plate2Data.add(createWellRow("C2", "SAMPLE", sampleRowIds.getFirst(), null, "R2")); + plate2Data.add(createWellRow("C3", "SAMPLE", sampleRowIds.getFirst(), null, "R2")); var plateData = List.of( new PlateManager.PlateData(null, plateType.getRowId(), null, null, plate1Data), @@ -1694,7 +1694,7 @@ public void testReplicateCrossPlateValidation() throws Exception assertCreatePlateSetThrows(expectedMessage, plateSetImpl, plateData, null); // Fixup rows by making all rows the same and resubmit - plate2Data.forEach(row -> row.put("sampleId", sampleRowIds.get(0))); + plate2Data.forEach(row -> row.put("sampleId", sampleRowIds.getFirst())); // Assert (expect no errors) createPlateSet(plateSetImpl, plateData, null); @@ -1721,12 +1721,12 @@ public void testControlValidation() throws Exception var plateData1 = List.of(new PlateManager.PlateData("PS1", plateType.getRowId(), null, null, PS1Data)); PlateSet plateSet1 = createPlateSet(plateSetImpl, plateData1, null); - List> dataPS2 = Arrays.asList(createWellRow("A1", "POSITIVE_CONTROL", sampleRowIds.get(0))); + List> dataPS2 = Arrays.asList(createWellRow("A1", "POSITIVE_CONTROL", sampleRowIds.getFirst())); var plateData2 = List.of(new PlateManager.PlateData("PS2", plateType.getRowId(), null, null, dataPS2)); // Act / Assert // Since the sample of index 0 is on PS1's plate, it is not a valid control for PS2's plate - String errorMsg = String.format("The sample \"%s\" is not a valid control.", sampleNames.get(0)); + String errorMsg = String.format("The sample \"%s\" is not a valid control.", sampleNames.getFirst()); assertCreatePlateSetThrows(errorMsg, plateSetImpl, plateData2, plateSet1.getRowId()); // Assert (expect no errors) @@ -1758,7 +1758,7 @@ public void testBuiltInColumns() throws Exception // Assert assertEquals(1, PPSPlateFields.size()); - assertEquals("SampleID", PPSPlateFields.get(0).getName()); + assertEquals("SampleID", PPSPlateFields.getFirst().getName()); assertEquals(4, APSPlateFields.size()); assertEquals("Type", APSPlateFields.get(0).getName()); @@ -1808,7 +1808,7 @@ public void testEnsureSampleWellTypeTriggerRespectsType() throws Exception List sampleRowIds = samples.stream().map(ExpObject::getRowId).sorted().toList(); List> data = List.of( - createWellRow("A1", "CONTROL", sampleRowIds.get(0)) + createWellRow("A1", "CONTROL", sampleRowIds.getFirst()) ); // Act @@ -1919,12 +1919,12 @@ public void testDeleteSampleWellReferencesUponSampleDelete() throws Exception var plateData = List.of(new PlateManager.PlateData(null, PLATE_TYPE_12_WELLS.getRowId(), null, null, wellData)); var PPS = createPlateSet(pps, plateData, null); - var ppsPlateRowId = PPS.getPlates().get(0).getRowId(); + var ppsPlateRowId = PPS.getPlates().getFirst().getRowId(); var aps = new PlateSetImpl(); aps.setType(PlateSetType.assay); var APS = createPlateSet(aps, plateData, PPS.getRowId()); - var apsPlateRowId = APS.getPlates().get(0).getRowId(); + var apsPlateRowId = APS.getPlates().getFirst().getRowId(); // Act // Formerly, this would result in a foreign key violation on the assay.well table diff --git a/assay/src/org/labkey/assay/plate/PlateSetImpl.java b/assay/src/org/labkey/assay/plate/PlateSetImpl.java index e17b57a09a7..7fe5db23cea 100644 --- a/assay/src/org/labkey/assay/plate/PlateSetImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateSetImpl.java @@ -147,7 +147,7 @@ public boolean isStandalone() } @Override - public List getPlates() + public List getPlates() { if (isNew()) return Collections.emptyList(); diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java index 75bced3f604..96bca37324f 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java @@ -19,7 +19,7 @@ public class LayoutEngine private final ReformatOptions _options; private Collection _sampleIds; private List _sourcePlates; - private List _targetPlates; + private List _targetPlates; private List _targetPlateData; private PlateType _targetPlateType; private Plate _targetTemplate; @@ -93,7 +93,7 @@ public void setSourcePlates(List sourcePlates) _sourcePlates = sourcePlates; } - public void setTargetPlates(List targetPlates) + public void setTargetPlates(List targetPlates) { _targetPlates = targetPlates; } diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java index 2809df13514..cef436ec356 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java @@ -59,7 +59,7 @@ record ExecutionContext( PlateType targetPlateType, List sourcePlates, Plate targetTemplate, - List targetPlates, + List targetPlates, List targetPlateData, Collection sampleIds, WellData.Cache wellDataCache diff --git a/assay/src/org/labkey/assay/plate/query/PlateSetTable.java b/assay/src/org/labkey/assay/plate/query/PlateSetTable.java index dc5cd4927d7..42499ff0ab4 100644 --- a/assay/src/org/labkey/assay/plate/query/PlateSetTable.java +++ b/assay/src/org/labkey/assay/plate/query/PlateSetTable.java @@ -194,7 +194,7 @@ public DataIteratorBuilder createImportDIB(User user, Container container, DataI // generate a value for the lsid final TableInfo plateSetTable = getQueryTable(); lsidGenerator.addColumn(plateSetTable.getColumn(PlateTable.Column.Lsid.name()), - (Supplier) () -> PlateManager.get().getLsid(PlateSet.class, container)); + (Supplier) () -> PlateManager.get().getLsid(PlateSet.class, container)); SimpleTranslator nameExpressionTranslator = new SimpleTranslator(lsidGenerator, context); nameExpressionTranslator.setDebugName("nameExpressionTranslator"); @@ -252,7 +252,7 @@ protected Map deleteRow( if (plateSet == null) throw new QueryUpdateServiceException(String.format("Plate set could not be found for ID : %d", rowId)); - List plates = plateSet.getPlates(); + List plates = plateSet.getPlates(); if (!plates.isEmpty()) throw new QueryUpdateServiceException(String.format("Plate set has %d plates associated with it and cannot be deleted.", plates.size())); From a912417528fdd4550aed113b96e5d0431da3f3e7 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Mon, 27 Apr 2026 19:09:35 -0700 Subject: [PATCH 6/9] Unit tests and other improvements --- assay/package-lock.json | 10629 +++++++++++----- assay/package.json | 44 +- .../PlateTemplateDesigner.scss | 139 +- .../PlateTemplateDesigner.tsx | 177 +- .../PlateTemplateDesigner.utils.test.ts | 152 + .../components/GroupTypesPanel.test.tsx | 338 + .../components/GroupTypesPanel.tsx | 406 +- .../components/MultiCreateDialog.test.tsx | 219 + .../components/MultiCreateDialog.tsx | 119 + .../components/RightPanel.test.tsx | 116 + .../components/RightPanel.tsx | 82 + .../components/ShiftPanel.test.tsx | 55 + .../components/ShiftPanel.tsx | 4 +- .../components/StatusBar.test.tsx | 133 + .../components/StatusBar.tsx | 32 +- .../components/TemplateGrid.test.tsx | 286 + .../components/TemplateGrid.tsx | 21 +- .../components/WarningPanel.test.tsx | 41 + .../components/WarningPanel.tsx | 16 +- .../components/WellGroupProperties.test.tsx | 168 + .../components/WellGroupProperties.tsx | 13 +- .../src/client/PlateTemplateDesigner/dev.tsx | 14 +- .../PlateTemplateDesigner/models.test.ts | 153 + .../client/PlateTemplateDesigner/models.ts | 10 +- assay/test/js/fileMock.js | 1 + assay/test/js/setup.ts | 1 + 26 files changed, 9585 insertions(+), 3784 deletions(-) create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts create mode 100644 assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/models.test.ts create mode 100644 assay/test/js/fileMock.js create mode 100644 assay/test/js/setup.ts diff --git a/assay/package-lock.json b/assay/package-lock.json index 3833d904a1c..f501dd2d16f 100644 --- a/assay/package-lock.json +++ b/assay/package-lock.json @@ -14,7 +14,12 @@ "@labkey/build": "9.1.1", "@types/jest": "30.0.0", "@types/react": "18.3.27", - "@types/react-dom": "18.3.7" + "@types/react-dom": "18.3.7", + "jest": "30.3.0", + "jest-cli": "30.3.0", + "jest-environment-jsdom": "30.3.0", + "jest-teamcity-reporter": "0.9.0", + "ts-jest": "29.4.6" } }, "node_modules/@adobe/css-tools": { @@ -23,6 +28,27 @@ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "license": "MIT" }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "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", @@ -477,6 +503,61 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", @@ -509,6 +590,32 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "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", @@ -525,6 +632,116 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "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", @@ -1698,6 +1915,128 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -1708,6 +2047,40 @@ "node": ">=14.17.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2155,485 +2528,797 @@ "react": "*" } }, - "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==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" } }, - "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==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "node_modules/@isaacs/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": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "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==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "sprintf-js": "~1.0.2" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=10.0" + "node": ">=6" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" }, - "peerDependencies": { - "tslib": "2" + "engines": { + "node": ">=8" } }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", - "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=8" } }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=8" } }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", - "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", - "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" + "@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", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "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", + "slash": "^3.0.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "tslib": "2" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", - "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/fs-print": "4.57.2", - "@jsonjoy.com/fs-snapshot": "4.57.2", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", - "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", + "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==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, - "peerDependencies": { - "tslib": "2" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", - "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", + "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": "Apache-2.0", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2" + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", - "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "tslib": "2" + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", - "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.2", - "tree-dump": "^1.1.0" + "expect": "30.3.0", + "jest-snapshot": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", - "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" + "@jest/get-type": "30.1.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", - "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "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==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, - "peerDependencies": { - "tslib": "2" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", - "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", - "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/base64": "17.67.0", - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0", - "@jsonjoy.com/json-pointer": "17.67.0", - "@jsonjoy.com/util": "17.67.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", - "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/util": "17.67.0" + "@types/node": "*", + "jest-regex-util": "30.0.1" }, "engines": { - "node": ">=10.0" + "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==", + "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", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "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", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "tslib": "2" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", - "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=10.0" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@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", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -2645,10 +3330,10 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2662,16 +3347,12 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, "engines": { "node": ">=10.0" }, @@ -2683,15 +3364,16 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", + "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" }, "engines": { "node": ">=10.0" @@ -2704,12 +3386,18 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", + "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, "engines": { "node": ">=10.0" }, @@ -2721,227 +3409,1668 @@ "tslib": "2" } }, - "node_modules/@labkey/api": { - "version": "1.51.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.1.tgz", - "integrity": "sha512-RORsQpToUXkGsZMqMfqW+5d8g3r09s2Pojjz4z66hZR3nXw6K6U7xaXih/+96vFwbJ7BeqUsbv71+5dxX6Bmfg==", - "license": "Apache-2.0" - }, - "node_modules/@labkey/build": { - "version": "9.1.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/build/-/@labkey/build-9.1.1.tgz", - "integrity": "sha512-+AQnP+dLBiCu0V60DC8UQBtZWolr2L+r80Hvz7785Wvm3/FvHu01eEwQFKa9sv9VMNLRIvDxxPL/6nVLmySDEg==", + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", + "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@babel/core": "~7.29.0", - "@babel/plugin-transform-class-properties": "~7.28.6", - "@babel/plugin-transform-object-rest-spread": "~7.28.6", - "@babel/preset-env": "~7.29.2", - "@babel/preset-react": "~7.28.5", - "@babel/preset-typescript": "~7.28.5", - "@pmmmwh/react-refresh-webpack-plugin": "~0.6.2", - "ajv": "~8.18.0", - "babel-loader": "~10.1.1", - "bootstrap-sass": "~3.4.3", - "copy-webpack-plugin": "~14.0.0", - "cross-env": "~10.1.0", - "css-loader": "~7.1.4", - "fork-ts-checker-webpack-plugin": "~9.1.0", - "html-webpack-plugin": "~5.6.7", - "mini-css-extract-plugin": "~2.10.1", - "react-refresh": "~0.18.0", - "resolve-url-loader": "~5.0.0", - "rimraf": "~6.1.3", - "sass": "~1.99.0", - "sass-loader": "~16.0.7", - "source-map-loader": "~5.0.0", - "style-loader": "~4.0.0", - "typescript": "~5.9.3", - "webpack": "~5.106.2", - "webpack-bundle-analyzer": "~5.3.0", - "webpack-cli": "~7.0.2", - "webpack-dev-server": "~5.2.3" - } - }, - "node_modules/@labkey/components": { - "version": "7.31.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.31.1.tgz", - "integrity": "sha512-AsyqBEPM2afKzNFicSZVljNBqqzZ5qJ2KKI7rNqBBbRyU7ut4mAZ8nUmTEdv31g/Uj8wvRlkI7RrnasE4TE3EQ==", - "license": "SEE LICENSE IN LICENSE.txt", - "dependencies": { - "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.51.1", - "@testing-library/dom": "~10.4.1", - "@testing-library/jest-dom": "~6.9.1", - "@testing-library/react": "~16.3.2", - "@testing-library/user-event": "~14.6.1", - "bootstrap": "~3.4.1", - "classnames": "~2.5.1", - "date-fns": "~3.6.0", - "date-fns-tz": "~3.2.0", - "font-awesome": "~4.7.0", - "immer": "~10.1.3", - "immutable": "~3.8.3", - "normalizr": "~3.6.2", - "numeral": "~2.0.6", - "papaparse": "5.5.3", - "react": "~18.3.1", - "react-color": "~2.19.3", - "react-datepicker": "~7.6.0", - "react-dom": "~18.3.1", - "react-router-dom": "~6.30.1", - "react-select": "~5.10.2", - "react-treebeard": "~3.2.4", - "vis-data": "~8.0.3", - "vis-network": "~10.0.2" + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", + "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 16" + "node": ">=10.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", + "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, + "license": "Apache-2.0", "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2" }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", + "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", + "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.57.2", + "tree-dump": "^1.1.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", + "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@labkey/api": { + "version": "1.51.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.1.tgz", + "integrity": "sha512-RORsQpToUXkGsZMqMfqW+5d8g3r09s2Pojjz4z66hZR3nXw6K6U7xaXih/+96vFwbJ7BeqUsbv71+5dxX6Bmfg==", + "license": "Apache-2.0" + }, + "node_modules/@labkey/build": { + "version": "9.1.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/build/-/@labkey/build-9.1.1.tgz", + "integrity": "sha512-+AQnP+dLBiCu0V60DC8UQBtZWolr2L+r80Hvz7785Wvm3/FvHu01eEwQFKa9sv9VMNLRIvDxxPL/6nVLmySDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.29.0", + "@babel/plugin-transform-class-properties": "~7.28.6", + "@babel/plugin-transform-object-rest-spread": "~7.28.6", + "@babel/preset-env": "~7.29.2", + "@babel/preset-react": "~7.28.5", + "@babel/preset-typescript": "~7.28.5", + "@pmmmwh/react-refresh-webpack-plugin": "~0.6.2", + "ajv": "~8.18.0", + "babel-loader": "~10.1.1", + "bootstrap-sass": "~3.4.3", + "copy-webpack-plugin": "~14.0.0", + "cross-env": "~10.1.0", + "css-loader": "~7.1.4", + "fork-ts-checker-webpack-plugin": "~9.1.0", + "html-webpack-plugin": "~5.6.7", + "mini-css-extract-plugin": "~2.10.1", + "react-refresh": "~0.18.0", + "resolve-url-loader": "~5.0.0", + "rimraf": "~6.1.3", + "sass": "~1.99.0", + "sass-loader": "~16.0.7", + "source-map-loader": "~5.0.0", + "style-loader": "~4.0.0", + "typescript": "~5.9.3", + "webpack": "~5.106.2", + "webpack-bundle-analyzer": "~5.3.0", + "webpack-cli": "~7.0.2", + "webpack-dev-server": "~5.2.3" + } + }, + "node_modules/@labkey/components": { + "version": "7.31.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.31.1.tgz", + "integrity": "sha512-AsyqBEPM2afKzNFicSZVljNBqqzZ5qJ2KKI7rNqBBbRyU7ut4mAZ8nUmTEdv31g/Uj8wvRlkI7RrnasE4TE3EQ==", + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@hello-pangea/dnd": "18.0.1", + "@labkey/api": "1.51.1", + "@testing-library/dom": "~10.4.1", + "@testing-library/jest-dom": "~6.9.1", + "@testing-library/react": "~16.3.2", + "@testing-library/user-event": "~14.6.1", + "bootstrap": "~3.4.1", + "classnames": "~2.5.1", + "date-fns": "~3.6.0", + "date-fns-tz": "~3.2.0", + "font-awesome": "~4.7.0", + "immer": "~10.1.3", + "immutable": "~3.8.3", + "normalizr": "~3.6.2", + "numeral": "~2.0.6", + "papaparse": "5.5.3", + "react": "~18.3.1", + "react-color": "~2.19.3", + "react-datepicker": "~7.6.0", + "react-dom": "~18.3.1", + "react-router-dom": "~6.30.1", + "react-select": "~5.10.2", + "react-treebeard": "~3.2.4", + "vis-data": "~8.0.3", + "vis-network": "~10.0.2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", + "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "anser": "^2.1.1", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "@types/webpack": "5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <6.0.0", + "webpack": "^5.0.0", + "webpack-dev-server": "^4.8.0 || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/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/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "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==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "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==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "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==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "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==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "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==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" + ] + }, + "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==", + "cpu": [ + "arm" ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "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==", "cpu": [ "arm" ], @@ -2950,40 +5079,141 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] + }, + "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==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "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==", + "cpu": [ + "arm64" + ], + "dev": true, + "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==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "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==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "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==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "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==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "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==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "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==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "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==", "cpu": [ - "arm" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "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==", "cpu": [ "arm64" ], @@ -2991,1975 +5221,2579 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "win32" + ] }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "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==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "win32" + ] }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "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==", "cpu": [ "x64" ], "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 0.6" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "bin": { + "acorn": "bin/acorn" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10.0.0" + "node": ">=10.13.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "acorn": "^8.14.0" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=8.9" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 14" } }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@peculiar/asn1-csr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "node_modules/anser": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", + "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", - "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "engines": { + "node": ">=8" } }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", - "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "dequal": "^2.0.3" } }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" } }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" }, "engines": { - "node": ">=20.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", - "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", + "node_modules/babel-loader": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", + "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", "dev": true, "license": "MIT", "dependencies": { - "anser": "^2.1.1", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "html-entities": "^2.1.0", - "schema-utils": "^4.2.0", - "source-map": "^0.7.3" + "find-up": "^5.0.0" }, "engines": { - "node": ">=18.12" + "node": "^18.20.0 || ^20.10.0 || >=22.0.0" }, "peerDependencies": { - "@types/webpack": "5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <6.0.0", - "webpack": "^5.0.0", - "webpack-dev-server": "^4.8.0 || 5.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "1.x" + "@babel/core": "^7.12.0 || ^8.0.0-beta.1", + "@rspack/core": "^1.0.0 || ^2.0.0-0", + "webpack": ">=5.61.0" }, "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { + "@rspack/core": { "optional": true }, - "webpack-plugin-serve": { + "webpack": { "optional": true } } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, + "node_modules/babel-plugin-emotion": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "node_modules/babel-plugin-emotion/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT" + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "license": "MIT" + }, + "node_modules/babel-plugin-emotion/node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/babel-plugin-emotion/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/babel-plugin-emotion/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-emotion/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT" + }, + "node_modules/babel-plugin-emotion/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=0.8.0" + } + }, + "node_modules/babel-plugin-emotion/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" } }, - "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==", + "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==", "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" + "@types/babel__core": "^7.20.5" }, "engines": { - "node": ">=18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" }, "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" + "node": ">=10", + "npm": ">=6" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node": ">=10" } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" }, "peerDependencies": { - "@testing-library/dom": ">=7.21.4" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", "dev": true, "license": "MIT", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", + "license": "MIT" + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "engines": { + "node": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "license": "MIT", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "ms": "2.0.0" } }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" } }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, + "node_modules/bootstrap": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", + "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", + "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" + "engines": { + "node": ">=6" } }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "node_modules/bootstrap-sass": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", + "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==", + "dev": true, + "license": "MIT" + }, + "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==", "dev": true, "license": "MIT", "dependencies": { - "@types/istanbul-lib-report": "*" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "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==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "engines": { - "node": ">=10" + "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" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/@types/jest/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==", + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "fast-json-stable-stringify": "2.x" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 6" } }, - "node_modules/@types/jest/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/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*" + "engines": { + "node": ">=6" } }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", - "dependencies": { - "@types/express": "*" + "engines": { + "node": ">=10" } }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" + "engines": { + "node": ">=6.0" } }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "license": "MIT", - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">=8" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "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==", "dev": true, "license": "MIT" }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" } }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "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": "MIT" + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, "license": "MIT" }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" + "engines": { + "node": ">= 12" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@xtuc/long": "4.2.2" + "ms": "2.0.0" } }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } + "license": "MIT" }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "engines": { + "node": ">=0.8" } }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "engines": { + "node": ">= 0.6" } }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } + "license": "MIT" }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "dev": true, - "license": "Apache-2.0" + "license": "MIT" }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/copy-webpack-plugin": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", + "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^7.0.3", + "tinyglobby": "^0.2.12" }, "engines": { - "node": ">= 0.6" + "node": ">= 20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" } }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/core-js-pure": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, "engines": { - "node": ">=10.13.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "acorn": "^8.14.0" + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.0" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" }, "engines": { - "node": ">=0.4.0" + "node": ">=20" } }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=8.9" + "node": ">= 8" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "tiny-invariant": "^1.0.6" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "ajv": "^8.0.0" + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" }, "peerDependenciesMeta": { - "ajv": { + "@rspack/core": { + "optional": true + }, + "webpack": { "optional": true } } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/anser": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", - "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", + "license": "ISC", "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", + "semver": "bin/semver.js" + }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/ansi-styles": { + "node_modules/css-select": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 8" + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" + "bin": { + "cssesc": "bin/cssesc" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=4" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "dequal": "^2.0.3" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/asn1js": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.5", - "tslib": "^2.8.1" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" } }, - "node_modules/babel-loader": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", - "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", - "dev": true, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "find-up": "^5.0.0" + "ms": "^2.1.3" }, "engines": { - "node": "^18.20.0 || ^20.10.0 || >=22.0.0" + "node": ">=6.0" }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", "peerDependencies": { - "@babel/core": "^7.12.0 || ^8.0.0-beta.1", - "@rspack/core": "^1.0.0 || ^2.0.0-0", - "webpack": ">=5.61.0" + "babel-plugin-macros": "^3.1.0" }, "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { + "babel-plugin-macros": { "optional": true } } }, - "node_modules/babel-plugin-emotion": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", - "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/serialize": "^0.11.16", - "babel-plugin-macros": "^2.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^1.0.5", - "find-root": "^1.1.0", - "source-map": "^0.5.7" + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-emotion/node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "license": "MIT" - }, - "node_modules/babel-plugin-emotion/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "license": "MIT" + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/babel-plugin-emotion/node_modules/@emotion/serialize": { - "version": "0.11.16", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", - "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, "license": "MIT", "dependencies": { - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/unitless": "0.7.5", - "@emotion/utils": "0.11.3", - "csstype": "^2.5.7" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-plugin-emotion/node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "license": "MIT" - }, - "node_modules/babel-plugin-emotion/node_modules/@emotion/utils": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", - "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", - "license": "MIT" + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/babel-plugin-emotion/node_modules/babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-emotion/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/babel-plugin-emotion/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-emotion/node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", - "license": "MIT" - }, - "node_modules/babel-plugin-emotion/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">= 0.8" } }, - "node_modules/babel-plugin-emotion/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, "engines": { - "node": ">=10", - "npm": ">=6" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", - "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.8", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", - "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", - "core-js-compat": "^3.48.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } + "license": "MIT" }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", - "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" + "@leichtgewicht/ip-codec": "^2.0.1" }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "utila": "~0.4" } }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT" + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dev": true, "license": "MIT", - "engines": { - "node": "*" + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/binary-extensions": { + "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, "engines": { - "node": ">=8" + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } + "license": "MIT" }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "node_modules/electron-to-chromium": { + "version": "1.5.341", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.341.tgz", + "integrity": "sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==", "dev": true, "license": "ISC" }, - "node_modules/bootstrap": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", - "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", - "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/bootstrap-sass": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", - "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==", + "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/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==", + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">= 4" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "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==", + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=10.13.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true, - "license": "MIT" + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", "dev": true, "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" + "bin": { + "envinfo": "dist/cli.js" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "is-arrayish": "^0.2.1" } }, - "node_modules/bytestreamjs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" } }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, "engines": { "node": ">= 0.4" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "es-errors": "^1.3.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=0.10.0" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.0" + "node": ">= 0.6" } }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.8.x" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { - "source-map": "~0.6.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">= 10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "@jest/expect-utils": "30.3.0", + "@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" }, "engines": { - "node": ">=6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dev": true, "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, "engines": { - "node": ">=6" + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "ms": "2.0.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 4.9.1" } }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": ">= 1.43.0 < 2" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8" } }, - "node_modules/compression/node_modules/debug": { + "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", @@ -4969,240 +7803,188 @@ "ms": "2.0.0" } }, - "node_modules/compression/node_modules/ms": { + "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT" + "node_modules/font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", + "license": "(OFL-1.1 AND MIT)", + "engines": { + "node": ">=0.10.3" + } }, - "node_modules/copy-webpack-plugin": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", - "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^7.0.3", - "tinyglobby": "^0.2.12" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">= 20.9.0" + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1" + "license": "ISC", + "engines": { + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", - "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", "dev": true, "license": "MIT", "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" + "node": ">=14.21.3" }, "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">3.6.0", + "webpack": "^5.11.0" } }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "license": "MIT", - "dependencies": { - "tiny-invariant": "^1.0.6" - } + "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" }, - "node_modules/css-loader": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", - "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.40", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.6.3" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } } }, - "node_modules/css-loader/node_modules/semver": { + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", @@ -5215,109 +7997,124 @@ "node": ">=10" } }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "node": ">= 0.6" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-fns-tz": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", - "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", - "peerDependencies": { - "date-fns": "^3.0.0 || ^4.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=6.9.0" } }, - "node_modules/deep-equal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", - "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.5.1" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5326,852 +8123,999 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, "engines": { - "node": ">=18" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10.13.0" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "18 || 20 || >=22" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, "engines": { - "node": ">= 0.8" + "node": "18 || 20 || >=22" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, "engines": { - "node": ">=6" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/destroy": { + "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true, "license": "MIT" }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": ">=6" + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT" - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT", - "dependencies": { - "utila": "~0.4" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" + "engines": { + "node": ">=8" } }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "es-define-property": "^1.0.0" }, "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/electron-to-chromium": { - "version": "1.5.341", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.341.tgz", - "integrity": "sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==", + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=10.13.0" + "node": ">=18" } }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" }, - "node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, "bin": { - "envinfo": "dist/cli.js" + "html-minifier-terser": "cli.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" + "node": ">=12" } }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "node_modules/html-webpack-plugin": { + "version": "5.6.7", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", + "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", "dev": true, "license": "MIT", "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8.0.0" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 14" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=4.0" + "node": ">= 14" } }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "license": "BSD-2-Clause", + "license": "Apache-2.0", "engines": { - "node": ">=4.0" + "node": ">=10.17.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=10.18" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">= 0.6" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, + "node_modules/immutable": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.3.tgz", + "integrity": "sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==", "license": "MIT", "engines": { - "node": ">=0.8.x" + "node": ">=0.10.0" } }, - "node_modules/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", - "dev": true, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.3.0", - "@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" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">= 0.10.0" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=0.8.19" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "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==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4.9.1" + "node": ">=10.13.0" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", "dependencies": { - "websocket-driver": ">=0.5.1" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=0.8.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, "engines": { - "node": ">=12.0.0" + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": ">= 0.8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=0.12.0" } }, - "node_modules/font-awesome": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", - "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", - "license": "(OFL-1.1 AND MIT)", + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.3" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", - "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.16.7", - "chalk": "^4.1.2", - "chokidar": "^4.0.1", - "cosmiconfig": "^8.2.0", - "deepmerge": "^4.2.2", - "fs-extra": "^10.0.0", - "memfs": "^3.4.1", - "minimatch": "^3.0.4", - "node-abort-controller": "^3.0.1", - "schema-utils": "^3.1.1", - "semver": "^7.3.5", - "tapable": "^2.2.1" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=14.21.3" + "node": ">= 0.4" }, - "peerDependencies": { - "typescript": ">3.6.0", - "webpack": "^5.11.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "engines": { + "node": ">=8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=10" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "node_modules/istanbul-lib-instrument/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", @@ -6184,929 +9128,1050 @@ "node": ">=10" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=10" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=10" } }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "node_modules/istanbul-reports/node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "Unlicense" + "license": "MIT" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, - "engines": { - "node": ">= 0.4" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "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==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@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", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { - "node": ">=10.13.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "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==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "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", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "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==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.5" + "@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", + "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", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", "dependencies": { - "es-define-property": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, "engines": { - "node": ">= 0.4" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "has-symbols": "^1.0.3" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "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", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", "dev": true, "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "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/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">=12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/html-webpack-plugin": { - "version": "5.6.7", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", - "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, "engines": { - "node": ">=10.13.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "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==", "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], "license": "MIT", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "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/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" }, "engines": { - "node": ">= 0.8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" }, "engines": { - "node": ">=8.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" + "@jest/types": "30.3.0", + "@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", + "picomatch": "^4.0.3", + "walker": "^1.0.8" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "optionalDependencies": { + "fsevents": "^2.3.3" } }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, "engines": { - "node": ">=10.18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "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==", "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" }, - "peerDependencies": { - "postcss": "^8.1.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/immer": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", - "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/immutable": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.3.tgz", - "integrity": "sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==", + "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==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "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==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "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": "ISC" + "license": "MIT" }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "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==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, "engines": { - "node": ">=10.13.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "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/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "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==", + "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "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==", "dev": true, "license": "MIT", - "bin": { - "is-docker": "cli.js" + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "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==", "dev": true, "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@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", + "@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", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-network-error": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", - "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.12.0" + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "node_modules/jest-runner/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@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", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "balanced-match": "^1.0.0" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "is-inside-container": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=16" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-diff": { + "node_modules/jest-snapshot": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@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/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "expect": "30.3.0", + "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", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { + "node_modules/jest-snapshot/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", @@ -7119,7 +10184,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-diff/node_modules/pretty-format": { + "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==", @@ -7134,86 +10199,70 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/react-is": { + "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-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==", + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "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" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10" } }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/jest-teamcity-reporter": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/jest-teamcity-reporter/-/jest-teamcity-reporter-0.9.0.tgz", + "integrity": "sha512-q6W+ZaJSCIXmxC9wsY67zNn+vwG/EgKJygYJYH860jih5zS6mc2ZFc4v78gh6rgzgM9/siUtQm7SnRunYuWmVw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT" }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "node_modules/jest-util": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, "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": { + "node_modules/jest-validate": { "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==", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", + "@jest/get-type": "30.1.0", "@jest/types": "30.3.0", - "@types/stack-utils": "^2.0.3", + "camelcase": "^6.3.0", "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3", - "pretty-format": "30.3.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "leven": "^3.1.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { + "node_modules/jest-validate/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", @@ -7226,7 +10275,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-message-util/node_modules/pretty-format": { + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", @@ -7241,51 +10303,28 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/react-is": { + "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-mock": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", - "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.3.0", - "@types/node": "*", - "jest-util": "30.3.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util": { + "node_modules/jest-watcher": { "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", "dev": true, "license": "MIT", "dependencies": { + "@jest/test-result": "30.3.0", "@jest/types": "30.3.0", "@types/node": "*", + "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7341,6 +10380,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7408,9 +10487,19 @@ "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", "dev": true, "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/lines-and-columns": { @@ -7483,6 +10572,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7524,6 +10620,52 @@ "lz-string": "bin/bin.js" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/material-colors": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", @@ -7668,6 +10810,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -7718,6 +10870,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -7777,6 +10939,29 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -7820,6 +11005,13 @@ "license": "MIT", "optional": true }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -7843,6 +11035,19 @@ "integrity": "sha512-30qCybsBaCBciotorvuOZTCGEg2AXrJfADMT2Kk/lvpIAcipHdK0zc33nNtwKzyfQAqIJXAcqET6YgflYUgsoQ==", "license": "MIT" }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -7865,6 +11070,13 @@ "node": "*" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7942,6 +11154,32 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -8085,6 +11323,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8116,6 +11380,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8194,6 +11468,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -8489,6 +11773,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", @@ -8948,6 +12249,16 @@ "strip-ansi": "^6.0.1" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -9082,6 +12393,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -9192,6 +12510,19 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -9567,6 +12898,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -9698,6 +13036,13 @@ "wbuf": "^1.7.3" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9748,6 +13093,51 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/string-width-cjs": { + "name": "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/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9761,6 +13151,40 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "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/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -9773,6 +13197,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", @@ -9821,6 +13258,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tabbable": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", @@ -9899,7 +13359,44 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/thingies": { "version": "2.6.0", @@ -9954,6 +13451,33 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9987,6 +13511,32 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tree-dump": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", @@ -10004,6 +13554,85 @@ "tslib": "2" } }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10031,6 +13660,29 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -10059,6 +13711,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -10130,6 +13796,41 @@ "node": ">= 0.8" } }, + "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==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "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" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -10228,6 +13929,21 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -10317,6 +14033,29 @@ "vis-util": ">=6.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -10341,6 +14080,16 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/webpack": { "version": "5.106.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", @@ -10747,6 +14496,44 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10770,6 +14557,84 @@ "dev": true, "license": "MIT" }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "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/wrap-ansi-cjs": { + "name": "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -10808,6 +14673,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -10824,6 +14716,35 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/assay/package.json b/assay/package.json index 7e67a515ade..e4b0b40973e 100644 --- a/assay/package.json +++ b/assay/package.json @@ -9,7 +9,42 @@ "start-link": "cross-env LINK=true npm run start", "build-dev": "npm run clean && cross-env NODE_ENV=development webpack --config node_modules/@labkey/build/webpack/dev.config.js --color", "build-prod": "npm run clean && cross-env NODE_ENV=production PROD_SOURCE_MAP=source-map webpack --config node_modules/@labkey/build/webpack/prod.config.js --color --progress --profile", - "clean": "rimraf resources/web/assay/gen && rimraf resources/views/gen && rimraf resources/web/gen" + "clean": "rimraf resources/web/assay/gen && rimraf resources/views/gen && rimraf resources/web/gen", + "test": "cross-env NODE_ENV=test jest" + }, + "jest": { + "globals": { + "LABKEY": { + "container": { + "formats": { + "dateFormat": "yyyy-MM-dd" + } + }, + "user": { + "id": 1004 + }, + "project": {}, + "moduleContext": {} + } + }, + "moduleNameMapper": { + "\\.(scss|css)$": "/test/js/fileMock.js" + }, + "moduleFileExtensions": ["tsx", "ts", "js"], + "preset": "ts-jest", + "setupFilesAfterEnv": ["/test/js/setup.ts"], + "testEnvironment": "jsdom", + "testMatch": null, + "testRegex": "(\\.(test))\\.(ts|tsx)$", + "testResultsProcessor": "jest-teamcity-reporter", + "transform": { + "^.+\\.tsx?$": [ + "ts-jest", + { + "tsconfig": "node_modules/@labkey/build/webpack/tsconfig.test.json" + } + ] + } }, "dependencies": { "@labkey/components": "7.31.1" @@ -18,6 +53,11 @@ "@labkey/build": "9.1.1", "@types/jest": "30.0.0", "@types/react": "18.3.27", - "@types/react-dom": "18.3.7" + "@types/react-dom": "18.3.7", + "jest": "30.3.0", + "jest-cli": "30.3.0", + "jest-environment-jsdom": "30.3.0", + "jest-teamcity-reporter": "0.9.0", + "ts-jest": "29.4.6" } } diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 46162a3fde6..015dfac6eab 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -31,10 +31,23 @@ * ────────────────────────────────────────────────────────────────────────────── */ +// Visually-hidden utility — renders only to assistive technology +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + // Root container .plate-template-designer { padding: 12px 16px; - font-size: 13px; + font-size: 0.8125rem; &__error { color: #c00; @@ -59,7 +72,7 @@ padding: 4px 6px; border: 1px solid #ccc; border-radius: 3px; - font-size: 13px; + font-size: 0.8125rem; width: 280px; } @@ -79,6 +92,12 @@ &__right { flex: 1; min-width: 200px; + + // Guarantee hidden tabpanels (properties / warnings) take no space even if + // the framework's CSS reset omits the [hidden]{display:none} UA rule. + [hidden] { + display: none; + } } } @@ -98,7 +117,7 @@ border-radius: 3px; background: #f5f5f5; cursor: pointer; - font-size: 13px; + font-size: 0.8125rem; &:hover:not(:disabled) { background: #e8e8e8; @@ -130,6 +149,13 @@ &__status { color: #555; } + + // Validation error message (e.g., empty plate name) — uses role="alert" in JSX + &__error { + color: #c00; + font-weight: bold; + margin-left: 12px; + } } // ── GroupTypesPanel ─────────────────────────────────────────────────────────── @@ -152,7 +178,7 @@ border: none; background: transparent; cursor: pointer; - font-size: 12px; + font-size: 0.75rem; border-right: 1px solid #ddd; &:hover { @@ -172,11 +198,17 @@ align-items: flex-start; gap: 12px; padding: 8px; + + // Some CSS frameworks omit the UA-stylesheet [hidden]{display:none} rule. + // The higher specificity (class + attribute) ensures this wins over display:flex. + &[hidden] { + display: none; + } } - // Group list column — fixed width with a minimum so short lists don't collapse + // Group list column — min-width prevents short lists from collapsing &__groups { - flex: 0 0 160px; + flex-shrink: 0; min-width: 280px; min-height: 60px; } @@ -227,7 +259,7 @@ padding: 1px 4px; border: 1px solid #337ab7; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; min-width: 0; &--error { @@ -238,25 +270,34 @@ // Validation error shown below the create row or the rename input &__name-error { color: #c00; - font-size: 12px; + font-size: 0.75rem; margin-top: 2px; } - // Rename + delete buttons, visible only for the active group row + // Rename + delete buttons; always rendered to keep row height uniform. + // Inactive rows get --hidden: visibility:hidden preserves layout space while + // pointer-events:none blocks mouse interaction. &__group-actions { display: flex; gap: 2px; margin-left: auto; flex-shrink: 0; + + &--hidden { + visibility: hidden; + pointer-events: none; + } } &__action-btn { - padding: 2px 5px; + padding: 5px 7px; + min-height: 24px; + min-width: 24px; border: 1px solid #ccc; border-radius: 2px; background: transparent; cursor: pointer; - font-size: 11px; + font-size: 0.6875rem; line-height: 1; color: #555; @@ -287,13 +328,13 @@ padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; min-width: 0; } &__add-btn { padding: 3px 8px; - font-size: 12px; + font-size: 0.75rem; border: 1px solid #aaa; border-radius: 3px; background: #f5f5f5; @@ -346,32 +387,43 @@ &__title { font-weight: bold; - font-size: 14px; + font-size: 0.875rem; margin-bottom: 14px; } - &__table { - width: 100%; - border-collapse: collapse; + // Flex column container replacing the old layout table + &__form { + display: flex; + flex-direction: column; + gap: 10px; + } - td { - padding-bottom: 10px; - } + // Each label + input row + &__field { + display: flex; + align-items: flex-start; + gap: 16px; } // Label cells carry id attributes and are referenced via aria-labelledby on the inputs &__label { - padding: 6px 16px 6px 0; + padding-top: 5px; white-space: nowrap; - font-size: 13px; - vertical-align: middle; + font-size: 0.8125rem; + min-width: 80px; + } + + // Wrapper for count input + error message (stacked vertically) + &__count-area { + display: flex; + flex-direction: column; } &__input { padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 13px; + font-size: 0.8125rem; width: 100%; box-sizing: border-box; @@ -382,12 +434,11 @@ &__error { color: #c00; - font-size: 12px; + font-size: 0.75rem; margin-top: 2px; } &__buttons { - padding-top: 10px; display: flex; gap: 6px; justify-content: flex-end; @@ -424,7 +475,7 @@ border-radius: 3px; background: #f5f5f5; cursor: pointer; - font-size: 14px; + font-size: 0.875rem; line-height: 1; &:hover { @@ -433,7 +484,7 @@ } &__label { - font-size: 10px; + font-size: 0.625rem; color: #666; text-align: center; line-height: 1; @@ -459,7 +510,7 @@ &__col-header { text-align: center; - font-size: 11px; + font-size: 0.6875rem; font-weight: bold; width: 28px; height: 22px; @@ -469,7 +520,7 @@ &__row-header { text-align: center; - font-size: 11px; + font-size: 0.6875rem; font-weight: bold; width: 22px; background: #f0f0f0; @@ -508,7 +559,7 @@ border: none; background: transparent; cursor: pointer; - font-size: 12px; + font-size: 0.75rem; border-bottom: 2px solid transparent; margin-bottom: -1px; @@ -554,7 +605,7 @@ &__no-props { color: #767676; font-style: italic; - font-size: 12px; + font-size: 0.75rem; } &__table { @@ -584,18 +635,20 @@ padding: 3px 4px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; width: 100%; box-sizing: border-box; } &__delete-btn { - padding: 2px 5px; + padding: 5px 7px; + min-height: 24px; + min-width: 24px; border: 1px solid #ccc; border-radius: 2px; background: transparent; cursor: pointer; - font-size: 11px; + font-size: 0.6875rem; line-height: 1; color: #c00; @@ -614,7 +667,7 @@ padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; width: 100%; box-sizing: border-box; } @@ -623,7 +676,7 @@ padding: 3px 5px; border: 1px solid #ccc; border-radius: 3px; - font-size: 12px; + font-size: 0.75rem; width: 100%; box-sizing: border-box; } @@ -634,7 +687,7 @@ border-radius: 3px; background: #f5f5f5; cursor: pointer; - font-size: 12px; + font-size: 0.75rem; white-space: nowrap; &:hover:not(:disabled) { @@ -657,16 +710,10 @@ padding: 10px; background: #fffbe6; - &__title { - font-weight: bold; - color: #7a5800; - margin-bottom: 6px; - } - &__none { color: #767676; font-style: italic; - font-size: 12px; + font-size: 0.75rem; } &__list { @@ -676,7 +723,7 @@ &__item { color: #7a5800; - font-size: 12px; + font-size: 0.75rem; margin-bottom: 3px; } } diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index d9459e6a23e..29a69e19167 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -4,16 +4,14 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import classNames from 'classnames'; import { ActionURL, Ajax, Utils } from '@labkey/api'; import { PlateTemplate, Position, WellGroup, computeWarnings } from './models'; import { StatusBar } from './components/StatusBar'; import { GroupTypesPanel } from './components/GroupTypesPanel'; +import { RightPanel } from './components/RightPanel'; import { ShiftPanel } from './components/ShiftPanel'; import { TemplateGrid } from './components/TemplateGrid'; -import { WellGroupProperties } from './components/WellGroupProperties'; -import { WarningPanel } from './components/WarningPanel'; import './PlateTemplateDesigner.scss'; @@ -51,21 +49,22 @@ import './PlateTemplateDesigner.scss'; * * ─── Cell interaction ─────────────────────────────────────────────────────────── * Two cell callbacks are distinguished: - * `handleCellAssign` — idempotent add; also evicts the cell from any other group - * of the same type (one cell can only belong to one group per type). Used during - * drag operations. - * `handleCellToggle` — pure on/off; does not steal from siblings. Used for - * single-click (no drag movement). + * `handleDragRect` — paints a rectangle; also evicts those cells from sibling + * groups of the same type (one cell can only belong to one group per type). + * Used during drag operations. + * `handleCellToggle` — toggle: if the cell is already in the active group, remove + * it; otherwise add it and evict it from any sibling group of the same type. + * Used for single-click (no drag movement). */ const COLORS = [ '#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', - '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac', - '#6ba3be', '#ffbe7d', '#ff9d9a', '#86bcb6', '#8cd17d', - '#f1ce63', '#d4a6c8', '#ffb7c5', '#c7a97e', '#d7d5cf', + '#ecb830', '#9b59b6', '#e84878', '#7a4222', '#888888', + '#30c068', '#ccd828', '#4848cc', '#d04018', '#18a8c0', + '#c030a8', '#8caa28', '#583848', '#c8d8e8', '#204888', ]; -function assignColors(groups: WellGroup[]): Map { +export function assignColors(groups: WellGroup[]): Map { const map = new Map(); groups.forEach((g, i) => { map.set(g.rowId, COLORS[i % COLORS.length]); @@ -73,6 +72,40 @@ function assignColors(groups: WellGroup[]): Map { return map; } +/** + * Toggles a single cell in the active group: + * - If the cell is already in the active group → remove it (no sibling changes). + * - If the cell is absent → add it to the active group and evict it from any + * other group of the same type so a cell never belongs to two groups of one type. + */ +export function toggleCell(groups: WellGroup[], activeGroupRowId: number, row: number, col: number): WellGroup[] { + const activeGroup = groups.find(g => g.rowId === activeGroupRowId); + if (!activeGroup) return groups; + const isInActiveGroup = activeGroup.positions.some(p => p.row === row && p.col === col); + const activeType = activeGroup.type; + return groups.map(g => { + if (g.rowId === activeGroupRowId) { + if (isInActiveGroup) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return { ...g, positions: [...g.positions, { row, col }] }; + } + // When adding: evict the cell from every sibling group of the same type + if (!isInActiveGroup && g.type === activeType) { + return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; + } + return g; + }); +} + +export function isSameOrigin(url: string): boolean { + try { + return new URL(url, window.location.origin).origin === window.location.origin; + } catch { + return false; + } +} + export function PlateTemplateDesigner(): JSX.Element { const [plate, setPlate] = useState(null); const [activeGroup, setActiveGroup] = useState(null); @@ -192,46 +225,37 @@ export function PlateTemplateDesigner(): JSX.Element { setIsDirty(true); }, []); - // Pure toggle: add the cell if absent, remove it if present const handleCellToggle = useCallback((row: number, col: number) => { const activeGroup = activeGroupRef.current; if (!activeGroup) return; - setPlate(prev => { - if (!prev) return null; - const updatedGroups = prev.groups.map(g => { - if (g.rowId !== activeGroup.rowId) return g; - const hasCell = g.positions.some(p => p.row === row && p.col === col); - if (hasCell) { - return { ...g, positions: g.positions.filter(p => !(p.row === row && p.col === col)) }; - } - return { ...g, positions: [...g.positions, { row, col }] }; - }); - return { ...prev, groups: updatedGroups }; - }); + setPlate(prev => prev ? { ...prev, groups: toggleCell(prev.groups, activeGroup.rowId, row, col) } : null); setIsDirty(true); }, []); const handleAddGroup = useCallback((type: string, name: string) => { - if (!plate) return; const rowId = nextGroupIdRef.current--; - const newGroup: WellGroup = { - rowId, - type, - name, - positions: [], - properties: {}, - allowNewGroups: plate.canCreateGroupsByType?.[type] ?? false, - }; - setPlate(prev => prev ? { ...prev, groups: [...prev.groups, newGroup] } : null); const colorIndex = nextColorIndexRef.current++; + setPlate(prev => { + if (!prev) return null; + const newGroup: WellGroup = { + rowId, + type, + name, + positions: [], + properties: {}, + allowNewGroups: prev.canCreateGroupsByType?.[type] ?? false, + }; + return { ...prev, groups: [...prev.groups, newGroup] }; + }); setColorMap(prev => { const next = new Map(prev); next.set(rowId, COLORS[colorIndex % COLORS.length]); return next; }); - setActiveGroup(newGroup); + // allowNewGroups is a stub here; the plate-sync effect re-derives it from the updated plate. + setActiveGroup({ rowId, type, name, positions: [], properties: {}, allowNewGroups: false }); setIsDirty(true); - }, [plate]); + }, []); const handleShift = useCallback((verticalShift: number, horizontalShift: number) => { setPlate(prev => { @@ -295,20 +319,13 @@ export function PlateTemplateDesigner(): JSX.Element { setIsDirty(true); }, []); - const warningCount = useMemo(() => { - if (!plate?.showWarningPanel) return 0; - return computeWarnings(plate).length; + const warnings = useMemo(() => { + if (!plate?.showWarningPanel) return []; + return computeWarnings(plate); }, [plate]); const navigateAway = useCallback(() => { const returnURL = ActionURL.getParameter('returnURL') || ActionURL.getParameter('returnUrl'); - const isSameOrigin = (url: string) => { - try { - return new URL(url, window.location.origin).origin === window.location.origin; - } catch { - return false; - } - }; window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); }, []); @@ -365,6 +382,11 @@ export function PlateTemplateDesigner(): JSX.Element { navigateAway(); }, [navigateAway]); + const handleTabChange = useCallback((tab: string) => { + setActiveTab(tab); + setActiveGroup(null); + }, []); + // Warn on unsaved navigation useEffect(() => { const handler = (e: BeforeUnloadEvent) => { @@ -411,6 +433,7 @@ export function PlateTemplateDesigner(): JSX.Element { { setActiveTab(tab); setActiveGroup(null); }} + onTabChange={handleTabChange} onAddGroup={handleAddGroup} onDeleteGroup={handleDeleteGroup} onRenameGroup={handleRenameGroup} @@ -457,55 +480,15 @@ export function PlateTemplateDesigner(): JSX.Element { {/* Right panel: WellGroupProperties and (if enabled) a Warnings tab. The tab strip only renders when showWarningPanel is true; otherwise WellGroupProperties fills the full right column without tabs. */} -
- {plate.showWarningPanel && ( -
- - -
- )} - {(!plate.showWarningPanel || rightTab === 'properties') && ( -
- -
- )} - {plate.showWarningPanel && rightTab === 'warnings' && ( -
- -
- )} -
+ ); diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts new file mode 100644 index 00000000000..ca49ffc27dd --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.utils.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import { WellGroup } from './models'; +import { assignColors, isSameOrigin, toggleCell } from './PlateTemplateDesigner'; + +function makeGroup(rowId: number): WellGroup { + return { rowId, type: 'SPECIMEN', name: `Group ${rowId}`, positions: [], properties: {}, allowNewGroups: false }; +} + +function makeGroupAt(rowId: number, type: string, coords: Array<[number, number]>): WellGroup { + return { rowId, type, name: `Group ${rowId}`, positions: coords.map(([row, col]) => ({ row, col })), properties: {}, allowNewGroups: false }; +} + +describe('assignColors', () => { + test('returns an empty map for an empty group list', () => { + expect(assignColors([])).toEqual(new Map()); + }); + + test('assigns a color entry for each group', () => { + const groups = [makeGroup(1), makeGroup(2), makeGroup(3)]; + const map = assignColors(groups); + expect(map.size).toBe(3); + expect(map.get(1)).toBeDefined(); + expect(map.get(2)).toBeDefined(); + expect(map.get(3)).toBeDefined(); + }); + + test('uses group rowId as the map key, not the array index', () => { + const groups = [makeGroup(10), makeGroup(20)]; + const map = assignColors(groups); + expect(map.has(10)).toBe(true); + expect(map.has(20)).toBe(true); + expect(map.has(0)).toBe(false); + }); + + test('assigns distinct colors to the first 20 groups', () => { + const groups = Array.from({ length: 20 }, (_, i) => makeGroup(i + 1)); + const map = assignColors(groups); + const colors = Array.from(map.values()); + expect(new Set(colors).size).toBe(20); + }); + + test('wraps color assignment after 20 groups (21st group gets same color as 1st)', () => { + const groups = Array.from({ length: 21 }, (_, i) => makeGroup(i + 1)); + const map = assignColors(groups); + expect(map.get(21)).toBe(map.get(1)); + }); + + test('assigns colors in array order, not by rowId value', () => { + // rowId 99 comes first in the array, so it gets the first color + const groups = [makeGroup(99), makeGroup(1)]; + const map = assignColors(groups); + const firstColor = assignColors([makeGroup(1)]).get(1); + expect(map.get(99)).toBe(firstColor); + }); +}); + +describe('toggleCell', () => { + test('adds cell to active group when the cell is absent', () => { + const groups = [makeGroupAt(1, 'SPECIMEN', [])]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); + }); + + test('removes cell from active group when the cell is already present', () => { + const groups = [makeGroupAt(1, 'SPECIMEN', [[0, 0]])]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([]); + }); + + test('evicts cell from a sibling group of the same type when adding', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', []), // active — does not have the cell + makeGroupAt(2, 'SPECIMEN', [[0, 0]]), // sibling — owns the cell + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); // added to active + expect(result[1].positions).toEqual([]); // evicted from sibling + }); + + test('does not evict from a group of a different type when adding', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', []), + makeGroupAt(2, 'CONTROL', [[0, 0]]), // different type — must be untouched + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); + expect(result[1].positions).toEqual([{ row: 0, col: 0 }]); // unchanged + }); + + test('does not evict from sibling groups when removing (toggle off)', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', [[0, 0]]), // active — owns the cell + makeGroupAt(2, 'SPECIMEN', [[0, 0]]), // sibling — also owns the cell (edge case) + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([]); // removed from active + expect(result[1].positions).toEqual([{ row: 0, col: 0 }]); // sibling unchanged + }); + + test('leaves unrelated cells in the sibling group intact', () => { + const groups = [ + makeGroupAt(1, 'SPECIMEN', []), + makeGroupAt(2, 'SPECIMEN', [[0, 0], [1, 1]]), // sibling owns (0,0) and (1,1) + ]; + const result = toggleCell(groups, 1, 0, 0); + expect(result[0].positions).toEqual([{ row: 0, col: 0 }]); + expect(result[1].positions).toEqual([{ row: 1, col: 1 }]); // only (0,0) evicted + }); + + test('returns groups unchanged when activeGroupRowId is not found', () => { + const groups = [makeGroupAt(1, 'SPECIMEN', [])]; + expect(toggleCell(groups, 99, 0, 0)).toEqual(groups); + }); +}); + +describe('isSameOrigin', () => { + // jsdom sets window.location.origin to 'http://localhost' + + test('returns true for a URL on the same origin', () => { + expect(isSameOrigin('http://localhost/some/path')).toBe(true); + }); + + test('returns true for a root-relative URL (same origin by construction)', () => { + expect(isSameOrigin('/labkey/plate/plateList.view')).toBe(true); + }); + + test('returns false for a different hostname', () => { + expect(isSameOrigin('http://evil.com/path')).toBe(false); + }); + + test('returns false for a different scheme', () => { + expect(isSameOrigin('https://localhost/path')).toBe(false); + }); + + test('returns false for a different port', () => { + expect(isSameOrigin('http://localhost:8080/path')).toBe(false); + }); + + test('returns false for a javascript: URL (XSS guard)', () => { + expect(isSameOrigin('javascript:alert(1)')).toBe(false); + }); + + test('returns false for an absolute URL with an invalid host (throws during construction)', () => { + // 'http://a b' has a space in the hostname which is invalid; the URL constructor throws, + // and the catch block returns false. + expect(isSameOrigin('http://a b/path')).toBe(false); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx new file mode 100644 index 00000000000..03d1f6ba963 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { PlateTemplate, WellGroup } from '../models'; +import { GroupTypesPanel } from './GroupTypesPanel'; + +function makeGroup(overrides: Partial = {}): WellGroup { + return { + rowId: 1, + type: 'SPECIMEN', + name: 'Group A', + positions: [], + properties: {}, + allowNewGroups: true, + ...overrides, + }; +} + +function makePlate(overrides: Partial = {}): PlateTemplate { + return { + rowId: 0, + name: 'Test Plate', + type: 'assay', + rows: 8, + cols: 12, + groupTypes: ['SPECIMEN', 'CONTROL'], + canCreateGroupsByType: { SPECIMEN: true, CONTROL: true }, + groups: [], + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: false, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', + ...overrides, + }; +} + +function renderPanel(overrides: Partial> = {}) { + const props = { + plate: makePlate(), + activeGroup: null, + activeTab: 'SPECIMEN', + colorMap: new Map(), + onGroupSelect: jest.fn(), + onTabChange: jest.fn(), + onAddGroup: jest.fn(), + onDeleteGroup: jest.fn(), + onRenameGroup: jest.fn(), + ...overrides, + }; + const result = render(); + return { ...result, props }; +} + +describe('GroupTypesPanel — create row: select vs input', () => { + test('shows a contains only the unused defaults', () => { + renderPanel({ + plate: makePlate({ + typesToDefaultGroups: { SPECIMEN: ['Virus', 'Cell Control'] }, + groups: [makeGroup({ name: 'Virus' })], // 'Virus' already used + }), + }); + const select = screen.getByRole('combobox', { name: 'Group name' }); + const options = Array.from(select.querySelectorAll('option')).map(o => o.textContent); + expect(options).toEqual(['Cell Control']); + }); + + test('shows a text when all defaults are used', () => { + renderPanel({ + plate: makePlate({ + typesToDefaultGroups: { SPECIMEN: ['Virus'] }, + groups: [makeGroup({ name: 'Virus' })], + }), + }); + expect(screen.getByRole('textbox', { name: 'Group name' })).toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: 'Group name' })).toBeNull(); + }); + + test('shows a text when no defaults are configured for the type', () => { + renderPanel({ plate: makePlate({ typesToDefaultGroups: {} }) }); + expect(screen.getByRole('textbox', { name: 'Group name' })).toBeInTheDocument(); + }); +}); + +describe('GroupTypesPanel — create row: name conflict detection', () => { + test('Create button is enabled for a unique name', async () => { + renderPanel({ plate: makePlate({ groups: [makeGroup({ name: 'Existing' })] }) }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'New Group'); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + }); + + test('Create button is disabled and error shown when name conflicts', async () => { + renderPanel({ plate: makePlate({ groups: [makeGroup({ name: 'Existing' })] }) }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'Existing'); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + expect(screen.getByText(/already exists in this type/i)).toBeInTheDocument(); + }); + + test('conflict only checks groups of the same type', async () => { + // A group named 'Shared Name' in CONTROL should not conflict with SPECIMEN create input + renderPanel({ + plate: makePlate({ + groups: [makeGroup({ rowId: 2, type: 'CONTROL', name: 'Shared Name' })], + }), + }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'Shared Name'); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + expect(screen.queryByText(/already exists/i)).toBeNull(); + }); + + test('clicking Create calls onAddGroup with the trimmed name', async () => { + const { props } = renderPanel(); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), ' My Group '); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'My Group'); + }); + + test('pressing Enter in the name input calls onAddGroup', async () => { + const { props } = renderPanel(); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'My Group{Enter}'); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'My Group'); + }); + + test('Create button is disabled when name is empty', () => { + renderPanel(); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + }); +}); + +describe('GroupTypesPanel — inline rename', () => { + function renderWithActiveGroup() { + const group1 = makeGroup({ rowId: 1, name: 'Group A' }); + const group2 = makeGroup({ rowId: 2, name: 'Group B' }); + return renderPanel({ + plate: makePlate({ groups: [group1, group2] }), + activeGroup: group1, + }); + } + + test('rename button is visible for the active group when allowNewGroups is true', () => { + renderWithActiveGroup(); + expect(screen.getByRole('button', { name: 'Rename Group A' })).toBeInTheDocument(); + }); + + test('clicking rename shows an inline input', async () => { + renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + expect(screen.getByRole('textbox', { name: 'Rename Group A' })).toBeInTheDocument(); + }); + + test('pressing Enter with a non-conflicting name calls onRenameGroup', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Group C{Enter}'); + expect(props.onRenameGroup).toHaveBeenCalledWith(1, 'Group C'); + }); + + test('pressing Enter with a conflicting name shows an error and does not rename', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Group B{Enter}'); + expect(props.onRenameGroup).not.toHaveBeenCalled(); + expect(screen.getByText(/"Group B" is already used/i)).toBeInTheDocument(); + }); + + test('blurring with a conflicting name reverts silently (no error, no rename)', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Group B'); + await userEvent.tab(); // blur the input + expect(props.onRenameGroup).not.toHaveBeenCalled(); + expect(screen.queryByText(/"Group B" is already used/i)).toBeNull(); + expect(screen.queryByRole('textbox', { name: 'Rename Group A' })).toBeNull(); + }); + + test('pressing Escape cancels rename without calling onRenameGroup', async () => { + const { props } = renderWithActiveGroup(); + await userEvent.click(screen.getByRole('button', { name: 'Rename Group A' })); + const input = screen.getByRole('textbox', { name: 'Rename Group A' }); + await userEvent.clear(input); + await userEvent.type(input, 'Something{Escape}'); + expect(props.onRenameGroup).not.toHaveBeenCalled(); + expect(screen.queryByRole('textbox', { name: 'Rename Group A' })).toBeNull(); + }); +}); + +describe('GroupTypesPanel — tab switching resets create input', () => { + test('switching activeTab resets the create name to empty (no defaults)', async () => { + const { rerender, props } = renderPanel({ + plate: makePlate({ canCreateGroupsByType: { SPECIMEN: true, CONTROL: true } }), + activeTab: 'SPECIMEN', + }); + await userEvent.type(screen.getByRole('textbox', { name: 'Group name' }), 'In Progress'); + expect(screen.getByRole('textbox', { name: 'Group name' })).toHaveValue('In Progress'); + + rerender( + + ); + expect(screen.getByRole('textbox', { name: 'Group name' })).toHaveValue(''); + }); + + test('switching activeTab resets to the first unused default of the new tab', () => { + const plate = makePlate({ + canCreateGroupsByType: { SPECIMEN: true, CONTROL: true }, + typesToDefaultGroups: { CONTROL: ['Positive', 'Negative'] }, + }); + const { rerender, props } = renderPanel({ plate, activeTab: 'SPECIMEN' }); + + rerender(); + + const select = screen.getByRole('combobox', { name: 'Group name' }); + expect(select).toHaveValue('Positive'); + }); +}); + +describe('GroupTypesPanel — tab click', () => { + test('clicking a tab calls onTabChange with the tab key', async () => { + const { props } = renderPanel({ activeTab: 'SPECIMEN' }); + await userEvent.click(screen.getByRole('tab', { name: 'CONTROL' })); + expect(props.onTabChange).toHaveBeenCalledWith('CONTROL'); + }); +}); + +describe('GroupTypesPanel — group selection', () => { + function renderWithGroup() { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + return renderPanel({ + plate: makePlate({ groups: [group] }), + activeGroup: null, + }); + } + + test('clicking a group row calls onGroupSelect with that group', async () => { + const { props } = renderWithGroup(); + await userEvent.click(screen.getByRole('option', { name: 'Group A' })); + expect(props.onGroupSelect).toHaveBeenCalledWith( + expect.objectContaining({ rowId: 1, name: 'Group A' }) + ); + }); + + test('pressing Enter on a group row calls onGroupSelect', () => { + const { props } = renderWithGroup(); + fireEvent.keyDown(screen.getByRole('option', { name: 'Group A' }), { key: 'Enter' }); + expect(props.onGroupSelect).toHaveBeenCalledWith( + expect.objectContaining({ rowId: 1, name: 'Group A' }) + ); + }); + + test('pressing Space on a group row calls onGroupSelect', () => { + const { props } = renderWithGroup(); + fireEvent.keyDown(screen.getByRole('option', { name: 'Group A' }), { key: ' ' }); + expect(props.onGroupSelect).toHaveBeenCalledWith( + expect.objectContaining({ rowId: 1, name: 'Group A' }) + ); + }); +}); + +describe('GroupTypesPanel — delete group', () => { + function renderWithActiveGroup() { + const group = makeGroup({ rowId: 1, name: 'Group A' }); + return renderPanel({ + plate: makePlate({ groups: [group] }), + activeGroup: group, + }); + } + + test('delete button calls onDeleteGroup when user confirms', async () => { + const { props } = renderWithActiveGroup(); + jest.spyOn(window, 'confirm').mockReturnValue(true); + await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + expect(props.onDeleteGroup).toHaveBeenCalledWith(1); + }); + + test('delete button does not call onDeleteGroup when user cancels', async () => { + const { props } = renderWithActiveGroup(); + jest.spyOn(window, 'confirm').mockReturnValue(false); + await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + expect(props.onDeleteGroup).not.toHaveBeenCalled(); + }); +}); + +describe('GroupTypesPanel — multi-create dialog', () => { + // Give the SPECIMEN type a default name so newGroupName starts as 'Virus', + // which becomes the dialog's initialBaseName. + function renderWithDefault() { + return renderPanel({ + plate: makePlate({ typesToDefaultGroups: { SPECIMEN: ['Virus'] } }), + }); + } + + test('clicking "Create multiple..." opens the dialog', async () => { + renderWithDefault(); + await userEvent.click(screen.getByRole('button', { name: 'Create multiple...' })); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + test('confirming multi-create calls onAddGroup for each generated name', async () => { + const { props } = renderWithDefault(); + await userEvent.click(screen.getByRole('button', { name: 'Create multiple...' })); + // Dialog opens with initialBaseName='Virus', default count=2 + await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Create' })); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'Virus 1'); + expect(props.onAddGroup).toHaveBeenCalledWith('SPECIMEN', 'Virus 2'); + }); + + test('closing the dialog with Cancel hides it', async () => { + renderWithDefault(); + await userEvent.click(screen.getByRole('button', { name: 'Create multiple...' })); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Cancel' })); + expect(screen.queryByRole('dialog')).toBeNull(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 986d5b74d1c..851ec7072da 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -3,12 +3,13 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; +import { MultiCreateDialog } from './MultiCreateDialog'; -interface Props { +interface GroupTypesPanelProps { plate: PlateTemplate; activeGroup: WellGroup | null; activeTab: string; @@ -74,16 +75,12 @@ export function GroupTypesPanel({ onDeleteGroup, onRenameGroup, children, -}: Props): JSX.Element { +}: GroupTypesPanelProps): JSX.Element { const [newGroupName, setNewGroupName] = useState(''); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); const [renameError, setRenameError] = useState(null); const [multiCreateOpen, setMultiCreateOpen] = useState(false); - const [multiBaseName, setMultiBaseName] = useState(''); - const [multiCount, setMultiCount] = useState('2'); - const [multiCountError, setMultiCountError] = useState(''); - const dialogRef = useRef(null); // Stable derived list — memoized so useMemo and useEffect deps are stable. const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); @@ -112,34 +109,6 @@ export function GroupTypesPanel({ } }, [unusedDefaults]); // eslint-disable-line react-hooks/exhaustive-deps - // Focus trap for multi-create dialog - useEffect(() => { - if (!multiCreateOpen || !dialogRef.current) return; - const dialog = dialogRef.current; - const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; - const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - setMultiCreateOpen(false); - return; - } - if (e.key !== 'Tab') return; - const focusable = getFocusable(); - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (e.shiftKey) { - if (document.activeElement === first) { e.preventDefault(); last?.focus(); } - } else { - if (document.activeElement === last) { e.preventDefault(); first?.focus(); } - } - }; - - dialog.addEventListener('keydown', handleKeyDown); - getFocusable()[0]?.focus(); - return () => dialog.removeEventListener('keydown', handleKeyDown); - }, [multiCreateOpen]); - const handleCreate = () => { const trimmed = newGroupName.trim(); if (!trimmed || createNameConflicts) return; @@ -147,33 +116,6 @@ export function GroupTypesPanel({ setNewGroupName(''); }; - const openMultiCreate = () => { - setMultiBaseName(newGroupName.trim()); - setMultiCount('2'); - setMultiCountError(''); - setMultiCreateOpen(true); - // Focus is handled by the focus-trap effect above - }; - - const handleMultiCreate = () => { - const count = parseInt(multiCount, 10); - if (isNaN(count) || count < 1) { - setMultiCountError(`"${multiCount}" is not a valid count.`); - return; - } - const baseName = multiBaseName.trim(); - if (!baseName) return; - const existingNames = new Set(groupsOfType.map(g => g.name)); - const namesToCreate = Array.from({ length: count }, (_, i) => `${baseName} ${i + 1}`) - .filter(name => !existingNames.has(name)); - if (namesToCreate.length === 0) { - setMultiCountError(`All ${count} generated name${count === 1 ? '' : 's'} already exist in this type.`); - return; - } - namesToCreate.forEach(name => onAddGroup(activeTab, name)); - setMultiCreateOpen(false); - }; - const handleDeleteClick = (e: React.MouseEvent, group: WellGroup) => { e.stopPropagation(); if (window.confirm(`Delete well group "${group.name}"?`)) { @@ -225,204 +167,172 @@ export function GroupTypesPanel({ ))} -
-
- {groupsOfType.map(group => { - const color = colorMap.get(group.rowId); - const isActive = activeGroup?.rowId === group.rowId; - const isRenaming = renamingId === group.rowId; - return ( - -
{ if (!isRenaming) onGroupSelect(group); }} - onKeyDown={e => { - if (!isRenaming && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault(); - onGroupSelect(group); - } - }} - > -
+ ))} {multiCreateOpen && ( -
setMultiCreateOpen(false)}> -
e.stopPropagation()} - > -
Create Multiple Groups
- - - - - - - - - - - - - - -
Base Name - setMultiBaseName(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} - /> -
Count - { setMultiCount(e.target.value); setMultiCountError(''); }} - onKeyDown={e => { if (e.key === 'Enter') handleMultiCreate(); if (e.key === 'Escape') setMultiCreateOpen(false); }} - /> - {multiCountError &&
{multiCountError}
} -
- - - -
-
-
+ g.name))} + onClose={() => setMultiCreateOpen(false)} + onConfirm={names => { + names.forEach(name => onAddGroup(activeTab, name)); + setMultiCreateOpen(false); + }} + /> )}
); diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx new file mode 100644 index 00000000000..395716da0bd --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.test.tsx @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { MultiCreateDialog } from './MultiCreateDialog'; + +function renderDialog(overrides: Partial> = {}) { + const props = { + initialBaseName: 'Sample', + existingNames: new Set(), + onClose: jest.fn(), + onConfirm: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('MultiCreateDialog', () => { + describe('initial state', () => { + test('pre-populates base name from initialBaseName', () => { + renderDialog({ initialBaseName: 'Virus' }); + expect(screen.getByLabelText(/Base Name/i)).toHaveValue('Virus'); + }); + + test('defaults count to 2', () => { + renderDialog(); + expect(screen.getByLabelText(/Count/i)).toHaveValue(2); + }); + + test('Create button is disabled when base name is empty', () => { + renderDialog({ initialBaseName: '' }); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + }); + + test('Create button is enabled when base name is non-empty', () => { + renderDialog({ initialBaseName: 'Sample' }); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + }); + }); + + describe('successful creation', () => { + test('calls onConfirm with generated names using base name and count', async () => { + const { onConfirm } = renderDialog({ initialBaseName: 'Sample', existingNames: new Set() }); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '3'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2', 'Sample 3']); + }); + + test('trims whitespace from base name before generating names', async () => { + const { onConfirm } = renderDialog({ initialBaseName: ' Sample ' }); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2']); + }); + + test('filters out names already in existingNames', async () => { + const { onConfirm } = renderDialog({ + initialBaseName: 'Sample', + existingNames: new Set(['Sample 1', 'Sample 3']), + }); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '3'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + // Sample 1 and Sample 3 are already taken; only Sample 2 is new + expect(onConfirm).toHaveBeenCalledWith(['Sample 2']); + }); + }); + + describe('validation errors', () => { + test('shows error and does not call onConfirm when count is not a number', async () => { + const { onConfirm } = renderDialog(); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), 'abc'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).not.toHaveBeenCalled(); + expect(screen.getByText(/not a valid count/i)).toBeInTheDocument(); + }); + + test('shows error when count is less than 1', async () => { + const { onConfirm } = renderDialog(); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '0'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).not.toHaveBeenCalled(); + expect(screen.getByText(/not a valid count/i)).toBeInTheDocument(); + }); + + test('shows error and does not call onConfirm when all generated names already exist', async () => { + const { onConfirm } = renderDialog({ + initialBaseName: 'Sample', + existingNames: new Set(['Sample 1', 'Sample 2']), + }); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onConfirm).not.toHaveBeenCalled(); + expect(screen.getByText(/already exist/i)).toBeInTheDocument(); + }); + + test('count error clears when count input is changed', async () => { + renderDialog(); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '0'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(screen.getByText(/not a valid count/i)).toBeInTheDocument(); + + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '3'); + expect(screen.queryByText(/not a valid count/i)).toBeNull(); + }); + }); + + describe('cancel / close', () => { + test('clicking Cancel calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + test('clicking the overlay calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.click(document.querySelector('.multi-create-dialog__overlay')); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + test('clicking inside the dialog does not call onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.click(document.querySelector('.multi-create-dialog')); + expect(onClose).not.toHaveBeenCalled(); + }); + + test('pressing Escape on the base name input calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.type(screen.getByLabelText(/Base Name/i), '{Escape}'); + // Both the input's onKeyDown and the dialog's focus-trap listener fire on Escape + // (the event bubbles), so onClose is called twice. Both calls are idempotent. + expect(onClose).toHaveBeenCalled(); + }); + + test('pressing Escape on the count input calls onClose', async () => { + const { onClose } = renderDialog(); + await userEvent.type(screen.getByLabelText(/Count/i), '{Escape}'); + expect(onClose).toHaveBeenCalled(); + }); + }); + + describe('keyboard submit', () => { + test('pressing Enter in the base name input submits', async () => { + const { onConfirm } = renderDialog({ initialBaseName: 'Sample' }); + await userEvent.type(screen.getByLabelText(/Base Name/i), '{Enter}'); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2']); + }); + + test('pressing Enter in the count input submits', async () => { + const { onConfirm } = renderDialog({ initialBaseName: 'Sample' }); + await userEvent.type(screen.getByLabelText(/Count/i), '{Enter}'); + expect(onConfirm).toHaveBeenCalledWith(['Sample 1', 'Sample 2']); + }); + }); + + describe('focus trap', () => { + // Focusable order: Base Name → Count → Cancel → Create. + // userEvent.tab() fires Tab keydown events that bubble to the dialog's native listener. + test('Tab from last focusable element (Create) wraps focus to first (Base Name)', async () => { + renderDialog({ initialBaseName: 'Sample' }); // Create button enabled + // After mount the useEffect focuses the first element (Base Name); tab forward to Create. + await userEvent.tab(); // → Count + await userEvent.tab(); // → Cancel + await userEvent.tab(); // → Create (last) + await userEvent.tab(); // → focus trap wraps back to Base Name + expect(document.activeElement).toBe(screen.getByLabelText(/Base Name/i)); + }); + + test('Shift+Tab from first focusable element (Base Name) wraps focus to last (Create)', async () => { + renderDialog({ initialBaseName: 'Sample' }); // Create button enabled + // After mount, Base Name is focused (first focusable). Shift+Tab wraps to Create (last). + await userEvent.keyboard('{Shift>}{Tab}{/Shift}'); + expect(document.activeElement).toBe( + within(screen.getByRole('dialog')).getByRole('button', { name: 'Create' }) + ); + }); + }); + + describe('base name input', () => { + test('typing in the base name input updates its value', async () => { + renderDialog({ initialBaseName: '' }); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); + await userEvent.type(screen.getByLabelText(/Base Name/i), 'NewName'); + expect(screen.getByLabelText(/Base Name/i)).toHaveValue('NewName'); + expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled(); + }); + + test('pressing Enter in count input with empty base name does not submit', async () => { + // The count input's onKeyDown calls handleCreate() without guarding baseName. + // handleCreate's own guard (if (!trimmedBase) return) must prevent submission. + const { onConfirm } = renderDialog({ initialBaseName: '' }); + await userEvent.type(screen.getByLabelText(/Count/i), '{Enter}'); + expect(onConfirm).not.toHaveBeenCalled(); + }); + }); + + describe('singular error message', () => { + test('uses singular "name" when count is 1 and that name already exists', async () => { + // Exercises the parsedCount === 1 branch of the ternary in the "all names exist" error. + renderDialog({ + initialBaseName: 'Sample', + existingNames: new Set(['Sample 1']), + }); + await userEvent.clear(screen.getByLabelText(/Count/i)); + await userEvent.type(screen.getByLabelText(/Count/i), '1'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(screen.getByText(/all 1 generated name already exist/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx new file mode 100644 index 00000000000..a86e2a127f4 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React, { useEffect, useRef, useState } from 'react'; + +interface MultiCreateDialogProps { + initialBaseName: string; + existingNames: Set; + onClose: () => void; + onConfirm: (names: string[]) => void; +} + +export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onConfirm }: MultiCreateDialogProps): JSX.Element { + const [baseName, setBaseName] = useState(initialBaseName); + const [count, setCount] = useState('2'); + const [countError, setCountError] = useState(''); + const dialogRef = useRef(null); + + // Focus trap: runs once on mount since the component only renders when the dialog is open. + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { onClose(); return; } + if (e.key !== 'Tab') return; + const focusable = getFocusable(); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last?.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first?.focus(); } + } + }; + + dialog.addEventListener('keydown', handleKeyDown); + getFocusable()[0]?.focus(); + return () => dialog.removeEventListener('keydown', handleKeyDown); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const handleCreate = () => { + const parsedCount = parseInt(count, 10); + if (isNaN(parsedCount) || parsedCount < 1) { + setCountError(`"${count}" is not a valid count.`); + return; + } + const trimmedBase = baseName.trim(); + if (!trimmedBase) return; + const namesToCreate = Array.from({ length: parsedCount }, (_, i) => `${trimmedBase} ${i + 1}`) + .filter(name => !existingNames.has(name)); + if (namesToCreate.length === 0) { + setCountError(`All ${parsedCount} generated name${parsedCount === 1 ? '' : 's'} already exist in this type.`); + return; + } + onConfirm(namesToCreate); + }; + + return ( +
+
e.stopPropagation()} + > +
Create Multiple Groups
+
+
+ Base Name + setBaseName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> +
+
+ Count +
+ { setCount(e.target.value); setCountError(''); }} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> + {countError &&
{countError}
} +
+
+
+ + +
+
+
+
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx new file mode 100644 index 00000000000..ccd23a5188c --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { RightPanel } from './RightPanel'; + +function renderPanel(overrides: Partial> = {}) { + const props = { + showWarningPanel: false, + rightTab: 'properties' as const, + onRightTabChange: jest.fn(), + warnings: [], + activeGroup: null, + onPropertyChange: jest.fn(), + onDeleteProperty: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('RightPanel — without warning panel', () => { + test('renders WellGroupProperties with no tab strip', () => { + renderPanel({ showWarningPanel: false }); + expect(screen.queryByRole('tablist')).toBeNull(); + expect(screen.queryByRole('tabpanel')).toBeNull(); + // WellGroupProperties empty-state text is present + expect(screen.getByText(/select a well group/i)).toBeInTheDocument(); + }); + + test('properties div has no role or aria-labelledby when showWarningPanel is false', () => { + renderPanel({ showWarningPanel: false }); + // The wrapper div around WellGroupProperties should have no role + const tabpanels = document.querySelectorAll('[role="tabpanel"]'); + expect(tabpanels).toHaveLength(0); + }); +}); + +describe('RightPanel — with warning panel, properties tab', () => { + test('renders a tablist with two tabs', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + expect(screen.getByRole('tablist')).toBeInTheDocument(); + expect(screen.getAllByRole('tab')).toHaveLength(2); + }); + + test('properties tab is aria-selected, warnings tab is not', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'false'); + }); + + test('properties tabpanel is rendered with correct id and aria-labelledby', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + const panel = document.getElementById('right-panel-properties'); + expect(panel).toBeInTheDocument(); + expect(panel).toHaveAttribute('role', 'tabpanel'); + expect(panel).toHaveAttribute('aria-labelledby', 'right-tab-properties'); + }); + + test('warnings tabpanel is hidden when rightTab is "properties"', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + expect(document.getElementById('right-panel-warnings')).toHaveAttribute('hidden'); + }); + + test('warnings tab label shows count badge when warnings exist', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: ['w1', 'w2', 'w3'] }); + expect(screen.getByRole('tab', { name: 'Warnings (3)' })).toBeInTheDocument(); + }); + + test('warnings tab label shows plain "Warnings" when list is empty', () => { + renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: [] }); + expect(screen.getByRole('tab', { name: 'Warnings' })).toBeInTheDocument(); + }); + + test('clicking warnings tab calls onRightTabChange with "warnings"', async () => { + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + await userEvent.click(screen.getByRole('tab', { name: 'Warnings' })); + expect(onRightTabChange).toHaveBeenCalledWith('warnings'); + }); + + test('clicking properties tab calls onRightTabChange with "properties"', async () => { + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + await userEvent.click(screen.getByRole('tab', { name: 'Well Group Properties' })); + expect(onRightTabChange).toHaveBeenCalledWith('properties'); + }); +}); + +describe('RightPanel — with warning panel, warnings tab', () => { + test('renders warnings tabpanel; properties tabpanel is hidden', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: ['A1: warning'] }); + expect(document.getElementById('right-panel-warnings')).toBeInTheDocument(); + expect(document.getElementById('right-panel-properties')).toHaveAttribute('hidden'); + }); + + test('warnings tab is aria-selected, properties tab is not', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'false'); + }); + + test('warnings tabpanel has correct aria-labelledby', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + const panel = document.getElementById('right-panel-warnings'); + expect(panel).toHaveAttribute('aria-labelledby', 'right-tab-warnings'); + }); + + test('WarningPanel content is visible in warnings tab', () => { + renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: [] }); + expect(screen.getByText('No warnings.')).toBeInTheDocument(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx new file mode 100644 index 00000000000..4d24c11247f --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import classNames from 'classnames'; + +import { WellGroup } from '../models'; +import { WellGroupProperties } from './WellGroupProperties'; +import { WarningPanel } from './WarningPanel'; + +interface RightPanelProps { + showWarningPanel: boolean; + rightTab: 'properties' | 'warnings'; + onRightTabChange: (tab: 'properties' | 'warnings') => void; + warnings: string[]; + activeGroup: WellGroup | null; + onPropertyChange: (groupRowId: number, key: string, value: string) => void; + onDeleteProperty: (groupRowId: number, key: string) => void; +} + +export function RightPanel(props: RightPanelProps): JSX.Element { + const { showWarningPanel, rightTab, onRightTabChange, warnings, activeGroup, onPropertyChange, onDeleteProperty } = props; + const warningCount = warnings.length; + + return ( +
+ {showWarningPanel && ( +
+ + +
+ )} + + {showWarningPanel && ( + + )} +
+ ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx new file mode 100644 index 00000000000..75b2c242f40 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { ShiftPanel } from './ShiftPanel'; + +describe('ShiftPanel', () => { + test('renders four directional buttons', () => { + render(); + expect(screen.getByRole('button', { name: 'Shift up' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Shift down' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Shift left' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Shift right' })).toBeInTheDocument(); + }); + + test('Shift up calls onShift(1, 0) — moves cells up (decreases row index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift up' })); + expect(onShift).toHaveBeenCalledWith(1, 0); + }); + + test('Shift down calls onShift(-1, 0) — moves cells down (increases row index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift down' })); + expect(onShift).toHaveBeenCalledWith(-1, 0); + }); + + test('Shift left calls onShift(0, 1) — moves cells left (decreases col index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift left' })); + expect(onShift).toHaveBeenCalledWith(0, 1); + }); + + test('Shift right calls onShift(0, -1) — moves cells right (increases col index)', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift right' })); + expect(onShift).toHaveBeenCalledWith(0, -1); + }); + + test('each button only fires once per click', async () => { + const onShift = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: 'Shift up' })); + expect(onShift).toHaveBeenCalledTimes(1); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx index 43c3d3cb2a0..08b1d913eb3 100644 --- a/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/ShiftPanel.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -interface Props { +interface ShiftPanelProps { onShift: (verticalShift: number, horizontalShift: number) => void; } @@ -23,7 +23,7 @@ interface Props { * Shifts apply to every group of the active type simultaneously, preserving relative layout * between groups. Only the active tab's type is affected; other types are unchanged. */ -export function ShiftPanel({ onShift }: Props): JSX.Element { +export function ShiftPanel({ onShift }: ShiftPanelProps): JSX.Element { return (
diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx new file mode 100644 index 00000000000..e4242b0eeb2 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { StatusBar } from './StatusBar'; + +function renderStatusBar(overrides: Partial> = {}) { + const props = { + isDirty: false, + status: '', + plateName: 'My Plate', + onSaveAndClose: jest.fn(), + onSave: jest.fn(), + onCancel: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('StatusBar', () => { + describe('button states', () => { + test('Save button is disabled when plate is not dirty', () => { + renderStatusBar({ isDirty: false }); + expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); + }); + + test('Save button is enabled when plate is dirty', () => { + renderStatusBar({ isDirty: true }); + expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled(); + }); + + test('Save & Close and Cancel are always enabled', () => { + renderStatusBar({ isDirty: false }); + expect(screen.getByRole('button', { name: /Save & Close/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled(); + }); + }); + + describe('dirty indicator', () => { + test('shows "Unsaved changes" when dirty', () => { + renderStatusBar({ isDirty: true }); + expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); + }); + + test('shows nothing in dirty indicator when clean', () => { + renderStatusBar({ isDirty: false }); + // The element is present but empty + const statuses = document.querySelectorAll('.status-bar__dirty'); + expect(statuses).toHaveLength(1); + expect(statuses[0]).toHaveTextContent(''); + }); + + test('displays transient status text', () => { + renderStatusBar({ status: 'Saved.' }); + expect(screen.getByText('Saved.')).toBeInTheDocument(); + }); + }); + + describe('validation', () => { + test('clicking Save with empty plate name shows error and does not call onSave', async () => { + const { onSave } = renderStatusBar({ plateName: '', isDirty: true }); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).not.toHaveBeenCalled(); + expect(screen.getByRole('alert')).toHaveTextContent(/plate name/i); + }); + + test('clicking Save with whitespace-only plate name shows error', async () => { + const { onSave } = renderStatusBar({ plateName: ' ', isDirty: true }); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).not.toHaveBeenCalled(); + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); + + test('clicking Save with valid plate name calls onSave and shows no error', async () => { + const { onSave } = renderStatusBar({ plateName: 'My Plate', isDirty: true }); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).toHaveBeenCalledTimes(1); + expect(screen.queryByRole('alert')).toBeNull(); + }); + + test('clicking Save & Close with empty name shows error and does not call onSaveAndClose', async () => { + const { onSaveAndClose } = renderStatusBar({ plateName: '' }); + await userEvent.click(screen.getByRole('button', { name: /Save & Close/i })); + expect(onSaveAndClose).not.toHaveBeenCalled(); + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); + + test('clicking Save & Close with valid name calls onSaveAndClose', async () => { + const { onSaveAndClose } = renderStatusBar({ plateName: 'My Plate' }); + await userEvent.click(screen.getByRole('button', { name: /Save & Close/i })); + expect(onSaveAndClose).toHaveBeenCalledTimes(1); + }); + + test('error clears on a subsequent successful save', async () => { + // First render with empty name to trigger error + const { rerender } = render( + + ); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.getByRole('alert')).toBeInTheDocument(); + + // Re-render with a valid name — the error clears on the next successful validate + const onSave = jest.fn(); + rerender( + + ); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(onSave).toHaveBeenCalledTimes(1); + expect(screen.queryByRole('alert')).toBeNull(); + }); + }); + + describe('Cancel', () => { + test('clicking Cancel calls onCancel', async () => { + const { onCancel } = renderStatusBar(); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + test('Cancel does not call onSave or onSaveAndClose', async () => { + const { onSave, onSaveAndClose } = renderStatusBar(); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onSave).not.toHaveBeenCalled(); + expect(onSaveAndClose).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx index 9afc2600133..2af5b740051 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -3,11 +3,12 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; -interface Props { +interface StatusBarProps { isDirty: boolean; status: string; + plateName: string; onSaveAndClose: () => void; onSave: () => void; onCancel: () => void; @@ -28,13 +29,33 @@ interface Props { * The "Unsaved changes" indicator and transient status text ("Saving…", "Saved.") * use `role="status"` so screen readers announce them as they appear. */ -export function StatusBar({ isDirty, status, onSaveAndClose, onSave, onCancel }: Props): JSX.Element { +export function StatusBar({ isDirty, status, plateName, onSaveAndClose, onSave, onCancel }: StatusBarProps): JSX.Element { + const [error, setError] = useState(null); + + // Clear stale validation error once the user has filled in the plate name + useEffect(() => { + if (plateName.trim()) setError(null); + }, [plateName]); + + const validate = (): boolean => { + if (!plateName.trim()) { + setError('Please enter a plate name before saving.'); + return false; + } + setError(null); + return true; + }; + + const validateAndSave = () => { if (validate()) onSave(); }; + + const validateAndSaveAndClose = () => { if (validate()) onSaveAndClose(); }; + return (
- - {isDirty ? 'Unsaved changes' : ''} {status} + {error && {error}}
); } diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx new file mode 100644 index 00000000000..79add5b0c0e --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.test.tsx @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import { PlateTemplate, WellGroup } from '../models'; +import { TemplateGrid } from './TemplateGrid'; + +function makePlate(rows = 4, cols = 4, groups: WellGroup[] = []): PlateTemplate { + return { + rowId: 1, + name: 'Test', + type: 'assay', + rows, + cols, + groupTypes: ['SPECIMEN'], + canCreateGroupsByType: {}, + groups, + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: false, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', + }; +} + +function renderGrid(overrides: Partial> = {}) { + const props = { + plate: makePlate(), + activeGroup: null, + activeTab: 'SPECIMEN', + colorMap: new Map(), + onDragRect: jest.fn(), + onCellToggle: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +// Helpers for getting cells by their aria-label (e.g. "A1", "B3") +function getCell(label: string) { + return screen.getByLabelText(label); +} + +describe('TemplateGrid — rendering', () => { + test('renders the correct number of data cells', () => { + renderGrid({ plate: makePlate(3, 4) }); + // 3 rows × 4 cols = 12 cells, all labeled A1..C4 + expect(screen.getByLabelText('A1')).toBeInTheDocument(); + expect(screen.getByLabelText('C4')).toBeInTheDocument(); + }); + + test('renders column headers 1..cols', () => { + renderGrid({ plate: makePlate(2, 3) }); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + }); + + test('renders row headers A..H for 8 rows', () => { + renderGrid({ plate: makePlate(8, 1) }); + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'].forEach(letter => { + expect(screen.getAllByText(letter)[0]).toBeInTheDocument(); + }); + }); + + test('cell aria-label includes group name when cell is assigned', () => { + const group: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map([[1, '#ff0000']]), + }); + expect(screen.getByLabelText('A1: Sample 1')).toBeInTheDocument(); + }); + + test('cell tooltip is just the location when unassigned', () => { + renderGrid({ plate: makePlate(2, 2) }); + expect(screen.getByLabelText('B2')).toBeInTheDocument(); + }); +}); + +describe('TemplateGrid — roving tabindex', () => { + test('cell (0,0) has tabIndex=0 before any focus interaction', () => { + renderGrid(); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + expect(getCell('A2')).toHaveAttribute('tabindex', '-1'); + }); + + test('all other cells have tabIndex=-1 initially', () => { + renderGrid({ plate: makePlate(2, 2) }); + ['A2', 'B1', 'B2'].forEach(label => { + expect(getCell(label)).toHaveAttribute('tabindex', '-1'); + }); + }); + + test('focusing a cell updates the tab stop to that cell', () => { + renderGrid({ plate: makePlate(2, 2) }); + fireEvent.focus(getCell('B2')); + expect(getCell('B2')).toHaveAttribute('tabindex', '0'); + expect(getCell('A1')).toHaveAttribute('tabindex', '-1'); + }); +}); + +describe('TemplateGrid — keyboard navigation', () => { + test('ArrowRight moves focus from A1 to A2', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowRight' }); + expect(getCell('A2')).toHaveAttribute('tabindex', '0'); + expect(getCell('A1')).toHaveAttribute('tabindex', '-1'); + }); + + test('ArrowDown moves focus from A1 to B1', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowDown' }); + expect(getCell('B1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowLeft moves focus from A2 to A1', () => { + renderGrid(); + fireEvent.focus(getCell('A2')); + fireEvent.keyDown(getCell('A2'), { key: 'ArrowLeft' }); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowUp moves focus from B1 to A1', () => { + renderGrid(); + fireEvent.focus(getCell('B1')); + fireEvent.keyDown(getCell('B1'), { key: 'ArrowUp' }); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowLeft at column 0 does nothing', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowLeft' }); + // A1 should remain the tab stop + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowUp at row 0 does nothing', () => { + renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'ArrowUp' }); + expect(getCell('A1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowDown at last row does nothing', () => { + renderGrid({ plate: makePlate(4, 4) }); + fireEvent.focus(getCell('D1')); + fireEvent.keyDown(getCell('D1'), { key: 'ArrowDown' }); + expect(getCell('D1')).toHaveAttribute('tabindex', '0'); + }); + + test('ArrowRight at last column does nothing', () => { + renderGrid({ plate: makePlate(4, 4) }); + fireEvent.focus(getCell('A4')); + fireEvent.keyDown(getCell('A4'), { key: 'ArrowRight' }); + expect(getCell('A4')).toHaveAttribute('tabindex', '0'); + }); +}); + +describe('TemplateGrid — keyboard cell toggle', () => { + test('Space key calls onCellToggle with the cell coordinates', () => { + const { onCellToggle } = renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: ' ' }); + expect(onCellToggle).toHaveBeenCalledWith(0, 0); + }); + + test('Enter key calls onCellToggle with the cell coordinates', () => { + const { onCellToggle } = renderGrid(); + fireEvent.keyDown(getCell('B3'), { key: 'Enter' }); + expect(onCellToggle).toHaveBeenCalledWith(1, 2); + }); + + test('other keys do not call onCellToggle', () => { + const { onCellToggle } = renderGrid(); + fireEvent.keyDown(getCell('A1'), { key: 'Tab' }); + expect(onCellToggle).not.toHaveBeenCalled(); + }); +}); + +describe('TemplateGrid — mouse click (no drag)', () => { + test('mousedown + mouseup on the same cell calls onCellToggle', () => { + const { onCellToggle } = renderGrid(); + const cell = getCell('A1'); + fireEvent.mouseDown(cell, { button: 0 }); + fireEvent.mouseUp(cell); + expect(onCellToggle).toHaveBeenCalledWith(0, 0); + }); + + test('right-click (button !== 0) does not start a drag', () => { + const { onCellToggle, onDragRect } = renderGrid(); + const cell = getCell('A1'); + fireEvent.mouseDown(cell, { button: 2 }); + fireEvent.mouseUp(cell); + expect(onCellToggle).not.toHaveBeenCalled(); + expect(onDragRect).not.toHaveBeenCalled(); + }); +}); + +describe('TemplateGrid — mouse drag', () => { + test('mousedown then mouseenter a different cell calls onDragRect (not onCellToggle)', () => { + const { onDragRect, onCellToggle } = renderGrid(); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + expect(onDragRect).toHaveBeenCalledWith(0, 0, 1, 1, false, []); + expect(onCellToggle).not.toHaveBeenCalled(); + }); + + test('drag started on a cell already in the active group uses unselect mode', () => { + const activeGroup: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + const { onDragRect } = renderGrid({ activeGroup }); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + // isUnselect should be true since A1 is already in the active group + expect(onDragRect).toHaveBeenCalledWith(0, 0, 1, 1, true, activeGroup.positions); + }); + + test('drag started on an empty cell uses select mode', () => { + const activeGroup: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [], properties: {}, allowNewGroups: false, + }; + const { onDragRect } = renderGrid({ activeGroup }); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + expect(onDragRect).toHaveBeenCalledWith(0, 0, 1, 1, false, []); + }); + + test('mouseleave the grid resets drag state so subsequent mouseenter does not fire', () => { + const { onDragRect } = renderGrid(); + const grid = document.querySelector('.template-grid') as HTMLElement; + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseLeave(grid); // drag cancelled + fireEvent.mouseEnter(getCell('B2')); // should be ignored + expect(onDragRect).not.toHaveBeenCalled(); + }); + + test('mouseup after drag does not call onCellToggle', () => { + const { onCellToggle } = renderGrid(); + fireEvent.mouseDown(getCell('A1'), { button: 0 }); + fireEvent.mouseEnter(getCell('B2')); + fireEvent.mouseUp(getCell('B2')); + expect(onCellToggle).not.toHaveBeenCalled(); + }); +}); + +describe('TemplateGrid — colorMap fallback', () => { + test('cell gets #f5f5f5 background when its group rowId is not in colorMap', () => { + const group: WellGroup = { + rowId: 1, type: 'SPECIMEN', name: 'Sample 1', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + renderGrid({ + plate: makePlate(2, 2, [group]), + colorMap: new Map(), // rowId 1 not in map → falls back to '#f5f5f5' + }); + const cell = screen.getByLabelText('A1: Sample 1'); + expect(cell).toBeInTheDocument(); + expect(cell).toHaveStyle({ backgroundColor: '#f5f5f5' }); + }); + + test('group of a type other than activeTab is excluded from position map', () => { + const controlGroup: WellGroup = { + rowId: 2, type: 'CONTROL', name: 'Virus', + positions: [{ row: 0, col: 0 }], properties: {}, allowNewGroups: false, + }; + renderGrid({ + plate: makePlate(2, 2, [controlGroup]), + colorMap: new Map([[2, '#00ff00']]), + activeTab: 'SPECIMEN', // CONTROL !== SPECIMEN → group is skipped + }); + // Cell is not labeled with the CONTROL group name since that type is inactive + expect(screen.getByLabelText('A1')).toBeInTheDocument(); + expect(screen.queryByLabelText('A1: Virus')).toBeNull(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index 67600997651..dc7a2cc0558 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import { PlateTemplate, Position, WellGroup } from '../models'; -interface Props { +interface TemplateGridProps { plate: PlateTemplate; activeGroup: WellGroup | null; activeTab: string; @@ -50,7 +50,7 @@ function getRowLabel(row: number): string { * Drag state is also cleaned up on mouseleave of the outer div, preventing stuck * drag state when the pointer exits the grid. */ -export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRect, onCellToggle }: Props): JSX.Element { +export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRect, onCellToggle }: TemplateGridProps): JSX.Element { const isDragging = useRef(false); const hasMoved = useRef(false); const startCell = useRef<{ row: number; col: number } | null>(null); @@ -77,6 +77,18 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return map; }, [plate, activeTab, colorMap]); + // Pre-compute a Set of "row,col" keys for the active group's positions so each cell + // can do an O(1) membership check instead of scanning the positions array on every render. + const activeGroupPositionSet = useMemo(() => { + const set = new Set(); + if (activeGroup) { + for (const p of activeGroup.positions) { + set.add(`${p.row},${p.col}`); + } + } + return set; + }, [activeGroup]); + const handleMouseDown = useCallback((row: number, col: number, e: React.MouseEvent) => { if (e.button !== 0) return; isDragging.current = true; @@ -146,7 +158,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return (
- +
{Array.from({ length: plate.cols }, (_, col) => { const entry = positionMap.get(`${row},${col}`); - const isActiveGroupCell = activeGroup?.positions.some(p => p.row === row && p.col === col); + const isActiveGroupCell = activeGroupPositionSet.has(`${row},${col}`); const location = `${getRowLabel(row)}${col + 1}`; const tooltip = entry ? `${location}: ${entry.groupName}` : location; const isTabStop = focusedCell @@ -180,7 +192,6 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe 'template-grid__cell--active': isActiveGroupCell, })} style={{ backgroundColor: entry?.color ?? '#f5f5f5' }} - title={tooltip} aria-label={tooltip} onMouseDown={e => handleMouseDown(row, col, e)} onMouseEnter={() => handleMouseEnter(row, col)} diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx new file mode 100644 index 00000000000..1a740ce48d5 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { WarningPanel } from './WarningPanel'; + +describe('WarningPanel', () => { + test('shows "No warnings." and no list when warnings is empty', () => { + render(); + expect(screen.getByText('No warnings.')).toBeInTheDocument(); + expect(screen.queryByRole('list')).toBeNull(); + }); + + test('renders a list item for each warning', () => { + const warnings = ['A1: replicate with no specimen', 'B3: in both specimen and control']; + render(); + expect(screen.queryByText('No warnings.')).toBeNull(); + expect(screen.getAllByRole('listitem')).toHaveLength(2); + expect(screen.getByText('A1: replicate with no specimen')).toBeInTheDocument(); + expect(screen.getByText('B3: in both specimen and control')).toBeInTheDocument(); + }); + + test('renders a single warning without "No warnings."', () => { + render(); + expect(screen.getAllByRole('listitem')).toHaveLength(1); + expect(screen.queryByText('No warnings.')).toBeNull(); + }); + + test('switches from "No warnings." to a list when warnings change', () => { + const { rerender } = render(); + expect(screen.getByText('No warnings.')).toBeInTheDocument(); + + rerender(); + expect(screen.queryByText('No warnings.')).toBeNull(); + expect(screen.getByText('A1: some warning')).toBeInTheDocument(); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx index ca49a6ed61c..dabb40c5998 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WarningPanel.tsx @@ -3,28 +3,24 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useMemo } from 'react'; +import React from 'react'; -import { PlateTemplate, computeWarnings } from '../models'; - -interface Props { - plate: PlateTemplate; +interface WarningPanelProps { + warnings: string[]; } /** * Displays the list of validation warnings for the current plate layout. * - * Warnings are recomputed synchronously from the latest plate state on each render. + * Warnings are computed in the parent (PlateTemplateDesigner) and passed as a prop so the + * computation is not duplicated between the count badge and this panel. * The panel is only shown when `plate.showWarningPanel` is true, which is controlled by the * server-side assay type configuration (not all assay types use the REPLICATE/SPECIMEN/CONTROL * group semantics that produce warnings). */ -export function WarningPanel({ plate }: Props): JSX.Element { - const warnings = useMemo(() => computeWarnings(plate), [plate]); - +export function WarningPanel({ warnings }: WarningPanelProps): JSX.Element { return (
-
Warnings
{warnings.length === 0 ? (
No warnings.
) : ( diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx new file mode 100644 index 00000000000..c3506d6e135 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { WellGroup } from '../models'; +import { WellGroupProperties } from './WellGroupProperties'; + +function makeGroup(overrides: Partial = {}): WellGroup { + return { + rowId: 1, + type: 'SPECIMEN', + name: 'Group A', + positions: [], + properties: {}, + allowNewGroups: true, + ...overrides, + }; +} + +function renderProps(activeGroup: WellGroup | null, overrides: Partial> = {}) { + const props = { + activeGroup, + onPropertyChange: jest.fn(), + onDeleteProperty: jest.fn(), + ...overrides, + }; + render(); + return props; +} + +describe('WellGroupProperties', () => { + describe('empty state', () => { + test('shows placeholder text when no group is selected', () => { + renderProps(null); + expect(screen.getByText(/select a well group/i)).toBeInTheDocument(); + }); + + test('does not render a table when no group is selected', () => { + renderProps(null); + expect(screen.queryByRole('table')).toBeNull(); + }); + }); + + describe('group display', () => { + test('renders the group name as a title', () => { + renderProps(makeGroup({ name: 'My Group' })); + expect(screen.getByText('My Group')).toBeInTheDocument(); + }); + + test('shows "No properties defined" when group has no properties', () => { + renderProps(makeGroup({ properties: {} })); + expect(screen.getByText(/No properties defined/i)).toBeInTheDocument(); + }); + + test('renders a row for each existing property', () => { + renderProps(makeGroup({ properties: { conc: '1.0', dilution: '2x' } })); + expect(screen.getByText('conc')).toBeInTheDocument(); + expect(screen.getByDisplayValue('1.0')).toBeInTheDocument(); + expect(screen.getByText('dilution')).toBeInTheDocument(); + expect(screen.getByDisplayValue('2x')).toBeInTheDocument(); + }); + + test('renders a delete button per property', () => { + renderProps(makeGroup({ properties: { a: '1', b: '2' } })); + expect(screen.getAllByRole('button', { name: /Delete property/i })).toHaveLength(2); + }); + }); + + describe('editing existing properties', () => { + test('changing a value input calls onPropertyChange with groupRowId, key, and new value', () => { + const group = makeGroup({ properties: { conc: '1.0' } }); + const { onPropertyChange } = renderProps(group); + // The input is controlled (value comes from activeGroup.properties), so use + // fireEvent.change to fire a single synthetic event without the re-render cycle + // that makes userEvent.type fight the controlled value. + fireEvent.change(screen.getByLabelText('conc'), { target: { value: '5.0' } }); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'conc', '5.0'); + }); + + test('clicking the delete button calls onDeleteProperty with groupRowId and key', async () => { + const group = makeGroup({ properties: { conc: '1.0' } }); + const { onDeleteProperty } = renderProps(group); + await userEvent.click(screen.getByRole('button', { name: 'Delete property conc' })); + expect(onDeleteProperty).toHaveBeenCalledWith(group.rowId, 'conc'); + }); + }); + + describe('adding a new property', () => { + test('Add button is disabled when the key input is empty', () => { + renderProps(makeGroup()); + expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled(); + }); + + test('Add button is enabled when a key is typed', async () => { + renderProps(makeGroup()); + await userEvent.type(screen.getByLabelText('Property name'), 'newKey'); + expect(screen.getByRole('button', { name: 'Add' })).toBeEnabled(); + }); + + test('clicking Add calls onPropertyChange with the new key and value then clears inputs', async () => { + const group = makeGroup(); + const { onPropertyChange } = renderProps(group); + await userEvent.type(screen.getByLabelText('Property name'), 'dose'); + await userEvent.type(screen.getByLabelText('Property value'), '10mg'); + await userEvent.click(screen.getByRole('button', { name: 'Add' })); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'dose', '10mg'); + expect(screen.getByLabelText('Property name')).toHaveValue(''); + expect(screen.getByLabelText('Property value')).toHaveValue(''); + }); + + test('pressing Enter in the key input calls onPropertyChange', async () => { + const group = makeGroup(); + const { onPropertyChange } = renderProps(group); + await userEvent.type(screen.getByLabelText('Property name'), 'dose'); + await userEvent.type(screen.getByLabelText('Property value'), '10mg'); + await userEvent.type(screen.getByLabelText('Property name'), '{Enter}'); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'dose', '10mg'); + }); + + test('pressing Enter in the value input calls onPropertyChange', async () => { + const group = makeGroup(); + const { onPropertyChange } = renderProps(group); + await userEvent.type(screen.getByLabelText('Property name'), 'dose'); + await userEvent.type(screen.getByLabelText('Property value'), '10mg'); + await userEvent.type(screen.getByLabelText('Property value'), '{Enter}'); + expect(onPropertyChange).toHaveBeenCalledWith(group.rowId, 'dose', '10mg'); + }); + + test('Add does not fire when key is whitespace-only', async () => { + const { onPropertyChange } = renderProps(makeGroup()); + await userEvent.type(screen.getByLabelText('Property name'), ' '); + await userEvent.click(screen.getByRole('button', { name: 'Add' })); + expect(onPropertyChange).not.toHaveBeenCalled(); + }); + + test('pressing Enter with a whitespace-only key does not call onPropertyChange', async () => { + // The Add button is disabled for whitespace, but the Enter key handler on the + // input calls handleAdd() directly, which must guard against empty trimmed keys. + const { onPropertyChange } = renderProps(makeGroup()); + await userEvent.type(screen.getByLabelText('Property name'), ' {Enter}'); + expect(onPropertyChange).not.toHaveBeenCalled(); + }); + }); + + describe('known bug: inputs not reset when active group changes', () => { + // This documents the existing behavior where newKey/newValue are NOT reset + // when the active group prop changes (see review finding #15). + // The inputs retain their values across group switches until the component unmounts. + test('newKey input retains value when activeGroup prop changes', async () => { + const group1 = makeGroup({ rowId: 1, name: 'Group 1' }); + const group2 = makeGroup({ rowId: 2, name: 'Group 2' }); + const { rerender } = render( + + ); + await userEvent.type(screen.getByLabelText('Property name'), 'stale-key'); + rerender( + + ); + // Bug: input still shows the value from group1's editing session + expect(screen.getByLabelText('Property name')).toHaveValue('stale-key'); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx index 67e242a2d68..62061b6badd 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { WellGroup } from '../models'; -interface Props { +interface WellGroupPropertiesProps { activeGroup: WellGroup | null; onPropertyChange: (groupRowId: number, key: string, value: string) => void; onDeleteProperty: (groupRowId: number, key: string) => void; @@ -27,7 +27,7 @@ interface Props { * - Adding: the footer row accepts a new key + value; "Add" (or Enter) commits the pair. * The new-key input is the gate — the Add button stays disabled until a key is typed. */ -export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: Props): JSX.Element { +export function WellGroupProperties({ activeGroup, onPropertyChange, onDeleteProperty }: WellGroupPropertiesProps): JSX.Element { const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); @@ -53,6 +53,13 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro
{activeGroup.name}
@@ -161,7 +173,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe {getRowLabel(row)}
+ + + + + + + {propEntries.length === 0 && ( @@ -67,7 +74,7 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro className="well-group-properties__value" type="text" aria-label={key} - value={value ?? ''} + value={value} onChange={e => onPropertyChange(activeGroup.rowId, key, e.target.value)} /> diff --git a/assay/src/client/PlateTemplateDesigner/dev.tsx b/assay/src/client/PlateTemplateDesigner/dev.tsx index e0208c5e08a..d75b4201270 100644 --- a/assay/src/client/PlateTemplateDesigner/dev.tsx +++ b/assay/src/client/PlateTemplateDesigner/dev.tsx @@ -10,12 +10,8 @@ import { ServerContextProvider, withAppUser } from '@labkey/components'; import { PlateTemplateDesigner } from './PlateTemplateDesigner'; -const render = () => { - createRoot(document.getElementById('app')).render( - - - - ); -}; - -render(); +createRoot(document.getElementById('app')).render( + + + +); diff --git a/assay/src/client/PlateTemplateDesigner/models.test.ts b/assay/src/client/PlateTemplateDesigner/models.test.ts new file mode 100644 index 00000000000..90872a9b1ae --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/models.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import { + PlateTemplate, + WellGroup, + computeWarnings, + GROUP_TYPE_CONTROL, + GROUP_TYPE_REPLICATE, + GROUP_TYPE_SPECIMEN, +} from './models'; + +function makePlate(groups: Partial[]): PlateTemplate { + return { + rowId: 1, + name: 'Test Plate', + type: 'assay', + rows: 8, + cols: 12, + groupTypes: [GROUP_TYPE_SPECIMEN, GROUP_TYPE_CONTROL, GROUP_TYPE_REPLICATE], + canCreateGroupsByType: {}, + groups: groups.map((g, i) => ({ + rowId: i + 1, + type: GROUP_TYPE_SPECIMEN, + name: `Group ${i + 1}`, + positions: [], + properties: {}, + allowNewGroups: false, + ...g, + })), + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: true, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', + }; +} + +describe('computeWarnings', () => { + test('returns no warnings for an empty plate', () => { + expect(computeWarnings(makePlate([]))).toEqual([]); + }); + + test('returns no warnings when no cells are assigned', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [] }, + { type: GROUP_TYPE_SPECIMEN, positions: [] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('warns when a REPLICATE cell has no SPECIMEN or CONTROL', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('A1'); + expect(warnings[0]).toContain('replicate'); + }); + + test('no warning when REPLICATE cell is also in a SPECIMEN group', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_SPECIMEN, positions: [{ row: 0, col: 0 }] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('no warning when REPLICATE cell is also in a CONTROL group', () => { + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_CONTROL, positions: [{ row: 0, col: 0 }] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('warns when a cell is in both SPECIMEN and CONTROL groups', () => { + const plate = makePlate([ + { type: GROUP_TYPE_SPECIMEN, positions: [{ row: 1, col: 2 }] }, + { type: GROUP_TYPE_CONTROL, positions: [{ row: 1, col: 2 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('B3'); + expect(warnings[0]).toContain('specimen'); + expect(warnings[0]).toContain('control'); + }); + + test('cell in SPECIMEN + CONTROL + REPLICATE produces only the specimen/control warning', () => { + // REPLICATE warning suppressed because CONTROL is present + const plate = makePlate([ + { type: GROUP_TYPE_SPECIMEN, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_CONTROL, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('specimen'); + expect(warnings[0]).toContain('control'); + }); + + test('cell in an unrelated group type produces no warning', () => { + const plate = makePlate([ + { type: 'UNKNOWN', positions: [{ row: 0, col: 0 }] }, + ]); + expect(computeWarnings(plate)).toEqual([]); + }); + + test('produces separate warnings for multiple problem cells', () => { + const plate = makePlate([ + // Two orphan replicates + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }, { row: 0, col: 1 }] }, + ]); + const warnings = computeWarnings(plate); + expect(warnings).toHaveLength(2); + }); + + test('uses correct spreadsheet cell labels', () => { + // row 0 col 0 → A1 + // row 1 col 11 → B12 + // row 7 col 11 → H12 + const plate = makePlate([ + { + type: GROUP_TYPE_REPLICATE, + positions: [ + { row: 0, col: 0 }, + { row: 1, col: 11 }, + { row: 7, col: 11 }, + ], + }, + ]); + const warnings = computeWarnings(plate); + const labels = warnings.map(w => w.split(':')[0]); + expect(labels).toContain('A1'); + expect(labels).toContain('B12'); + expect(labels).toContain('H12'); + }); + + test('a cell in the same group type twice (two groups, same type) still counts as one type', () => { + // Two REPLICATE groups covering the same cell — the cell is still only REPLICATE-typed + const plate = makePlate([ + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + { type: GROUP_TYPE_REPLICATE, positions: [{ row: 0, col: 0 }] }, + ]); + const warnings = computeWarnings(plate); + // Only one warning for the cell (not two) + expect(warnings).toHaveLength(1); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/models.ts b/assay/src/client/PlateTemplateDesigner/models.ts index 7d8dd6c431f..b8e120e63cd 100644 --- a/assay/src/client/PlateTemplateDesigner/models.ts +++ b/assay/src/client/PlateTemplateDesigner/models.ts @@ -47,6 +47,10 @@ export interface PlateTemplate { * - A cell can appear in multiple groups of different types (e.g. SPECIMEN + REPLICATE together is fine). * - Cell labels use spreadsheet notation: row → letter (A=0, B=1, …), col → 1-based number. */ +export const GROUP_TYPE_REPLICATE = 'REPLICATE'; +export const GROUP_TYPE_SPECIMEN = 'SPECIMEN'; +export const GROUP_TYPE_CONTROL = 'CONTROL'; + export function computeWarnings(plate: PlateTemplate): string[] { // Build a map from cell position → set of group types that include it. const cellTypes = new Map>(); @@ -61,9 +65,9 @@ export function computeWarnings(plate: PlateTemplate): string[] { for (const [key, types] of cellTypes.entries()) { const [row, col] = key.split(',').map(Number); const cellLabel = `${String.fromCharCode(65 + row)}${col + 1}`; - const hasReplicate = types.has('REPLICATE'); - const hasSpecimen = types.has('SPECIMEN'); - const hasControl = types.has('CONTROL'); + const hasReplicate = types.has(GROUP_TYPE_REPLICATE); + const hasSpecimen = types.has(GROUP_TYPE_SPECIMEN); + const hasControl = types.has(GROUP_TYPE_CONTROL); if (hasReplicate && !(hasSpecimen || hasControl)) { warnings.push(`${cellLabel}: Well is a replicate, but is not part of a specimen or control group.`); } diff --git a/assay/test/js/fileMock.js b/assay/test/js/fileMock.js new file mode 100644 index 00000000000..f053ebf7976 --- /dev/null +++ b/assay/test/js/fileMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/assay/test/js/setup.ts b/assay/test/js/setup.ts new file mode 100644 index 00000000000..7b0828bfa80 --- /dev/null +++ b/assay/test/js/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; From 6851d9c12c3665e3f712b4b246b30c7e4aa3df58 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Tue, 28 Apr 2026 09:45:06 -0700 Subject: [PATCH 7/9] Another checkpoint --- .../PlateTemplateDesigner.scss | 23 +-- .../PlateTemplateDesigner.test.tsx | 141 ++++++++++++++++++ .../PlateTemplateDesigner.tsx | 13 +- .../components/GroupTypesPanel.tsx | 126 +++++++++++----- .../components/MultiCreateDialog.tsx | 117 ++++++++------- .../components/RightPanel.test.tsx | 32 ++-- .../components/RightPanel.tsx | 65 ++++---- .../components/StatusBar.tsx | 3 +- .../components/TabButton.test.tsx | 88 +++++++++++ .../components/TabButton.tsx | 37 +++++ .../components/TemplateGrid.tsx | 10 +- .../components/WellGroupProperties.tsx | 9 +- 12 files changed, 513 insertions(+), 151 deletions(-) create mode 100644 assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx create mode 100644 assay/src/client/PlateTemplateDesigner/components/TabButton.tsx diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss index 015dfac6eab..f814107cf20 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.scss @@ -314,6 +314,14 @@ } } + // Inline "Delete?" prompt label shown before the Yes / No confirmation buttons. + &__confirm-text { + font-size: 0.75rem; + color: #333; + margin-right: 2px; + white-space: nowrap; + } + // Row that holds the new-group name input + Create / Create multiple buttons &__create-row { display: flex; @@ -364,9 +372,10 @@ } // ── Multi-create dialog ─────────────────────────────────────────────────────── -// Modal overlay + dialog for batch-creating N numbered groups. -// The overlay captures click-outside-to-close; the inner dialog stops propagation. -// Focus is trapped inside the dialog while open (see GroupTypesPanel focus-trap effect). +// Modal dialog for batch-creating N numbered groups. +// Uses the native element with showModal() for top-layer placement, focus +// restoration on close, and a browser-managed backdrop. Focus is trapped inside the +// dialog while open (see MultiCreateDialog focus-trap effect). .multi-create-dialog { background: #fff; border: 1px solid #ccc; @@ -375,14 +384,8 @@ padding: 20px; min-width: 300px; - &__overlay { - position: fixed; - inset: 0; + &::backdrop { background: rgba(0, 0, 0, 0.3); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; } &__title { diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx new file mode 100644 index 00000000000..eadbde173c6 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.test.tsx @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { Ajax } from '@labkey/api'; + +import { PlateTemplateDesigner } from './PlateTemplateDesigner'; +import { PlateTemplate } from './models'; + +jest.mock('@labkey/api', () => ({ + ActionURL: { + getParameter: jest.fn().mockReturnValue(null), + buildURL: jest.fn().mockReturnValue('/mock-url'), + }, + Ajax: { + request: jest.fn(), + }, + Utils: { + // Pass the callback through unchanged so tests can invoke it directly. + getCallbackWrapper: (fn: any) => fn, + }, +})); + +const mockPlate: PlateTemplate = { + rowId: 1, + name: 'Test Plate', + type: 'assay', + rows: 4, + cols: 4, + groupTypes: ['SPECIMEN', 'CONTROL'], + canCreateGroupsByType: { SPECIMEN: true, CONTROL: true }, + groups: [], + plateProperties: {}, + typesToDefaultGroups: {}, + showWarningPanel: false, + existingTemplateNames: [], + copyMode: false, + defaultPlateName: '', +}; + +describe('PlateTemplateDesigner', () => { + let successCallback: ((response: any) => void) | undefined; + let failureCallback: ((response: any) => void) | undefined; + + beforeEach(() => { + jest.clearAllMocks(); + (Ajax.request as jest.Mock).mockImplementation(({ success, failure }) => { + successCallback = success; + failureCallback = failure; + }); + }); + + afterEach(() => { + successCallback = undefined; + failureCallback = undefined; + }); + + describe('initial load', () => { + test('shows loading state before data arrives', () => { + render(); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + test('calls Ajax.request on mount', () => { + render(); + expect(Ajax.request).toHaveBeenCalledTimes(1); + }); + + test('renders the plate name input after a successful load', () => { + render(); + act(() => successCallback?.({ data: mockPlate })); + expect(screen.getByDisplayValue('Test Plate')).toBeInTheDocument(); + }); + + test('renders group type tabs after a successful load', () => { + render(); + act(() => successCallback?.({ data: mockPlate })); + expect(screen.getByRole('tab', { name: 'SPECIMEN' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'CONTROL' })).toBeInTheDocument(); + }); + + test('uses defaultPlateName as the initial plate name when provided', () => { + render(); + act(() => successCallback?.({ + data: { ...mockPlate, defaultPlateName: 'Copy of Test Plate', name: 'Test Plate' }, + })); + expect(screen.getByDisplayValue('Copy of Test Plate')).toBeInTheDocument(); + }); + + test('starts in dirty state when copyMode is true', () => { + render(); + act(() => successCallback?.({ data: { ...mockPlate, copyMode: true } })); + expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); + }); + }); + + describe('load failure', () => { + test('renders the server exception message on failure', () => { + render(); + act(() => failureCallback?.({ exception: 'Server error' })); + expect(screen.getByText('Server error')).toBeInTheDocument(); + }); + + test('renders a fallback message when exception is missing', () => { + render(); + act(() => failureCallback?.({})); + expect(screen.getByText('Failed to load plate template.')).toBeInTheDocument(); + }); + }); + + describe('plate name editing', () => { + async function renderLoaded() { + render(); + act(() => successCallback?.({ data: mockPlate })); + return screen.getByDisplayValue('Test Plate'); + } + + test('changing the plate name marks the form dirty', async () => { + const input = await renderLoaded(); + await userEvent.clear(input); + await userEvent.type(input, 'My Plate'); + expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); + }); + + test('Save button is disabled when form is clean', async () => { + await renderLoaded(); + expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); + }); + + test('Save button is enabled after a name change', async () => { + const input = await renderLoaded(); + await userEvent.clear(input); + await userEvent.type(input, 'My Plate'); + expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled(); + }); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 29a69e19167..23a55c1c63b 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -9,7 +9,7 @@ import { ActionURL, Ajax, Utils } from '@labkey/api'; import { PlateTemplate, Position, WellGroup, computeWarnings } from './models'; import { StatusBar } from './components/StatusBar'; import { GroupTypesPanel } from './components/GroupTypesPanel'; -import { RightPanel } from './components/RightPanel'; +import { RIGHT_TAB_PROPERTIES, RightPanel, RightTab } from './components/RightPanel'; import { ShiftPanel } from './components/ShiftPanel'; import { TemplateGrid } from './components/TemplateGrid'; @@ -110,7 +110,7 @@ export function PlateTemplateDesigner(): JSX.Element { const [plate, setPlate] = useState(null); const [activeGroup, setActiveGroup] = useState(null); const [activeTab, setActiveTab] = useState(''); - const [rightTab, setRightTab] = useState<'properties' | 'warnings'>('properties'); + const [rightTab, setRightTab] = useState(RIGHT_TAB_PROPERTIES); const [isDirty, setIsDirty] = useState(false); const [status, setStatus] = useState(''); const [colorMap, setColorMap] = useState>(new Map()); @@ -387,6 +387,13 @@ export function PlateTemplateDesigner(): JSX.Element { setActiveGroup(null); }, []); + // Clear pending status timer on unmount to prevent setState on an unmounted component. + useEffect(() => { + return () => { + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + }; + }, []); + // Warn on unsaved navigation useEffect(() => { const handler = (e: BeforeUnloadEvent) => { @@ -425,7 +432,7 @@ export function PlateTemplateDesigner(): JSX.Element { } if (!plate) { - return
Loading...
; + return
Loading...
; } return ( diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 851ec7072da..449ceab4524 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { PlateTemplate, WellGroup } from '../models'; import { MultiCreateDialog } from './MultiCreateDialog'; +import { TabButton } from './TabButton'; interface GroupTypesPanelProps { plate: PlateTemplate; @@ -81,6 +82,8 @@ export function GroupTypesPanel({ const [renameValue, setRenameValue] = useState(''); const [renameError, setRenameError] = useState(null); const [multiCreateOpen, setMultiCreateOpen] = useState(false); + // rowId of the group awaiting inline delete confirmation; null when no confirmation is pending. + const [confirmDeleteId, setConfirmDeleteId] = useState(null); // Stable derived list — memoized so useMemo and useEffect deps are stable. const groupsOfType = useMemo(() => plate.groups.filter(g => g.type === activeTab), [plate, activeTab]); @@ -96,10 +99,11 @@ export function GroupTypesPanel({ return defaults.filter(d => !groupsOfType.some(g => g.name === d)); }, [plate, activeTab, groupsOfType]); - // Reset create-input when tab changes + // Reset create-input and transient UI state when tab changes useEffect(() => { setNewGroupName(unusedDefaults[0] ?? ''); setRenamingId(null); + setConfirmDeleteId(null); }, [activeTab]); // eslint-disable-line react-hooks/exhaustive-deps // Advance to next unused default when the current one gets used @@ -118,9 +122,18 @@ export function GroupTypesPanel({ const handleDeleteClick = (e: React.MouseEvent, group: WellGroup) => { e.stopPropagation(); - if (window.confirm(`Delete well group "${group.name}"?`)) { - onDeleteGroup(group.rowId); - } + setConfirmDeleteId(group.rowId); + }; + + const handleDeleteConfirm = (e: React.MouseEvent, rowId: number) => { + e.stopPropagation(); + onDeleteGroup(rowId); + setConfirmDeleteId(null); + }; + + const handleDeleteCancel = (e: React.MouseEvent) => { + e.stopPropagation(); + setConfirmDeleteId(null); }; const handleRenameClick = (e: React.MouseEvent, group: WellGroup) => { @@ -150,21 +163,33 @@ export function GroupTypesPanel({ return (
-
+
) => { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; + const tabs = Array.from(e.currentTarget.querySelectorAll('[role="tab"]')); + const currentIndex = tabs.findIndex(t => t === document.activeElement); + if (currentIndex === -1) return; + e.preventDefault(); + const next = e.key === 'ArrowLeft' + ? (currentIndex - 1 + tabs.length) % tabs.length + : (currentIndex + 1) % tabs.length; + tabs[next].click(); + tabs[next].focus(); + }} + > {plate.groupTypes.map(type => ( - + ))}
{plate.groupTypes.map(type => ( @@ -178,7 +203,7 @@ export function GroupTypesPanel({ > {type === activeTab && ( <> -
+
{groupsOfType.map(group => { const color = colorMap.get(group.rowId); const isActive = activeGroup?.rowId === group.rowId; @@ -189,8 +214,8 @@ export function GroupTypesPanel({ className={classNames('group-types-panel__group', { 'group-types-panel__group--active': isActive, })} - role="option" - aria-selected={isActive} + role="listitem" + aria-current={isActive ? true : undefined} tabIndex={0} onClick={() => { if (!isRenaming) onGroupSelect(group); }} onKeyDown={e => { @@ -231,31 +256,52 @@ export function GroupTypesPanel({ The --hidden modifier keeps them invisible and non-interactive on unselected / renaming rows. */} {group.allowNewGroups && ( - - - + + + ) : ( + - + + + + ) )}
{isRenaming && renameError && ( diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx index a86e2a127f4..bb27be5c028 100644 --- a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx @@ -16,17 +16,20 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC const [baseName, setBaseName] = useState(initialBaseName); const [count, setCount] = useState('2'); const [countError, setCountError] = useState(''); - const dialogRef = useRef(null); + const dialogRef = useRef(null); - // Focus trap: runs once on mount since the component only renders when the dialog is open. useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; + + // Open as a modal (top layer + native backdrop; also restores focus on close). + dialog.showModal(); + + // Focus trap: cycle Tab/Shift-Tab within the dialog. const focusableSelectors = 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; const getFocusable = () => Array.from(dialog.querySelectorAll(focusableSelectors)); const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { onClose(); return; } if (e.key !== 'Tab') return; const focusable = getFocusable(); const first = focusable[0]; @@ -38,9 +41,24 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC } }; + // The native fires a `cancel` event on Escape before closing itself. + // Prevent the browser's default close so we control the unmounting via onClose. + const handleCancel = (e: Event) => { + e.preventDefault(); + onClose(); + }; + dialog.addEventListener('keydown', handleKeyDown); + dialog.addEventListener('cancel', handleCancel); + + // Move focus to the first focusable element inside the dialog. getFocusable()[0]?.focus(); - return () => dialog.removeEventListener('keydown', handleKeyDown); + + return () => { + dialog.removeEventListener('keydown', handleKeyDown); + dialog.removeEventListener('cancel', handleCancel); + if (dialog.open) dialog.close(); + }; }, []); // eslint-disable-line react-hooks/exhaustive-deps const handleCreate = () => { @@ -61,59 +79,54 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC }; return ( -
-
e.stopPropagation()} - > -
Create Multiple Groups
-
-
- Base Name + +
Create Multiple Groups
+
+
+ Base Name + setBaseName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> +
+
+ Count +
setBaseName(e.target.value)} + className="multi-create-dialog__input multi-create-dialog__input--count" + type="number" + min="1" + aria-labelledby="multi-create-count-label" + aria-describedby={countError ? 'multi-create-count-error' : undefined} + aria-invalid={!!countError} + value={count} + onChange={e => { setCount(e.target.value); setCountError(''); }} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} /> + {countError &&
{countError}
}
-
- Count -
- { setCount(e.target.value); setCountError(''); }} - onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} - /> - {countError &&
{countError}
} -
-
-
- - -
+
+
+ +
-
+
); } diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx index ccd23a5188c..c7cffcb5936 100644 --- a/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; -import { RightPanel } from './RightPanel'; +import { RIGHT_TAB_PROPERTIES, RIGHT_TAB_WARNINGS, RightPanel } from './RightPanel'; function renderPanel(overrides: Partial> = {}) { const props = { showWarningPanel: false, - rightTab: 'properties' as const, + rightTab: RIGHT_TAB_PROPERTIES, onRightTabChange: jest.fn(), warnings: [], activeGroup: null, @@ -43,19 +43,19 @@ describe('RightPanel — without warning panel', () => { describe('RightPanel — with warning panel, properties tab', () => { test('renders a tablist with two tabs', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); expect(screen.getByRole('tablist')).toBeInTheDocument(); expect(screen.getAllByRole('tab')).toHaveLength(2); }); test('properties tab is aria-selected, warnings tab is not', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'false'); }); test('properties tabpanel is rendered with correct id and aria-labelledby', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); const panel = document.getElementById('right-panel-properties'); expect(panel).toBeInTheDocument(); expect(panel).toHaveAttribute('role', 'tabpanel'); @@ -63,54 +63,54 @@ describe('RightPanel — with warning panel, properties tab', () => { }); test('warnings tabpanel is hidden when rightTab is "properties"', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); expect(document.getElementById('right-panel-warnings')).toHaveAttribute('hidden'); }); test('warnings tab label shows count badge when warnings exist', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: ['w1', 'w2', 'w3'] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES, warnings: ['w1', 'w2', 'w3'] }); expect(screen.getByRole('tab', { name: 'Warnings (3)' })).toBeInTheDocument(); }); test('warnings tab label shows plain "Warnings" when list is empty', () => { - renderPanel({ showWarningPanel: true, rightTab: 'properties', warnings: [] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES, warnings: [] }); expect(screen.getByRole('tab', { name: 'Warnings' })).toBeInTheDocument(); }); test('clicking warnings tab calls onRightTabChange with "warnings"', async () => { - const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'properties' }); + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_PROPERTIES }); await userEvent.click(screen.getByRole('tab', { name: 'Warnings' })); - expect(onRightTabChange).toHaveBeenCalledWith('warnings'); + expect(onRightTabChange).toHaveBeenCalledWith(RIGHT_TAB_WARNINGS); }); test('clicking properties tab calls onRightTabChange with "properties"', async () => { - const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + const { onRightTabChange } = renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS }); await userEvent.click(screen.getByRole('tab', { name: 'Well Group Properties' })); - expect(onRightTabChange).toHaveBeenCalledWith('properties'); + expect(onRightTabChange).toHaveBeenCalledWith(RIGHT_TAB_PROPERTIES); }); }); describe('RightPanel — with warning panel, warnings tab', () => { test('renders warnings tabpanel; properties tabpanel is hidden', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: ['A1: warning'] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS, warnings: ['A1: warning'] }); expect(document.getElementById('right-panel-warnings')).toBeInTheDocument(); expect(document.getElementById('right-panel-properties')).toHaveAttribute('hidden'); }); test('warnings tab is aria-selected, properties tab is not', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS }); expect(screen.getByRole('tab', { name: 'Warnings' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('tab', { name: 'Well Group Properties' })).toHaveAttribute('aria-selected', 'false'); }); test('warnings tabpanel has correct aria-labelledby', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings' }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS }); const panel = document.getElementById('right-panel-warnings'); expect(panel).toHaveAttribute('aria-labelledby', 'right-tab-warnings'); }); test('WarningPanel content is visible in warnings tab', () => { - renderPanel({ showWarningPanel: true, rightTab: 'warnings', warnings: [] }); + renderPanel({ showWarningPanel: true, rightTab: RIGHT_TAB_WARNINGS, warnings: [] }); expect(screen.getByText('No warnings.')).toBeInTheDocument(); }); }); diff --git a/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx index 4d24c11247f..9e8e55579eb 100644 --- a/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/RightPanel.tsx @@ -4,16 +4,20 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React from 'react'; -import classNames from 'classnames'; import { WellGroup } from '../models'; +import { TabButton } from './TabButton'; import { WellGroupProperties } from './WellGroupProperties'; import { WarningPanel } from './WarningPanel'; +export const RIGHT_TAB_PROPERTIES = 'properties' as const; +export const RIGHT_TAB_WARNINGS = 'warnings' as const; +export type RightTab = typeof RIGHT_TAB_PROPERTIES | typeof RIGHT_TAB_WARNINGS; + interface RightPanelProps { showWarningPanel: boolean; - rightTab: 'properties' | 'warnings'; - onRightTabChange: (tab: 'properties' | 'warnings') => void; + rightTab: RightTab; + onRightTabChange: (tab: RightTab) => void; warnings: string[]; activeGroup: WellGroup | null; onPropertyChange: (groupRowId: number, key: string, value: string) => void; @@ -26,40 +30,49 @@ export function RightPanel(props: RightPanelProps): JSX.Element { return (
- {showWarningPanel && ( -
- - +
)} diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx index 2af5b740051..80a539bd161 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.tsx @@ -48,7 +48,8 @@ export function StatusBar({ isDirty, status, plateName, onSaveAndClose, onSave, const validateAndSave = () => { if (validate()) onSave(); }; - const validateAndSaveAndClose = () => { if (validate()) onSaveAndClose(); }; + // Skip validation when there is nothing to save — the parent will navigate away without writing. + const validateAndSaveAndClose = () => { if (!isDirty || validate()) onSaveAndClose(); }; return (
diff --git a/assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx b/assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx new file mode 100644 index 00000000000..c229a5ef014 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TabButton.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { TabButton } from './TabButton'; + +function renderTab(overrides: Partial> = {}) { + const props = { + id: 'my-tab', + panelId: 'my-panel', + isActive: false, + baseClass: 'my-tab', + onClick: jest.fn(), + children: 'Tab Label', + ...overrides, + }; + render(); + return props; +} + +describe('TabButton', () => { + test('renders with role="tab"', () => { + renderTab(); + expect(screen.getByRole('tab')).toBeInTheDocument(); + }); + + test('sets id on the button element', () => { + renderTab({ id: 'right-tab-properties' }); + expect(screen.getByRole('tab')).toHaveAttribute('id', 'right-tab-properties'); + }); + + test('sets aria-controls to panelId', () => { + renderTab({ panelId: 'right-panel-properties' }); + expect(screen.getByRole('tab')).toHaveAttribute('aria-controls', 'right-panel-properties'); + }); + + test('aria-selected is true when isActive is true', () => { + renderTab({ isActive: true }); + expect(screen.getByRole('tab', { selected: true })).toBeInTheDocument(); + }); + + test('aria-selected is false when isActive is false', () => { + renderTab({ isActive: false }); + expect(screen.getByRole('tab', { selected: false })).toBeInTheDocument(); + }); + + test('applies baseClass to the button', () => { + renderTab({ baseClass: 'my-tab' }); + expect(screen.getByRole('tab')).toHaveClass('my-tab'); + }); + + test('adds --active when isActive is true', () => { + renderTab({ baseClass: 'my-tab', isActive: true }); + expect(screen.getByRole('tab')).toHaveClass('my-tab--active'); + }); + + test('does not add --active when isActive is false', () => { + renderTab({ baseClass: 'my-tab', isActive: false }); + expect(screen.getByRole('tab')).not.toHaveClass('my-tab--active'); + }); + + test('applies extraClassName when provided', () => { + renderTab({ extraClassName: 'my-tab--warn' }); + expect(screen.getByRole('tab')).toHaveClass('my-tab--warn'); + }); + + test('does not add unexpected classes when extraClassName is omitted', () => { + renderTab({ baseClass: 'my-tab', isActive: false }); + expect(screen.getByRole('tab')).toHaveClass('my-tab'); + expect(screen.getByRole('tab').className.trim()).toBe('my-tab'); + }); + + test('renders children', () => { + renderTab({ children: 'Well Group Properties' }); + expect(screen.getByText('Well Group Properties')).toBeInTheDocument(); + }); + + test('calls onClick when clicked', async () => { + const { onClick } = renderTab(); + await userEvent.click(screen.getByRole('tab')); + expect(onClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/assay/src/client/PlateTemplateDesigner/components/TabButton.tsx b/assay/src/client/PlateTemplateDesigner/components/TabButton.tsx new file mode 100644 index 00000000000..5809c4ea9f8 --- /dev/null +++ b/assay/src/client/PlateTemplateDesigner/components/TabButton.tsx @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +import React from 'react'; +import classNames from 'classnames'; + +interface TabButtonProps { + id: string; + panelId: string; + isActive: boolean; + baseClass: string; + extraClassName?: string; + onClick: () => void; + children: React.ReactNode; +} + +/** + * A single ARIA tab button. Handles `role="tab"`, `aria-controls`, `aria-selected`, + * and the BEM `--active` modifier. Use inside a `role="tablist"` container. + */ +export function TabButton({ id, panelId, isActive, baseClass, extraClassName, onClick, children }: TabButtonProps): JSX.Element { + return ( + + ); +} diff --git a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx index dc7a2cc0558..7882dfd4771 100644 --- a/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/TemplateGrid.tsx @@ -97,7 +97,9 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe dragIsUnselect.current = activeGroup?.positions.some(p => p.row === row && p.col === col) ?? false; // Snapshot the current positions NOW, from the prop, before any drag events can modify state. preDragPositions.current = activeGroup?.positions ?? []; - e.preventDefault(); + // Note: text selection during drag is already prevented by `user-select: none` in CSS, + // so e.preventDefault() is not needed here and is intentionally omitted so the browser's + // default focus-on-mousedown behaviour is preserved. }, [activeGroup]); const handleMouseEnter = useCallback((row: number, col: number) => { @@ -109,6 +111,9 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe // Called on mouseup over a specific cell — handles click-toggle const handleCellMouseUp = useCallback((row: number, col: number) => { if (isDragging.current && !hasMoved.current) { + // Explicitly move focus to the clicked cell so arrow-key navigation + // picks up from the correct position after a mouse interaction. + cellRefs.current.get(`${row},${col}`)?.focus(); onCellToggle(row, col); } }, [onCellToggle]); @@ -158,7 +163,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return (
-
PropertyValueActions
+
@@ -182,6 +187,7 @@ export function TemplateGrid({ plate, activeGroup, activeTab, colorMap, onDragRe return ( { const key = `${row},${col}`; if (el) cellRefs.current.set(key, el); diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx index 62061b6badd..1e520886c77 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.tsx @@ -3,7 +3,7 @@ * * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { WellGroup } from '../models'; @@ -31,6 +31,13 @@ export function WellGroupProperties({ activeGroup, onPropertyChange, onDeletePro const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); + // Reset draft inputs when the selected group changes so stale text from a + // previous group cannot accidentally be committed to the newly selected one. + useEffect(() => { + setNewKey(''); + setNewValue(''); + }, [activeGroup?.rowId]); + if (!activeGroup) { return (
From 28daef787f527a2acea922ee6cd173cb0e173fe7 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Tue, 28 Apr 2026 17:30:11 -0700 Subject: [PATCH 8/9] Code review improvements, generics updates --- .../org/labkey/api/assay/plate/Plate.java | 6 +- .../PlateTemplateDesigner.tsx | 4 +- .../src/org/labkey/assay/PlateController.java | 381 +++++++++--------- .../assay/plate/PlateDataServiceImpl.java | 4 +- .../src/org/labkey/assay/plate/PlateImpl.java | 15 +- .../org/labkey/assay/plate/PlateManager.java | 16 +- .../labkey/assay/plate/PlateManagerTest.java | 8 +- .../org/labkey/assay/plate/WellGroupImpl.java | 4 +- 8 files changed, 224 insertions(+), 214 deletions(-) diff --git a/assay/api-src/org/labkey/api/assay/plate/Plate.java b/assay/api-src/org/labkey/api/assay/plate/Plate.java index c754bdcd7e2..ea0c8973633 100644 --- a/assay/api-src/org/labkey/api/assay/plate/Plate.java +++ b/assay/api-src/org/labkey/api/assay/plate/Plate.java @@ -66,11 +66,11 @@ public interface Plate extends PropertySet, Identifiable @Nullable WellGroup getWellGroup(int rowId); - @NotNull List getWellGroups(); + @NotNull List getWellGroups(); - @NotNull List getWellGroups(Position position); + @NotNull List getWellGroups(Position position); - @NotNull List getWellGroups(WellGroup.Type type); + @NotNull List getWellGroups(WellGroup.Type type); @NotNull Map> getWellGroupMap(); diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 23a55c1c63b..6489b79f15c 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -142,7 +142,7 @@ export function PlateTemplateDesigner(): JSX.Element { params.copy = copy; Ajax.request({ - url: ActionURL.buildURL('plate', 'getTemplateDefinition.api'), + url: ActionURL.buildURL('plate', 'getDesignerTemplateDefinition.api'), method: 'GET', params, success: Utils.getCallbackWrapper((response: { data: PlateTemplate }) => { @@ -338,7 +338,7 @@ export function PlateTemplateDesigner(): JSX.Element { const requestSave = useCallback((currentPlate: PlateTemplate, onSuccess: (response: { data: { rowId: number } }) => void) => { setStatus('Saving...'); Ajax.request({ - url: ActionURL.buildURL('plate', 'saveTemplate.api'), + url: ActionURL.buildURL('plate', 'saveDesignerTemplate.api'), method: 'POST', jsonData: currentPlate, success: Utils.getCallbackWrapper(onSuccess), diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index dffcee52849..4469022ce8a 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -15,15 +15,12 @@ */ package org.labkey.assay; -import org.apache.commons.io.input.BoundedInputStream; import org.apache.logging.log4j.Logger; import org.json.JSONArray; import org.json.JSONObject; import org.labkey.api.action.ApiJsonForm; -import org.labkey.api.action.ApiUsageException; -import org.labkey.api.action.BaseApiAction; import org.labkey.api.action.FormHandlerAction; -import org.labkey.api.action.NullSafeBindException; +import org.labkey.api.action.JsonInputLimit; import org.labkey.api.action.FormViewAction; import org.labkey.api.action.GWTServiceAction; import org.labkey.api.action.Marshal; @@ -112,7 +109,7 @@ public class PlateController extends SpringActionController private static final SpringActionController.DefaultActionResolver _actionResolver = new DefaultActionResolver(PlateController.class); private static final Logger LOG = LogHelper.getLogger(PlateController.class, "Controller for plate related actions"); - record SubmittedGroup(int rowId, String type, String name, List positions, Map properties) + public record SubmittedGroup(int rowId, String type, String name, List positions, Map properties) { public static SubmittedGroup from(JSONObject g) { @@ -245,6 +242,130 @@ public void setRowId(int rowId) } } + @Marshal(Marshaller.JSONObject) + @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + @JsonInputLimit(10 * 1024 * 1024) + public static class SaveDesignerTemplateAction extends MutatingApiAction + { + private PlateType _plateType; + + @Override + public Object execute(CreatePlateForm form, BindException errors) throws Exception + { + Long rowId = form.getRowId(); + String name = form.getName(); + boolean updateExisting = false; + PlateImpl plate; + + if (rowId != null && rowId > 0) + { + plate = PlateManager.get().getPlate(getContainer(), rowId); + if (plate == null) + throw new NotFoundException("Plate template not found: " + rowId); + Plate conflict = PlateManager.get().getPlateByName(getContainer(), name); + if (conflict != null && !conflict.getRowId().equals(plate.getRowId())) + throw new ValidationException("A plate template with name '" + name + "' already exists."); + if (!plate.getAssayType().equals(form.getAssayType())) + throw new ValidationException("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + name + "'"); + if (plate.getRows() != form.getRows() || plate.getColumns() != form.getCols()) + throw new ValidationException("Plate template dimensions cannot be changed for '" + name + "'"); + updateExisting = true; + } + else + { + if (PlateManager.get().getPlateByName(getContainer(), name) != null) + throw new ValidationException("A plate template with name '" + name + "' already exists."); + plate = PlateManager.get().createPlate(getContainer(), form.getAssayType(), _plateType); + plate.setTemplate(true); + } + + plate.setName(name); + plate.setProperties(form.getPlateProperties()); + + List submittedGroups = form.getGroups(); + Set submittedGroupIds = new HashSet<>(); + for (SubmittedGroup g : submittedGroups) + if (g.rowId() > 0) + submittedGroupIds.add(g.rowId()); + + // Mark well groups absent from the submission for deletion + List existingWellGroups = plate.getWellGroups(); + for (WellGroup existingGroup : existingWellGroups) + if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) + plate.markWellGroupForDeletion(existingGroup); + + // Update existing or create new well groups + for (SubmittedGroup gm : submittedGroups) + { + WellGroup.Type groupType; + try + { + groupType = WellGroup.Type.valueOf(gm.type()); + } + catch (IllegalArgumentException e) + { + throw new ValidationException("Unknown well group type: '" + gm.type() + "'"); + } + + List positions = new ArrayList<>(); + for (PlatePosition p : gm.positions()) + positions.add(plate.getPosition(p.row(), p.col())); + + WellGroupImpl group; + if (updateExisting && gm.rowId() > 0) + { + group = findExistingWellGroup(existingWellGroups, gm.rowId()); + if (group == null) + throw new ValidationException("Well group " + gm.rowId() + " was not found."); + if (group.getType() != groupType) + throw new ValidationException("Well group type cannot be changed: " + gm.name()); + group.setName(gm.name()); + group.setPositions(positions); + plate.storeWellGroup(group); + } + else + { + group = plate.addWellGroup(gm.name(), groupType, positions); + } + group.setProperties(gm.properties()); + } + + PlateLayoutHandler handler = PlateManager.get().getPlateLayoutHandler(plate.getAssayType()); + if (handler == null) + throw new NotFoundException("Invalid assay type"); + handler.validatePlate(getContainer(), getUser(), plate); + long savedRowId = PlateService.get().save(getContainer(), getUser(), plate); + return success(Map.of("rowId", savedRowId)); + } + + private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) + { + for (WellGroupImpl wg : wellGroups) + if (wg.getRowId() != null && wg.getRowId() == rowId) + return wg; + return null; + } + + @Override + public void validateForm(CreatePlateForm form, Errors errors) + { + if (form.getGroups() == null || form.getGroups().isEmpty()) + { + errors.reject(ERROR_REQUIRED, "At least one group is required."); + } + // Template designer (groups) path: plateType resolved by rowId lookup on update, + // or by rows×cols on create. + if (form.getRowId() == null || form.getRowId() <= 0) + { + _plateType = form.getPlateType() != null + ? PlateManager.get().getPlateType(form.getPlateType()) + : PlateService.get().getPlateType(form.getRows(), form.getCols()); + if (_plateType == null) + errors.reject(ERROR_REQUIRED, "The plate type (" + form.getRows() + " x " + form.getCols() + ") does not exist."); + } + } + } + @RequiresPermission(ReadPermission.class) public static class PlateDetailsAction extends SimpleRedirectAction { @@ -262,7 +383,7 @@ public ActionURL getRedirectURL(RowIdForm form) } @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public class GetTemplateDefinitionAction extends ReadOnlyApiAction + public class GetDesignerTemplateDefinitionAction extends ReadOnlyApiAction { @Override public Object execute(DesignerForm form, BindException errors) throws Exception @@ -286,13 +407,13 @@ public Object execute(DesignerForm form, BindException errors) throws Exception if (templateName != null) { if (plateId == null) - throw new Exception("plateId is required when templateName is specified."); + throw new NotFoundException("plateId is required when templateName is specified."); template = PlateService.get().getPlate(getContainer(), plateId); if (template == null) - throw new NotFoundException("Plate '" + templateName + "' does not exist."); + throw new NotFoundException("Plate referenced by plateId does not exist."); handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); if (handler == null) - throw new Exception("Plate template type '" + template.getAssayType() + "' does not exist."); + throw new ValidationException("Plate template type '" + template.getAssayType() + "' does not exist."); } else { @@ -303,10 +424,10 @@ public Object execute(DesignerForm form, BindException errors) throws Exception handler = PlateManager.get().getPlateLayoutHandler(assayTypeName); if (handler == null) - throw new Exception("Plate template type '" + assayTypeName + "' does not exist."); + throw new ValidationException("Plate template type '" + assayTypeName + "' does not exist."); PlateType plateType = PlateService.get().getPlateType(rowCount, colCount); if (plateType == null) - throw new Exception("The plate type (" + rowCount + " x " + colCount + ") does not exist."); + throw new ValidationException("The plate type (" + rowCount + " x " + colCount + ") does not exist."); template = handler.createPlate(templateTypeName, getContainer(), plateType); } @@ -395,182 +516,6 @@ public Object execute(DesignerForm form, BindException errors) throws Exception } } - public static class SaveTemplateForm implements ApiJsonForm - { - private JSONObject _json; - - @Override - public void bindJson(JSONObject json) - { - _json = json; - } - - public JSONObject getJson() - { - return _json != null ? _json : new JSONObject(); - } - } - - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) - public static class SaveTemplateAction extends MutatingApiAction - { - private static final int MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB - - @Override - protected BaseApiAction.FormAndErrors populateJacksonForm() throws Exception - { - byte[] bytes; - try (BoundedInputStream bounded = BoundedInputStream.builder() - .setInputStream(getViewContext().getRequest().getInputStream()) - .setMaxCount((long) MAX_BODY_BYTES + 1) - .get()) - { - bytes = bounded.readAllBytes(); - } - if (bytes.length > MAX_BODY_BYTES) - throw new ApiUsageException("Request body exceeds maximum allowed size of 10 MB."); - String body = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); - JSONObject jsonObj = body.isEmpty() ? new JSONObject() : new JSONObject(body); - SaveTemplateForm form = new SaveTemplateForm(); - form.bindJson(jsonObj); - return new BaseApiAction.FormAndErrors<>(form, new NullSafeBindException(form, "form")); - } - - @Override - public Object execute(SaveTemplateForm form, BindException errors) throws Exception - { - JSONObject json = form.getJson(); - - long rowId = json.optLong("rowId", -1); - String name = json.getString("name"); - String type = json.getString("type"); - int rows = json.getInt("rows"); - int cols = json.getInt("cols"); - JSONArray groupsJson = json.optJSONArray("groups"); - JSONObject platePropsJson = json.optJSONObject("plateProperties"); - - Map plateProperties = new HashMap<>(); - if (platePropsJson != null) - { - for (String key : platePropsJson.keySet()) - plateProperties.put(key, platePropsJson.get(key)); - } - - boolean updateExisting = false; - PlateImpl plate; - if (rowId > 0) - { - plate = PlateManager.get().getPlate(getContainer(), rowId); - if (plate == null) - throw new NotFoundException("Plate template not found: " + rowId); - // Check for a conflicting name from a different plate - Plate conflict = PlateManager.get().getPlateByName(getContainer(), name); - if (conflict != null && !conflict.getRowId().equals(plate.getRowId())) - throw new ApiUsageException("A plate template with name '" + name + "' already exists."); - if (!plate.getAssayType().equals(type)) - throw new ApiUsageException("Plate template type '" + plate.getAssayType() + "' cannot be changed for '" + name + "'"); - if (plate.getRows() != rows || plate.getColumns() != cols) - throw new ApiUsageException("Plate template dimensions cannot be changed for '" + name + "'"); - updateExisting = true; - } - else - { - if (PlateManager.get().getPlateByName(getContainer(), name) != null) - throw new ApiUsageException("A plate template with name '" + name + "' already exists."); - PlateType plateType = PlateService.get().getPlateType(rows, cols); - if (plateType == null) - throw new NotFoundException("The plate type (" + rows + " x " + cols + ") does not exist."); - plate = PlateManager.get().createPlate(getContainer(), type, plateType); - } - - plate.setName(name); - plate.setProperties(plateProperties); - - // Parse groups from JSON - List submittedGroups = new ArrayList<>(); - Set submittedGroupIds = new HashSet<>(); - if (groupsJson != null) - { - for (int i = 0; i < groupsJson.length(); i++) - { - SubmittedGroup g = SubmittedGroup.from(groupsJson.getJSONObject(i)); - submittedGroups.add(g); - if (g.rowId > 0) - submittedGroupIds.add(g.rowId); - } - } - - // Mark well groups not in submission for deletion - List existingWellGroups = plate.getWellGroups(); - for (WellGroup existingGroup : existingWellGroups) - { - if (existingGroup.getRowId() != null && !submittedGroupIds.contains(existingGroup.getRowId())) - plate.markWellGroupForDeletion(existingGroup); - } - - // Update or create well groups - for (SubmittedGroup gm : submittedGroups) - { - int gRowId = gm.rowId(); - String groupTypeName = gm.type(); - WellGroup.Type groupType; - try - { - groupType = WellGroup.Type.valueOf(groupTypeName); - } - catch (IllegalArgumentException e) - { - throw new ApiUsageException("Unknown well group type: '" + groupTypeName + "'"); - } - List posList = gm.positions(); - List positions = new ArrayList<>(); - for (PlatePosition p : posList) - positions.add(plate.getPosition(p.row, p.col)); - - Map props = gm.properties(); - - WellGroupImpl group; - if (updateExisting && gRowId > 0) - { - WellGroupImpl existing = findExistingWellGroup(existingWellGroups, gRowId); - if (existing == null) - throw new Exception("Well group " + gRowId + " was not found."); - if (existing.getType() != groupType) - throw new Exception("Well group type cannot be changed: " + gm.name()); - existing.setName(gm.name); - existing.setPositions(positions); - plate.storeWellGroup(existing); - group = existing; - } - else - { - group = plate.addWellGroup(gm.name, groupType, positions); - } - group.setProperties(props); - } - - PlateLayoutHandler plateLayoutHandler = PlateManager.get().getPlateLayoutHandler(plate.getAssayType()); - - if (plateLayoutHandler == null) - { - throw new NotFoundException("Invalid assay type"); - } - plateLayoutHandler.validatePlate(getContainer(), getUser(), plate); - long savedRowId = PlateService.get().save(getContainer(), getUser(), plate); - return success(Map.of("rowId", savedRowId)); - } - - private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) - { - for (WellGroup wg : wellGroups) - { - if (wg.getRowId() != null && wg.getRowId() == rowId) - return (WellGroupImpl) wg; - } - return null; - } - } - @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) public static class DesignerAction extends SimpleViewAction { @@ -959,6 +904,13 @@ public static class CreatePlateForm implements ApiJsonForm private boolean _template; private Long _templateId; + // Template designer fields (groups path) + private Long _rowId; + private int _rows; + private int _cols; + private List _groups; // non-null activates the template-upsert path + private final Map _plateProperties = new HashMap<>(); + public String getDescription() { return _description; @@ -1004,6 +956,31 @@ public Long getTemplateId() return _templateId; } + public Long getRowId() + { + return _rowId; + } + + public int getRows() + { + return _rows; + } + + public int getCols() + { + return _cols; + } + + public List getGroups() + { + return _groups; + } + + public Map getPlateProperties() + { + return _plateProperties; + } + @Override public void bindJson(JSONObject json) { @@ -1031,6 +1008,33 @@ public void bindJson(JSONObject json) if (json.has("templateId")) _templateId = json.getLong("templateId"); + // Template designer fields + if (json.has("rowId")) + _rowId = json.getLong("rowId"); + // "type" is how the React designer sends assayType + if (json.has("type") && !json.has("assayType")) + _assayType = json.getString("type"); + if (json.has("rows")) + _rows = json.getInt("rows"); + if (json.has("cols")) + _cols = json.getInt("cols"); + if (json.has("plateProperties")) + { + JSONObject props = json.getJSONObject("plateProperties"); + for (String key : props.keySet()) + { + Object val = props.get(key); + _plateProperties.put(key, val == JSONObject.NULL ? null : val); + } + } + if (json.has("groups")) + { + _groups = new ArrayList<>(); + JSONArray arr = json.getJSONArray("groups"); + for (int i = 0; i < arr.length(); i++) + _groups.add(SubmittedGroup.from(arr.getJSONObject(i))); + } + if (json.has("data")) { _data = new ArrayList<>(); @@ -1052,6 +1056,7 @@ public void bindJson(JSONObject json) @Marshal(Marshaller.JSONObject) @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) + @JsonInputLimit(10 * 1024 * 1024) public static class CreatePlateAction extends MutatingApiAction { private PlateType _plateType; @@ -1061,6 +1066,8 @@ public void validateForm(CreatePlateForm form, Errors errors) { if (form.getPlateType() == null) errors.reject(ERROR_REQUIRED, "Plate \"plateType\" is required."); + if (form.getGroups() != null) + errors.reject(ERROR_REQUIRED, "Group values are not supported."); _plateType = PlateManager.get().getPlateType(form.getPlateType()); if (_plateType == null) diff --git a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java index 9efbde2e7a1..af614c9465f 100644 --- a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java @@ -198,7 +198,7 @@ public long saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exc // first, mark well groups not submitted for saving as deleted Set groups = gwtPlate.getGroups(); - List existingWellGroups = plate.getWellGroups(); + List existingWellGroups = plate.getWellGroups(); for (WellGroup existingWellGroup : existingWellGroups) { if (groups.stream().noneMatch(g-> g.getRowId() == existingWellGroup.getRowId())) @@ -246,7 +246,7 @@ public long saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exc } } - private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) + private WellGroupImpl findExistingWellGroup(List wellGroups, int rowId) { for (WellGroup wellGroup : wellGroups) { diff --git a/assay/src/org/labkey/assay/plate/PlateImpl.java b/assay/src/org/labkey/assay/plate/PlateImpl.java index 9934c2921de..0cfbca7fb86 100644 --- a/assay/src/org/labkey/assay/plate/PlateImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateImpl.java @@ -244,6 +244,9 @@ public WellGroupImpl storeWellGroup(WellGroupImpl group) if (_groups == null) _groups = new HashMap<>(); Map groupsByType = _groups.computeIfAbsent(group.getType(), k -> new LinkedHashMap<>()); + // If this group has a rowId, remove any stale entry keyed under the old name (rename case). + if (group.getRowId() != null) + groupsByType.values().removeIf(existing -> group.getRowId().equals(existing.getRowId())); groupsByType.put(group.getName(), group); if (!wellGroupsInOrder(groupsByType)) { @@ -281,14 +284,14 @@ private boolean wellGroupsInOrder(Map groups) @JsonIgnore @Override - public @NotNull List getWellGroups(Position position) + public @NotNull List getWellGroups(Position position) { - List wellGroups = getWellGroups(); + List wellGroups = getWellGroups(); if (wellGroups.isEmpty()) return Collections.emptyList(); - List groups = new ArrayList<>(); - for (WellGroup group : wellGroups) + List groups = new ArrayList<>(); + for (WellGroupImpl group : wellGroups) { if (group.contains(position)) groups.add(group); @@ -299,12 +302,12 @@ private boolean wellGroupsInOrder(Map groups) @JsonIgnore @Override - public @NotNull List getWellGroups() + public @NotNull List getWellGroups() { if (_groups == null) return Collections.emptyList(); - List allGroups = new ArrayList<>(); + List allGroups = new ArrayList<>(); for (Map groups : _groups.values()) allGroups.addAll(groups.values()); diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index c12203b1708..72138aeca1b 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -1213,9 +1213,9 @@ private long savePlateImpl( // create/update well groups QueryUpdateService wellGroupQus = getWellGroupUpdateService(container, user); - for (WellGroup group : plate.getWellGroups()) + for (WellGroupImpl wellgroup : plate.getWellGroups()) { - WellGroupImpl wellgroup = (WellGroupImpl) group; + assert !wellgroup._deleted; String wellGroupInstanceLsid = wellgroup.getLSID(); Map wellGroupRow; @@ -1239,8 +1239,8 @@ private long savePlateImpl( if (wellGroupErrors.hasErrors()) throw wellGroupErrors; - wellGroupInstanceLsid = (String) insertedRows.get(0).get(WellTable.Column.Lsid.name()); - wellgroup = ObjectFactory.Registry.getFactory(WellGroupImpl.class).fromMap(wellgroup, insertedRows.get(0)); + wellGroupInstanceLsid = (String) insertedRows.getFirst().get(WellTable.Column.Lsid.name()); + wellgroup = ObjectFactory.Registry.getFactory(WellGroupImpl.class).fromMap(wellgroup, insertedRows.getFirst()); savePropertyBag(container, user, wellGroupInstanceLsid, wellgroup.getProperties(), false); } } @@ -1398,7 +1398,7 @@ private long savePlateImpl( // return a list of wellId and wellGroupId pairs private List> getWellGroupPositions(Plate plate, Position position) { - List groups = plate.getWellGroups(position); + List groups = plate.getWellGroups(position); List> wellGroupPositions = new ArrayList<>(groups.size()); for (WellGroup group : groups) @@ -1583,7 +1583,7 @@ public void beforeDeleteWellGroup(Container container, Integer wellGroupId) DbScope scope = schema.getSchema().getScope(); assert scope.isTransactionActive(); - new SqlExecutor(scope).execute("" + + new SqlExecutor(scope).execute( "DELETE FROM " + schema.getTableInfoWellGroupPositions() + " WHERE wellGroupId = ?", wellGroupId); } @@ -1594,7 +1594,7 @@ private void deleteWellGroupPositions(Plate plate) DbScope scope = schema.getSchema().getScope(); assert scope.isTransactionActive(); - new SqlExecutor(scope).execute("" + + new SqlExecutor(scope).execute( "DELETE FROM " + schema.getTableInfoWellGroupPositions() + " WHERE wellId IN (SELECT rowId FROM " + schema.getTableInfoWell() + " WHERE plateId=?)", plate.getRowId()); } @@ -2025,7 +2025,7 @@ else if (isDuplicatePlateName(container, user, name, destinationPlateSet)) // Save the plate long plateId = savePlateImpl(container, user, newPlate, true, null, true); - newPlate = (PlateImpl) getPlate(container, plateId); + newPlate = getPlate(container, plateId); if (newPlate == null) throw new IllegalStateException("Unexpected failure. Failed to retrieve plate after save (pre-commit)."); diff --git a/assay/src/org/labkey/assay/plate/PlateManagerTest.java b/assay/src/org/labkey/assay/plate/PlateManagerTest.java index a60e5aacb43..77efd75ea70 100644 --- a/assay/src/org/labkey/assay/plate/PlateManagerTest.java +++ b/assay/src/org/labkey/assay/plate/PlateManagerTest.java @@ -230,14 +230,14 @@ public void testCreatePlateTemplate() throws Exception assertNotNull(savedTemplate.getLSID()); assertEquals(plateType.getRowId(), savedTemplate.getPlateType().getRowId()); - List wellGroups = savedTemplate.getWellGroups(); + List wellGroups = savedTemplate.getWellGroups(); assertEquals(3, wellGroups.size()); // TsvPlateTypeHandler creates two CONTROL well groups "Positive" and "Negative" - List controlWellGroups = savedTemplate.getWellGroups(WellGroup.Type.CONTROL); + List controlWellGroups = savedTemplate.getWellGroups(WellGroup.Type.CONTROL); assertEquals(2, controlWellGroups.size()); - List sampleWellGroups = savedTemplate.getWellGroups(WellGroup.Type.SAMPLE); + List sampleWellGroups = savedTemplate.getWellGroups(WellGroup.Type.SAMPLE); assertEquals(1, sampleWellGroups.size()); WellGroup savedWg1 = sampleWellGroups.getFirst(); assertEquals("wg1", savedWg1.getName()); @@ -292,7 +292,7 @@ public void testCreatePlateTemplate() throws Exception assertNotNull(updatedWg2); // verify deleted well group - List updatedControlWellGroups = updatedTemplate.getWellGroups(WellGroup.Type.CONTROL); + List updatedControlWellGroups = updatedTemplate.getWellGroups(WellGroup.Type.CONTROL); assertEquals(1, updatedControlWellGroups.size()); // verify added positions diff --git a/assay/src/org/labkey/assay/plate/WellGroupImpl.java b/assay/src/org/labkey/assay/plate/WellGroupImpl.java index 857c6fb47d9..19c7e530c8b 100644 --- a/assay/src/org/labkey/assay/plate/WellGroupImpl.java +++ b/assay/src/org/labkey/assay/plate/WellGroupImpl.java @@ -209,7 +209,7 @@ public synchronized Set getOverlappingGroups() _overlappingGroups = new LinkedHashSet<>(); for (Position position : getPositions()) { - List groups = _plate.getWellGroups(position); + List groups = _plate.getWellGroups(position); for (WellGroup group : groups) { if (group != this) @@ -313,7 +313,7 @@ public Plate getPlate() { if (_plate == null && _plateId != null) { - _plate = (PlateImpl) PlateCache.getPlate(getContainer(), _plateId); + _plate = PlateCache.getPlate(getContainer(), _plateId); } return _plate; } From 82f1f83b7abfd88560c26ab66b94e98bcb514957 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Tue, 28 Apr 2026 18:28:08 -0700 Subject: [PATCH 9/9] Unit test fixup, bounds validation --- .../PlateTemplateDesigner.tsx | 4 +- .../components/GroupTypesPanel.test.tsx | 6 +- .../components/GroupTypesPanel.tsx | 6 +- .../components/MultiCreateDialog.tsx | 79 ++++++++++--------- .../components/StatusBar.test.tsx | 2 +- .../components/WellGroupProperties.test.tsx | 10 +-- .../src/org/labkey/assay/PlateController.java | 4 + assay/test/js/setup.ts | 9 +++ 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx index 6489b79f15c..5025beb9a56 100644 --- a/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx +++ b/assay/src/client/PlateTemplateDesigner/PlateTemplateDesigner.tsx @@ -122,6 +122,8 @@ export function PlateTemplateDesigner(): JSX.Element { const activeGroupRef = useRef(null); activeGroupRef.current = activeGroup; const nextColorIndexRef = useRef(0); // Monotonically increasing; never decrements on delete so colors stay unique + // Capture returnURL once at mount; handleSave strips query params via replaceState, so reading from the URL later would return null. + const returnURLRef = useRef(ActionURL.getParameter('returnUrl')); useEffect(() => { const templateName = ActionURL.getParameter('templateName'); @@ -325,7 +327,7 @@ export function PlateTemplateDesigner(): JSX.Element { }, [plate]); const navigateAway = useCallback(() => { - const returnURL = ActionURL.getParameter('returnURL') || ActionURL.getParameter('returnUrl'); + const returnURL = returnURLRef.current; window.location.href = (returnURL && isSameOrigin(returnURL)) ? returnURL : ActionURL.buildURL('plate', 'plateList'); }, []); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx index 03d1f6ba963..71f463a3db7 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.test.tsx @@ -291,15 +291,17 @@ describe('GroupTypesPanel — delete group', () => { test('delete button calls onDeleteGroup when user confirms', async () => { const { props } = renderWithActiveGroup(); - jest.spyOn(window, 'confirm').mockReturnValue(true); await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + // First click shows inline confirmation; click Yes to confirm + await userEvent.click(screen.getByRole('button', { name: 'Confirm delete Group A' })); expect(props.onDeleteGroup).toHaveBeenCalledWith(1); }); test('delete button does not call onDeleteGroup when user cancels', async () => { const { props } = renderWithActiveGroup(); - jest.spyOn(window, 'confirm').mockReturnValue(false); await userEvent.click(screen.getByRole('button', { name: 'Delete Group A' })); + // First click shows inline confirmation; click No to cancel + await userEvent.click(screen.getByRole('button', { name: 'Cancel delete Group A' })); expect(props.onDeleteGroup).not.toHaveBeenCalled(); }); }); diff --git a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx index 449ceab4524..b397e149783 100644 --- a/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/GroupTypesPanel.tsx @@ -203,7 +203,7 @@ export function GroupTypesPanel({ > {type === activeTab && ( <> -
+
{groupsOfType.map(group => { const color = colorMap.get(group.rowId); const isActive = activeGroup?.rowId === group.rowId; @@ -214,8 +214,8 @@ export function GroupTypesPanel({ className={classNames('group-types-panel__group', { 'group-types-panel__group--active': isActive, })} - role="listitem" - aria-current={isActive ? true : undefined} + role="option" + aria-selected={isActive} tabIndex={0} onClick={() => { if (!isRenaming) onGroupSelect(group); }} onKeyDown={e => { diff --git a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx index bb27be5c028..edb2b7456e6 100644 --- a/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/MultiCreateDialog.tsx @@ -81,50 +81,53 @@ export function MultiCreateDialog({ initialBaseName, existingNames, onClose, onC return ( -
Create Multiple Groups
-
-
- Base Name - setBaseName(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} - /> -
-
- Count -
+
e.stopPropagation()}> +
Create Multiple Groups
+
+
+ Base Name { setCount(e.target.value); setCountError(''); }} + className="multi-create-dialog__input" + type="text" + aria-labelledby="multi-create-base-name-label" + value={baseName} + onChange={e => setBaseName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} /> - {countError &&
{countError}
}
-
-
- - +
+ Count +
+ { setCount(e.target.value); setCountError(''); }} + onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') onClose(); }} + /> + {countError &&
{countError}
} +
+
+
+ + +
diff --git a/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx index e4242b0eeb2..9ca66dba283 100644 --- a/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/StatusBar.test.tsx @@ -85,7 +85,7 @@ describe('StatusBar', () => { }); test('clicking Save & Close with empty name shows error and does not call onSaveAndClose', async () => { - const { onSaveAndClose } = renderStatusBar({ plateName: '' }); + const { onSaveAndClose } = renderStatusBar({ plateName: '', isDirty: true }); await userEvent.click(screen.getByRole('button', { name: /Save & Close/i })); expect(onSaveAndClose).not.toHaveBeenCalled(); expect(screen.getByRole('alert')).toBeInTheDocument(); diff --git a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx index c3506d6e135..879bcd515ab 100644 --- a/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx +++ b/assay/src/client/PlateTemplateDesigner/components/WellGroupProperties.test.tsx @@ -147,11 +147,8 @@ describe('WellGroupProperties', () => { }); }); - describe('known bug: inputs not reset when active group changes', () => { - // This documents the existing behavior where newKey/newValue are NOT reset - // when the active group prop changes (see review finding #15). - // The inputs retain their values across group switches until the component unmounts. - test('newKey input retains value when activeGroup prop changes', async () => { + describe('inputs reset when active group changes', () => { + test('newKey input is cleared when activeGroup prop changes', async () => { const group1 = makeGroup({ rowId: 1, name: 'Group 1' }); const group2 = makeGroup({ rowId: 2, name: 'Group 2' }); const { rerender } = render( @@ -161,8 +158,7 @@ describe('WellGroupProperties', () => { rerender( ); - // Bug: input still shows the value from group1's editing session - expect(screen.getByLabelText('Property name')).toHaveValue('stale-key'); + expect(screen.getByLabelText('Property name')).toHaveValue(''); }); }); }); diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index 4469022ce8a..0e7ed1cc1a0 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -309,7 +309,11 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti List positions = new ArrayList<>(); for (PlatePosition p : gm.positions()) + { + if (p.row() < 0 || p.row() >= plate.getRows() || p.col() < 0 || p.col() >= plate.getColumns()) + throw new ValidationException("Position (" + p.row() + ", " + p.col() + ") is out of bounds for a " + plate.getRows() + " x " + plate.getColumns() + " plate."); positions.add(plate.getPosition(p.row(), p.col())); + } WellGroupImpl group; if (updateExisting && gm.rowId() > 0) diff --git a/assay/test/js/setup.ts b/assay/test/js/setup.ts index 7b0828bfa80..95ccacbca05 100644 --- a/assay/test/js/setup.ts +++ b/assay/test/js/setup.ts @@ -1 +1,10 @@ import '@testing-library/jest-dom'; + +// jsdom does not implement HTMLDialogElement.showModal / .close. +// These stubs are enough for component tests to run. +HTMLDialogElement.prototype.showModal = function () { + this.setAttribute('open', ''); +}; +HTMLDialogElement.prototype.close = function () { + this.removeAttribute('open'); +};