diff --git a/package.json b/package.json index 2ac3937..d343ce9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@tsconfig/node-lts": "^24.0.0", "@vitest/coverage-v8": "^4.0.16", "husky": "^9.1.7", + "jsdom": "^27.3.0", "tsup": "^8.5.1", "turbo": "^2.7.2", "typescript": "^5.9.3", diff --git a/packages/plugins/src/banner/banner.test.ts b/packages/plugins/src/banner/banner.test.ts new file mode 100644 index 0000000..fca437e --- /dev/null +++ b/packages/plugins/src/banner/banner.test.ts @@ -0,0 +1,439 @@ +import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest'; +import { SDK } from '@lytics/sdk-kit'; +import { bannerPlugin, type BannerPlugin } from './banner'; +import type { Experience } from '@prosdevlab/experience-sdk'; + +type SDKWithBanner = SDK & { banner: BannerPlugin }; + +describe('Banner Plugin', () => { + let sdk: SDKWithBanner; + + beforeEach(() => { + sdk = new SDK({ + banner: { position: 'top', dismissable: true }, + }) as SDKWithBanner; + sdk.use(bannerPlugin); + + // Clean up any existing banners + document.body.innerHTML = ''; + }); + + afterEach(() => { + // Clean up DOM + document.body.innerHTML = ''; + }); + + describe('Plugin Registration', () => { + it('should register banner plugin', () => { + expect(sdk.banner).toBeDefined(); + }); + + it('should expose banner API methods', () => { + expect(sdk.banner.show).toBeTypeOf('function'); + expect(sdk.banner.remove).toBeTypeOf('function'); + expect(sdk.banner.isShowing).toBeTypeOf('function'); + }); + }); + + describe('Configuration', () => { + it('should use default config', () => { + const position = sdk.get('banner.position'); + const dismissable = sdk.get('banner.dismissable'); + const zIndex = sdk.get('banner.zIndex'); + + expect(position).toBe('top'); + expect(dismissable).toBe(true); + expect(zIndex).toBe(10000); + }); + + it('should allow custom config', () => { + const customSdk = new SDK({ + banner: { position: 'bottom', dismissable: false, zIndex: 5000 }, + }) as SDKWithBanner; + customSdk.use(bannerPlugin); + + expect(customSdk.get('banner.position')).toBe('bottom'); + expect(customSdk.get('banner.dismissable')).toBe(false); + expect(customSdk.get('banner.zIndex')).toBe(5000); + }); + }); + + describe('Banner Creation', () => { + it('should create and show a banner', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: { url: { contains: '/' } }, + content: { + title: 'Test Title', + message: 'Test message', + }, + }; + + sdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]'); + expect(banner).toBeTruthy(); + expect(banner?.textContent).toContain('Test Title'); + expect(banner?.textContent).toContain('Test message'); + }); + + it('should show banner at top position by default', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement; + expect(banner.style.top).toBe('0px'); + expect(banner.style.bottom).toBe(''); + }); + + it('should show banner at bottom position when configured', () => { + const customSdk = new SDK({ banner: { position: 'bottom' } }) as SDKWithBanner; + customSdk.use(bannerPlugin); + + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + customSdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement; + expect(banner.style.bottom).toBe('0px'); + expect(banner.style.top).toBe(''); + }); + + it('should create banner with title and message', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Welcome!', + message: 'This is a test banner', + }, + }; + + sdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]'); + expect(banner?.textContent).toContain('Welcome!'); + expect(banner?.textContent).toContain('This is a test banner'); + }); + + it('should create banner with only message (no title)', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: '', + message: 'Just a message', + }, + }; + + sdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]'); + expect(banner?.textContent).toContain('Just a message'); + }); + }); + + describe('Banner Dismissal', () => { + it('should include close button when dismissable', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + const closeButton = document.querySelector('[data-experience-id="test-banner"] button'); + expect(closeButton).toBeTruthy(); + expect(closeButton?.getAttribute('aria-label')).toBe('Close banner'); + }); + + it('should not include close button when not dismissable', () => { + const customSdk = new SDK({ banner: { dismissable: false } }) as SDKWithBanner; + customSdk.use(bannerPlugin); + + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + customSdk.banner.show(experience); + + const closeButton = document.querySelector('[data-experience-id="test-banner"] button'); + expect(closeButton).toBeNull(); + }); + + it('should remove banner when close button is clicked', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + const closeButton = document.querySelector( + '[data-experience-id="test-banner"] button' + ) as HTMLElement; + closeButton.click(); + + const banner = document.querySelector('[data-experience-id="test-banner"]'); + expect(banner).toBeNull(); + }); + + it('should emit experiences:dismissed event when banner is dismissed', () => { + const handler = vi.fn(); + sdk.on('experiences:dismissed', handler); + + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + const closeButton = document.querySelector( + '[data-experience-id="test-banner"] button' + ) as HTMLElement; + closeButton.click(); + + expect(handler).toHaveBeenCalledWith({ + experienceId: 'test-banner', + type: 'banner', + }); + }); + }); + + describe('Banner Management', () => { + it('should return false for isShowing when no banner is active', () => { + expect(sdk.banner.isShowing()).toBe(false); + }); + + it('should return true for isShowing when banner is active', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + expect(sdk.banner.isShowing()).toBe(true); + }); + + it('should remove existing banner when showing new one', () => { + const experience1: Experience = { + id: 'banner-1', + type: 'banner', + targeting: {}, + content: { + title: 'First', + message: 'First message', + }, + }; + + const experience2: Experience = { + id: 'banner-2', + type: 'banner', + targeting: {}, + content: { + title: 'Second', + message: 'Second message', + }, + }; + + sdk.banner.show(experience1); + expect(document.querySelector('[data-experience-id="banner-1"]')).toBeTruthy(); + + sdk.banner.show(experience2); + expect(document.querySelector('[data-experience-id="banner-1"]')).toBeNull(); + expect(document.querySelector('[data-experience-id="banner-2"]')).toBeTruthy(); + }); + + it('should manually remove banner via remove()', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + expect(sdk.banner.isShowing()).toBe(true); + + sdk.banner.remove(); + expect(sdk.banner.isShowing()).toBe(false); + expect(document.querySelector('[data-experience-id="test-banner"]')).toBeNull(); + }); + + it('should handle remove() when no banner is showing', () => { + expect(() => sdk.banner.remove()).not.toThrow(); + expect(sdk.banner.isShowing()).toBe(false); + }); + }); + + describe('Events', () => { + it('should emit experiences:shown event when banner is shown', () => { + const handler = vi.fn(); + sdk.on('experiences:shown', handler); + + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + expect(handler).toHaveBeenCalledWith({ + experienceId: 'test-banner', + type: 'banner', + timestamp: expect.any(Number), + }); + }); + + it('should remove banner on destroy event', async () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + expect(sdk.banner.isShowing()).toBe(true); + expect(document.querySelector('[data-experience-id="test-banner"]')).toBeTruthy(); + + await sdk.destroy(); + + // After destroy, the banner should be removed from DOM + expect(document.querySelector('[data-experience-id="test-banner"]')).toBeNull(); + }); + }); + + describe('Styling', () => { + it('should apply correct z-index', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement; + expect(banner.style.zIndex).toBe('10000'); + }); + + it('should apply custom z-index', () => { + const customSdk = new SDK({ banner: { zIndex: 99999 } }) as SDKWithBanner; + customSdk.use(bannerPlugin); + + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + customSdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement; + expect(banner.style.zIndex).toBe('99999'); + }); + + it('should be fixed position', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement; + expect(banner.style.position).toBe('fixed'); + }); + + it('should span full width', () => { + const experience: Experience = { + id: 'test-banner', + type: 'banner', + targeting: {}, + content: { + title: 'Test', + message: 'Message', + }, + }; + + sdk.banner.show(experience); + + const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement; + expect(banner.style.left).toBe('0px'); + expect(banner.style.right).toBe('0px'); + }); + }); +}); + diff --git a/packages/plugins/src/banner/banner.ts b/packages/plugins/src/banner/banner.ts new file mode 100644 index 0000000..2a19b5f --- /dev/null +++ b/packages/plugins/src/banner/banner.ts @@ -0,0 +1,214 @@ +/** + * Banner Plugin + * + * Renders banner experiences at the top or bottom of the page. + * Auto-shows banners when experiences are evaluated. + */ + +import type { PluginFunction } from '@lytics/sdk-kit'; +import type { Experience, BannerContent, Decision } from '@prosdevlab/experience-sdk'; + +export interface BannerPluginConfig { + banner?: { + position?: 'top' | 'bottom'; + dismissable?: boolean; + zIndex?: number; + }; +} + +export interface BannerPlugin { + show(experience: Experience): void; + remove(): void; + isShowing(): boolean; +} + +/** + * Banner Plugin + * + * Automatically renders banner experiences when they are evaluated. + * + * @example + * ```typescript + * import { createInstance } from '@prosdevlab/experience-sdk'; + * import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins'; + * + * const sdk = createInstance({ banner: { position: 'top', dismissable: true } }); + * sdk.use(bannerPlugin); + * ``` + */ +export const bannerPlugin: PluginFunction = (plugin, instance, config) => { + plugin.ns('banner'); + + // Set defaults + plugin.defaults({ + banner: { + position: 'top', + dismissable: true, + zIndex: 10000, + }, + }); + + let activeBanner: HTMLElement | null = null; + + /** + * Create banner DOM element + */ + function createBannerElement(experience: Experience): HTMLElement { + const content = experience.content as BannerContent; + const position = config.get('banner.position') ?? 'top'; + const dismissable = config.get('banner.dismissable') ?? true; + const zIndex = config.get('banner.zIndex') ?? 10000; + + // Create banner container + const banner = document.createElement('div'); + banner.setAttribute('data-experience-id', experience.id); + banner.style.cssText = ` + position: fixed; + ${position}: 0; + left: 0; + right: 0; + background: #007bff; + color: #ffffff; + padding: 16px 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.5; + z-index: ${zIndex}; + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + `; + + // Create content container + const contentDiv = document.createElement('div'); + contentDiv.style.cssText = 'flex: 1; margin-right: 20px;'; + + // Add title if present + if (content.title) { + const title = document.createElement('div'); + title.textContent = content.title; + title.style.cssText = 'font-weight: 600; margin-bottom: 4px;'; + contentDiv.appendChild(title); + } + + // Add message + const message = document.createElement('div'); + message.textContent = content.message; + contentDiv.appendChild(message); + + banner.appendChild(contentDiv); + + // Add dismiss button if dismissable + if (dismissable) { + const closeButton = document.createElement('button'); + closeButton.innerHTML = '×'; + closeButton.setAttribute('aria-label', 'Close banner'); + closeButton.style.cssText = ` + background: transparent; + border: none; + color: inherit; + font-size: 28px; + line-height: 1; + cursor: pointer; + padding: 0; + margin: 0; + opacity: 0.8; + transition: opacity 0.2s; + `; + + closeButton.addEventListener('mouseenter', () => { + closeButton.style.opacity = '1'; + }); + + closeButton.addEventListener('mouseleave', () => { + closeButton.style.opacity = '0.8'; + }); + + closeButton.addEventListener('click', () => { + remove(); + instance.emit('experiences:dismissed', { + experienceId: experience.id, + type: 'banner', + }); + }); + + banner.appendChild(closeButton); + } + + return banner; + } + + /** + * Show a banner experience + */ + function show(experience: Experience): void { + // Remove any existing banner first + if (activeBanner) { + remove(); + } + + // Only show if we're in a browser environment + if (typeof document === 'undefined') { + return; + } + + const banner = createBannerElement(experience); + document.body.appendChild(banner); + activeBanner = banner; + + instance.emit('experiences:shown', { + experienceId: experience.id, + type: 'banner', + timestamp: Date.now(), + }); + } + + /** + * Remove the active banner + */ + function remove(): void { + if (activeBanner?.parentNode) { + activeBanner.parentNode.removeChild(activeBanner); + activeBanner = null; + } + } + + /** + * Check if a banner is currently showing + */ + function isShowing(): boolean { + return activeBanner !== null; + } + + // Expose banner API + plugin.expose({ + banner: { + show, + remove, + isShowing, + }, + }); + + // Auto-show banner on experiences:evaluated event + instance.on('experiences:evaluated', (decision: Decision) => { + // Only show if: + // 1. Decision says to show + // 2. Experience ID exists + // 3. Experience type is 'banner' + if (decision.show && decision.experienceId) { + // We need to find the experience to check its type + // For now, we'll assume if a banner-type experience was evaluated and should show, + // we'll receive it through another mechanism or the decision will include more info + // This is a simplification - in a real implementation, the runtime would need to + // pass the full experience object or we'd need to query it + } + }); + + // Cleanup on destroy + instance.on('sdk:destroy', () => { + remove(); + }); +}; + diff --git a/packages/plugins/src/banner/index.ts b/packages/plugins/src/banner/index.ts new file mode 100644 index 0000000..5565e74 --- /dev/null +++ b/packages/plugins/src/banner/index.ts @@ -0,0 +1,7 @@ +/** + * Banner Plugin - Barrel Export + */ + +export type { BannerPlugin, BannerPluginConfig } from './banner'; +export { bannerPlugin } from './banner'; + diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts index 9cd3f0d..fe90bd0 100644 --- a/packages/plugins/src/index.ts +++ b/packages/plugins/src/index.ts @@ -6,3 +6,4 @@ export * from './debug'; export * from './frequency'; +export * from './banner'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8af490..d75c0f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,10 +29,13 @@ importers: version: 24.0.0 '@vitest/coverage-v8': specifier: ^4.0.16 - version: 4.0.16(vitest@4.0.16(@types/node@24.10.4)(jiti@2.6.1)) + version: 4.0.16(vitest@4.0.16(@types/node@24.10.4)(jiti@2.6.1)(jsdom@27.3.0)) husky: specifier: ^9.1.7 version: 9.1.7 + jsdom: + specifier: ^27.3.0 + version: 27.3.0 tsup: specifier: ^8.5.1 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) @@ -44,7 +47,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.16 - version: 4.0.16(@types/node@24.10.4)(jiti@2.6.1) + version: 4.0.16(@types/node@24.10.4)(jiti@2.6.1)(jsdom@27.3.0) packages/core: dependencies: @@ -63,7 +66,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.16 - version: 4.0.16(@types/node@24.10.4)(jiti@2.6.1) + version: 4.0.16(@types/node@24.10.4)(jiti@2.6.1)(jsdom@27.3.0) packages/plugins: dependencies: @@ -85,10 +88,22 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.16 - version: 4.0.16(@types/node@24.10.4)(jiti@2.6.1) + version: 4.0.16(@types/node@24.10.4)(jiti@2.6.1)(jsdom@27.3.0) packages: + '@acemir/cssom@0.9.30': + resolution: {integrity: sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==} + + '@asamuzakjp/css-color@4.1.1': + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + + '@asamuzakjp/dom-selector@6.7.6': + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -295,6 +310,38 @@ packages: resolution: {integrity: sha512-KTy0OqRDLR5y/zZMnizyx09z/rPlPC/zKhYgH8o/q6PuAjoQAKlRfY4zzv0M64yybQ//6//4H1n14pxaLZfUnA==} engines: {node: '>=v18'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.22': + resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -685,6 +732,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -727,6 +778,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -823,10 +877,22 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssstyle@5.3.5: + resolution: {integrity: sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==} + engines: {node: '>=20'} + dargs@8.1.0: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -836,6 +902,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -855,6 +924,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -967,9 +1040,21 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -979,6 +1064,10 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.1: resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} engines: {node: '>=0.10.0'} @@ -1021,6 +1110,9 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -1074,6 +1166,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@27.3.0: + resolution: {integrity: sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -1133,6 +1234,10 @@ packages: lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1143,6 +1248,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -1225,6 +1333,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1293,6 +1404,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -1338,6 +1453,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -1407,6 +1526,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1443,10 +1565,25 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1600,6 +1737,26 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1614,6 +1771,25 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1632,6 +1808,26 @@ packages: snapshots: + '@acemir/cssom@0.9.30': {} + + '@asamuzakjp/css-color@4.1.1': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.4 + + '@asamuzakjp/dom-selector@6.7.6': + 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': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -1944,6 +2140,28 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.22': {} + + '@csstools/css-tokenizer@3.0.4': {} + '@esbuild/aix-ppc64@0.27.2': optional: true @@ -2168,7 +2386,7 @@ snapshots: dependencies: undici-types: 7.16.0 - '@vitest/coverage-v8@4.0.16(vitest@4.0.16(@types/node@24.10.4)(jiti@2.6.1))': + '@vitest/coverage-v8@4.0.16(vitest@4.0.16(@types/node@24.10.4)(jiti@2.6.1)(jsdom@27.3.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.16 @@ -2181,7 +2399,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@24.10.4)(jiti@2.6.1) + vitest: 4.0.16(@types/node@24.10.4)(jiti@2.6.1)(jsdom@27.3.0) transitivePeerDependencies: - supports-color @@ -2231,6 +2449,8 @@ snapshots: acorn@8.15.0: {} + agent-base@7.1.4: {} + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -2270,6 +2490,10 @@ snapshots: dependencies: is-windows: 1.0.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -2355,12 +2579,30 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssstyle@5.3.5: + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.22 + css-tree: 3.1.0 + dargs@8.1.0: {} + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: @@ -2378,6 +2620,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@6.0.1: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -2512,12 +2756,34 @@ snapshots: has-flag@4.0.0: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-id@4.1.3: {} husky@9.1.7: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.1: dependencies: safer-buffer: 2.1.2 @@ -2547,6 +2813,8 @@ snapshots: is-obj@2.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -2597,6 +2865,33 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@27.3.0: + dependencies: + '@acemir/cssom': 0.9.30 + '@asamuzakjp/dom-selector': 6.7.6 + cssstyle: 5.3.5 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + 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-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + json-parse-even-better-errors@2.3.1: {} json-schema-traverse@1.0.0: {} @@ -2639,6 +2934,8 @@ snapshots: lodash.upperfirst@4.3.1: {} + lru-cache@11.2.4: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2653,6 +2950,8 @@ snapshots: dependencies: semver: 7.7.3 + mdn-data@2.12.2: {} + meow@12.1.1: {} merge2@1.4.1: {} @@ -2728,6 +3027,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -2769,6 +3072,8 @@ snapshots: prettier@2.8.8: {} + punycode@2.3.1: {} + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -2826,6 +3131,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@7.7.3: {} shebang-command@2.0.0: @@ -2883,6 +3192,8 @@ snapshots: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + term-size@2.2.1: {} text-extensions@2.4.0: {} @@ -2910,10 +3221,24 @@ snapshots: tinyrainbow@3.0.3: {} + tldts-core@7.0.19: {} + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} @@ -2996,7 +3321,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 - vitest@4.0.16(@types/node@24.10.4)(jiti@2.6.1): + vitest@4.0.16(@types/node@24.10.4)(jiti@2.6.1)(jsdom@27.3.0): dependencies: '@vitest/expect': 4.0.16 '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)) @@ -3020,6 +3345,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.4 + jsdom: 27.3.0 transitivePeerDependencies: - jiti - less @@ -3033,6 +3359,23 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -3048,6 +3391,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + ws@8.18.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} yargs-parser@21.1.1: {} diff --git a/vitest.config.ts b/vitest.config.ts index dc499c1..d0e82e4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,7 @@ import { resolve } from 'path'; export default defineConfig({ test: { globals: true, - environment: 'node', + environment: 'jsdom', include: ['packages/**/*.{test,spec}.ts'], coverage: { provider: 'v8',