diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index e633c46..b7eb416 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -22,14 +22,15 @@ jobs: - name: Install dependencies run: npm ci - - name: Run test suite - run: npm test + - name: Run test suite with coverage + run: npm run test:coverage - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - continue-on-error: true + files: ./coverage/lcov.info + fail_ci_if_error: false docker-build: name: Docker Build Test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a26a403..c381879 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,13 +27,15 @@ jobs: - name: Install dependencies run: npm ci - - name: Run test suite - run: npm test + - name: Run test suite with coverage + run: npm run test:coverage - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + fail_ci_if_error: false - name: Log into GitHub Container Registry uses: docker/login-action@v3 diff --git a/README.md b/README.md index b3c1cd6..960d6f5 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Dual fetch allows you to fetch _both_ ebook and audibooks at the same time in a ### Key Features - 🔎 **Search MAM**: Clean, responsive interface to search MyAnonamouse's extensive library +- ⚡ **Freeleach Wedges**: Redeem freeleach wedges before downloading and check your wedge count - 🌟 **Dual-Fetch Mode**: Search and download both ebook and audiobook simultaneously with streamlined selection workflow - đŸ‘‰ī¸ **One-click Downloads**: Instantly send torrents to your qBittorrent instance with a single click - 🤝 **Direct Integration**: Automatically authenticates with both MAM and qBittorrent APIs diff --git a/__tests__/add.test.mjs b/__tests__/add.test.mjs index a2a2bba..5c8bd0d 100644 --- a/__tests__/add.test.mjs +++ b/__tests__/add.test.mjs @@ -1,6 +1,7 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { POST } from '../app/api/add/route.js'; import * as qbittorrent from '../src/lib/qbittorrent'; +import * as userStatsRoute from '../app/api/user-stats/route.js'; vi.mock('../src/lib/config', () => ({ config: { qbUrl: 'http://qb', qbUser: 'user', qbPass: 'pass', qbCategory: 'cat' } @@ -9,8 +10,15 @@ vi.mock('../src/lib/qbittorrent', () => ({ qbLogin: vi.fn(async () => 'cookie'), qbAddUrl: vi.fn(async () => true) })); +vi.mock('../app/api/user-stats/route.js', () => ({ + bustStatsCache: vi.fn() +})); describe('add route', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('returns 400 if no downloadUrl provided', async () => { const req = { json: async () => ({}) }; const res = await POST(req); @@ -26,6 +34,15 @@ describe('add route', () => { expect(json.ok).toBe(true); }); + it('busts stats cache after successful download', async () => { + const req = { json: async () => ({ title: 'test', downloadUrl: 'magnet:?xt=...', category: 'cat' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(json.ok).toBe(true); + expect(userStatsRoute.bustStatsCache).toHaveBeenCalledTimes(1); + }); + it('returns 500 if qbittorrent throws', async () => { qbittorrent.qbAddUrl.mockImplementationOnce(() => { throw new Error('fail'); }); @@ -36,4 +53,187 @@ describe('add route', () => { expect(json.ok).toBe(false); expect(json.error).toMatch(/fail/); }); + + it('returns 500 with fallback message if error has no message', async () => { + qbittorrent.qbAddUrl.mockImplementationOnce(() => { throw 'string error'; }); + + const req = { json: async () => ({ title: 'test', downloadUrl: 'magnet:?xt=...', category: 'cat' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(json.ok).toBe(false); + expect(json.error).toBe('Add failed'); + }); + + it('does not bust cache if download fails', async () => { + qbittorrent.qbAddUrl.mockImplementationOnce(() => { throw new Error('fail'); }); + + const req = { json: async () => ({ title: 'test', downloadUrl: 'magnet:?xt=...', category: 'cat' }) }; + await POST(req); + + expect(userStatsRoute.bustStatsCache).not.toHaveBeenCalled(); + }); + + describe('wedge integration', () => { + beforeEach(() => { + vi.clearAllMocks(); + global.fetch = vi.fn(); + }); + + it('purchases FL wedge before adding torrent when useWedge is true', async () => { + // Mock successful wedge purchase + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const req = { + json: async () => ({ + title: 'Test Book', + downloadUrl: 'magnet:?xt=...', + torrentId: '12345', + useWedge: true + }), + nextUrl: { origin: 'http://localhost:3000' } + }; + + const res = await POST(req); + const json = await res.json(); + + expect(json.ok).toBe(true); + expect(json.wedgeUsed).toBe(true); + expect(global.fetch).toHaveBeenCalledWith( + 'http://localhost:3000/api/use-wedge', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ torrentId: '12345' }) + }) + ); + // Verify qBittorrent was called after wedge purchase + expect(qbittorrent.qbAddUrl).toHaveBeenCalled(); + }); + + it('returns error when wedge purchase fails', async () => { + // Mock failed wedge purchase + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 400, + json: async () => ({ success: false, error: 'Not enough wedges' }) + }); + + const req = { + json: async () => ({ + title: 'Test Book', + downloadUrl: 'magnet:?xt=...', + torrentId: '12345', + useWedge: true + }), + nextUrl: { origin: 'http://localhost:3000' } + }; + + const res = await POST(req); + const json = await res.json(); + + expect(json.ok).toBe(false); + expect(json.wedgeFailed).toBe(true); + expect(json.error).toContain('Not enough wedges'); + // Should NOT attempt to add torrent to qBittorrent + expect(qbittorrent.qbAddUrl).not.toHaveBeenCalled(); + }); + + it('does not bust cache when wedge purchase fails', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 400, + json: async () => ({ success: false, error: 'Error' }) + }); + + const req = { + json: async () => ({ + title: 'Test Book', + downloadUrl: 'magnet:?xt=...', + torrentId: '12345', + useWedge: true + }), + nextUrl: { origin: 'http://localhost:3000' } + }; + + await POST(req); + expect(userStatsRoute.bustStatsCache).not.toHaveBeenCalled(); + }); + + it('returns error when wedge response has ok=true but success=false', async () => { + // Mock wedge purchase with success: false + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: false, error: 'Insufficient bonus points' }) + }); + + const req = { + json: async () => ({ + title: 'Test Book', + downloadUrl: 'magnet:?xt=...', + torrentId: '12345', + useWedge: true + }), + nextUrl: { origin: 'http://localhost:3000' } + }; + + const res = await POST(req); + const json = await res.json(); + + expect(json.ok).toBe(false); + expect(json.wedgeFailed).toBe(true); + expect(json.error).toBe('Insufficient bonus points'); + expect(qbittorrent.qbAddUrl).not.toHaveBeenCalled(); + }); + + it('busts cache after successful wedge purchase and download', async () => { + // Mock successful wedge purchase + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const req = { + json: async () => ({ + title: 'Test Book', + downloadUrl: 'magnet:?xt=...', + torrentId: '12345', + useWedge: true + }), + nextUrl: { origin: 'http://localhost:3000' } + }; + + await POST(req); + + expect(userStatsRoute.bustStatsCache).toHaveBeenCalledTimes(1); + }); + + it('handles wedge purchase when response has no error message', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 500, + json: async () => ({ success: false }) + }); + + const req = { + json: async () => ({ + title: 'Test Book', + downloadUrl: 'magnet:?xt=...', + torrentId: '12345', + useWedge: true + }), + nextUrl: { origin: 'http://localhost:3000' } + }; + + const res = await POST(req); + const json = await res.json(); + + expect(json.ok).toBe(false); + expect(json.wedgeFailed).toBe(true); + expect(json.error).toBe('Failed to purchase FL wedge'); + }); + }); }); diff --git a/__tests__/helpers/test-factories.mjs b/__tests__/helpers/test-factories.mjs index d69b9b6..b14e8e5 100644 --- a/__tests__/helpers/test-factories.mjs +++ b/__tests__/helpers/test-factories.mjs @@ -12,6 +12,7 @@ export function createMamTorrent(overrides = {}) { filetype: 'epub', added: '2025-09-25', vip: 0, + free: 0, my_snatched: 0, author_info: '{"author":"Test Author"}', seeders: 15, diff --git a/__tests__/message-banner.test.jsx b/__tests__/message-banner.test.jsx new file mode 100644 index 0000000..e0bf8c9 --- /dev/null +++ b/__tests__/message-banner.test.jsx @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import MessageBanner from '../app/MessageBanner.jsx'; + +describe('MessageBanner', () => { + it('renders error message with red styling', () => { + render(); + + const banner = screen.getByText('Error occurred').closest('div'); + expect(screen.getByText('Error occurred')).toBeInTheDocument(); + expect(screen.getByText('❌')).toBeInTheDocument(); + expect(banner).toHaveClass('bg-red-100'); + expect(banner).toHaveClass('border-red-300'); + expect(banner).toHaveClass('text-red-900'); + }); + + it('renders success message with green styling', () => { + render(); + + const banner = screen.getByText('Success!').closest('div'); + expect(screen.getByText('Success!')).toBeInTheDocument(); + expect(screen.getByText('✅')).toBeInTheDocument(); + expect(banner).toHaveClass('bg-green-100'); + expect(banner).toHaveClass('border-green-300'); + expect(banner).toHaveClass('text-green-900'); + }); + + it('renders info message by default', () => { + render(); + + const banner = screen.getByText('Information').closest('div'); + expect(screen.getByText('Information')).toBeInTheDocument(); + expect(screen.getByText('â„šī¸')).toBeInTheDocument(); + expect(banner).toHaveClass('bg-blue-100'); + expect(banner).toHaveClass('border-blue-300'); + expect(banner).toHaveClass('text-blue-900'); + }); + + it('renders info message when type is explicitly set to info', () => { + render(); + + const banner = screen.getByText('Information').closest('div'); + expect(screen.getByText('Information')).toBeInTheDocument(); + expect(screen.getByText('â„šī¸')).toBeInTheDocument(); + expect(banner).toHaveClass('bg-blue-100'); + }); + + it('renders info message for unknown type', () => { + render(); + + const banner = screen.getByText('Unknown type').closest('div'); + expect(screen.getByText('Unknown type')).toBeInTheDocument(); + expect(screen.getByText('â„šī¸')).toBeInTheDocument(); + expect(banner).toHaveClass('bg-blue-100'); + }); + + it('applies all required CSS classes', () => { + render(); + + const banner = screen.getByText('Test').closest('div'); + expect(banner).toHaveClass('my-5'); + expect(banner).toHaveClass('p-4'); + expect(banner).toHaveClass('rounded-md'); + expect(banner).toHaveClass('flex'); + expect(banner).toHaveClass('items-center'); + expect(banner).toHaveClass('gap-2'); + }); + + it('applies correct font size to icon', () => { + render(); + + const icon = screen.getByText('❌'); + expect(icon).toHaveStyle({ fontSize: '20px' }); + }); + + it('renders text in a strong tag', () => { + render(); + + const strongElement = screen.getByText('Important message'); + expect(strongElement.tagName).toBe('STRONG'); + }); +}); diff --git a/__tests__/search.test.mjs b/__tests__/search.test.mjs index 7aee33a..004731a 100644 --- a/__tests__/search.test.mjs +++ b/__tests__/search.test.mjs @@ -35,6 +35,47 @@ describe('search route', () => { const json = await res.json(); expect(json.results.length).toBeGreaterThan(0); }); + + it('handles search results with missing/null fields', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + data: [{ + // All optional fields missing or null + id: null, + title: null, + size: null, + filetype: null, + added: null, + vip: 0, + free: 0, + my_snatched: 0, + author_info: null, + seeders: null, + leechers: null, + times_completed: null, + dl: null + }] + }), + text: async () => "", + }); + + const req = { url: 'http://localhost/api/search?q=test' }; + const res = await GET(req); + const json = await res.json(); + + expect(json.results.length).toBe(1); + const result = json.results[0]; + expect(result.id).toBeNull(); + expect(result.title).toBe(''); + expect(result.size).toBe(''); + expect(result.filetypes).toBe(''); + expect(result.addedDate).toBe(''); + expect(result.seeders).toBe('0'); + expect(result.leechers).toBe('0'); + expect(result.downloads).toBe('0'); + }); }); describe('token expiration handling', () => { diff --git a/__tests__/use-wedge.test.mjs b/__tests__/use-wedge.test.mjs new file mode 100644 index 0000000..fb622bb --- /dev/null +++ b/__tests__/use-wedge.test.mjs @@ -0,0 +1,204 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { POST } from '../app/api/use-wedge/route.js'; + +// Mock dependencies +vi.mock('../src/lib/config', () => ({ + readMamToken: vi.fn(() => 'mock-token-123') +})); + +vi.mock('../src/lib/constants', () => ({ + MAM_BASE: 'https://www.myanonamouse.net' +})); + +vi.mock('../src/lib/utilities', () => ({ + generateTimestamp: vi.fn(() => 1234567890) +})); + +// Mock fetch globally +global.fetch = vi.fn(); + +describe('use-wedge route', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 400 if no torrentId provided', async () => { + const req = { json: async () => ({}) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toMatch(/Torrent ID is required/); + }); + + it('returns 400 if torrentId is null', async () => { + const req = { json: async () => ({ torrentId: null }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toMatch(/Torrent ID is required/); + }); + + it('returns 400 if torrentId is empty string', async () => { + const req = { json: async () => ({ torrentId: '' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toMatch(/Torrent ID is required/); + }); + + it('successfully purchases FL wedge with valid torrentId', async () => { + // Mock successful MAM API response + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const req = { json: async () => ({ torrentId: '12345' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(json.success).toBe(true); + expect(json.message).toMatch(/FL wedge applied successfully/); + expect(json.torrentId).toBe('12345'); + + // Verify fetch was called with correct parameters + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('bonusBuy.php'), + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Cookie': 'mam_id=mock-token-123' + }) + }) + ); + + // Verify URL contains torrentid parameter + const fetchCall = global.fetch.mock.calls[0][0]; + expect(fetchCall).toContain('torrentid=12345'); + expect(fetchCall).toContain('spendtype=personalFL'); + }); + + it('returns 502 if MAM API returns non-ok response', async () => { + // Mock failed MAM API response + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 500, + text: async () => 'Internal Server Error' + }); + + const req = { json: async () => ({ torrentId: '12345' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(502); + expect(json.error).toMatch(/Failed to purchase FL wedge/); + }); + + it('returns 401 if MAM token is expired', async () => { + // Mock token expiration response + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 403, + text: async () => 'You are not signed in' + }); + + const req = { json: async () => ({ torrentId: '12345' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(401); + expect(json.error).toMatch(/MAM token has expired/); + expect(json.tokenExpired).toBe(true); + }); + + it('returns 400 if MAM API returns success: false', async () => { + // Mock MAM API returning success: false + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: false, error: 'Insufficient bonus points' }) + }); + + const req = { json: async () => ({ torrentId: '12345' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toMatch(/Insufficient bonus points/); + }); + + it('returns 400 if MAM API returns success: false without error message', async () => { + // Mock MAM API returning success: false with no error field + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: false }) + }); + + const req = { json: async () => ({ torrentId: '12345' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toBe('Failed to use FL wedge: Unknown error occurred'); + }); + + it('returns 400 if MAM API returns an error field', async () => { + // Mock MAM API returning error + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ error: 'Torrent not found' }) + }); + + const req = { json: async () => ({ torrentId: '99999' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(400); + expect(json.error).toMatch(/Torrent not found/); + }); + + it('returns 502 if MAM API returns invalid JSON', async () => { + // Mock invalid JSON response + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => { throw new Error('Invalid JSON'); }, + text: async () => 'Invalid response' + }); + + const req = { json: async () => ({ torrentId: '12345' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(502); + expect(json.error).toMatch(/Invalid response from MAM API/); + }); + + it('returns 500 if unexpected error occurs', async () => { + // Mock fetch throwing an error + global.fetch.mockRejectedValueOnce(new Error('Network error')); + + const req = { json: async () => ({ torrentId: '12345' }) }; + const res = await POST(req); + const json = await res.json(); + + expect(res.status).toBe(500); + expect(json.error).toMatch(/Network error|Failed to use FL wedge/); + }); + + it('works with numeric torrentId', async () => { + // Mock successful response + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const req = { json: async () => ({ torrentId: 67890 }) }; + const res = await POST(req); + const json = await res.json(); + + expect(json.success).toBe(true); + expect(json.torrentId).toBe(67890); + }); +}); diff --git a/__tests__/user-stats.test.mjs b/__tests__/user-stats.test.mjs new file mode 100644 index 0000000..08fac3d --- /dev/null +++ b/__tests__/user-stats.test.mjs @@ -0,0 +1,477 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { GET, bustStatsCache } from '../app/api/user-stats/route.js'; + +vi.mock('../src/lib/config', () => ({ + readMamToken: vi.fn(() => 'test-token-12345') +})); + +vi.mock('../src/lib/constants', () => ({ + MAM_BASE: 'https://www.myanonamouse.net' +})); + +global.fetch = vi.fn(); + +// Store original console methods +const originalConsoleLog = console.log; +const originalConsoleError = console.error; + +describe('user-stats route', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Clear the cache before each test + bustStatsCache(); + // Mock console to avoid noise in test output + console.log = vi.fn(); + console.error = vi.fn(); + }); + + afterEach(() => { + // Restore console + console.log = originalConsoleLog; + console.error = originalConsoleError; + }); + + describe('bustStatsCache', () => { + it('busts cache for specific token when provided', () => { + bustStatsCache('token1'); + expect(console.log).toHaveBeenCalledWith( + 'Busted user stats cache for specific token' + ); + }); + + it('busts all cache when no token provided', () => { + bustStatsCache(); + expect(console.log).toHaveBeenCalledWith( + 'Busted all user stats cache' + ); + }); + }); + + describe('GET endpoint', () => { + it('fetches user stats from MAM successfully', async () => { + const mockStats = { + uploaded: '50.5 GB', + downloaded: '25.2 GB', + ratio: '2.00', + username: 'testuser', + uid: '12345', + wedges: 10 + }; + + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockStats + }); + + const req = {}; + const res = await GET(req); + const json = await res.json(); + + expect(json.stats).toEqual({ + uploaded: '50.5 GB', + downloaded: '25.2 GB', + ratio: '2.00', + username: 'testuser', + uid: '12345', + flWedges: 10 + }); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('Fetched user stats for testuser') + ); + }); + + it('uses cached data when available and not expired', async () => { + const mockStats = { + uploaded: '50.5 GB', + downloaded: '25.2 GB', + ratio: '2.00', + username: 'testuser', + uid: '12345', + wedges: 10 + }; + + // First request - populates cache + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockStats + }); + + await GET({}); + + // Second request - should use cache + const res = await GET({}); + const json = await res.json(); + + expect(global.fetch).toHaveBeenCalledTimes(1); // Only called once + expect(console.log).toHaveBeenCalledWith('Returning cached user stats'); + expect(json.stats.username).toBe('testuser'); + }); + + it('refetches when cache is expired', async () => { + const mockStats = { + uploaded: '50.5 GB', + downloaded: '25.2 GB', + ratio: '2.00', + username: 'testuser', + uid: '12345', + wedges: 10 + }; + + // First request + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockStats + }); + + await GET({}); + + // Clear cache to simulate expiration + bustStatsCache(); + + // Second request + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ ...mockStats, ratio: '2.05' }) + }); + + const res = await GET({}); + const json = await res.json(); + + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(json.stats.ratio).toBe('2.05'); + }); + + it('returns 401 when MAM token is expired (403 response)', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 403, + text: async () => 'You are not signed in' + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(401); + expect(json.tokenExpired).toBe(true); + expect(json.error).toContain('MAM token has expired'); + expect(console.error).toHaveBeenCalledWith('MAM token has expired'); + }); + + it('returns 401 when MAM token is expired (403 with case variations)', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 403, + text: async () => 'You Are Not Signed In' + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(401); + expect(json.tokenExpired).toBe(true); + }); + + it('returns 502 for other HTTP errors', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 500, + text: async () => 'Internal Server Error' + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(502); + expect(json.error).toContain('Failed to fetch user stats: 500'); + }); + + it('returns 502 for 404 errors', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 404, + text: async () => 'Not Found' + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(502); + expect(json.error).toBe('Failed to fetch user stats: 404'); + }); + + it('detects HTML response as expired token', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => { throw new Error('Invalid JSON'); }, + text: async () => '...' + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(401); + expect(json.tokenExpired).toBe(true); + expect(json.error).toContain('MAM token has expired'); + expect(console.error).toHaveBeenCalledWith( + 'Detected HTML response, likely due to invalid/expired token' + ); + }); + + it('detects lowercase html response as expired token', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => { throw new Error('Invalid JSON'); }, + text: async () => 'Login required' + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(401); + expect(json.tokenExpired).toBe(true); + }); + + it('returns 502 for invalid JSON without HTML', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => { throw new Error('Invalid JSON'); }, + text: async () => 'Invalid response' + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(502); + expect(json.error).toBe('Invalid JSON from endpoint'); + }); + + it('handles missing data fields with defaults', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({}) // Empty response + }); + + const res = await GET({}); + const json = await res.json(); + + expect(json.stats).toEqual({ + uploaded: '0 B', + downloaded: '0 B', + ratio: '0.00', + username: null, + uid: null, + flWedges: 0 + }); + }); + + it('handles partial data fields with defaults', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + username: 'partialuser', + ratio: '1.50' + }) + }); + + const res = await GET({}); + const json = await res.json(); + + expect(json.stats).toEqual({ + uploaded: '0 B', + downloaded: '0 B', + ratio: '1.50', + username: 'partialuser', + uid: null, + flWedges: 0 + }); + }); + + it('handles top-level exceptions', async () => { + global.fetch.mockRejectedValueOnce(new Error('Network failure')); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(500); + expect(json.error).toContain('Network failure'); + expect(console.error).toHaveBeenCalledWith( + 'Error fetching user stats:', + expect.any(Error) + ); + }); + + it('handles exceptions without error message', async () => { + global.fetch.mockRejectedValueOnce('string error'); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(500); + expect(json.error).toBe('Failed to fetch user stats'); + }); + + it('caches stats with correct timestamp', async () => { + const mockStats = { + uploaded: '100 GB', + downloaded: '50 GB', + ratio: '2.00', + username: 'cacheuser', + uid: '99999', + wedges: 5 + }; + + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockStats + }); + + const beforeTime = Date.now(); + await GET({}); + const afterTime = Date.now(); + + // Second request should use cache + const res = await GET({}); + const json = await res.json(); + + expect(json.stats.username).toBe('cacheuser'); + expect(console.log).toHaveBeenCalledWith('Returning cached user stats'); + // Verify fetch was only called once (cache hit on second call) + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it('includes correct headers in MAM request', async () => { + const mockStats = { + uploaded: '50 GB', + downloaded: '25 GB', + ratio: '2.00', + username: 'testuser', + uid: '12345', + wedges: 10 + }; + + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockStats + }); + + await GET({}); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://www.myanonamouse.net/jsonLoad.php', + expect.objectContaining({ + method: 'GET', + headers: { + 'Accept': 'application/json, text/plain, */*', + 'Cookie': 'mam_id=test-token-12345', + 'Origin': 'https://www.myanonamouse.net', + 'Referer': 'https://www.myanonamouse.net/' + }, + cache: 'no-store' + }) + ); + }); + + it('handles text() failure in error path gracefully', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 500, + text: async () => { throw new Error('Cannot read text'); } + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(502); + expect(json.error).toBe('Failed to fetch user stats: 500'); + }); + + it('handles text() failure in JSON parse error path gracefully', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => { throw new Error('Invalid JSON'); }, + text: async () => { throw new Error('Cannot read text'); } + }); + + const res = await GET({}); + const json = await res.json(); + + expect(res.status).toBe(502); + expect(json.error).toBe('Invalid JSON from endpoint'); + }); + + it('correctly maps wedges field to flWedges', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + uploaded: '1 TB', + downloaded: '500 GB', + ratio: '2.00', + username: 'wedgeuser', + uid: '11111', + wedges: 25 + }) + }); + + const res = await GET({}); + const json = await res.json(); + + expect(json.stats.flWedges).toBe(25); + // Verify original 'wedges' key is not present + expect(json.stats.wedges).toBeUndefined(); + }); + + it('cleans up old cache entries when cache size exceeds 100', async () => { + // Import the config module to mock different tokens + const { readMamToken } = await import('../src/lib/config'); + + // Clear cache first + bustStatsCache(); + + const mockStats = { + uploaded: '100 GB', + downloaded: '50 GB', + ratio: '2.00', + username: 'user', + uid: '123', + wedges: 5 + }; + + // Mock Date.now to control timestamps + const baseTime = Date.now(); + let currentTime = baseTime; + vi.spyOn(Date, 'now').mockImplementation(() => currentTime); + + // Add 101 cache entries with different tokens + for (let i = 0; i < 101; i++) { + readMamToken.mockReturnValueOnce(`token-${i}`); + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ ...mockStats, uid: `${i}` }) + }); + + await GET({}); + currentTime += 1000; // Advance time by 1 second for each entry + } + + // The 101st request should trigger cleanup + // Cache should have removed old entries + // Verify by checking that we made 101 fetch calls (no cache hits) + expect(global.fetch).toHaveBeenCalledTimes(101); + + // Now advance time past TTL for the first entries + currentTime = baseTime + 31 * 60 * 1000; // 31 minutes later + + // Try to fetch with an old token - should fetch fresh (not cached) + readMamToken.mockReturnValueOnce('token-0'); + global.fetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockStats + }); + + await GET({}); + + // Should have made a fresh fetch (102 total) + expect(global.fetch).toHaveBeenCalledTimes(102); + }); + }); +}); diff --git a/__tests__/utilities.test.js b/__tests__/utilities.test.js index 3332d28..3bf56b1 100644 --- a/__tests__/utilities.test.js +++ b/__tests__/utilities.test.js @@ -8,7 +8,8 @@ import { parseSizeToBytes, formatBytesToSize, calculateNewRatio, - calculateRatioDiff + calculateRatioDiff, + generateTimestamp } from '../src/lib/utilities.js'; describe('utilities', () => { @@ -57,13 +58,18 @@ describe('utilities', () => { // Valid tokens expect(validateMamToken('CWX7gfubkHotwItFiZu0QmBNkvXcq_76fR6AZxPmSacmLAmkPEI')).toBe(true); expect(validateMamToken('a'.repeat(51))).toBe(true); // 51 chars + expect(validateMamToken('A'.repeat(51))).toBe(true); // uppercase + expect(validateMamToken('0'.repeat(51))).toBe(true); // numbers + expect(validateMamToken('a_b-c'.repeat(11))).toBe(true); // with underscores and hyphens (55 chars) // Invalid tokens expect(validateMamToken('short')).toBe(false); // too short expect(validateMamToken('')).toBe(false); // empty expect(validateMamToken(null)).toBe(false); // null expect(validateMamToken(undefined)).toBe(false); // undefined - expect(validateMamToken('contains spaces and symbols!')).toBe(false); // invalid chars + expect(validateMamToken('contains spaces and symbols!'.repeat(3))).toBe(false); // invalid chars but long enough + expect(validateMamToken('a'.repeat(51) + '!')).toBe(false); // 52 chars but has invalid char + expect(validateMamToken(123)).toBe(false); // number instead of string }); it('maskToken masks tokens correctly', () => { @@ -71,9 +77,27 @@ describe('utilities', () => { expect(maskToken('1234567890abcdef')).toBe('123456...cdef'); expect(maskToken('short')).toBe('***'); + expect(maskToken('1234567890')).toBe('***'); // exactly 10 chars + expect(maskToken('12345678')).toBe('***'); // 8 chars + expect(maskToken('123456')).toBe('***'); // 6 chars expect(maskToken('')).toBe(''); expect(maskToken(null)).toBe(''); expect(maskToken(undefined)).toBe(''); + expect(maskToken(' ')).toBe('***'); // whitespace only, trimmed to empty but still short + expect(maskToken(123)).toBe(''); // number instead of string + expect(maskToken({})).toBe(''); // object instead of string + expect(maskToken([])).toBe(''); // array instead of string + }); + + it('generateTimestamp returns current timestamp in milliseconds', () => { + const before = Date.now(); + const timestamp = generateTimestamp(); + const after = Date.now(); + + expect(timestamp).toBeGreaterThanOrEqual(before); + expect(timestamp).toBeLessThanOrEqual(after); + expect(typeof timestamp).toBe('number'); + expect(Number.isInteger(timestamp)).toBe(true); }); describe('parseSizeToBytes', () => { diff --git a/app/api/add/route.js b/app/api/add/route.js index 7549523..1276f90 100644 --- a/app/api/add/route.js +++ b/app/api/add/route.js @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { config } from "@/src/lib/config"; import { qbAddUrl, qbLogin } from "@/src/lib/qbittorrent"; +import { bustStatsCache } from "../user-stats/route.js"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -9,16 +10,51 @@ export async function POST(req) { const body = await req.json(); const title = body.title; const urlOrMagnet = body.downloadUrl; + const torrentId = body.torrentId; const category = body.category || config.qbCategory; // Use category from request or fallback to config + const useWedge = body.useWedge || false; + if (!urlOrMagnet) { return NextResponse.json({ ok: false, error: "No magnet or torrentUrl provided" }, { status: 400 }); } + try { + // If wedge is requested, purchase it first before adding torrent + if (useWedge) { + console.log(`Purchasing FL wedge for: ${title}`); + + const wedgeRes = await fetch(`${req.nextUrl.origin}/api/use-wedge`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ torrentId }) + }); + + const wedgeData = await wedgeRes.json(); + + if (!wedgeRes.ok || !wedgeData.success) { + const errorMsg = wedgeData.error || "Failed to purchase FL wedge"; + console.error(`FL wedge purchase failed for ${title}: ${errorMsg}`); + return NextResponse.json( + { ok: false, error: errorMsg, wedgeFailed: true }, + { status: wedgeRes.status } + ); + } + + console.log(`FL wedge successfully applied for: ${title}`); + } + + // Proceed with adding torrent to qBittorrent const cookie = await qbLogin(config.qbUrl, config.qbUser, config.qbPass); // TODO: clean up params await qbAddUrl(config.qbUrl, cookie, urlOrMagnet, category); - console.log(`Added to qBittorrent: ${title} (${category})`); - return NextResponse.json({ ok: true }); + console.log(`Added to qBittorrent: ${title} (${category})${useWedge ? ' with FL wedge' : ''}`); + + // Bust user stats cache since download affects stats + bustStatsCache(); + + return NextResponse.json({ ok: true, wedgeUsed: useWedge }); } catch (err) { console.error(`Failed to add to qBittorrent: ${title} (${category}) - ${err?.message || err}`); return NextResponse.json({ ok: false, error: err?.message || "Add failed" }, { status: 500 }); diff --git a/app/api/search/route.js b/app/api/search/route.js index 222f170..c03070f 100644 --- a/app/api/search/route.js +++ b/app/api/search/route.js @@ -87,6 +87,7 @@ export async function GET(req) { filetypes: item.filetype ?? "", addedDate: item.added ?? "", vip: Boolean(item.vip == 1), + freeleech: Boolean(item.free == 1), snatched: Boolean(item.my_snatched == 1), author: parseAuthorInfo(item.author_info), seeders: formatNumberWithCommas(item.seeders ?? 0), diff --git a/app/api/use-wedge/route.js b/app/api/use-wedge/route.js new file mode 100644 index 0000000..ee4cb80 --- /dev/null +++ b/app/api/use-wedge/route.js @@ -0,0 +1,97 @@ +import { NextResponse } from "next/server"; +import { readMamToken } from "@/src/lib/config"; +import { MAM_BASE } from "@/src/lib/constants"; +import { generateTimestamp } from "@/src/lib/utilities"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function POST(req) { + try { + const { torrentId } = await req.json(); + + if (!torrentId) { + return NextResponse.json( + { error: "Torrent ID is required" }, + { status: 400 } + ); + } + + const token = readMamToken(); + const timestamp = generateTimestamp(); + + // Call MAM bonus buy API to purchase freeleech wedge + const wedgeUrl = `${MAM_BASE}/json/bonusBuy.php/${timestamp}?spendtype=personalFL&torrentid=${torrentId}×tamp=${timestamp}`; + + console.log(`Attempting to use FL wedge for torrent ${torrentId}`); + + const res = await fetch(wedgeUrl, { + method: "GET", + headers: { + "Accept": "application/json, text/plain, */*", + "Cookie": `mam_id=${token}`, + "Origin": "https://www.myanonamouse.net", + "Referer": "https://www.myanonamouse.net/" + }, + cache: "no-store" + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + console.error(`Failed to purchase FL wedge: ${res.status} - ${text}`); + + // Check for MAM token expiration + if (res.status === 403 && text.toLowerCase().includes("you are not signed in")) { + return NextResponse.json( + { + error: "Your MAM token has expired or is invalid. Please update your token.", + tokenExpired: true + }, + { status: 401 } + ); + } + + return NextResponse.json( + { error: `Failed to purchase FL wedge: ${res.status}` }, + { status: 502 } + ); + } + + let data; + try { + data = await res.json(); + } catch { + const text = await res.text().catch(() => ""); + console.error("Invalid JSON response from wedge purchase API:", text); + return NextResponse.json( + { error: "Invalid response from MAM API" }, + { status: 502 } + ); + } + + // Check if the wedge purchase was successful + // MAM API returns success: true/false and may include error message + if (data.success === false || data.error) { + const errorMsg = data.error || "Unknown error occurred"; + console.error(`FL wedge purchase failed: ${errorMsg}`); + return NextResponse.json( + { error: `Failed to use FL wedge: ${errorMsg}` }, + { status: 400 } + ); + } + + console.log(`Successfully used FL wedge for torrent ${torrentId}`); + + return NextResponse.json({ + success: true, + message: "FL wedge applied successfully", + torrentId + }); + } catch (err) { + console.error("Error using FL wedge:", err); + return NextResponse.json( + { error: err?.message || "Failed to use FL wedge" }, + { status: 500 } + ); + } +} diff --git a/app/api/user-stats/route.js b/app/api/user-stats/route.js index f09d80e..3cf1dd3 100644 --- a/app/api/user-stats/route.js +++ b/app/api/user-stats/route.js @@ -9,6 +9,19 @@ export const dynamic = "force-dynamic"; const statsCache = new Map(); const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes +// Export function to bust cache (called after wedge purchases, etc.) +export function bustStatsCache(token = null) { + if (token) { + // Bust specific token's cache + statsCache.delete(token); + console.log("Busted user stats cache for specific token"); + } else { + // Bust all cache + statsCache.clear(); + console.log("Busted all user stats cache"); + } +} + export async function GET(req) { try { const token = readMamToken(); @@ -79,7 +92,8 @@ export async function GET(req) { downloaded: data.downloaded || "0 B", ratio: data.ratio || "0.00", username: data.username || null, - uid: data.uid || null + uid: data.uid || null, + flWedges: data.wedges || 0 }; // Cache the result diff --git a/app/components/DualDownloadButton.jsx b/app/components/DualDownloadButton.jsx index 91be4a0..0995590 100644 --- a/app/components/DualDownloadButton.jsx +++ b/app/components/DualDownloadButton.jsx @@ -1,42 +1,71 @@ import PropTypes from 'prop-types'; +import WedgeToggleButton from './WedgeToggleButton'; export default function DualDownloadButton({ audiobookSelected, bookSelected, onDownload, - loading + loading, + userStats, + useAudiobookWedge, + useBookWedge, + onToggleAudiobookWedge, + onToggleBookWedge }) { const bothSelected = audiobookSelected && bookSelected; const disabled = !bothSelected || loading; + const hasWedges = userStats?.flWedges > 0; return ( -
- + + {/* Download button */} + +
); } @@ -45,5 +74,15 @@ DualDownloadButton.propTypes = { audiobookSelected: PropTypes.bool.isRequired, bookSelected: PropTypes.bool.isRequired, onDownload: PropTypes.func.isRequired, - loading: PropTypes.bool.isRequired + loading: PropTypes.bool.isRequired, + userStats: PropTypes.shape({ + uploaded: PropTypes.string, + downloaded: PropTypes.string, + ratio: PropTypes.string, + flWedges: PropTypes.number + }), + useAudiobookWedge: PropTypes.bool, + useBookWedge: PropTypes.bool, + onToggleAudiobookWedge: PropTypes.func, + onToggleBookWedge: PropTypes.func }; diff --git a/app/components/DualSearchResultsList.jsx b/app/components/DualSearchResultsList.jsx index 522a512..1d0f828 100644 --- a/app/components/DualSearchResultsList.jsx +++ b/app/components/DualSearchResultsList.jsx @@ -1,5 +1,6 @@ import SearchResultItem from './SearchResultItem'; import ProgressIndicator from './ProgressIndicator'; +import WedgeToggleButton from './WedgeToggleButton'; import PropTypes from 'prop-types'; import { parseSizeToBytes, calculateNewRatio, calculateRatioDiff, formatBytesToSize } from '@/src/lib/utilities'; @@ -13,7 +14,11 @@ export default function DualSearchResultsList({ loading, onDownload, downloadLoading, - userStats + userStats, + useAudiobookWedge, + useBookWedge, + onToggleAudiobookWedge, + onToggleBookWedge }) { if (loading) { return ( @@ -53,13 +58,31 @@ export default function DualSearchResultsList({ if (audiobookBytes && bookBytes && uploadedBytes !== null && downloadedBytes !== null) { const totalBytes = audiobookBytes + bookBytes; - const projectedRatio = calculateNewRatio(uploadedBytes, downloadedBytes, totalBytes); - const diff = calculateRatioDiff(uploadedBytes, downloadedBytes, totalBytes); - combinedInfo = { - totalSize: formatBytesToSize(totalBytes), - projectedRatio, - diff - }; + + // Calculate bytes that will affect ratio (exclude items with FL wedge or already freeleech) + let bytesForRatio = 0; + const isBookFreeleech = useBookWedge || selectedBook.freeleech; + const isAudiobookFreeleech = useAudiobookWedge || selectedAudiobook.freeleech; + + if (!isBookFreeleech) bytesForRatio += bookBytes; + if (!isAudiobookFreeleech) bytesForRatio += audiobookBytes; + + // If both are freeleech, show "No Change" as ratio doesn't change + if (bytesForRatio === 0) { + combinedInfo = { + totalSize: formatBytesToSize(totalBytes), + projectedRatio: 'No Change', + diff: null + }; + } else { + const projectedRatio = calculateNewRatio(uploadedBytes, downloadedBytes, bytesForRatio); + const diff = calculateRatioDiff(uploadedBytes, downloadedBytes, bytesForRatio); + combinedInfo = { + totalSize: formatBytesToSize(totalBytes), + projectedRatio, + diff + }; + } } } @@ -94,25 +117,75 @@ export default function DualSearchResultsList({ return (
- {/* Progress Indicator for Desktop with inline button */} - - - {/* Combined info display when both selected */} - {combinedInfo && ( -
- Combined: - {combinedInfo.totalSize} - â€ĸ - New ratio: - {combinedInfo.projectedRatio} ({combinedInfo.diff}) + {/* Integrated Header: 2-Row Layout */} +
+ {/* Row 1: Progress + Download Button */} +
+ + + {/* Download Button - always visible, disabled until both selected */} +
+ {downloadButton} +
- )} + + {/* Row 2: FL Wedge toggles + Separator + Combined Info (only when both selected) */} + {bothSelected && ( +
+ {/* FL Wedge toggles */} + {userStats?.flWedges > 0 && (selectedBook?.freeleech === false || selectedAudiobook?.freeleech === false) ? ( +
+ Use FL Wedge: + {!selectedBook?.freeleech && ( + + )} + {!selectedAudiobook?.freeleech && ( + + )} +
+ ) : ( +
+ )} + + {/* Center Separator */} +
+ + {/* Combined info */} + {combinedInfo && ( +
+ + đŸ“Ļ + {combinedInfo.totalSize} + + â€ĸ + + 📊 + + {combinedInfo.diff ? `${combinedInfo.projectedRatio} (${combinedInfo.diff})` : combinedInfo.projectedRatio} + + +
+ )} +
+ )} +
+ {/* Two-column grid for results */}
{/* Left Column: Books */}
@@ -193,6 +266,11 @@ DualSearchResultsList.propTypes = { userStats: PropTypes.shape({ uploaded: PropTypes.string, downloaded: PropTypes.string, - ratio: PropTypes.string - }) + ratio: PropTypes.string, + flWedges: PropTypes.number + }), + useAudiobookWedge: PropTypes.bool, + useBookWedge: PropTypes.bool, + onToggleAudiobookWedge: PropTypes.func, + onToggleBookWedge: PropTypes.func }; diff --git a/app/components/MobileBottomSheet.jsx b/app/components/MobileBottomSheet.jsx new file mode 100644 index 0000000..5fd6c71 --- /dev/null +++ b/app/components/MobileBottomSheet.jsx @@ -0,0 +1,129 @@ +import PropTypes from 'prop-types'; +import WedgeToggleButton from './WedgeToggleButton'; + +export default function MobileBottomSheet({ + currentStep, + progress, + bothSelected, + onDownload, + loading, + userStats, + useAudiobookWedge, + useBookWedge, + onToggleAudiobookWedge, + onToggleBookWedge, + selectedBook, + selectedAudiobook +}) { + const disabled = !bothSelected || loading; + const hasWedges = userStats?.flWedges > 0; + const stepText = currentStep === 1 ? 'a Book' : 'an Audiobook'; + const showBookWedge = selectedBook && !selectedBook.freeleech; + const showAudiobookWedge = selectedAudiobook && !selectedAudiobook.freeleech; + + return ( + <> + {/* Backdrop */} +
+ + {/* Content container - single fixed element */} +
+
+ {/* Progress indicator (only show when not both selected) */} + {!bothSelected && ( +
+
+ + Step {currentStep} of 2: + Select {stepText} + + {progress}% +
+
+
+
+
+ )} + + {/* Wedge selector panel (only show when both selected and has wedges) */} + {bothSelected && hasWedges && (showBookWedge || showAudiobookWedge) && ( +
+
+ Use FL Wedge: + {showBookWedge && ( + + )} + {showAudiobookWedge && ( + + )} +
+
+ )} + + {/* Download button panel */} +
+ +
+
+
+ + ); +} + +MobileBottomSheet.propTypes = { + currentStep: PropTypes.number.isRequired, + progress: PropTypes.number.isRequired, + bothSelected: PropTypes.bool.isRequired, + onDownload: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, + userStats: PropTypes.shape({ + uploaded: PropTypes.string, + downloaded: PropTypes.string, + ratio: PropTypes.string, + flWedges: PropTypes.number + }), + useAudiobookWedge: PropTypes.bool, + useBookWedge: PropTypes.bool, + onToggleAudiobookWedge: PropTypes.func, + onToggleBookWedge: PropTypes.func, + selectedBook: PropTypes.object, + selectedAudiobook: PropTypes.object +}; diff --git a/app/components/ProgressIndicator.jsx b/app/components/ProgressIndicator.jsx index 8b65961..2631610 100644 --- a/app/components/ProgressIndicator.jsx +++ b/app/components/ProgressIndicator.jsx @@ -5,6 +5,7 @@ export default function ProgressIndicator({ totalSteps = 2, progress, mobile = false, + compact = false, actionButton = null }) { const stepText = progress === 100 ? 'Done' : (currentStep === 1 ? 'a Book' : 'an Audiobook'); @@ -35,6 +36,27 @@ export default function ProgressIndicator({ ); } + // Compact mode: For integrated header layout (no container, just progress elements) + if (compact) { + return ( +
+ + Step {currentStep} of {totalSteps}: + Select {stepText} + +
+
+
+
+
+ {progress}% +
+ ); + } + // Desktop: Flatter design with panel - progress bar with inline text and button return (
@@ -68,5 +90,6 @@ ProgressIndicator.propTypes = { totalSteps: PropTypes.number, progress: PropTypes.number.isRequired, mobile: PropTypes.bool, + compact: PropTypes.bool, actionButton: PropTypes.node }; diff --git a/app/components/SearchForm.jsx b/app/components/SearchForm.jsx index 01897b6..4ce9194 100644 --- a/app/components/SearchForm.jsx +++ b/app/components/SearchForm.jsx @@ -29,8 +29,17 @@ export default function SearchForm({ } }; + const handleSubmit = (e) => { + e.preventDefault(); + // Blur input to dismiss mobile keyboard + if (searchInputRef.current) { + searchInputRef.current.blur(); + } + onSubmit(e); + }; + return ( -
+
{ if (selectable && onSelect) { onSelect(result); } }; + const handleToggleWedge = (e) => { + e.stopPropagation(); // Prevent triggering parent click handlers + if (onToggleWedge) { + onToggleWedge(result.id); + } + }; + + const hasWedges = userStats?.flWedges > 0; + // Calculate projected ratio if user stats are available let projectedRatioDisplay = null; if (userStats && result.size) { - const sizeBytes = parseSizeToBytes(result.size); - const uploadedBytes = parseSizeToBytes(userStats.uploaded); - const downloadedBytes = parseSizeToBytes(userStats.downloaded); - - if (sizeBytes && uploadedBytes !== null && downloadedBytes !== null) { - const newRatio = calculateNewRatio(uploadedBytes, downloadedBytes, sizeBytes); - const diff = calculateRatioDiff(uploadedBytes, downloadedBytes, sizeBytes); - projectedRatioDisplay = `${newRatio} (${diff})`; + // If using FL wedge or torrent is already freeleech, show "No Change" as ratio doesn't change + if (useWedge || result.freeleech) { + projectedRatioDisplay = 'No Change'; + } else { + const sizeBytes = parseSizeToBytes(result.size); + const uploadedBytes = parseSizeToBytes(userStats.uploaded); + const downloadedBytes = parseSizeToBytes(userStats.downloaded); + + if (sizeBytes && uploadedBytes !== null && downloadedBytes !== null) { + const newRatio = calculateNewRatio(uploadedBytes, downloadedBytes, sizeBytes); + const diff = calculateRatioDiff(uploadedBytes, downloadedBytes, sizeBytes); + projectedRatioDisplay = `${newRatio} (${diff})`; + } } } @@ -46,23 +61,50 @@ export default function SearchResultItem({ result, onAddItem, selectable = false
{/* Basic torrent information */}
- {result.title} - - by - {result.author} - - {result.vip && ( - - VIP +
+ {result.vip && ( + + VIP + + )} + {result.freeleech && ( + + Freeleech + + )} + {result.title} + + by + {result.author} - )} + e.stopPropagation()} + > + + + + + + +
{/* Torrent metadata */}
@@ -86,20 +128,22 @@ export default function SearchResultItem({ result, onAddItem, selectable = false {/* Torrent action buttons */} {!selectable && (
-
- - View - +
+ {/* FL Wedge toggle button - left on mobile */} + {hasWedges && !result.snatched && !result.freeleech && !result.vip && ( + + )} + {/* Projected ratio - center on mobile, below on desktop */} {projectedRatioDisplay && !result.snatched && ( -
+
{projectedRatioDisplay}
)} + {/* Download button - right on mobile */}
+ {/* Projected ratio - below on desktop */} {projectedRatioDisplay && !result.snatched && (
{projectedRatioDisplay} @@ -138,6 +183,7 @@ SearchResultItem.propTypes = { torrentUrl: PropTypes.string.isRequired, downloadUrl: PropTypes.string.isRequired, vip: PropTypes.bool, + freeleech: PropTypes.bool, snatched: PropTypes.bool }).isRequired, onAddItem: PropTypes.func, @@ -147,6 +193,9 @@ SearchResultItem.propTypes = { userStats: PropTypes.shape({ uploaded: PropTypes.string, downloaded: PropTypes.string, - ratio: PropTypes.string - }) + ratio: PropTypes.string, + flWedges: PropTypes.number + }), + useWedge: PropTypes.bool, + onToggleWedge: PropTypes.func }; \ No newline at end of file diff --git a/app/components/SearchResultsList.jsx b/app/components/SearchResultsList.jsx index 595bed5..24250a6 100644 --- a/app/components/SearchResultsList.jsx +++ b/app/components/SearchResultsList.jsx @@ -1,7 +1,7 @@ import SearchResultItem from './SearchResultItem'; import PropTypes from 'prop-types'; -export default function SearchResultsList({ results, onAddItem, loading, userStats }) { +export default function SearchResultsList({ results, onAddItem, loading, userStats, singleModeWedges, onToggleWedge }) { if (!loading && results.length === 0) { return

â˜ī¸ Try a search to see results...

; } @@ -14,6 +14,8 @@ export default function SearchResultsList({ results, onAddItem, loading, userSta result={result} onAddItem={onAddItem} userStats={userStats} + useWedge={singleModeWedges?.[result.id] || false} + onToggleWedge={onToggleWedge} /> ))} @@ -27,6 +29,9 @@ SearchResultsList.propTypes = { userStats: PropTypes.shape({ uploaded: PropTypes.string, downloaded: PropTypes.string, - ratio: PropTypes.string - }) + ratio: PropTypes.string, + flWedges: PropTypes.number + }), + singleModeWedges: PropTypes.object, + onToggleWedge: PropTypes.func }; \ No newline at end of file diff --git a/app/components/SequentialSearchResults.jsx b/app/components/SequentialSearchResults.jsx index 234cc85..8895db4 100644 --- a/app/components/SequentialSearchResults.jsx +++ b/app/components/SequentialSearchResults.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; import SearchResultItem from './SearchResultItem'; -import ProgressIndicator from './ProgressIndicator'; +import MobileBottomSheet from './MobileBottomSheet'; import PropTypes from 'prop-types'; import { parseSizeToBytes, calculateNewRatio, calculateRatioDiff, formatBytesToSize } from '@/src/lib/utilities'; @@ -15,13 +15,20 @@ export default function SequentialSearchResults({ onSelectAudiobook, onSelectBook, loading, - userStats + userStats, + onDownload, + downloadLoading, + useAudiobookWedge, + useBookWedge, + onToggleAudiobookWedge, + onToggleBookWedge }) { const bookSectionRef = useRef(null); + const audiobookSectionRef = useRef(null); - // Auto-scroll to audiobook results when book is selected + // Auto-scroll to books section when search results first load useEffect(() => { - if (selectedBook && bookSectionRef.current) { + if (!loading && (bookResults.length > 0 || audiobookResults.length > 0) && bookSectionRef.current) { setTimeout(() => { bookSectionRef.current?.scrollIntoView({ behavior: 'smooth', @@ -29,6 +36,18 @@ export default function SequentialSearchResults({ }); }, AUTO_SCROLL_DELAY_MS); } + }, [loading, bookResults.length, audiobookResults.length]); + + // Auto-scroll to audiobook results when book is selected + useEffect(() => { + if (selectedBook && audiobookSectionRef.current) { + setTimeout(() => { + audiobookSectionRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + }, AUTO_SCROLL_DELAY_MS); + } }, [selectedBook]); if (loading) { @@ -67,23 +86,50 @@ export default function SequentialSearchResults({ if (audiobookBytes && bookBytes && uploadedBytes !== null && downloadedBytes !== null) { const totalBytes = audiobookBytes + bookBytes; - const projectedRatio = calculateNewRatio(uploadedBytes, downloadedBytes, totalBytes); - const diff = calculateRatioDiff(uploadedBytes, downloadedBytes, totalBytes); - combinedInfo = { - totalSize: formatBytesToSize(totalBytes), - projectedRatio, - diff - }; + + // Calculate bytes that will affect ratio (exclude items with FL wedge or already freeleech) + let bytesForRatio = 0; + const isBookFreeleech = useBookWedge || selectedBook.freeleech; + const isAudiobookFreeleech = useAudiobookWedge || selectedAudiobook.freeleech; + + if (!isBookFreeleech) bytesForRatio += bookBytes; + if (!isAudiobookFreeleech) bytesForRatio += audiobookBytes; + + // If both are freeleech, show "No Change" as ratio doesn't change + if (bytesForRatio === 0) { + combinedInfo = { + totalSize: formatBytesToSize(totalBytes), + projectedRatio: 'No Change', + diff: null + }; + } else { + const projectedRatio = calculateNewRatio(uploadedBytes, downloadedBytes, bytesForRatio); + const diff = calculateRatioDiff(uploadedBytes, downloadedBytes, bytesForRatio); + combinedInfo = { + totalSize: formatBytesToSize(totalBytes), + projectedRatio, + diff + }; + } } } return ( -
- {/* Progress Indicator - Fixed at bottom on mobile */} - + {/* Mobile bottom sheet with progress indicator and download button */} + {/* Combined info display when both selected */} @@ -93,12 +139,14 @@ export default function SequentialSearchResults({ {combinedInfo.totalSize} â€ĸ New ratio: - {combinedInfo.projectedRatio} ({combinedInfo.diff}) + + {combinedInfo.diff ? `${combinedInfo.projectedRatio} (${combinedInfo.diff})` : combinedInfo.projectedRatio} +
)} {/* Step 1: Book Selection */} -
+
{selectedBook ? ( // Collapsed view showing selected book
@@ -153,7 +201,7 @@ export default function SequentialSearchResults({ {/* Step 2: Audiobook Selection (only shown after book selected) */} {selectedBook && ( -
+
{selectedAudiobook ? ( // Collapsed view showing selected audiobook
@@ -235,6 +283,13 @@ SequentialSearchResults.propTypes = { userStats: PropTypes.shape({ uploaded: PropTypes.string, downloaded: PropTypes.string, - ratio: PropTypes.string - }) + ratio: PropTypes.string, + flWedges: PropTypes.number + }), + onDownload: PropTypes.func.isRequired, + downloadLoading: PropTypes.bool.isRequired, + useAudiobookWedge: PropTypes.bool, + useBookWedge: PropTypes.bool, + onToggleAudiobookWedge: PropTypes.func, + onToggleBookWedge: PropTypes.func }; diff --git a/app/components/UserStatsBar.jsx b/app/components/UserStatsBar.jsx index a894843..2a469f3 100644 --- a/app/components/UserStatsBar.jsx +++ b/app/components/UserStatsBar.jsx @@ -29,30 +29,49 @@ export default function UserStatsBar({ stats, loading, error }) { return (
-
+ {/* Mobile: 2x2 grid layout with vertical divider */} +
- - U: - Upload: - + U: + {stats.uploaded} +
+
+
+ D: + {stats.downloaded} +
+
+ R: + {stats.ratio} +
+ {/* Vertical divider spans both rows */} +
+ FL: + {stats.flWedges || 0} +
+
+ + {/* Desktop: horizontal layout with dots */} +
+
+ Upload: {stats.uploaded}
â€ĸ
- - D: - Download: - + Download: {stats.downloaded}
â€ĸ
- - R: - Ratio: - + Ratio: {stats.ratio}
+ â€ĸ +
+ FL Wedges: + {stats.flWedges || 0} +
); @@ -63,7 +82,8 @@ UserStatsBar.propTypes = { uploaded: PropTypes.string.isRequired, downloaded: PropTypes.string.isRequired, ratio: PropTypes.string.isRequired, - username: PropTypes.string + username: PropTypes.string, + flWedges: PropTypes.number }), loading: PropTypes.bool, error: PropTypes.string diff --git a/app/components/WedgeToggleButton.jsx b/app/components/WedgeToggleButton.jsx new file mode 100644 index 0000000..2828dac --- /dev/null +++ b/app/components/WedgeToggleButton.jsx @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; + +export default function WedgeToggleButton({ + active, + onClick, + label, + size = 'small' // 'small' for icon-only, 'large' for icon + text +}) { + const sizeClasses = size === 'small' + ? 'p-1.5' + : 'px-3 py-1.5'; + + const iconSize = size === 'small' ? 'w-4 h-4' : 'w-4 h-4'; + + return ( + + ); +} + +WedgeToggleButton.propTypes = { + active: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + label: PropTypes.string, + size: PropTypes.oneOf(['small', 'large']) +}; diff --git a/app/page.jsx b/app/page.jsx index cd77114..5552e5f 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -5,7 +5,6 @@ import MessageBanner from './MessageBanner'; import Header from './components/Header'; import SearchForm from './components/SearchForm'; import SearchResultsList from './components/SearchResultsList'; -import DualDownloadButton from './components/DualDownloadButton'; import DualSearchResultsList from './components/DualSearchResultsList'; import SequentialSearchResults from './components/SequentialSearchResults'; import UserStatsBar from './components/UserStatsBar'; @@ -36,6 +35,11 @@ function SearchPage() { const [selectedAudiobook, setSelectedAudiobook] = useState(null); const [selectedBook, setSelectedBook] = useState(null); const [dualDownloadLoading, setDualDownloadLoading] = useState(false); + + // FL Wedge state + const [singleModeWedges, setSingleModeWedges] = useState({}); // { torrentId: boolean } + const [useAudiobookWedge, setUseAudiobookWedge] = useState(false); + const [useBookWedge, setUseBookWedge] = useState(false); // Load saved category from localStorage on mount useEffect(() => { @@ -75,6 +79,9 @@ function SearchPage() { setSelectedAudiobook(null); setSelectedBook(null); setMessage(null); + setSingleModeWedges({}); + setUseAudiobookWedge(false); + setUseBookWedge(false); try { // Handle "both" mode with parallel searches @@ -265,22 +272,37 @@ function SearchPage() { // Map search category to qBittorrent category const qbCategory = searchCategory === "audiobooks" ? "audiobooks" : "books"; + // Check if wedge should be used for this item + const useWedge = singleModeWedges[item.id] || false; + const res = await fetch(`/api/add`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: item.title, downloadUrl: item.downloadUrl, - category: qbCategory + torrentId: item.id, + category: qbCategory, + useWedge }) }); const data = await res.json(); if (!res.ok || !data.ok) throw new Error(data.error || "Add failed"); - setMessage({ type: "success", text: `Queued: ${item.title}` }); + + setMessage({ + type: "success", + text: `Queued: ${item.title}${useWedge ? ' (FL Wedge applied)' : ''}` + }); + + // Refresh user stats if wedge was used + if (useWedge && data.wedgeUsed) { + fetchUserStats(); + } // Clear search and scroll to top setQ(""); setResults([]); + setSingleModeWedges({}); window.scrollTo({ top: 0, behavior: "smooth" }); // Remove message after 3 seconds @@ -290,7 +312,7 @@ function SearchPage() { } catch (err) { setMessage({ type: "error", text: err?.message || "Add failed" }); } - }, [searchCategory]); + }, [searchCategory, singleModeWedges, fetchUserStats]); const clearResults = useCallback(() => { setResults([]); @@ -299,6 +321,9 @@ function SearchPage() { setSelectedAudiobook(null); setSelectedBook(null); setMessage(null); + setSingleModeWedges({}); + setUseAudiobookWedge(false); + setUseBookWedge(false); }, []); // Dual-mode selection handlers @@ -330,7 +355,9 @@ function SearchPage() { body: JSON.stringify({ title: selectedAudiobook.title, downloadUrl: selectedAudiobook.downloadUrl, - category: 'audiobooks' + torrentId: selectedAudiobook.id, + category: 'audiobooks', + useWedge: useAudiobookWedge }) }), fetch('/api/add', { @@ -339,7 +366,9 @@ function SearchPage() { body: JSON.stringify({ title: selectedBook.title, downloadUrl: selectedBook.downloadUrl, - category: 'books' + torrentId: selectedBook.id, + category: 'books', + useWedge: useBookWedge }) }) ]); @@ -353,11 +382,21 @@ function SearchPage() { const audiobookSuccess = audiobookRes.ok && audiobookData.ok; const bookSuccess = bookRes.ok && bookData.ok; + // Refresh stats if any wedge was used + if ((useAudiobookWedge && audiobookSuccess) || (useBookWedge && bookSuccess)) { + fetchUserStats(); + } + if (audiobookSuccess && bookSuccess) { // Both succeeded + const wedgeInfo = []; + if (useAudiobookWedge) wedgeInfo.push('audiobook FL'); + if (useBookWedge) wedgeInfo.push('book FL'); + const wedgeText = wedgeInfo.length > 0 ? ` (${wedgeInfo.join(', ')} applied)` : ''; + setMessage({ type: 'success', - text: `✓ Queued 2 items: ${selectedBook.title} + ${selectedAudiobook.title}` + text: `✓ Queued 2 items: ${selectedBook.title} + ${selectedAudiobook.title}${wedgeText}` }); // Clear and reset @@ -366,6 +405,8 @@ function SearchPage() { setBookResults([]); setSelectedAudiobook(null); setSelectedBook(null); + setUseAudiobookWedge(false); + setUseBookWedge(false); window.scrollTo({ top: 0, behavior: 'smooth' }); setTimeout(() => setMessage(null), SUCCESS_MESSAGE_DURATION_MS); @@ -401,7 +442,23 @@ function SearchPage() { } finally { setDualDownloadLoading(false); } - }, [selectedAudiobook, selectedBook]); + }, [selectedAudiobook, selectedBook, useAudiobookWedge, useBookWedge, fetchUserStats]); + + // Wedge toggle handlers + const handleToggleSingleWedge = useCallback((torrentId) => { + setSingleModeWedges(prev => ({ + ...prev, + [torrentId]: !prev[torrentId] + })); + }, []); + + const handleToggleAudiobookWedge = useCallback(() => { + setUseAudiobookWedge(prev => !prev); + }, []); + + const handleToggleBookWedge = useCallback(() => { + setUseBookWedge(prev => !prev); + }, []); const handleTokenUpdate = (tokenExists) => { setMamTokenExists(tokenExists); @@ -468,16 +525,6 @@ function SearchPage() { {searchCategory === 'both' ? ( <> - {/* Mobile: Download button separate */} -
- -
- {/* Desktop: side-by-side */}
- {/* Mobile: sequential */} + {/* Mobile: sequential with unified bottom sheet */}
@@ -514,6 +571,8 @@ function SearchPage() { onAddItem={addItem} loading={loading} userStats={userStats} + singleModeWedges={singleModeWedges} + onToggleWedge={handleToggleSingleWedge} /> )} diff --git a/docs/assets/dual-mobile.png b/docs/assets/dual-mobile.png index 56c6f5d..aa451e2 100644 Binary files a/docs/assets/dual-mobile.png and b/docs/assets/dual-mobile.png differ diff --git a/docs/assets/dual-web.png b/docs/assets/dual-web.png index b56b753..2ac488e 100644 Binary files a/docs/assets/dual-web.png and b/docs/assets/dual-web.png differ diff --git a/docs/assets/main-screenshot.png b/docs/assets/main-screenshot.png index cf76aef..3f78833 100644 Binary files a/docs/assets/main-screenshot.png and b/docs/assets/main-screenshot.png differ diff --git a/package-lock.json b/package-lock.json index 2b034bd..774e24c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "scurry", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "scurry", - "version": "2.2.0", + "version": "2.3.0", "dependencies": { "next": "16.1.4", "react": "19.2.3", @@ -14,16 +14,35 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.21", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.0.0", "eslint-config-next": "16.1.4", + "jsdom": "^27.4.0", "postcss": "^8.5.6", "tailwindcss": "^4.1.11", "vitest": "^4.0.18" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -51,6 +70,61 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -184,6 +258,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -244,6 +328,48 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -292,6 +418,153 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz", + "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==", + "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-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", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -911,6 +1184,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.9.0.tgz", + "integrity": "sha512-lagqsvnk09NKogQaN/XrtlWeUF8SRhT12odMvbTIIaVObqzwAogL6jhR4DAp0gPuKoM1AOVrKUshJpRdpMFrww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1482,9 +1773,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1704,6 +1995,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.56.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", @@ -2353,6 +2651,91 @@ "tailwindcss": "4.1.11" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "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/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -2364,6 +2747,58 @@ "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==", + "dev": true, + "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/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -2942,6 +3377,58 @@ "win32" ] }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -3100,6 +3587,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "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", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3117,6 +3614,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3327,6 +3834,25 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3438,6 +3964,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3668,6 +4204,53 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -3675,6 +4258,30 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3747,6 +4354,13 @@ } } }, + "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/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3790,6 +4404,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3813,6 +4437,13 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3856,6 +4487,19 @@ "node": ">=10.13.0" } }, + "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/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -5011,16 +5655,64 @@ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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": "MIT" + }, + "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", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "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": "MIT", "dependencies": { - "hermes-estree": "0.25.1" + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, "node_modules/ignore": { @@ -5060,6 +5752,16 @@ "node": ">=0.8.19" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5344,6 +6046,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", @@ -5503,6 +6212,45 @@ "dev": true, "license": "ISC" }, + "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-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": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -5551,6 +6299,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "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", @@ -5943,6 +6732,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5953,6 +6752,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5963,6 +6803,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5987,6 +6834,16 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6420,6 +7277,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6530,6 +7400,41 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6603,6 +7508,30 @@ "dev": true, "license": "MIT" }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6647,6 +7576,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6823,6 +7762,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -7234,6 +8186,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "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", @@ -7296,6 +8261,13 @@ "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/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", @@ -7418,6 +8390,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7441,6 +8433,32 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -7911,6 +8929,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "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": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8043,6 +9108,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 364e132..60a9bad 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,19 @@ { "name": "scurry", + "type": "module", "author": { "name": "Mason Fox", "email": "masonfox22@gmail.com" }, "private": true, - "version": "2.4.0", + "version": "2.5.0", "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest", "test:ui": "vitest --ui" }, @@ -22,11 +24,16 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.21", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.0.0", "eslint-config-next": "16.1.4", + "jsdom": "^27.4.0", "postcss": "^8.5.6", "tailwindcss": "^4.1.11", "vitest": "^4.0.18" diff --git a/public/images/freeleech.gif b/public/images/freeleech.gif new file mode 100644 index 0000000..c735646 Binary files /dev/null and b/public/images/freeleech.gif differ diff --git a/public/images/vip.png b/public/images/vip.png new file mode 100644 index 0000000..923c9e3 Binary files /dev/null and b/public/images/vip.png differ diff --git a/src/lib/utilities.js b/src/lib/utilities.js index 87d27ca..c426e5a 100644 --- a/src/lib/utilities.js +++ b/src/lib/utilities.js @@ -166,3 +166,11 @@ export function calculateRatioDiff(uploadedBytes, downloadedBytes, additionalByt const diff = parseFloat(newRatio) - currentRatio; return diff.toFixed(4); } + +/** + * Generates Unix timestamp in milliseconds + * @returns {number} - Current Unix timestamp in milliseconds + */ +export function generateTimestamp() { + return Date.now(); +} diff --git a/vitest.config.mjs b/vitest.config.mjs index 9bd04dd..1a118a1 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -1,19 +1,44 @@ import { defineConfig } from 'vitest/config'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; +import react from '@vitejs/plugin-react'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ + plugins: [react()], test: { globals: true, - environment: 'node', + environment: 'jsdom', setupFiles: ['./vitest.setup.js'], - include: ['**/__tests__/**/*.js', '**/__tests__/**/*.mjs', '**/?(*.)+(spec|test).js', '**/?(*.)+(spec|test).mjs'], + include: ['**/__tests__/**/*.js', '**/__tests__/**/*.mjs', '**/__tests__/**/*.jsx', '**/?(*.)+(spec|test).js', '**/?(*.)+(spec|test).mjs', '**/?(*.)+(spec|test).jsx'], exclude: ['**/__tests__/helpers/**', '**/node_modules/**', '**/dist/**', '**/build/**'], coverage: { provider: 'v8', - reporter: ['text', 'json', 'html'], + reporter: ['text', 'json', 'html', 'lcov'], + include: ['app/**/*.{js,jsx}', 'src/**/*.{js,jsx}'], + exclude: [ + '**/__tests__/**', + '**/*.test.{js,jsx,mjs}', + '**/*.spec.{js,jsx,mjs}', + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/.next/**', + '**/coverage/**', + 'app/layout.js', + 'app/components/**', + 'app/page.jsx', + 'app/login/page.jsx', + ], + all: true, + clean: true, + thresholds: { + lines: 60, + functions: 60, + branches: 60, + statements: 60 + } }, }, resolve: { diff --git a/yarn.lock b/yarn.lock index bf407f9..48f3965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,16 @@ # yarn lockfile v1 +"@acemir/cssom@^0.9.28": + version "0.9.31" + resolved "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz" + integrity sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA== + +"@adobe/css-tools@^4.4.0": + version "4.4.4" + resolved "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" @@ -15,7 +25,34 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.28.6": +"@asamuzakjp/css-color@^4.1.1": + version "4.1.1" + resolved "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz" + integrity sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + lru-cache "^11.2.4" + +"@asamuzakjp/dom-selector@^6.7.6": + version "6.7.6" + resolved "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz" + integrity sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg== + dependencies: + "@asamuzakjp/nwsapi" "^2.3.9" + bidi-js "^1.0.3" + css-tree "^3.1.0" + is-potential-custom-element-name "^1.0.1" + lru-cache "^11.2.4" + +"@asamuzakjp/nwsapi@^2.3.9": + version "2.3.9" + resolved "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz" + integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q== + +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.28.6": version "7.28.6" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz" integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== @@ -29,7 +66,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz" integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== -"@babel/core@^7.0.0", "@babel/core@^7.24.4": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.24.4", "@babel/core@^7.28.5": version "7.28.6" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz" integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw== @@ -94,6 +131,11 @@ "@babel/helper-validator-identifier" "^7.28.5" "@babel/traverse" "^7.28.6" +"@babel/helper-plugin-utils@^7.27.1": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + "@babel/helper-string-parser@^7.27.1": version "7.27.1" resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" @@ -117,13 +159,32 @@ "@babel/template" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/parser@^7.24.4", "@babel/parser@^7.28.6": +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.4", "@babel/parser@^7.28.5", "@babel/parser@^7.28.6": version "7.28.6" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz" integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== dependencies: "@babel/types" "^7.28.6" +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/runtime@^7.12.5": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + "@babel/template@^7.28.6": version "7.28.6" resolved "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz" @@ -146,7 +207,7 @@ "@babel/types" "^7.28.6" debug "^4.3.1" -"@babel/types@^7.26.0", "@babel/types@^7.28.6": +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.26.0", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6": version "7.28.6" resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz" integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== @@ -154,6 +215,44 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@bcoe/v8-coverage@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz" + integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== + +"@csstools/color-helpers@^5.1.0": + 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== + +"@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== + +"@csstools/css-color-parser@^3.1.0": + 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== + dependencies: + "@csstools/color-helpers" "^5.1.0" + "@csstools/css-calc" "^2.1.4" + +"@csstools/css-parser-algorithms@^3.0.5": + 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== + +"@csstools/css-syntax-patches-for-csstree@^1.0.21": + version "1.0.25" + resolved "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz" + integrity sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q== + +"@csstools/css-tokenizer@^3.0.4": + version "3.0.4" + resolved "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== + "@emnapi/core@^1.4.3": version "1.4.5" resolved "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz" @@ -249,6 +348,11 @@ "@eslint/core" "^0.17.0" levn "^0.4.1" +"@exodus/bytes@^1.6.0": + version "1.9.0" + resolved "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.9.0.tgz" + integrity sha512-lagqsvnk09NKogQaN/XrtlWeUF8SRhT12odMvbTIIaVObqzwAogL6jhR4DAp0gPuKoM1AOVrKUshJpRdpMFrww== + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" @@ -334,10 +438,10 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": - version "0.3.29" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz" - integrity sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ== +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -395,6 +499,11 @@ resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz" integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww== +"@rolldown/pluginutils@1.0.0-beta.53": + version "1.0.0-beta.53" + resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz" + integrity sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ== + "@rollup/rollup-linux-x64-gnu@4.56.0": version "4.56.0" resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz" @@ -477,6 +586,39 @@ postcss "^8.4.41" tailwindcss "4.1.11" +"@testing-library/dom@^10.0.0": + version "10.4.1" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + 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" + +"@testing-library/jest-dom@^6.9.1": + 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== + 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" + +"@testing-library/react@^16.3.2": + 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== + dependencies: + "@babel/runtime" "^7.12.5" + "@tybys/wasm-util@^0.10.0": version "0.10.0" resolved "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz" @@ -484,6 +626,44 @@ dependencies: tslib "^2.4.0" +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@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== + dependencies: + "@babel/types" "^7.0.0" + +"@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== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@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== + dependencies: + "@babel/types" "^7.28.2" + "@types/chai@^5.2.2": version "5.2.3" resolved "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz" @@ -618,6 +798,34 @@ 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== +"@vitejs/plugin-react@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz" + integrity sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ== + dependencies: + "@babel/core" "^7.28.5" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-beta.53" + "@types/babel__core" "^7.20.5" + react-refresh "^0.18.0" + +"@vitest/coverage-v8@^4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz" + integrity sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg== + dependencies: + "@bcoe/v8-coverage" "^1.0.2" + "@vitest/utils" "4.0.18" + ast-v8-to-istanbul "^0.3.10" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-reports "^3.2.0" + magicast "^0.5.1" + obug "^2.1.1" + std-env "^3.10.0" + tinyrainbow "^3.0.3" + "@vitest/expect@4.0.18": version "4.0.18" resolved "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz" @@ -699,6 +907,11 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -709,6 +922,11 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" @@ -716,16 +934,28 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^5.3.2: +aria-query@^5.0.0, aria-query@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz" @@ -827,6 +1057,15 @@ ast-types-flow@^0.0.8: resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== +ast-v8-to-istanbul@^0.3.10: + version "0.3.10" + resolved "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz" + integrity sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.31" + estree-walker "^3.0.3" + js-tokens "^9.0.1" + async-function@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz" @@ -878,6 +1117,13 @@ baseline-browser-mapping@^2.8.3: resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz" integrity sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ== +bidi-js@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz" + integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw== + dependencies: + require-from-string "^2.0.2" + brace-expansion@^1.1.7: version "1.1.12" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" @@ -1000,11 +1246,42 @@ cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +css-tree@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz" + integrity sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w== + dependencies: + mdn-data "2.12.2" + source-map-js "^1.0.1" + +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + +cssstyle@^5.3.4: + version "5.3.7" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz" + integrity sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ== + dependencies: + "@asamuzakjp/css-color" "^4.1.1" + "@csstools/css-syntax-patches-for-csstree" "^1.0.21" + css-tree "^3.1.0" + lru-cache "^11.2.4" + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +data-urls@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz" + integrity sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ== + dependencies: + whatwg-mimetype "^5.0.0" + whatwg-url "^15.1.0" + data-view-buffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz" @@ -1039,13 +1316,18 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3, debug@4: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" +decimal.js@^10.6.0: + version "10.6.0" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz" + integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -1069,6 +1351,11 @@ define-properties@^1.1.3, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-libc@^2.0.3, detect-libc@^2.0.4, detect-libc@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" @@ -1081,6 +1368,16 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-accessibility-api@^0.6.3: + 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== + dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" @@ -1108,6 +1405,11 @@ enhanced-resolve@^5.18.1: graceful-fs "^4.2.4" tapable "^2.2.0" +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0: version "1.24.0" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz" @@ -1762,6 +2064,34 @@ hermes-parser@^0.25.1: dependencies: hermes-estree "0.25.1" +html-encoding-sniffer@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz" + integrity sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg== + dependencies: + "@exodus/bytes" "^1.6.0" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-proxy-agent@^7.0.2: + 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== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^7.0.6: + version "7.0.6" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + ignore@^5.2.0: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" @@ -1785,6 +2115,11 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + internal-slot@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz" @@ -1917,6 +2252,11 @@ is-number@^7.0.0: resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-potential-custom-element-name@^1.0.1: + 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== + is-regex@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz" @@ -1993,6 +2333,28 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2: + 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== + +istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-reports@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + iterator.prototype@^1.1.4: version "1.1.5" resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz" @@ -2015,6 +2377,11 @@ jiti@*, jiti@^2.4.2, jiti@>=1.21.0: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-tokens@^9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz" + integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== + js-yaml@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz" @@ -2022,6 +2389,32 @@ js-yaml@^4.1.1: dependencies: argparse "^2.0.1" +jsdom@*, jsdom@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz" + integrity sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ== + dependencies: + "@acemir/cssom" "^0.9.28" + "@asamuzakjp/dom-selector" "^6.7.6" + "@exodus/bytes" "^1.6.0" + cssstyle "^5.3.4" + data-urls "^6.0.0" + decimal.js "^10.6.0" + html-encoding-sniffer "^6.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.6" + is-potential-custom-element-name "^1.0.1" + parse5 "^8.0.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^6.0.0" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^8.0.0" + whatwg-mimetype "^4.0.0" + whatwg-url "^15.1.0" + ws "^8.18.3" + xml-name-validator "^5.0.0" + jsesc@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" @@ -2138,6 +2531,11 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lru-cache@^11.2.4: + version "11.2.4" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz" + integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" @@ -2145,6 +2543,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-string@^0.30.17, magic-string@^0.30.21: version "0.30.21" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz" @@ -2152,11 +2555,32 @@ magic-string@^0.30.17, magic-string@^0.30.21: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" +magicast@^0.5.1: + version "0.5.1" + resolved "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz" + integrity sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + source-map-js "^1.2.1" + +make-dir@^4.0.0: + 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== + dependencies: + semver "^7.5.3" + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +mdn-data@2.12.2: + version "2.12.2" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz" + integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" @@ -2170,6 +2594,11 @@ micromatch@^4.0.4: braces "^3.0.3" picomatch "^2.3.1" +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -2376,6 +2805,13 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse5@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz" + integrity sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA== + dependencies: + entities "^6.0.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -2396,7 +2832,7 @@ pathe@^2.0.3: resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== -picocolors@^1.0.0, picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.1, picocolors@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -2444,6 +2880,15 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" @@ -2453,7 +2898,7 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -2463,7 +2908,7 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -"react-dom@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react-dom@19.2.3: +"react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react-dom@19.2.3: version "19.2.3" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz" integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== @@ -2475,11 +2920,29 @@ react-is@^16.13.1: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^19.2.3, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", react@19.2.3: +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-refresh@^0.18.0: + version "0.18.0" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz" + integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== + +"react@^18.0.0 || ^19.0.0", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^19.2.3, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", react@19.2.3: version "19.2.3" resolved "https://registry.npmjs.org/react/-/react-19.2.3.tgz" integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz" @@ -2506,6 +2969,11 @@ regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: gopd "^1.2.0" set-function-name "^2.0.2" +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -2608,6 +3076,13 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.27.0: version "0.27.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz" @@ -2618,6 +3093,11 @@ semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.5.3: + version "7.7.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + semver@^7.7.1: version "7.7.3" resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" @@ -2759,7 +3239,7 @@ sirv@^3.0.2: mrmime "^2.0.0" totalist "^3.0.0" -source-map-js@^1.0.2, source-map-js@^1.2.1: +source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -2860,6 +3340,13 @@ strip-bom@^3.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" @@ -2884,6 +3371,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symbol-tree@^3.2.4: + 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== + tailwindcss@^4.1.11, tailwindcss@4.1.11: version "4.1.11" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz" @@ -2929,6 +3421,18 @@ tinyrainbow@^3.0.3: resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz" integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q== +tldts-core@^7.0.19: + version "7.0.19" + resolved "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz" + integrity sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A== + +tldts@^7.0.5: + version "7.0.19" + resolved "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz" + integrity sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA== + dependencies: + tldts-core "^7.0.19" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -2941,6 +3445,20 @@ totalist@^3.0.0: resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz" integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== +tough-cookie@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz" + integrity sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w== + dependencies: + tldts "^7.0.5" + +tr46@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz" + integrity sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw== + dependencies: + punycode "^2.3.1" + ts-api-utils@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz" @@ -3080,7 +3598,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -"vite@^6.0.0 || ^7.0.0", "vite@^6.0.0 || ^7.0.0-0": +"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^6.0.0 || ^7.0.0", "vite@^6.0.0 || ^7.0.0-0": version "7.3.1" resolved "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz" integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== @@ -3120,6 +3638,36 @@ vitest@^4.0.18, vitest@4.0.18: vite "^6.0.0 || ^7.0.0" why-is-node-running "^2.3.0" +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + +webidl-conversions@^8.0.0: + version "8.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz" + integrity sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ== + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-mimetype@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz" + integrity sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw== + +whatwg-url@^15.1.0: + version "15.1.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz" + integrity sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g== + dependencies: + tr46 "^6.0.0" + webidl-conversions "^8.0.0" + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz" @@ -3193,6 +3741,21 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +ws@^8.18.3: + version "8.19.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz" + integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg== + +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"