diff --git a/packages/angular/package.json b/packages/angular/package.json index a5e843f86..6f2deac09 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -54,6 +54,7 @@ "@angular/core": "^21.2.17", "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.2.17", "@angular/router": "^21.0.0", "@lucide/build-icons": "workspace:*", "@lucide/helpers": "workspace:*", diff --git a/packages/angular/src/lucide-dynamic-icon.spec.ts b/packages/angular/src/lucide-dynamic-icon.spec.ts index 04103ea7f..bd9e1fbb8 100644 --- a/packages/angular/src/lucide-dynamic-icon.spec.ts +++ b/packages/angular/src/lucide-dynamic-icon.spec.ts @@ -22,7 +22,12 @@ describe('LucideDynamicIcon', () => { let component: LucideDynamicIcon; let fixture: ComponentFixture; let icon: WritableSignal; - const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr); + const expectSvgClasses = (classes: string[]) => { + for (const cssClass of classes) { + expect(fixture.nativeElement.classList.contains(cssClass)).toBe(true); + } + }; + const getRenderedChildren = () => Array.from(fixture.nativeElement.children) as Element[]; const testIcon: LucideIconData = { name: 'demo', node: [['polyline', { points: '1 1 22 22' }]], @@ -30,11 +35,23 @@ describe('LucideDynamicIcon', () => { const testIcon2: LucideIconData = { name: 'demo-other', node: [ - ['circle', { cx: 12, cy: 12, r: 8 }], + ['circle', { cx: 12, cy: 12, r: 8, fill: 'currentColor' }], ['polyline', { points: '1 1 22 22' }], ], aliases: ['demo-2'], }; + const supportedShapesIcon: LucideIconData = { + name: 'supported-shapes', + node: [ + ['path', { d: 'm1 1 2 2', fill: 'currentColor', key: 'path-key' }], + ['line', { x1: 1, x2: 2, y1: 3, y2: 4, key: 'line-key' }], + ['polygon', { points: '1 1 2 2 3 1', key: 'polygon-key' }], + ['polyline', { points: '1 1 2 2 3 1', key: 'polyline-key' }], + ['circle', { cx: 12, cy: 12, r: 8, fill: 'currentColor', key: 'circle-key' }], + ['ellipse', { cx: 12, cy: 12, rx: 8, ry: 4, key: 'ellipse-key' }], + ['rect', { x: 1, y: 2, width: 3, height: 4, rx: 5, ry: 6, key: 'rect-key' }], + ], + }; function createComponent() { return TestBed.createComponent(LucideDynamicIcon, { inferTagName: true, @@ -58,15 +75,33 @@ describe('LucideDynamicIcon', () => { it('should render children', () => { icon.set(testIcon2); fixture.detectChanges(); - expect(fixture.nativeElement.innerHTML).toBe( - '', + const children = getRenderedChildren(); + expect(children.map((child) => child.tagName.toLowerCase())).toEqual(['circle', 'polyline']); + expect(children[0].outerHTML).toBe( + '', ); + expect(children[1].outerHTML).toBe(''); + }); + + it('should render supported SVG shapes and attributes', () => { + icon.set(supportedShapesIcon); + fixture.detectChanges(); + const children = getRenderedChildren(); + expect(children.map((child) => child.outerHTML)).toEqual([ + '', + '', + '', + '', + '', + '', + '', + ]); }); it('should remove children on change', () => { icon.set(null); fixture.detectChanges(); - expect(fixture.nativeElement.innerHTML).toBe(''); + expect(getRenderedChildren()).toEqual([]); }); describe('iconInput', () => { @@ -74,9 +109,9 @@ describe('LucideDynamicIcon', () => { icon.set(testIcon); fixture.detectChanges(); expect(component['icon']()).toBe(testIcon); - expect(fixture.nativeElement.innerHTML).toBe( - '', - ); + expect(getRenderedChildren().map((child) => child.outerHTML)).toEqual([ + '', + ]); }); it('should support LucideIcon input', () => { icon.set(LucideActivity); @@ -97,23 +132,24 @@ describe('LucideDynamicIcon', () => { describe('class', () => { it('should add all classes', () => { fixture.detectChanges(); - expect(getSvgAttribute('class')).toBe('lucide lucide-demo'); + expectSvgClasses(['lucide', 'lucide-demo']); }); it('should add backwards compatible classes from aliases', () => { icon.set(testIcon2); fixture.detectChanges(); - expect(getSvgAttribute('class')).toBe('lucide lucide-demo-other lucide-demo-2'); + expectSvgClasses(['lucide', 'lucide-demo-other', 'lucide-demo-2']); }); it('should add class icon if available', () => { icon.set(LucideActivity); fixture.detectChanges(); - expect(getSvgAttribute('class')).toBe('lucide lucide-activity'); + expectSvgClasses(['lucide', 'lucide-activity']); }); it('should remove class on change', () => { icon.set(null); fixture.detectChanges(); - expect(getSvgAttribute('class')).toBe('lucide'); + expectSvgClasses(['lucide']); + expect(fixture.nativeElement.classList.contains('lucide-demo')).toBe(false); }); }); diff --git a/packages/angular/src/lucide-hydration.spec.ts b/packages/angular/src/lucide-hydration.spec.ts new file mode 100644 index 000000000..8521fa8e0 --- /dev/null +++ b/packages/angular/src/lucide-hydration.spec.ts @@ -0,0 +1,107 @@ +import { ApplicationRef, Component, destroyPlatform, Provider, Type } from '@angular/core'; +import { bootstrapApplication, provideClientHydration } from '@angular/platform-browser'; +import { provideServerRendering, renderApplication } from '@angular/platform-server'; +import { LucideDynamicIcon } from './lucide-dynamic-icon'; +import { provideLucideIcons } from './lucide-icons'; +import { LucideBadgeAlert } from './icons/badge-alert'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'lucide-static-hydration-root', + template: ``, + imports: [LucideBadgeAlert], +}) +class StaticHydrationRootComponent {} + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'lucide-dynamic-hydration-root', + template: ``, + imports: [LucideDynamicIcon], +}) +class DynamicHydrationRootComponent {} + +describe('Lucide hydration', () => { + let appRef: ApplicationRef | undefined; + let warnSpy: ReturnType; + + beforeEach(() => { + warnSpy = vi.spyOn(console, 'warn'); + }); + + afterEach(() => { + warnSpy.mockRestore(); + appRef?.destroy(); + appRef = undefined; + document.body.innerHTML = ''; + delete (globalThis as { ngServerMode?: boolean }).ngServerMode; + destroyPlatform(); + }); + + async function renderAndHydrate( + rootComponent: Type, + selector: string, + providers: Provider[] = [], + ) { + delete (globalThis as { ngServerMode?: boolean }).ngServerMode; + const appConfig = { + providers: [provideClientHydration(), ...providers], + }; + const serverAppConfig = { + providers: [provideServerRendering(), provideClientHydration(), ...providers], + }; + const html = await renderApplication( + (context) => bootstrapApplication(rootComponent, serverAppConfig, context), + { + document: `<${selector}>`, + url: 'http://localhost/', + allowedHosts: ['localhost'], + }, + ); + (globalThis as { ngServerMode?: boolean }).ngServerMode = false; + const serverDocument = new DOMParser().parseFromString(html, 'text/html'); + expectBadgeAlertShapeNodes(serverDocument.querySelector('svg') as SVGSVGElement); + document.head.innerHTML = serverDocument.head.innerHTML; + document.body.innerHTML = serverDocument.body.innerHTML; + + appRef = await bootstrapApplication(rootComponent, appConfig); + await appRef.whenStable(); + + const hasMissingHydrationWarning = warnSpy.mock.calls.some((call: unknown[]) => + String(call[0]).includes('NG0505'), + ); + expect(hasMissingHydrationWarning).toBe(false); + + return document.querySelector('svg') as SVGSVGElement; + } + + function expectBadgeAlertShapeNodes(svg: SVGSVGElement) { + expect(Array.from(svg.children).map((child) => child.tagName.toLowerCase())).toEqual([ + 'path', + 'line', + 'line', + ]); + } + + it('should hydrate static icon children without duplicating SSR nodes', async () => { + const svg = await renderAndHydrate( + StaticHydrationRootComponent, + 'lucide-static-hydration-root', + ); + + expectBadgeAlertShapeNodes(svg); + for (const child of Array.from(svg.children)) { + expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); + } + }); + + it('should hydrate dynamic icon children without duplicating SSR nodes', async () => { + const svg = await renderAndHydrate( + DynamicHydrationRootComponent, + 'lucide-dynamic-hydration-root', + [provideLucideIcons(LucideBadgeAlert)], + ); + + expectBadgeAlertShapeNodes(svg); + }); +}); diff --git a/packages/angular/src/lucide-icon-base.spec.ts b/packages/angular/src/lucide-icon-base.spec.ts index ff0154357..f90f91596 100644 --- a/packages/angular/src/lucide-icon-base.spec.ts +++ b/packages/angular/src/lucide-icon-base.spec.ts @@ -33,6 +33,12 @@ export class LucideCircleCheck extends LucideIconBase { protected override readonly icon = signal(LucideCircleCheck.icon); } +@Component({ + template: ``, + imports: [LucideCircleCheck], +}) +class ClassHostComponent {} + describe('LucideIconBase', () => { let component: LucideCircleCheck; let fixture: ComponentFixture; @@ -42,6 +48,12 @@ describe('LucideIconBase', () => { let strokeWidth: WritableSignal; let absoluteStrokeWidth: WritableSignal; const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr); + const expectSvgClasses = (classes: string[], svg: SVGElement = fixture.nativeElement) => { + for (const cssClass of classes) { + expect(svg.classList.contains(cssClass)).toBe(true); + } + }; + const getRenderedChildren = () => Array.from(fixture.nativeElement.children) as Element[]; function createComponent() { return TestBed.createComponent(LucideCircleCheck, { inferTagName: true, @@ -71,15 +83,25 @@ describe('LucideIconBase', () => { it('should render children', () => { fixture.detectChanges(); - expect(fixture.nativeElement.innerHTML).toBe( - '', - ); + const children = getRenderedChildren(); + expect(children.map((child) => child.tagName.toLowerCase())).toEqual(['circle', 'path']); + expect(children[0].outerHTML).toBe(''); + expect(children[1].outerHTML).toBe(''); }); describe('class', () => { it('should add all classes', () => { fixture.detectChanges(); - expect(getSvgAttribute('class')).toBe('lucide lucide-circle-check lucide-check-circle-2'); + expectSvgClasses(['lucide', 'lucide-circle-check', 'lucide-check-circle-2']); + }); + it('should preserve user classes', () => { + const hostFixture = TestBed.createComponent(ClassHostComponent); + hostFixture.detectChanges(); + const svg = hostFixture.nativeElement.querySelector('svg'); + expectSvgClasses( + ['custom-icon', 'lucide', 'lucide-circle-check', 'lucide-check-circle-2'], + svg, + ); }); }); @@ -142,13 +164,15 @@ describe('LucideIconBase', () => { }); it('should not set vector-effect on children', () => { absoluteStrokeWidth.set(false); - for (const child of fixture.nativeElement.children) { + fixture.detectChanges(); + for (const child of getRenderedChildren()) { expect(child.getAttribute('vector-effect')).toBeNull(); } }); it('should set vector-effect on children', () => { absoluteStrokeWidth.set(true); - for (const child of fixture.nativeElement.children) { + fixture.detectChanges(); + for (const child of getRenderedChildren()) { expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); } }); @@ -228,13 +252,15 @@ describe('LucideIconBase', () => { }); describe('absoluteStrokeWidth', () => { it('should use absoluteStrokeWidth from config', () => { - for (const child of fixture.nativeElement.children) { + fixture.detectChanges(); + for (const child of getRenderedChildren()) { expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); } }); it('should override absoluteStrokeWidth', () => { absoluteStrokeWidth.set(false); - for (const child of fixture.nativeElement.children) { + fixture.detectChanges(); + for (const child of getRenderedChildren()) { expect(child.getAttribute('vector-effect')).toBeNull(); } }); diff --git a/packages/angular/src/lucide-icon-base.ts b/packages/angular/src/lucide-icon-base.ts index 0775d0fbf..4290da872 100644 --- a/packages/angular/src/lucide-icon-base.ts +++ b/packages/angular/src/lucide-icon-base.ts @@ -1,13 +1,4 @@ -import { - Component, - effect, - ElementRef, - inject, - input, - Renderer2, - Signal, - viewChild, -} from '@angular/core'; +import { Component, computed, inject, input, Signal } from '@angular/core'; import { LUCIDE_CONFIG } from './lucide-config'; import { LucideIconData, Nullable } from './types'; import defaultAttributes from './default-attributes'; @@ -23,6 +14,7 @@ import { lucideIconTemplate } from './lucide-icon-template'; host: { ...defaultAttributes, class: 'lucide', + '[class]': 'iconClasses()', '[attr.width]': 'size()', '[attr.height]': 'size()', '[attr.stroke]': 'color()', @@ -32,10 +24,28 @@ import { lucideIconTemplate } from './lucide-icon-template'; }) export abstract class LucideIconBase { protected abstract readonly icon: Signal>; + protected readonly iconNodes = computed(() => { + const node = this.icon()?.node ?? []; + if (!this.absoluteStrokeWidth()) { + return node; + } + return node.map(([name, attrs]) => [ + name, + { + 'vector-effect': 'non-scaling-stroke', + ...attrs, + }, + ]); + }); + protected readonly iconClasses = computed(() => { + const icon = this.icon(); + if (!icon) { + return ''; + } + const { name, aliases = [] } = icon; + return [name, ...aliases].map((item) => `lucide-${item}`).join(' '); + }); protected readonly iconConfig = inject(LUCIDE_CONFIG); - protected readonly elRef = inject(ElementRef); - protected readonly renderer = inject(Renderer2); - protected readonly contentRef = viewChild.required('contentRef'); /** * An optional accessible label for the icon. * - If provided, it will add the title as an [`` element](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/title). @@ -76,43 +86,4 @@ export abstract class LucideIconBase { readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, { transform: (value: Nullable) => value ?? this.iconConfig.absoluteStrokeWidth, }); - - constructor() { - effect((onCleanup) => { - const icon = this.icon(); - if (icon) { - const absoluteStrokeWidth = this.absoluteStrokeWidth(); - const { name, node, aliases = [] } = icon; - const classes = [name, ...aliases].map((item) => `lucide-${item}`); - for (const cssClass of classes) { - this.renderer.addClass(this.elRef.nativeElement, cssClass); - } - const contentRef = this.contentRef(); - const refChild = contentRef.nativeElement; - const elements = node.map(([name, attrs]) => { - const element = this.renderer.createElement(name, 'http://www.w3.org/2000/svg'); - if (absoluteStrokeWidth) { - this.renderer.setAttribute(element, 'vector-effect', 'non-scaling-stroke'); - } - Object.entries(attrs).forEach(([name, value]) => - this.renderer.setAttribute( - element, - name, - typeof value === 'number' ? value.toString(10) : value, - ), - ); - this.renderer.insertBefore(this.elRef.nativeElement, element, refChild); - return element; - }); - onCleanup(() => { - elements.forEach((element) => - this.renderer.removeChild(this.elRef.nativeElement, element), - ); - for (const cssClass of classes) { - this.renderer.removeClass(this.elRef.nativeElement, cssClass); - } - }); - } - }); - } } diff --git a/packages/angular/src/lucide-icon-template.ts b/packages/angular/src/lucide-icon-template.ts index 85b2e8b5e..1ae677ecb 100644 --- a/packages/angular/src/lucide-icon-template.ts +++ b/packages/angular/src/lucide-icon-template.ts @@ -6,5 +6,66 @@ export const lucideIconTemplate = `@if (title(); as titleValue) { {{ titleValue }} } - +@for (child of iconNodes(); track child[1]['key'] ?? $index) { + @let attrs = child[1]; + @switch (child[0]) { + @case ('path') { + + } + @case ('line') { + + } + @case ('polygon') { + + } + @case ('polyline') { + + } + @case ('circle') { + + } + @case ('ellipse') { + + } + @case ('rect') { + + } + } +} `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe7ac1811..a67288bb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ importers: version: 21.1.0(eslint@8.57.1)(typescript@5.9.3) '@angular/build': specifier: ^21.0.3 - version: 21.2.3(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.17)(typescript@5.9.3))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(@angular/platform-browser@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)))(@types/node@24.10.1)(chokidar@5.0.0)(jiti@2.7.0)(less@4.2.0)(ng-packagr@21.2.1(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.17)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.8)(stylus@0.56.0)(terser@5.47.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0)(yaml@2.8.0) + version: 21.2.3(3c7ec986daf57b70e1407066650c30a3) '@angular/cli': specifier: ^21.0.3 version: 21.2.3(@types/node@24.10.1)(chokidar@5.0.0) @@ -279,6 +279,9 @@ importers: '@angular/platform-browser': specifier: ^21.0.0 version: 21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)) + '@angular/platform-server': + specifier: ^21.2.17 + version: 21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(@angular/platform-browser@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)))(rxjs@7.8.2) '@angular/router': specifier: ^21.0.0 version: 21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(@angular/platform-browser@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)))(rxjs@7.8.2) @@ -1151,6 +1154,16 @@ packages: '@angular/animations': optional: true + '@angular/platform-server@21.2.17': + resolution: {integrity: sha512-ll5mbZHyUjZp+aL5eFCcusZBMLYv2fbxuPAIhAHFpu55kC9zh7PFlbdINbr4kT/bjxYb4isw5nDbGj9+n+RHWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@angular/common': 21.2.17 + '@angular/compiler': 21.2.17 + '@angular/core': 21.2.17 + '@angular/platform-browser': 21.2.17 + rxjs: ^6.5.3 || ^7.4.0 + '@angular/router@21.2.5': resolution: {integrity: sha512-yQGhTVGvh8OMW3auj13+g+OCSQj7gyBQON/2X4LuCvIUG71NPV6Fqzfk9DKTKaXpqo0FThy8/LPJ0Lsy3CRejg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -12627,6 +12640,10 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xhr2@0.2.1: + resolution: {integrity: sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==} + engines: {node: '>= 6'} + xhr@2.6.0: resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} @@ -13110,7 +13127,7 @@ snapshots: eslint: 8.57.1 typescript: 5.9.3 - '@angular/build@21.2.3(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.17)(typescript@5.9.3))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(@angular/platform-browser@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)))(@types/node@24.10.1)(chokidar@5.0.0)(jiti@2.7.0)(less@4.2.0)(ng-packagr@21.2.1(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.17)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.8)(stylus@0.56.0)(terser@5.47.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0)(yaml@2.8.0)': + '@angular/build@21.2.3(3c7ec986daf57b70e1407066650c30a3)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.3(chokidar@5.0.0) @@ -13146,11 +13163,12 @@ snapshots: optionalDependencies: '@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2) '@angular/platform-browser': 21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)) + '@angular/platform-server': 21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(@angular/platform-browser@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)))(rxjs@7.8.2) less: 4.2.0 lmdb: 3.5.1 ng-packagr: 21.2.1(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.17)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) postcss: 8.5.8 - vitest: 4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12(vitest@4.1.0))(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) + vitest: 4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12)(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) transitivePeerDependencies: - '@types/node' - chokidar @@ -13238,6 +13256,16 @@ snapshots: '@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2) tslib: 2.8.1 + '@angular/platform-server@21.2.17(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/compiler@21.2.17)(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(@angular/platform-browser@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/compiler': 21.2.17 + '@angular/core': 21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2) + '@angular/platform-browser': 21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)) + rxjs: 7.8.2 + tslib: 2.8.1 + xhr2: 0.2.1 + '@angular/router@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(@angular/platform-browser@21.2.5(@angular/common@21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)))(rxjs@7.8.2)': dependencies: '@angular/common': 21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2))(rxjs@7.8.2) @@ -17142,7 +17170,7 @@ snapshots: '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0) '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/template': 7.28.6 + '@babel/template': 7.29.7 '@react-native/babel-plugin-codegen': 0.76.9(@babel/preset-env@7.28.5(@babel/core@7.29.0)) babel-plugin-syntax-hermes-parser: 0.25.1 babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) @@ -18559,7 +18587,7 @@ snapshots: '@vitest/mocker': 4.1.1(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) playwright: 1.58.2 tinyrainbow: 3.0.3 - vitest: 4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12(vitest@4.1.0))(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) + vitest: 4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12)(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) transitivePeerDependencies: - bufferutil - msw @@ -18593,7 +18621,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12(vitest@4.1.0))(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) + vitest: 4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12)(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) ws: 8.20.1 transitivePeerDependencies: - bufferutil @@ -22144,7 +22172,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -22985,7 +23013,7 @@ snapshots: dependencies: '@babel/traverse': 7.28.6 '@babel/traverse--for-generate-function-map': '@babel/traverse@7.29.7' - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.81.5 @@ -23022,8 +23050,8 @@ snapshots: metro-transform-plugins@0.81.5: dependencies: '@babel/core': 7.28.5 - '@babel/generator': 7.29.1 - '@babel/template': 7.28.6 + '@babel/generator': 7.29.7 + '@babel/template': 7.29.7 '@babel/traverse': 7.28.6 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 @@ -23033,9 +23061,9 @@ snapshots: metro-transform-worker@0.81.5: dependencies: '@babel/core': 7.28.5 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 flow-enums-runtime: 0.0.6 metro: 0.81.5 metro-babel-transformer: 0.81.5 @@ -23052,13 +23080,13 @@ snapshots: metro@0.81.5: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/parser': 7.29.3 - '@babel/template': 7.27.2 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 '@babel/traverse': 7.28.5 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -26628,7 +26656,7 @@ snapshots: - typescript - universal-cookie - vitest@4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12(vitest@4.1.0))(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)): + vitest@4.1.0(@types/node@24.10.1)(@vitest/browser-playwright@4.1.1)(@vitest/ui@4.0.12)(jsdom@27.4.0)(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)): dependencies: '@vitest/expect': 4.1.0 '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@24.10.1)(jiti@2.7.0)(less@4.2.0)(sass@1.97.3)(stylus@0.56.0)(terser@5.47.1)(yaml@2.8.0)) @@ -26919,6 +26947,8 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 + xhr2@0.2.1: {} + xhr@2.6.0: dependencies: global: 4.4.0