diff --git a/apps/app/src/app/pages/(components)/components/(input-otp)/input-otp--form.example.ts b/apps/app/src/app/pages/(components)/components/(input-otp)/input-otp--form.example.ts index a6bd768023..9c95a21e30 100644 --- a/apps/app/src/app/pages/(components)/components/(input-otp)/input-otp--form.example.ts +++ b/apps/app/src/app/pages/(components)/components/(input-otp)/input-otp--form.example.ts @@ -1,10 +1,10 @@ import { afterNextRender, Component, computed, inject, type OnDestroy, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { BrnInputOtp } from '@spartan-ng/brain/input-otp'; +import { toast } from '@spartan-ng/brain/sonner'; import { HlmButton } from '@spartan-ng/helm/button'; import { HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSlot } from '@spartan-ng/helm/input-otp'; import { HlmToaster } from '@spartan-ng/helm/sonner'; -import { toast } from 'ngx-sonner'; @Component({ selector: 'spartan-input-otp-form', diff --git a/apps/app/src/app/pages/(components)/components/(sidebar)/sidebar.preview.ts b/apps/app/src/app/pages/(components)/components/(sidebar)/sidebar.preview.ts index 422e4d34f3..78bef48bd2 100644 --- a/apps/app/src/app/pages/(components)/components/(sidebar)/sidebar.preview.ts +++ b/apps/app/src/app/pages/(components)/components/(sidebar)/sidebar.preview.ts @@ -541,7 +541,7 @@ import { lucideMap, lucidePlus, } from '@ng-icons/lucide'; -import { toast } from 'ngx-sonner'; +import { toast } from '@spartan-ng/brain/sonner'; import { HlmToasterImports } from '@spartan-ng/helm/sonner'; @Component({ diff --git a/apps/app/src/app/pages/(components)/components/(sonner)/sonner--description.example.ts b/apps/app/src/app/pages/(components)/components/(sonner)/sonner--description.example.ts index ad7fe5c624..17891fcf4d 100644 --- a/apps/app/src/app/pages/(components)/components/(sonner)/sonner--description.example.ts +++ b/apps/app/src/app/pages/(components)/components/(sonner)/sonner--description.example.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { toast } from '@spartan-ng/brain/sonner'; import { HlmButtonImports } from '@spartan-ng/helm/button'; -import { toast } from 'ngx-sonner'; @Component({ selector: 'spartan-sonner-description-example', diff --git a/apps/app/src/app/pages/(components)/components/(sonner)/sonner--position.example.ts b/apps/app/src/app/pages/(components)/components/(sonner)/sonner--position.example.ts index 64624f7b80..e77ae1b8dd 100644 --- a/apps/app/src/app/pages/(components)/components/(sonner)/sonner--position.example.ts +++ b/apps/app/src/app/pages/(components)/components/(sonner)/sonner--position.example.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { toast } from '@spartan-ng/brain/sonner'; import { HlmButtonImports } from '@spartan-ng/helm/button'; -import { toast } from 'ngx-sonner'; @Component({ selector: 'spartan-sonner-position-example', diff --git a/apps/app/src/app/pages/(components)/components/(sonner)/sonner--types.example.ts b/apps/app/src/app/pages/(components)/components/(sonner)/sonner--types.example.ts index 8aff3bccb7..20089035d5 100644 --- a/apps/app/src/app/pages/(components)/components/(sonner)/sonner--types.example.ts +++ b/apps/app/src/app/pages/(components)/components/(sonner)/sonner--types.example.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { toast } from '@spartan-ng/brain/sonner'; import { HlmButtonImports } from '@spartan-ng/helm/button'; -import { toast } from 'ngx-sonner'; @Component({ selector: 'spartan-sonner-types-example', diff --git a/apps/app/src/app/pages/(components)/components/(sonner)/sonner.page.ts b/apps/app/src/app/pages/(components)/components/(sonner)/sonner.page.ts index 1b0bec0aa7..339f564bf3 100644 --- a/apps/app/src/app/pages/(components)/components/(sonner)/sonner.page.ts +++ b/apps/app/src/app/pages/(components)/components/(sonner)/sonner.page.ts @@ -15,7 +15,6 @@ import { Tabs } from '../../../../shared/layout/tabs'; import { TabsCli } from '../../../../shared/layout/tabs-cli'; import { UIApiDocs } from '../../../../shared/layout/ui-docs-section/ui-docs-section'; import { metaWith } from '../../../../shared/meta/meta.util'; -import { link } from '../../../../shared/typography/link'; import { SonnerDescriptionExample } from './sonner--description.example'; import { SonnerPositionExample } from './sonner--position.example'; import { SonnerTypesExample } from './sonner--types.example'; @@ -57,15 +56,6 @@ export const routeMeta: RouteMeta = { - About -

- Sonner is built on top of - ngx-sonner - by - @tutkli - . -

- Installation @@ -107,6 +97,9 @@ export const routeMeta: RouteMeta = { + Brain API + + Helm API diff --git a/apps/app/src/app/pages/(components)/components/(sonner)/sonner.preview.ts b/apps/app/src/app/pages/(components)/components/(sonner)/sonner.preview.ts index f1b90c8abc..a5b82a82a8 100644 --- a/apps/app/src/app/pages/(components)/components/(sonner)/sonner.preview.ts +++ b/apps/app/src/app/pages/(components)/components/(sonner)/sonner.preview.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; +import { toast } from '@spartan-ng/brain/sonner'; import { HlmButtonImports } from '@spartan-ng/helm/button'; import { HlmToasterImports } from '@spartan-ng/helm/sonner'; -import { toast } from 'ngx-sonner'; @Component({ selector: 'spartan-sonner-preview', @@ -24,7 +24,7 @@ export class SonnerPreview { } export const defaultImports = ` -import { toast } from 'ngx-sonner'; +import { toast } from '@spartan-ng/brain/sonner'; `; export const defaultSkeleton = ` toast('Event has been created.'); diff --git a/apps/app/src/app/pages/(documentation)/documentation/about.page.ts b/apps/app/src/app/pages/(documentation)/documentation/about.page.ts index 8ccb7b1d12..57e5efbf40 100644 --- a/apps/app/src/app/pages/(documentation)/documentation/about.page.ts +++ b/apps/app/src/app/pages/(documentation)/documentation/about.page.ts @@ -75,13 +75,6 @@ const aboutLink = 'h-6 underline text-sm px-0.5'; AnalogJs - The full-stack Angular meta-framework powering spartan/stack. -
  • - - ngx-sonner - - - Elegant toast notifications by - Tutkli. -
  • { 'marked-gfm-heading-id', 'marked-highlight', 'prismjs/**/*', - 'ngx-sonner', '@ng-icons/remixicon', 'luxon', '@angular/cdk/portal', diff --git a/apps/ui-storybook/stories/sonner.stories.ts b/apps/ui-storybook/stories/sonner.stories.ts index 13476eba7d..b6259b8f57 100644 --- a/apps/ui-storybook/stories/sonner.stories.ts +++ b/apps/ui-storybook/stories/sonner.stories.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; +import { toast } from '@spartan-ng/brain/sonner'; import { HlmButton } from '@spartan-ng/helm/button'; import { HlmToaster } from '@spartan-ng/helm/sonner'; import type { Meta, StoryObj } from '@storybook/angular'; import { moduleMetadata } from '@storybook/angular'; -import { toast } from 'ngx-sonner'; const meta: Meta = { title: 'Sonner', diff --git a/libs/brain/sonner/README.md b/libs/brain/sonner/README.md new file mode 100644 index 0000000000..3ed1caeb37 --- /dev/null +++ b/libs/brain/sonner/README.md @@ -0,0 +1,3 @@ +# @spartan-ng/brain/sonner + +Secondary entry point of `@spartan-ng/brain`. It can be used by importing from `@spartan-ng/brain/sonner`. diff --git a/libs/brain/sonner/ng-package.json b/libs/brain/sonner/ng-package.json new file mode 100644 index 0000000000..b3e53d699a --- /dev/null +++ b/libs/brain/sonner/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/brain/sonner/src/index.ts b/libs/brain/sonner/src/index.ts new file mode 100644 index 0000000000..98fbfa3758 --- /dev/null +++ b/libs/brain/sonner/src/index.ts @@ -0,0 +1,8 @@ +import { BrnSonnerToaster } from './lib/brn-toaster'; + +export * from './lib/brn-toaster'; +export * from './lib/brn-toaster.token'; +export * from './lib/state'; +export * from './lib/types'; + +export const BrnSonnerImports = [BrnSonnerToaster] as const; diff --git a/libs/brain/sonner/src/lib/brn-icon.ts b/libs/brain/sonner/src/lib/brn-icon.ts new file mode 100644 index 0000000000..e2502179b5 --- /dev/null +++ b/libs/brain/sonner/src/lib/brn-icon.ts @@ -0,0 +1,51 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import type { ToastTypes } from './types'; + +@Component({ + selector: 'brn-sonner-icon', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @switch (type()) { + @case ('success') { + + + + } + @case ('error') { + + + + } + @case ('info') { + + + + } + @case ('warning') { + + + + + } + } + `, +}) +export class BrnSonnerIcon { + public readonly type = input('default'); +} diff --git a/libs/brain/sonner/src/lib/brn-loader.ts b/libs/brain/sonner/src/lib/brn-loader.ts new file mode 100644 index 0000000000..b29fe24a12 --- /dev/null +++ b/libs/brain/sonner/src/lib/brn-loader.ts @@ -0,0 +1,20 @@ +import type { BooleanInput } from '@angular/cdk/coercion'; +import { booleanAttribute, ChangeDetectionStrategy, Component, input } from '@angular/core'; + +@Component({ + selector: 'brn-sonner-loader', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
    +
    + @for (_ of _bars; track $index) { +
    + } +
    +
    + `, +}) +export class BrnSonnerLoader { + public readonly isVisible = input.required({ transform: booleanAttribute }); + protected readonly _bars = Array(12).fill(0); +} diff --git a/libs/brain/sonner/src/lib/brn-toast.ts b/libs/brain/sonner/src/lib/brn-toast.ts new file mode 100644 index 0000000000..875b1eb5b6 --- /dev/null +++ b/libs/brain/sonner/src/lib/brn-toast.ts @@ -0,0 +1,430 @@ +import { NgComponentOutlet } from '@angular/common'; +import { + afterRenderEffect, + type AfterViewInit, + ChangeDetectionStrategy, + Component, + computed, + effect, + ElementRef, + input, + linkedSignal, + type OnDestroy, + signal, + viewChild, +} from '@angular/core'; +import clsx from 'clsx'; +import { defaultClasses, injectBrnSonnerToasterConfig } from './brn-toaster.token'; +import { AsComponentPipe } from './pipes/as-component.pipe'; +import { IsStringPipe } from './pipes/is-string.pipe'; +import { toastState } from './state'; +import type { ToastProps } from './types'; + +@Component({ + selector: 'brn-sonner-toast', + imports: [NgComponentOutlet, IsStringPipe, AsComponentPipe], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
  • + @if (_closeButton() && !toast().component) { + + } + + @if (toast().component) { + + } @else { + @if (_toastType() !== 'default' || toast().icon || toast().promise) { +
    + @if (_toastType() === 'loading' && !toast().icon) { + + } + @if (toast().icon) { + + } @else { + @switch (_toastType()) { + @case ('success') { + + } + @case ('error') { + + } + @case ('warning') { + + } + @case ('info') { + + } + } + } +
    + } +
    + @if (toast().title; as title) { +
    + @if (title | isString) { + {{ toast().title }} + } @else { + + } +
    + } + @if (toast().description; as description) { +
    + @if (description | isString) { + {{ toast().description }} + } @else { + + } +
    + } +
    + @if (toast().cancel; as cancel) { + + } + @if (toast().action; as action) { + + } + } +
  • + `, +}) +export class BrnSonnerToast implements AfterViewInit, OnDestroy { + private readonly _config = injectBrnSonnerToasterConfig(); + + protected readonly _toasts = computed(() => toastState.toasts().filter((t) => t.position === this.position())); + protected readonly _heights = computed(() => toastState.heights().filter((h) => h.position === this.position())); + protected readonly _removeHeight = toastState.removeHeight; + protected readonly _addHeight = toastState.addHeight; + protected readonly _dismiss = toastState.dismiss; + + public readonly toast = input.required(); + public readonly index = input.required(); + public readonly expanded = input.required(); + public readonly invert = input.required(); + public readonly position = input.required(); + public readonly visibleToasts = input.required(); + public readonly expandByDefault = input.required(); + public readonly closeButton = input.required(); + public readonly interacting = input.required(); + public readonly cancelButtonStyle = input(); + public readonly actionButtonStyle = input(); + public readonly duration = input(this._config.toastLifetime); + public readonly descriptionClass = input(''); + public readonly classes = input({}); + public readonly unstyled = input(false); + public readonly userClass = input('', { alias: 'class' }); + public readonly style = input>({}); + + protected readonly _mounted = signal(false); + protected readonly _removed = signal(false); + protected readonly _swiping = signal(false); + protected readonly _swipeOut = signal(false); + private readonly _offsetBeforeRemove = signal(0); + private readonly _initialHeight = signal(0); + + private readonly _toastRef = viewChild.required>('toastRef'); + + protected readonly _classes = computed(() => ({ + ...defaultClasses, + ...this.classes(), + })); + + protected readonly _isFront = computed(() => this.index() === 0); + protected readonly _isVisible = computed(() => this.index() + 1 <= this.visibleToasts()); + protected readonly _toastType = computed(() => this.toast().type ?? 'default'); + private readonly _toastClass = computed(() => this.toast().class ?? ''); + private readonly _toastPosition = computed(() => this.toast().position ?? this.position()); + protected readonly _toastDescriptionClass = computed(() => this.toast().descriptionClass ?? ''); + + private readonly _heightIndex = computed(() => + this._heights().findIndex((height) => height.toastId === this.toast().id), + ); + + private readonly _offset = linkedSignal({ + source: () => ({ + heightIndex: this._heightIndex(), + toastsHeightBefore: this._toastsHeightBefore(), + }), + computation: ({ heightIndex, toastsHeightBefore }) => + Math.round(heightIndex * this._config.gap + toastsHeightBefore), + }); + + private _closeTimerStartTimeRef = 0; + private _lastCloseTimerStartTimeRef = 0; + private _pointerStartRef: { x: number; y: number } | null = null; + + protected readonly _coords = computed(() => this._toastPosition().split('-')); + private readonly _toastsHeightBefore = computed(() => + this._heights().reduce((prev, curr, reducerIndex) => { + if (reducerIndex >= this._heightIndex()) return prev; + return prev + curr.height; + }, 0), + ); + protected readonly _invert = computed(() => this.toast().invert ?? this.invert()); + protected readonly _closeButton = computed(() => this.toast().closeButton ?? this.closeButton()); + protected readonly _disabled = computed(() => this._toastType() === 'loading'); + + private _timeoutId: ReturnType | undefined; + private _remainingTime = 0; + + private readonly _isPromiseLoadingOrInfiniteDuration = computed( + () => + (this.toast().promise && this._toastType() === 'loading') || this.toast().duration === Number.POSITIVE_INFINITY, + ); + + protected readonly _toastClasses = computed(() => + clsx( + this.userClass(), + this._toastClass(), + this._classes().toast, + this.toast().classes?.toast, + this._classes()[this._toastType()], + this.toast().classes?.[this._toastType()], + ), + ); + protected readonly _toastStyle = computed(() => ({ + '--index': `${this.index()}`, + '--toasts-before': `${this.index()}`, + '--z-index': `${this._toasts().length - this.index()}`, + '--offset': `${this._removed() ? this._offsetBeforeRemove() : this._offset()}px`, + '--initial-height': this.expandByDefault() ? 'auto' : `${this._initialHeight()}px`, + ...this.style(), + })); + protected readonly _actionButtonClasses = computed(() => + clsx(this._classes().actionButton, this.toast().classes?.actionButton), + ); + protected readonly _cancelButtonClasses = computed(() => + clsx(this._classes().cancelButton, this.toast().classes?.cancelButton), + ); + protected readonly _toastDescriptionClasses = computed(() => + clsx( + this.descriptionClass(), + this._toastDescriptionClass(), + this._classes().description, + this.toast().classes?.description, + ), + ); + protected readonly _titleClasses = computed(() => clsx(this._classes().title, this.toast().classes?.title)); + protected readonly _closeButtonClasses = computed(() => + clsx(this._classes().closeButton, this.toast().classes?.closeButton), + ); + + constructor() { + effect(() => { + if (this.toast().updated) { + // if the toast has been updated after the initial render, + // we want to reset the timer and set the remaining time to the + // new duration + clearTimeout(this._timeoutId); + this._remainingTime = this.toast().duration ?? this.duration() ?? this._config.toastLifetime; + this.startTimer(); + } + }); + + afterRenderEffect((onCleanup) => { + if (!this._isPromiseLoadingOrInfiniteDuration()) { + if (this.expanded() || this.interacting()) { + this.pauseTimer(); + } else { + this.startTimer(); + } + } + + onCleanup(() => clearTimeout(this._timeoutId)); + }); + + effect(() => { + if (this.toast().delete) { + this.deleteToast(); + } + }); + } + + ngAfterViewInit() { + this._remainingTime = this.toast().duration ?? this.duration() ?? this._config.toastLifetime; + this._mounted.set(true); + const height = this._toastRef().nativeElement.getBoundingClientRect().height; + this._initialHeight.set(height); + this._addHeight({ toastId: this.toast().id, height, position: this._toastPosition() }); + } + + ngOnDestroy() { + clearTimeout(this._timeoutId); + this._removeHeight(this.toast().id); + } + + deleteToast() { + this._removed.set(true); + this._offsetBeforeRemove.set(this._offset()); + + this._removeHeight(this.toast().id); + + setTimeout(() => { + this._dismiss(this.toast().id); + }, this._config.timeBeforeUnmount); + } + + // If toast's duration changes, it will be out of sync with the + // remainingAtTimeout, so we know we need to restart the timer + // with the new duration + + // Pause the timer on each hover + pauseTimer() { + if (this._lastCloseTimerStartTimeRef < this._closeTimerStartTimeRef) { + // Get the elapsed time since the timer started + const elapsedTime = new Date().getTime() - this._closeTimerStartTimeRef; + this._remainingTime = this._remainingTime - elapsedTime; + } + + this._lastCloseTimerStartTimeRef = new Date().getTime(); + } + + startTimer() { + this._closeTimerStartTimeRef = new Date().getTime(); + // Let the toast know it has started + this._timeoutId = setTimeout(() => { + this.toast().onAutoClose?.(this.toast()); + this.deleteToast(); + }, this._remainingTime); + } + + onPointerDown(event: PointerEvent) { + if (this._disabled() || !this.toast().dismissible) return; + + this._offsetBeforeRemove.set(this._offset()); + const target = event.target as HTMLElement; + // Ensure we maintain correct pointer capture even when going outside the toast (e.g. when swiping) + target.setPointerCapture(event.pointerId); + if (target.tagName === 'BUTTON') { + return; + } + this._swiping.set(true); + this._pointerStartRef = { x: event.clientX, y: event.clientY }; + } + + onPointerUp() { + if (this._swipeOut() || !this.toast().dismissible) return; + + this._pointerStartRef = null; + const swipeAmount = Number( + this._toastRef().nativeElement.style.getPropertyValue('--swipe-amount').replace('px', '') || 0, + ); + + // Remove only if threshold is met + if (Math.abs(swipeAmount) >= this._config.swipeThreshold) { + this._offsetBeforeRemove.set(this._offset()); + this.toast().onDismiss?.(this.toast()); + this.deleteToast(); + this._swipeOut.set(true); + return; + } + + this._toastRef().nativeElement.style.setProperty('--swipe-amount', '0px'); + this._swiping.set(false); + } + + onPointerMove(event: PointerEvent) { + if (!this._pointerStartRef || !this.toast().dismissible) return; + + const yPosition = event.clientY - this._pointerStartRef.y; + const xPosition = event.clientX - this._pointerStartRef.x; + + const clamp = this._coords()[0] === 'top' ? Math.min : Math.max; + const clampedY = clamp(0, yPosition); + const swipeStartThreshold = event.pointerType === 'touch' ? 10 : 2; + const isAllowedToSwipe = Math.abs(clampedY) > swipeStartThreshold; + + if (isAllowedToSwipe) { + this._toastRef().nativeElement.style.setProperty('--swipe-amount', `${yPosition}px`); + } else if (Math.abs(xPosition) > swipeStartThreshold) { + // User is swiping in wrong direction, so we disable swipe gesture + // for the current pointer down interaction + this._pointerStartRef = null; + } + } + + onCloseButtonClick() { + if (this._disabled() || !this.toast().dismissible) return; + this.deleteToast(); + this.toast().onDismiss?.(this.toast()); + } + + onCancelClick() { + const toast = this.toast(); + if (!toast.dismissible) return; + this.deleteToast(); + if (toast.cancel?.onClick) { + toast.cancel.onClick(); + } + } + + onActionClick(event: MouseEvent) { + const toast = this.toast(); + toast.action?.onClick(event); + if (event.defaultPrevented) return; + this.deleteToast(); + } +} diff --git a/libs/brain/sonner/src/lib/brn-toaster.css b/libs/brain/sonner/src/lib/brn-toaster.css new file mode 100644 index 0000000000..72ed370343 --- /dev/null +++ b/libs/brain/sonner/src/lib/brn-toaster.css @@ -0,0 +1,667 @@ +html[dir='ltr'], +[data-sonner-toaster][dir='ltr'] { + --toast-icon-margin-start: var(--brn-sonner-toast-icon-margin-start, -3px); + --toast-icon-margin-end: var(--brn-sonner-toast-icon-margin-end, 4px); + --toast-svg-margin-start: var(--brn-sonner-toast-svg-margin-start, -1px); + --toast-svg-margin-end: var(--brn-sonner-toast-svg-margin-end, 0px); + --toast-button-margin-start: var(--brn-sonner-toast-button-margin-start, auto); + --toast-button-margin-end: var(--brn-sonner-toast-button-margin-end, 0); + --toast-close-button-start: var(--brn-sonner-toast-close-button-start, 0); + --toast-close-button-end: var(--brn-sonner-toast-close-button-end, unset); + --toast-close-button-transform: var(--brn-sonner-toast-close-button-transform, translate(-35%, -35%)); +} + +html[dir='rtl'], +[data-sonner-toaster][dir='rtl'] { + --toast-icon-margin-start: var(--brn-sonner-rtl-toast-icon-margin-start, 4px); + --toast-icon-margin-end: var(--brn-sonner-rtl-toast-icon-margin-end, -3px); + --toast-svg-margin-start: var(--brn-sonner-rtl-toast-svg-margin-start, 0px); + --toast-svg-margin-end: var(--brn-sonner-rtl-toast-svg-margin-end, -1px); + --toast-button-margin-start: var(--brn-sonner-rtl-toast-button-margin-start, 0); + --toast-button-margin-end: var(--brn-sonner-rtl-toast-button-margin-end, auto); + --toast-close-button-start: var(--brn-sonner-rtl-toast-close-button-start, unset); + --toast-close-button-end: var(--brn-sonner-rtl-toast-close-button-end, 0); + --toast-close-button-transform: var(--brn-sonner-rtl-toast-close-button-transform, translate(35%, -35%)); +} + +[data-sonner-toaster] { + position: fixed; + width: var(--width); + font-family: var( + --brn-sonner-font-family, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + Segoe UI, + Roboto, + Helvetica Neue, + Arial, + Noto Sans, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji + ); + --gray1: hsl(0, 0%, 99%); + --gray2: hsl(0, 0%, 97.3%); + --gray3: hsl(0, 0%, 95.1%); + --gray4: hsl(0, 0%, 93%); + --gray5: hsl(0, 0%, 90.9%); + --gray6: hsl(0, 0%, 88.7%); + --gray7: hsl(0, 0%, 85.8%); + --gray8: hsl(0, 0%, 78%); + --gray9: hsl(0, 0%, 56.1%); + --gray10: hsl(0, 0%, 52.3%); + --gray11: hsl(0, 0%, 43.5%); + --gray12: hsl(0, 0%, 9%); + --border-radius: var(--brn-sonner-border-radius, 8px); + box-sizing: border-box; + padding: 0; + margin: 0; + list-style: none; + outline: none; + z-index: 999999999; +} + +[data-sonner-toaster][data-x-position='right'] { + right: max(var(--offset), env(safe-area-inset-right)); +} + +[data-sonner-toaster][data-x-position='left'] { + left: max(var(--offset), env(safe-area-inset-left)); +} + +[data-sonner-toaster][data-x-position='center'] { + left: 50%; + transform: translateX(-50%); +} + +[data-sonner-toaster][data-y-position='top'] { + top: max(var(--offset), env(safe-area-inset-top)); +} + +[data-sonner-toaster][data-y-position='bottom'] { + bottom: max(var(--offset), env(safe-area-inset-bottom)); +} + +[data-sonner-toast] { + --y: translateY(100%); + --lift-amount: calc(var(--lift) * var(--gap)); + z-index: var(--z-index); + position: absolute; + opacity: 0; + transform: var(--y); + filter: blur(0); + /* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */ + touch-action: none; + transition: + transform 400ms, + opacity 400ms, + height 400ms, + box-shadow 200ms; + box-sizing: border-box; + outline: none; + overflow-wrap: anywhere; +} + +[data-sonner-toast][data-styled='true'] { + padding: 16px; + background: var(--normal-bg); + border: 1px solid var(--normal-border); + color: var(--normal-text); + border-radius: var(--border-radius); + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); + width: var(--width); + font-size: 13px; + display: flex; + align-items: center; + gap: 6px; +} + +[data-sonner-toast]:focus-visible { + box-shadow: + 0px 4px 12px rgba(0, 0, 0, 0.1), + 0 0 0 2px rgba(0, 0, 0, 0.2); +} + +[data-sonner-toast][data-y-position='top'] { + top: 0; + --y: translateY(-100%); + --lift: 1; + --lift-amount: calc(1 * var(--gap)); +} + +[data-sonner-toast][data-y-position='bottom'] { + bottom: 0; + --y: translateY(100%); + --lift: -1; + --lift-amount: calc(var(--lift) * var(--gap)); +} + +[data-sonner-toast] [data-description] { + font-weight: 400; + line-height: 1.4; + color: inherit; +} + +[data-sonner-toast] [data-title] { + font-weight: 500; + line-height: 1.5; + color: inherit; +} + +[data-sonner-toast] [data-icon] { + display: flex; + height: 16px; + width: 16px; + position: relative; + justify-content: flex-start; + align-items: center; + flex-shrink: 0; + margin-left: var(--toast-icon-margin-start); + margin-right: var(--toast-icon-margin-end); +} + +[data-sonner-toast][data-promise='true'] [data-icon] > svg { + opacity: 0; + transform: scale(0.8); + transform-origin: center; + animation: sonner-fade-in 300ms ease forwards; +} + +[data-sonner-toast] [data-icon] > * { + flex-shrink: 0; +} + +[data-sonner-toast] [data-icon] svg { + margin-left: var(--toast-svg-margin-start); + margin-right: var(--toast-svg-margin-end); +} + +[data-sonner-toast] [data-content] { + display: flex; + flex-direction: column; + gap: 2px; +} + +[data-sonner-toast] [data-button] { + border-radius: 4px; + padding-left: 8px; + padding-right: 8px; + height: 24px; + font-size: 12px; + color: var(--normal-bg); + background: var(--normal-text); + margin-left: var(--toast-button-margin-start); + margin-right: var(--toast-button-margin-end); + border: none; + cursor: pointer; + outline: none; + display: flex; + align-items: center; + flex-shrink: 0; + transition: + opacity 400ms, + box-shadow 200ms; +} + +[data-sonner-toast] [data-button]:focus-visible { + box-shadow: var(--brn-sonner-toast-focus-box-shadow, 0 0 0 2px rgba(0, 0, 0, 0.4)); +} + +[data-sonner-toast] [data-button]:first-of-type { + margin-left: var(--toast-button-margin-start); + margin-right: var(--toast-button-margin-end); +} + +[data-sonner-toast] [data-cancel] { + color: var(--normal-text); + background: rgba(0, 0, 0, 0.08); +} + +[data-sonner-toast][data-theme='dark'] [data-cancel] { + background: rgba(255, 255, 255, 0.3); +} + +[data-sonner-toast] [data-close-button] { + position: absolute; + left: var(--toast-close-button-start); + right: var(--toast-close-button-end); + top: 0; + height: 20px; + width: 20px; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + background: var(--brn-sonner-toast-close-button-background, var(--gray1)); + color: var(--brn-sonner-toast-close-button-color, var(--gray12)); + border: var(--brn-sonner-toast-close-button-border, 1px solid var(--gray4)); + transform: var(--toast-close-button-transform); + border-radius: 50%; + cursor: pointer; + z-index: 1; + transition: + opacity 100ms, + background 200ms, + border-color 200ms; +} + +[data-sonner-toast] [data-close-button]:focus-visible { + box-shadow: + 0px 4px 12px rgba(0, 0, 0, 0.1), + 0 0 0 2px rgba(0, 0, 0, 0.2); +} + +[data-sonner-toast] [data-disabled='true'] { + cursor: not-allowed; +} + +[data-sonner-toast]:hover [data-close-button]:hover { + background: var(--brn-sonner-toast-close-button-hover-background, var(--gray2)); + color: var(--brn-sonner-toast-close-button-hover-color, var(--gray12)); + border-color: var(--brn-sonner-toast-close-button-hover-border-color, var(--gray5)); +} + +/* Leave a ghost div to avoid setting hover to false when swiping out */ +[data-sonner-toast][data-swiping='true']:before { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 100%; + z-index: -1; +} + +[data-sonner-toast][data-y-position='top'][data-swiping='true']:before { + /* y 50% needed to distribute height additional height evenly */ + bottom: 50%; + transform: scaleY(3) translateY(50%); +} + +[data-sonner-toast][data-y-position='bottom'][data-swiping='true']:before { + /* y -50% needed to distribute height additional height evenly */ + top: 50%; + transform: scaleY(3) translateY(-50%); +} + +/* Leave a ghost div to avoid setting hover to false when transitioning out */ +[data-sonner-toast][data-swiping='false'][data-removed='true']:before { + content: ''; + position: absolute; + inset: 0; + transform: scaleY(2); +} + +/* Needed to avoid setting hover to false when inbetween toasts */ +[data-sonner-toast]:after { + content: ''; + position: absolute; + left: 0; + height: calc(var(--gap) + 1px); + bottom: 100%; + width: 100%; +} + +[data-sonner-toast][data-mounted='true'] { + --y: translateY(0); + opacity: 1; +} + +[data-sonner-toast][data-expanded='false'][data-front='false'] { + --scale: var(--toasts-before) * 0.05 + 1; + --y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale))); + height: var(--front-toast-height); +} + +[data-sonner-toast] > * { + transition: opacity 400ms; +} + +[data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true'] > * { + opacity: 0; +} + +[data-sonner-toast][data-visible='false'] { + opacity: 0; + pointer-events: none; +} + +[data-sonner-toast][data-mounted='true'][data-expanded='true'] { + --y: translateY(calc(var(--lift) * var(--offset))); + height: var(--initial-height); +} + +[data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false'] { + --y: translateY(calc(var(--lift) * -100%)); + opacity: 0; +} + +[data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true'] { + --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%)); + opacity: 0; +} + +[data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false'] { + --y: translateY(40%); + opacity: 0; + transition: + transform 500ms, + opacity 200ms; +} + +/* Bump up the height to make sure hover state doesn't get set to false */ +[data-sonner-toast][data-removed='true'][data-front='false']:before { + height: calc(var(--initial-height) + 20%); +} + +[data-sonner-toast][data-swiping='true'] { + transform: var(--y) translateY(var(--swipe-amount, 0px)); + transition: none; +} + +[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'], +[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] { + animation: swipe-out 200ms ease-out forwards; +} + +@keyframes swipe-out { + from { + transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount))); + opacity: 1; + } + + to { + transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%)); + opacity: 0; + } +} + +@media (max-width: 600px) { + [data-sonner-toaster] { + position: fixed; + --mobile-offset: 16px; + right: var(--mobile-offset); + left: var(--mobile-offset); + width: 100%; + } + + [data-sonner-toaster] [data-sonner-toast] { + left: 0; + right: 0; + width: calc(100% - 32px); + } + + [data-sonner-toaster][data-x-position='left'] { + left: var(--mobile-offset); + } + + [data-sonner-toaster][data-y-position='bottom'] { + bottom: 20px; + } + + [data-sonner-toaster][data-y-position='top'] { + top: 20px; + } + + [data-sonner-toaster][data-x-position='center'] { + left: var(--mobile-offset); + right: var(--mobile-offset); + transform: none; + } +} + +[data-sonner-toaster][data-theme='light'] { + --normal-bg: var(--brn-sonner-toast-normal-background, #fff); + --normal-border: var(--brn-sonner-toast-normal-border-color, var(--gray4)); + --normal-text: var(--brn-sonner-toast-normal-color, var(--gray12)); + + --success-bg: var(--brn-sonner-toast-success-background, hsl(143, 85%, 96%)); + --success-border: var(--brn-sonner-toast-success-border, hsl(145, 92%, 91%)); + --success-text: var(--brn-sonner-toast-success-color, hsl(140, 100%, 27%)); + + --info-bg: var(--brn-sonner-toast-info-background, hsl(208, 100%, 97%)); + --info-border: var(--brn-sonner-toast-info-border, hsl(221, 91%, 91%)); + --info-text: var(--brn-sonner-toast-info-color, hsl(210, 92%, 45%)); + + --warning-bg: var(--brn-sonner-toast-warning-background, hsl(49, 100%, 97%)); + --warning-border: var(--brn-sonner-toast-warning-border, hsl(49, 91%, 91%)); + --warning-text: var(--brn-sonner-toast-warning-color, hsl(31, 92%, 45%)); + + --error-bg: var(--brn-sonner-toast-error-background, hsl(359, 100%, 97%)); + --error-border: var(--brn-sonner-toast-error-border, hsl(359, 100%, 94%)); + --error-text: var(--brn-sonner-toast-error-color, hsl(360, 100%, 45%)); +} + +[data-sonner-toaster][data-theme='light'] [data-sonner-toast][data-invert='true'] { + --normal-bg: var(--brn-sonner-toast-inverse-normal-background, #000); + --normal-border: var(--brn-sonner-toast-inverse-normal-border-color, hsl(0, 0%, 20%)); + --normal-text: var(--brn-sonner-toast-inverse-normal-color, var(--gray1)); +} + +[data-sonner-toaster][data-theme='dark'] [data-sonner-toast][data-invert='true'] { + --normal-bg: var(--brn-sonner-toast-inverse-dark-normal-background, #fff); + --normal-border: var(--brn-sonner-toast-inverse-dark-normal-border-color, var(--gray3)); + --normal-text: var(--brn-sonner-toast-inverse-dark-normal-color, var(--gray12)); +} + +[data-sonner-toaster][data-theme='dark'] { + --normal-bg: var(--brn-sonner-toast-dark-normal-background, #000); + --normal-border: var(--brn-sonner-toast-dark-normal-border-color, hsl(0, 0%, 20%)); + --normal-text: var(--brn-sonner-toast-dark-normal-color, var(--gray1)); + + --success-bg: var(--brn-sonner-toast-dark-success-background, hsl(150, 100%, 6%)); + --success-border: var(--brn-sonner-toast-dark-success-border, hsl(147, 100%, 12%)); + --success-text: var(--brn-sonner-toast-dark-success-color, hsl(150, 86%, 65%)); + + --info-bg: var(--brn-sonner-toast-dark-info-background, hsl(215, 100%, 6%)); + --info-border: var(--brn-sonner-toast-dark-info-border, hsl(223, 100%, 12%)); + --info-text: var(--brn-sonner-toast-dark-info-color, hsl(216, 87%, 65%)); + + --warning-bg: var(--brn-sonner-toast-dark-warning-background, hsl(64, 100%, 6%)); + --warning-border: var(--brn-sonner-toast-dark-warning-border, hsl(60, 100%, 12%)); + --warning-text: var(--brn-sonner-toast-dark-warning-color, hsl(46, 87%, 65%)); + + --error-bg: var(--brn-sonner-toast-dark-error-background, hsl(358, 76%, 10%)); + --error-border: var(--brn-sonner-toast-dark-error-border, hsl(357, 89%, 16%)); + --error-text: var(--brn-sonner-toast-dark-error-color, hsl(358, 100%, 81%)); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='success'] { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='success'] [data-close-button] { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='info'] { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='info'] [data-close-button] { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='warning'] { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='warning'] [data-close-button] { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='error'] { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); +} + +[data-rich-colors='true'] [data-sonner-toast][data-type='error'] [data-close-button] { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); +} + +.sonner-loading-wrapper { + --size: 16px; + height: var(--size); + width: var(--size); + position: absolute; + inset: 0; + z-index: 10; +} + +.sonner-loading-wrapper[data-visible='false'] { + transform-origin: center; + animation: sonner-fade-out 0.2s ease forwards; +} + +.sonner-spinner { + position: relative; + top: 50%; + left: 50%; + height: var(--size); + width: var(--size); +} + +.sonner-loading-bar { + animation: sonner-spin 1.2s linear infinite; + background: var(--gray11); + border-radius: 6px; + height: 8%; + left: -10%; + position: absolute; + top: -3.9%; + width: 24%; +} + +.sonner-loading-bar:nth-child(1) { + animation-delay: -1.2s; + transform: rotate(0.0001deg) translate(146%); +} + +.sonner-loading-bar:nth-child(2) { + animation-delay: -1.1s; + transform: rotate(30deg) translate(146%); +} + +.sonner-loading-bar:nth-child(3) { + animation-delay: -1s; + transform: rotate(60deg) translate(146%); +} + +.sonner-loading-bar:nth-child(4) { + animation-delay: -0.9s; + transform: rotate(90deg) translate(146%); +} + +.sonner-loading-bar:nth-child(5) { + animation-delay: -0.8s; + transform: rotate(120deg) translate(146%); +} + +.sonner-loading-bar:nth-child(6) { + animation-delay: -0.7s; + transform: rotate(150deg) translate(146%); +} + +.sonner-loading-bar:nth-child(7) { + animation-delay: -0.6s; + transform: rotate(180deg) translate(146%); +} + +.sonner-loading-bar:nth-child(8) { + animation-delay: -0.5s; + transform: rotate(210deg) translate(146%); +} + +.sonner-loading-bar:nth-child(9) { + animation-delay: -0.4s; + transform: rotate(240deg) translate(146%); +} + +.sonner-loading-bar:nth-child(10) { + animation-delay: -0.3s; + transform: rotate(270deg) translate(146%); +} + +.sonner-loading-bar:nth-child(11) { + animation-delay: -0.2s; + transform: rotate(300deg) translate(146%); +} + +.sonner-loading-bar:nth-child(12) { + animation-delay: -0.1s; + transform: rotate(330deg) translate(146%); +} + +@keyframes sonner-fade-in { + 0% { + opacity: 0; + transform: scale(0.8); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes sonner-fade-out { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.8); + } +} + +@keyframes sonner-spin { + 0% { + opacity: 1; + } + 100% { + opacity: 0.15; + } +} + +@media (prefers-reduced-motion) { + [data-sonner-toast], + [data-sonner-toast] > *, + .sonner-loading-bar { + transition: none !important; + animation: none !important; + } +} + +.sonner-loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transform-origin: center; + transition: + opacity 200ms, + transform 200ms; +} + +.sonner-loader[data-visible='false'] { + opacity: 0; + transform: scale(0.8) translate(-50%, -50%); +} diff --git a/libs/brain/sonner/src/lib/brn-toaster.spec.ts b/libs/brain/sonner/src/lib/brn-toaster.spec.ts new file mode 100644 index 0000000000..5618e8b7cc --- /dev/null +++ b/libs/brain/sonner/src/lib/brn-toaster.spec.ts @@ -0,0 +1,316 @@ +import { ChangeDetectionStrategy, Component, input, provideZonelessChangeDetection } from '@angular/core'; +import { render } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; +import { noop } from 'rxjs'; +import { BrnSonnerToaster } from './brn-toaster'; +import { toast, toastState } from './state'; +import type { ToasterProps } from './types'; + +type ToastFn = (t: typeof toast) => void; + +export type ToastTestInputs = { + callback: ToastFn; + theme?: ToasterProps['theme']; + closeButton?: ToasterProps['closeButton']; +}; + +@Component({ + selector: 'brn-sonner-test', + imports: [BrnSonnerToaster], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + + + + + + `, +}) +export class ToasterTestComponent { + public readonly callback = input.required(); + public readonly theme = input('light'); + public readonly closeButton = input(false); + + onClick() { + this.callback()(toast); + } +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +async function setup(inputs: ToastTestInputs) { + const user = userEvent.setup(); + const returned = await render(ToasterTestComponent, { + providers: [provideZonelessChangeDetection()], + componentInputs: inputs, + }); + const trigger = returned.getByTestId('trigger'); + + return { + trigger, + user, + ...returned, + }; +} + +describe('Toaster', () => { + beforeEach(() => { + toastState.reset(); + }); + + it('should render a toast', async () => { + const { user, trigger, container, getByText } = await setup({ + callback: (toast) => toast('Hello world'), + }); + + await user.click(trigger); + expect(getByText('Hello world')).toBeVisible(); + + const toasts = Array.from(container.querySelectorAll('[data-sonner-toast]')); + expect(toasts.length).toBe(1); + }); + + it('should show a toast with custom duration', async () => { + const { user, trigger, queryByText } = await setup({ + callback: (toast) => toast('Custom duration', { duration: 300 }), + }); + + expect(queryByText('Custom duration')).toBeNull(); + + await user.click(trigger); + expect(queryByText('Custom duration')).not.toBeNull(); + + await sleep(600); + expect(queryByText('Custom duration')).toBeNull(); + }); + + it('should reset duration on a toast update', async () => { + const { user, trigger, getByText, queryByText } = await setup({ + callback: (toast) => { + const id = toast('Loading', { duration: 2000 }); + + setTimeout(() => { + toast.success('Finished loading!', { id }); + }, 1500); + }, + }); + + await user.click(trigger); + expect(getByText('Loading')).toBeVisible(); + await sleep(1500); + expect(queryByText('Loading')).toBeNull(); + expect(getByText('Finished loading!')).toBeVisible(); + // there would only be ~.5 seconds left on the original toast + // so we're going to wait .5 seconds to make sure the timer is reset + await sleep(500); + expect(getByText('Finished loading!')).toBeVisible(); + // finally we'll wait another 1500ms to make sure the toast closes after 2 seconds + // since the original toast had a duration of 2 seconds + await sleep(2000); + expect(queryByText('Finished loading!')).toBeNull(); + }); + + it('should allow duration updates on toast update', async () => { + const { user, trigger, getByText, queryByText } = await setup({ + callback: (toast) => { + const id = toast('Loading', { duration: 2000 }); + + setTimeout(() => { + toast.success('Finished loading!', { id, duration: 4000 }); + }, 1000); + }, + }); + + await user.click(trigger); + expect(getByText('Loading')).toBeVisible(); + await sleep(1200); + expect(queryByText('Loading')).toBeNull(); + expect(getByText('Finished loading!')).toBeVisible(); + await sleep(2200); + expect(getByText('Finished loading!')).toBeVisible(); + }); + + it('should show correct toast content based on promise state', async () => { + const { user, trigger, queryByText, getByText } = await setup({ + callback: (toast) => + toast.promise( + () => + new Promise((resolve) => + setTimeout(() => { + resolve('Loaded'); + }, 2000), + ), + { + loading: 'Loading...', + success: (data) => data, + error: 'Error', + }, + ), + }); + + await user.click(trigger); + expect(getByText('Loading...')).toBeVisible(); + await sleep(2000); + expect(queryByText('Loading...')).toBeNull(); + expect(getByText('Loaded')).toBeVisible(); + }); + + it('should focus the toast when hotkey is pressed', async () => { + const { user, trigger, getByText } = await setup({ + callback: (toast) => toast('Hello world', { duration: 5000 }), + }); + + await user.click(trigger); + expect(getByText('Hello world')).toBeVisible(); + + await user.keyboard('{Alt>}T{/Alt}'); + await sleep(100); + expect(document.activeElement).toBeInstanceOf(HTMLOListElement); + }); + + it('should not immediately close the toast when reset', async () => { + const { user, trigger, getByText, queryByText } = await setup({ + callback: (toast) => { + const id = toast('Loading', { duration: 4000 }); + + setTimeout(() => { + toast.success('Finished loading!', { id }); + }, 1000); + }, + }); + + await user.click(trigger); + expect(getByText('Loading')).toBeVisible(); + await sleep(2050); + expect(queryByText('Loading')).toBeNull(); + expect(getByText('Finished loading!')).toBeVisible(); + await sleep(1000); + expect(getByText('Finished loading!')).toBeVisible(); + }); + + it('should render toast with custom class', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => + toast('Hello world', { + classes: { + toast: 'test-class', + }, + }), + }); + + await user.click(trigger); + const toast = container.querySelector('[data-sonner-toast]'); + expect(toast).not.toBeNull(); + expect(toast as Element).toHaveClass('test-class'); + }); + + it('should render cancel button custom styles', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => + toast('Hello world', { + cancel: { + label: 'Cancel', + }, + cancelButtonStyle: 'background-color: rgb(254, 226, 226)', + }), + }); + + await user.click(trigger); + const cancelButton = container.querySelector('[data-cancel]'); + expect(cancelButton).not.toBeNull(); + expect(cancelButton as Element).toHaveStyle('background-color: rgb(254, 226, 226)'); + }); + + it('should render action button custom styles', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => + toast('Hello world', { + action: { + label: 'Do something', + onClick: noop, + }, + actionButtonStyle: 'background-color: rgb(219, 239, 255)', + }), + }); + + await user.click(trigger); + const actionButton = container.querySelector('[data-button]'); + expect(actionButton).not.toBeNull(); + expect(actionButton as Element).toHaveStyle('background-color: rgb(219, 239, 255)'); + }); + + it('should reflect toaster dark theme correctly', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => toast('Hello world'), + theme: 'dark', + }); + + await user.click(trigger); + const toaster = container.querySelector('[data-sonner-toaster]'); + expect(toaster).not.toBeNull(); + expect(toaster as Element).toHaveAttribute('data-theme', 'dark'); + }); + + it('should show close button correctly', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => toast('Hello world'), + closeButton: true, + }); + + await user.click(trigger); + const closeButton = container.querySelector('[data-close-button]'); + expect(closeButton).not.toBeNull(); + }); + + it('should not show close button if the toast has closeButton false', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => toast('Hello world', { closeButton: false }), + closeButton: true, + }); + + await user.click(trigger); + const closeButton = container.querySelector('[data-close-button]'); + expect(closeButton).toBeNull(); + }); + + it('should show close button if the toast has closeButton true', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => toast('Hello world', { closeButton: true }), + closeButton: false, + }); + + await user.click(trigger); + const closeButton = container.querySelector('[data-close-button]'); + expect(closeButton).not.toBeNull(); + }); + + it('should render the custom icon when provided', async () => { + const { user, trigger, container } = await setup({ + callback: (toast) => toast.success('Hello world'), + }); + await user.click(trigger); + const icon = container.querySelector('[data-icon]'); + expect(icon).not.toBeNull(); + expect(icon).toContainHTML( + '', + ); + }); +}); diff --git a/libs/brain/sonner/src/lib/brn-toaster.token.ts b/libs/brain/sonner/src/lib/brn-toaster.token.ts new file mode 100644 index 0000000000..3d4615a799 --- /dev/null +++ b/libs/brain/sonner/src/lib/brn-toaster.token.ts @@ -0,0 +1,56 @@ +import { inject, InjectionToken, type ValueProvider } from '@angular/core'; +import type { ToastClassnames } from './types'; + +export interface BrnSonnerToasterConfig { + /** The maximum number of toasts visible at once */ + visibleToastsAmount: number; + /** The offset from the viewport for the toast container */ + viewPortOffset: string; + /** The default lifetime of a toast in milliseconds */ + toastLifetime: number; + /** The width of the toast in pixels */ + toastWidth: number; + /** The gap between toasts in pixels */ + gap: number; + /** The threshold in pixels for swipe to dismiss a toast */ + swipeThreshold: number; + /** The time in milliseconds before a toast is unmounted */ + timeBeforeUnmount: number; +} + +const defaultConfig: BrnSonnerToasterConfig = { + visibleToastsAmount: 3, + viewPortOffset: '32px', + toastLifetime: 4000, + toastWidth: 356, + gap: 14, + swipeThreshold: 20, + timeBeforeUnmount: 200, +}; + +const BrnSonnerToasterConfigToken = new InjectionToken('BrnSonnerToasterConfig'); + +export function provideBrnSonnerToasterConfig(config: Partial): ValueProvider { + return { provide: BrnSonnerToasterConfigToken, useValue: { ...defaultConfig, ...config } }; +} + +export function injectBrnSonnerToasterConfig(): BrnSonnerToasterConfig { + return inject(BrnSonnerToasterConfigToken, { optional: true }) ?? defaultConfig; +} + +export const defaultClasses: ToastClassnames = { + toast: '', + title: '', + description: '', + loader: '', + closeButton: '', + cancelButton: '', + actionButton: '', + action: '', + warning: '', + error: '', + success: '', + default: '', + info: '', + loading: '', +}; diff --git a/libs/brain/sonner/src/lib/brn-toaster.ts b/libs/brain/sonner/src/lib/brn-toaster.ts new file mode 100644 index 0000000000..659ad5ddb2 --- /dev/null +++ b/libs/brain/sonner/src/lib/brn-toaster.ts @@ -0,0 +1,300 @@ +import { Directionality } from '@angular/cdk/bidi'; +import type { BooleanInput, NumberInput } from '@angular/cdk/coercion'; +import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common'; +import { + afterNextRender, + booleanAttribute, + ChangeDetectionStrategy, + Component, + computed, + contentChild, + DestroyRef, + DOCUMENT, + ElementRef, + inject, + input, + linkedSignal, + numberAttribute, + PLATFORM_ID, + signal, + TemplateRef, + viewChild, + ViewEncapsulation, +} from '@angular/core'; +import { BrnSonnerIcon } from './brn-icon'; +import { BrnSonnerLoader } from './brn-loader'; +import { BrnSonnerToast } from './brn-toast'; +import { injectBrnSonnerToasterConfig } from './brn-toaster.token'; +import { ToastFilterPipe } from './pipes/toast-filter.pipe'; +import { toastState } from './state'; +import type { Position, Theme, ToasterProps } from './types'; + +@Component({ + selector: 'brn-sonner-toaster', + imports: [BrnSonnerToast, ToastFilterPipe, BrnSonnerIcon, BrnSonnerLoader, NgTemplateOutlet], + // eslint-disable-next-line @nx/workspace-avoid-component-styles + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrl: 'brn-toaster.css', + template: ` + @if (_toasts().length > 0) { +
    + @for (pos of _possiblePositions(); track pos) { +
      + @for (toast of _toasts() | toastFilter: $index : pos; track toast.id) { + + @if (_loadingIcon(); as loadingIcon) { + + } @else { + + } + + @if (_successIcon(); as successIcon) { + + } @else { + + } + + @if (_errorIcon(); as errorIcon) { + + } @else { + + } + + @if (_warningIcon(); as warningIcon) { + + } @else { + + } + + @if (_infoIcon(); as infoIcon) { + + } @else { + + } + + } +
    + } +
    + } + `, +}) +export class BrnSonnerToaster { + private readonly _platformId = inject(PLATFORM_ID); + private readonly _config = injectBrnSonnerToasterConfig(); + private readonly _document = inject(DOCUMENT); + private readonly _window = this._document.defaultView; + private readonly _dir = inject(Directionality); + + protected readonly _toasts = toastState.toasts; + private readonly _heights = toastState.heights; + private readonly _reset = toastState.reset; + + public readonly invert = input(false, { + transform: booleanAttribute, + }); + public readonly theme = input('light'); + public readonly position = input('bottom-right'); + public readonly hotKey = input(['altKey', 'KeyT']); + public readonly richColors = input(false, { + transform: booleanAttribute, + }); + public readonly expand = input(false, { + transform: booleanAttribute, + }); + public readonly duration = input(this._config.toastLifetime, { + transform: numberAttribute, + }); + public readonly visibleToasts = input(this._config.visibleToastsAmount, { + transform: numberAttribute, + }); + public readonly closeButton = input(false, { + transform: booleanAttribute, + }); + public readonly toastOptions = input({}); + public readonly offset = input(null); + public readonly userClass = input('', { alias: 'class' }); + public readonly style = input>({}); + + protected readonly _possiblePositions = computed( + () => + Array.from( + new Set( + [ + this.position(), + ...this._toasts() + .filter((toast) => toast.position) + .map((toast) => toast.position), + ].filter(Boolean), + ), + ) as Position[], + ); + + protected readonly _expanded = linkedSignal({ + source: this._toasts, + computation: (toasts) => toasts.length < 1, + }); + protected readonly _actualTheme = linkedSignal({ + source: this.theme, + computation: (newTheme) => this.getActualTheme(newTheme), + }); + protected readonly _interacting = signal(false); + + /** internal **/ + public readonly direction = this._dir.valueSignal; + + private readonly _listRef = viewChild>('listRef'); + private readonly _lastFocusedElementRef = signal(null); + private readonly _isFocusWithinRef = signal(false); + + protected readonly _hotKeyLabel = computed(() => this.hotKey().join('+').replace(/Key/g, '').replace(/Digit/g, '')); + + protected readonly _toasterStyles = computed(() => ({ + '--front-toast-height': `${this._heights()[0]?.height}px`, + '--offset': + typeof this.offset() === 'number' ? `${this.offset()}px` : (this.offset() ?? `${this._config.viewPortOffset}`), + '--width': `${this._config.toastWidth}px`, + '--gap': `${this._config.gap}px`, + ...this.style(), + })); + + protected readonly _loadingIcon = contentChild('loadingIcon', { read: TemplateRef }); + protected readonly _successIcon = contentChild('successIcon', { read: TemplateRef }); + protected readonly _errorIcon = contentChild('errorIcon', { read: TemplateRef }); + protected readonly _warningIcon = contentChild('warningIcon', { read: TemplateRef }); + protected readonly _infoIcon = contentChild('infoIcon', { read: TemplateRef }); + + constructor() { + this._reset(); + + const destroyRef = inject(DestroyRef); + + afterNextRender(() => { + this._document.addEventListener('keydown', this.handleKeydown); + + const window = this._window; + if (window) { + this._window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', this.handleThemePreferenceChange); + } + + destroyRef.onDestroy(() => { + this._document.removeEventListener('keydown', this.handleKeydown); + if (window) { + window + .matchMedia('(prefers-color-scheme: dark)') + .removeEventListener('change', this.handleThemePreferenceChange); + } + }); + }); + } + + handleBlur(event: FocusEvent) { + if (this._isFocusWithinRef() && !(event.target as HTMLOListElement).contains(event.relatedTarget as HTMLElement)) { + this._isFocusWithinRef.set(false); + if (this._lastFocusedElementRef()) { + this._lastFocusedElementRef()?.focus({ preventScroll: true }); + this._lastFocusedElementRef.set(null); + } + } + } + + handleFocus(event: FocusEvent) { + const isNotDismissible = event.target instanceof HTMLElement && event.target.dataset['dismissible'] === 'false'; + + if (isNotDismissible) return; + + if (!this._isFocusWithinRef()) { + this._isFocusWithinRef.set(true); + this._lastFocusedElementRef.set(event.relatedTarget as HTMLElement); + } + } + + handlePointerDown(event: MouseEvent) { + const isNotDismissible = event.target instanceof HTMLElement && event.target.dataset['dismissible'] === 'false'; + + if (isNotDismissible) return; + this._interacting.set(true); + } + + handleMouseLeave() { + if (!this._interacting()) { + this._expanded.set(false); + } + } + + private readonly handleKeydown = (event: KeyboardEvent) => { + const listRef = this._listRef()?.nativeElement; + if (!listRef) return; + + const isHotkeyPressed = this.hotKey().every((key) => (event as never)[key] || event.code === key); + + if (isHotkeyPressed) { + this._expanded.set(true); + listRef.focus(); + } + + if ( + event.code === 'Escape' && + (this._document.activeElement === listRef || listRef.contains(this._document.activeElement)) + ) { + this._expanded.set(false); + } + }; + + private readonly handleThemePreferenceChange = ({ matches }: MediaQueryListEvent) => { + if (this.theme() === 'system') { + this._actualTheme.set(matches ? 'dark' : 'light'); + } + }; + + private getActualTheme(theme: Theme): Theme { + if (theme !== 'system') { + return theme; + } + + if (isPlatformBrowser(this._platformId) && this._window) { + const prefersDark = this._window.matchMedia?.('(prefers-color-scheme: dark)').matches; + return prefersDark ? 'dark' : 'light'; + } + + return 'light'; + } +} diff --git a/libs/brain/sonner/src/lib/pipes/as-component.pipe.ts b/libs/brain/sonner/src/lib/pipes/as-component.pipe.ts new file mode 100644 index 0000000000..b04f0fd5d0 --- /dev/null +++ b/libs/brain/sonner/src/lib/pipes/as-component.pipe.ts @@ -0,0 +1,8 @@ +import { Pipe, PipeTransform, Type } from '@angular/core'; + +@Pipe({ name: 'asComponent' }) +export class AsComponentPipe implements PipeTransform { + transform(value: unknown): Type { + return value as Type; + } +} diff --git a/libs/brain/sonner/src/lib/pipes/is-string.pipe.ts b/libs/brain/sonner/src/lib/pipes/is-string.pipe.ts new file mode 100644 index 0000000000..bdd0e9a7ee --- /dev/null +++ b/libs/brain/sonner/src/lib/pipes/is-string.pipe.ts @@ -0,0 +1,8 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'isString' }) +export class IsStringPipe implements PipeTransform { + transform(value: unknown): boolean { + return typeof value === 'string'; + } +} diff --git a/libs/brain/sonner/src/lib/pipes/toast-filter.pipe.ts b/libs/brain/sonner/src/lib/pipes/toast-filter.pipe.ts new file mode 100644 index 0000000000..c09316b13b --- /dev/null +++ b/libs/brain/sonner/src/lib/pipes/toast-filter.pipe.ts @@ -0,0 +1,9 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Position, ToastT } from '../types'; + +@Pipe({ name: 'toastFilter' }) +export class ToastFilterPipe implements PipeTransform { + transform(toasts: ToastT[], index: number, position: Position): ToastT[] { + return toasts.filter((toast) => (!toast.position && index === 0) || toast.position === position); + } +} diff --git a/libs/brain/sonner/src/lib/state.ts b/libs/brain/sonner/src/lib/state.ts new file mode 100644 index 0000000000..ab72a9a11a --- /dev/null +++ b/libs/brain/sonner/src/lib/state.ts @@ -0,0 +1,214 @@ +import { signal, type Type } from '@angular/core'; +import type { ExternalToast, HeightT, PromiseData, PromiseT, ToastT, ToastTypes } from './types'; + +let toastsCounter = 0; + +function createToastState() { + const toasts = signal([]); + const heights = signal([]); + + function addToast(data: ToastT) { + toasts.update((prev) => [data, ...prev]); + } + + function create( + data: ExternalToast & { + message?: string | Type; + type?: ToastTypes; + promise?: PromiseT; + }, + ) { + const { message, ...rest } = data; + const id = typeof data?.id === 'number' || (data.id && data.id?.length > 0) ? data.id : toastsCounter++; + + const dismissible = data.dismissible ?? true; + const type = data.type ?? 'default'; + + const alreadyExists = toasts().find((toast) => toast.id === id); + + if (alreadyExists) { + toasts.update((prev) => + prev.map((toast) => { + if (toast.id === id) { + return { + ...toast, + ...data, + id, + title: message, + dismissible, + type, + updated: true, + }; + } else return { ...toast, updated: false }; + }), + ); + } else { + addToast({ ...rest, id, title: message, dismissible: dismissible, type }); + } + + return id; + } + + function dismiss(id?: number | string) { + if (id === undefined) { + toasts.set([]); + return; + } + toasts.update((prev) => prev.filter((toast) => toast.id !== id)); + + return id; + } + + function message(message: string | Type, data?: ExternalToast) { + return create({ ...data, type: 'default', message }); + } + + function error(message: string | Type, data?: ExternalToast) { + return create({ ...data, type: 'error', message }); + } + + function success(message: string | Type, data?: ExternalToast) { + return create({ ...data, type: 'success', message }); + } + + function info(message: string | Type, data?: ExternalToast) { + return create({ ...data, type: 'info', message }); + } + + function warning(message: string | Type, data?: ExternalToast) { + return create({ ...data, type: 'warning', message }); + } + + function loading(message: string | Type, data?: ExternalToast) { + return create({ ...data, type: 'loading', message }); + } + + function promise(promise: PromiseT, data?: PromiseData) { + if (!data) return; + + let id: string | number | undefined = undefined; + if (data.loading !== undefined) { + id = create({ + ...data, + promise, + type: 'loading', + message: data.loading, + }); + } + + const p = promise instanceof Promise ? promise : promise(); + + let shouldDismiss = id !== undefined; + + p.then((response) => { + // @ts-expect-error: Incorrect response type + if (response && typeof response.ok === 'boolean' && !response.ok) { + shouldDismiss = false; + + const message = + typeof data.error === 'function' + ? // @ts-expect-error: TODO: Better function checking + data.error(`HTTP error! status: ${response.status}`) + : data.error; + create({ id, type: 'error', message }); + } else if (data.success !== undefined) { + shouldDismiss = false; + + const message = + typeof data.success === 'function' + ? // @ts-expect-error: TODO: Better function checking + data.success(response) + : data.success; + create({ id, type: 'success', message }); + } + }) + .catch((error) => { + if (data.error !== undefined) { + shouldDismiss = false; + const message = + // @ts-expect-error: TODO: Better function checking + typeof data.error === 'function' ? data.error(error) : data.error; + create({ id, type: 'error', message }); + } + }) + .finally(() => { + if (shouldDismiss) { + // Toast is still in load state (and will be indefinitely — dismiss it) + dismiss(id); + id = undefined; + } + + data.finally?.(); + }); + + return id; + } + + function custom(component: Type, data?: ExternalToast) { + const id = data?.id ?? toastsCounter++; + create({ component, id, ...data }); + + return id; + } + + function removeHeight(id: number | string) { + heights.update((prev) => prev.filter((height) => height.toastId !== id)); + } + + function addHeight(height: HeightT) { + heights.update((prev) => [height, ...prev].sort(sortHeights)); + } + + const sortHeights = (a: HeightT, b: HeightT) => + toasts().findIndex((t) => t.id === a.toastId) - toasts().findIndex((t) => t.id === b.toastId); + + function reset() { + toasts.set([]); + heights.set([]); + } + + return { + //methods + create, + addToast, + dismiss, + message, + error, + success, + info, + warning, + loading, + promise, + custom, + removeHeight, + addHeight, + reset, + // signals + toasts: toasts.asReadonly(), + heights: heights.asReadonly(), + }; +} + +export const toastState = createToastState(); + +// bind this to the toast function +function toastFunction(message: string | Type, data?: ExternalToast) { + return toastState.create({ + message, + ...data, + }); +} + +const basicToast = toastFunction; + +export const toast = Object.assign(basicToast, { + success: toastState.success, + info: toastState.info, + warning: toastState.warning, + error: toastState.error, + custom: toastState.custom, + message: toastState.message, + promise: toastState.promise, + dismiss: toastState.dismiss, + loading: toastState.loading, +}); diff --git a/libs/brain/sonner/src/lib/types.ts b/libs/brain/sonner/src/lib/types.ts new file mode 100644 index 0000000000..d5810cc156 --- /dev/null +++ b/libs/brain/sonner/src/lib/types.ts @@ -0,0 +1,300 @@ +import type { TemplateRef, Type } from '@angular/core'; + +export type Expand = T extends object ? (T extends infer O ? { [K in keyof O]: O[K] } : never) : T; + +export type ToastTypes = 'action' | 'success' | 'info' | 'warning' | 'error' | 'loading' | 'default'; + +export type PromiseT = Promise | (() => Promise); + +export type PromiseData = ExternalToast & { + loading?: string | Type; + success?: string | Type | ((data: ToastData) => Type | string); + error?: string | Type | ((error: unknown) => Type | string); + finally?: () => void | Promise; +}; + +export type ToastT = { + /** + * Custom id for the toast. + * + * @default autogenerated + */ + id: number | string; + title?: string | Type; + type?: ToastTypes; + /** + * Icon displayed in front of toast's text, aligned vertically. + */ + icon?: Type; + component?: Type; + componentProps?: Record; + /** + * Dark toast in light mode and vice versa. + * + * @default false + */ + invert?: boolean; + /** + * Adds a close button. + * + * @default false + */ + closeButton?: boolean; + /** + * If `false`, it'll prevent the user from dismissing the toast by swiping. + * + * @default true + */ + dismissible?: boolean; + /** + * Toast's description, renders underneath the title. + */ + description?: string | Type; + /** + * Time in milliseconds that should elapse before automatically closing the toast. + * + * @default 4000 + */ + duration?: number; + delete?: boolean; + /** + * Control the sensitivity of the toast for screen readers. + * + * @default false + */ + important?: boolean; + /** + * Renders a primary button, clicking it will close the toast. + */ + action?: { + label: string; + onClick: (event: MouseEvent) => void; + }; + /** + * Renders a secondary button, clicking it will close the toast. + */ + cancel?: { + label: string; + onClick?: () => void; + }; + /** + * The function gets called when either the close button is clicked, or the toast is swiped. + * + * @param toast + */ + onDismiss?: (toast: ToastT) => void; + /** + * Function that gets called when the toast disappears automatically after it's timeout (duration` prop). + * + * @param toast + */ + onAutoClose?: (toast: ToastT) => void; + promise?: PromiseT; + cancelButtonStyle?: string; + actionButtonStyle?: string; + style?: Record; + /** + * Removes the default styling, which allows for easier customization. + */ + unstyled?: boolean; + class?: string; + classes?: ToastClassnames; + descriptionClass?: string; + /** + * Position of the toast. + * + * @default 'bottom-right' + */ + position?: Position; + /** + * @internal This is used to determine if the toast has been updated to determine when to reset timer. + */ + updated?: boolean; +}; + +export type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'; + +export type HeightT = { + height: number; + toastId: number | string; + position: Position; +}; + +export type Theme = 'light' | 'dark' | 'system'; + +export type ToastToDismiss = { + id: number | string; + dismiss: boolean; +}; + +export type ExternalToast = Omit & { + id?: number | string; +}; + +export type ToasterProps = { + /** + * Dark toasts in light mode and vice versa. + * + * @default false + */ + invert: boolean; + + /** + * Toast's theme, either `light`, `dark`, or `system` + * + * @default 'light' + */ + theme: 'light' | 'dark' | 'system'; + + /** + * Place where the toasts will be rendered + * + * @default 'bottom-right' + */ + position: Position; + + /** + * Keyboard shortcut that will move focus to the toaster area. + * + * @default '⌥/alt + T' + */ + hotkey: string[]; + + /** + * Makes error and success state more colorful + * + * @default false + */ + richColors: boolean; + + /** + * Toasts will be expanded by default + * + * @default false + */ + expand: boolean; + + /** + * The duration of the toast in milliseconds. + * + * @default 4000 + */ + duration: number; + + /** + * Amount of visible toasts + * + * @default 3 + */ + visibleToasts: number; + + /** + * Adds a close button to all toasts, shows on hover + * + * @default false + */ + closeButton: boolean; + + /** + * These will act as default options for all toasts. + * + * @default {} + */ + toastOptions: ToastOptions; + + /** + * Offset from the edges of the screen. + * + * @default '32px' + */ + offset: string | number | null; + + /** + * Gap between toasts when expanded, in pixels. + * + * @default '14px' + */ + gap: number; + + /** + * Changes the default loading icon + * + * @default - + */ + loadingIcon: TemplateRef; +}; + +export type ToastOptions = { + /** + * The classes applied to the toast element. + */ + class?: string; + + /** + * The classes applied to the toast description element. + */ + descriptionClass?: string; + + /** + * The CSS styles applied to the toast element. + */ + style?: Record; + + /** + * The CSS styles applied to the cancel button element. + */ + cancelButtonStyle?: string; + + /** + * The CSS styles applied to the action button element. + */ + actionButtonStyle?: string; + + /** + * The duration of the toast in milliseconds. + */ + duration?: number; + + /** + * Whether the toast should be unstyled or not. + */ + unstyled?: boolean; + + /** + * Classes to apply to the various elements of the toast. + */ + classes?: Expand; +}; + +/** + * The classes applied to the various elements of the toast. + */ +export type ToastClassnames = { + toast?: string; + title?: string; + description?: string; + loader?: string; + closeButton?: string; + cancelButton?: string; + actionButton?: string; +} & ToastTypeClasses; + +type ToastTypeClasses = Partial>; + +export type ToastProps = { + toast: ToastT; + index: number; + expanded: boolean; + invert: boolean; + position: Position; + visibleToasts: number; + expandByDefault: boolean; + closeButton: boolean; + interacting: boolean; + cancelButtonStyle: string; + actionButtonStyle: string; + duration: number | null; + descriptionClass: string; + classes: ToastClassnames; + unstyled: boolean; +}; diff --git a/libs/cli/src/generators/healthcheck/generator.ts b/libs/cli/src/generators/healthcheck/generator.ts index b01aa8a9d1..1c578e4d26 100644 --- a/libs/cli/src/generators/healthcheck/generator.ts +++ b/libs/cli/src/generators/healthcheck/generator.ts @@ -24,6 +24,7 @@ import { scrollAreaHealthcheck } from './healthchecks/hlm-scroll-area'; import { selectHealthcheck } from './healthchecks/hlm-select'; import { moduleImportsHealthcheck } from './healthchecks/module-imports'; import { namingConventionHealthcheck } from './healthchecks/naming-conventions'; +import { sonnerHealthcheck } from './healthchecks/sonner'; import { versionHealthcheck } from './healthchecks/version'; import type { HealthcheckGeneratorSchema } from './schema'; import { promptUser } from './utils/prompt'; @@ -58,6 +59,7 @@ export async function healthcheckGenerator(tree: Tree, options: HealthcheckGener helmMenuHealthcheck, helmDialogHealthcheck, helmDialogPortalHealthcheck, + sonnerHealthcheck, ]; const failedReports: HealthcheckReport[] = []; diff --git a/libs/cli/src/generators/healthcheck/healthchecks/sonner.ts b/libs/cli/src/generators/healthcheck/healthchecks/sonner.ts new file mode 100644 index 0000000000..097b959704 --- /dev/null +++ b/libs/cli/src/generators/healthcheck/healthchecks/sonner.ts @@ -0,0 +1,39 @@ +import { visitNotIgnoredFiles } from '@nx/devkit'; +import migrateSonnerGenerator from '../../migrate-sonner/generator'; +import { HealthcheckSeverity, type Healthcheck } from '../healthchecks'; + +export const sonnerHealthcheck: Healthcheck = { + name: 'Sonner', + async detect(tree, failure, _) { + visitNotIgnoredFiles(tree, '/', (file) => { + // if the file is a .ts + if (!file.endsWith('.ts')) { + return; + } + + // skip hlm-toaster itself + if (file.endsWith('hlm-toaster.ts')) { + return; + } + + const contents = tree.read(file, 'utf-8'); + + if (!contents) { + return; + } + + if (contents.includes('ngx-sonner')) { + failure( + `The Sonner package (ngx-sonner) is deprecated. Please use the @spartan-ng/brain/sonner package instead.`, + HealthcheckSeverity.Error, + true, + ); + } + }); + }, + fix: async (tree) => { + await migrateSonnerGenerator(tree, { skipFormat: true }); + return true; + }, + prompt: 'Would you like to migrate ngx-sonner imports? Regenerate the helm sonner package.', +}; diff --git a/libs/cli/src/generators/migrate-sonner/compat.ts b/libs/cli/src/generators/migrate-sonner/compat.ts new file mode 100644 index 0000000000..73a6e1687b --- /dev/null +++ b/libs/cli/src/generators/migrate-sonner/compat.ts @@ -0,0 +1,4 @@ +import { convertNxGenerator } from '@nx/devkit'; +import { migrateSonnerGenerator } from './generator'; + +export default convertNxGenerator(migrateSonnerGenerator); diff --git a/libs/cli/src/generators/migrate-sonner/generator.spec.ts b/libs/cli/src/generators/migrate-sonner/generator.spec.ts new file mode 100644 index 0000000000..775ab3484c --- /dev/null +++ b/libs/cli/src/generators/migrate-sonner/generator.spec.ts @@ -0,0 +1,63 @@ +import { applicationGenerator, E2eTestRunner, UnitTestRunner } from '@nx/angular/generators'; +import type { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import migrateSonnerGenerator from './generator'; + +// patch some imports to avoid running the actual code +jest.mock('enquirer'); +jest.mock('@nx/devkit', () => { + const original = jest.requireActual('@nx/devkit'); + return { + ...original, + ensurePackage: (pkg: string) => jest.requireActual(pkg), + createProjectGraphAsync: jest.fn().mockResolvedValue({ + nodes: {}, + dependencies: {}, + }), + }; +}); + +describe('migrate-sonner generator', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + + await applicationGenerator(tree, { + name: 'app', + directory: 'app', + skipFormat: true, + e2eTestRunner: E2eTestRunner.None, + unitTestRunner: UnitTestRunner.None, + skipPackageJson: true, + skipTests: true, + }); + }); + + it('should replace ngx-sonner imports with @spartan-ng/brain/sonner (Standalone)', async () => { + tree.write( + 'app/src/app/app.component.ts', + ` + import { Component, signal } from '@angular/core'; + import { toast } from 'ngx-sonner'; + + @Component({ + template: \` + + \` + }) + export class AppModule { + showToast() { + toast.success('Hello World!'); + } + } + `, + ); + + await migrateSonnerGenerator(tree, { skipFormat: true }); + + const content = tree.read('app/src/app/app.component.ts', 'utf-8'); + expect(content).not.toContain(`import { toast } from 'ngx-sonner';`); + expect(content).toContain(`import { toast } from '@spartan-ng/brain/sonner';`); + }); +}); diff --git a/libs/cli/src/generators/migrate-sonner/generator.ts b/libs/cli/src/generators/migrate-sonner/generator.ts new file mode 100644 index 0000000000..0ce8e7b2d1 --- /dev/null +++ b/libs/cli/src/generators/migrate-sonner/generator.ts @@ -0,0 +1,42 @@ +import { formatFiles, type Tree } from '@nx/devkit'; +import { visitFiles } from '../../utils/visit-files'; +import type { MigrateSonnerGeneratorSchema } from './schema'; + +export async function migrateSonnerGenerator(tree: Tree, { skipFormat }: MigrateSonnerGeneratorSchema) { + updateImports(tree); + + if (!skipFormat) { + await formatFiles(tree); + } +} + +/** + * Migrate ngx-sonner imports to @spartan-ng/brain/sonner + */ +function updateImports(tree: Tree) { + visitFiles(tree, '/', (path) => { + // if this is not a typescript file then skip + if (!path.endsWith('.ts')) { + return; + } + + // skip hlm-toaster itself + if (path.endsWith('hlm-toaster.ts')) { + return; + } + + let content = tree.read(path, 'utf-8'); + + if (!content) { + return; + } + + if (content.includes('ngx-sonner')) { + content = content.replaceAll('ngx-sonner', '@spartan-ng/brain/sonner'); + } + + tree.write(path, content); + }); +} + +export default migrateSonnerGenerator; diff --git a/libs/cli/src/generators/migrate-sonner/schema.d.ts b/libs/cli/src/generators/migrate-sonner/schema.d.ts new file mode 100644 index 0000000000..9b6476a065 --- /dev/null +++ b/libs/cli/src/generators/migrate-sonner/schema.d.ts @@ -0,0 +1,3 @@ +export interface MigrateSonnerGeneratorSchema { + skipFormat?: boolean; +} diff --git a/libs/cli/src/generators/migrate-sonner/schema.json b/libs/cli/src/generators/migrate-sonner/schema.json new file mode 100644 index 0000000000..5c3a8607e8 --- /dev/null +++ b/libs/cli/src/generators/migrate-sonner/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "MigrateSonner", + "title": "", + "type": "object", + "properties": { + "skipFormat": { + "type": "boolean", + "default": false, + "description": "Skip formatting files" + } + }, + "required": [] +} diff --git a/libs/cli/src/generators/ui/libs/sonner/files/lib/hlm-toaster.ts.template b/libs/cli/src/generators/ui/libs/sonner/files/lib/hlm-toaster.ts.template index 46345a445e..e7da9381a0 100644 --- a/libs/cli/src/generators/ui/libs/sonner/files/lib/hlm-toaster.ts.template +++ b/libs/cli/src/generators/ui/libs/sonner/files/lib/hlm-toaster.ts.template @@ -1,15 +1,18 @@ import type { BooleanInput, NumberInput } from '@angular/cdk/coercion'; import { ChangeDetectionStrategy, Component, booleanAttribute, computed, input, numberAttribute } from '@angular/core'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideCircleCheck, lucideInfo, lucideLoader2, lucideOctagonX, lucideTriangleAlert } from '@ng-icons/lucide'; +import { BrnSonnerImports, type ToasterProps } from '@spartan-ng/brain/sonner'; import { hlm } from '<%- importAlias %>/utils'; import type { ClassValue } from 'clsx'; -import { NgxSonnerToaster, type ToasterProps } from 'ngx-sonner'; @Component({ selector: 'hlm-toaster', - imports: [NgxSonnerToaster], + imports: [BrnSonnerImports, NgIcon], + providers: [provideIcons({ lucideCircleCheck, lucideInfo, lucideTriangleAlert, lucideOctagonX, lucideLoader2 })], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - + > + + + + + + + + + + + + + + + + `, }) export class HlmToaster { @@ -51,7 +69,6 @@ export class HlmToaster { }); public readonly toastOptions = input({}); public readonly offset = input(null); - public readonly dir = input('auto'); public readonly userClass = input('', { alias: 'class' }); public readonly userStyle = input>( { diff --git a/libs/cli/src/generators/ui/supported-ui-libraries.json b/libs/cli/src/generators/ui/supported-ui-libraries.json index 47a200e128..a116e6ec10 100644 --- a/libs/cli/src/generators/ui/supported-ui-libraries.json +++ b/libs/cli/src/generators/ui/supported-ui-libraries.json @@ -419,8 +419,10 @@ "peerDependencies": { "@angular/cdk": ">=20.0.0 <22.0.0", "@angular/core": ">=20.0.0 <22.0.0", - "clsx": "^2.1.1", - "ngx-sonner": ">=3.0.0" + "@ng-icons/core": ">=32.0.0 <34.0.0", + "@ng-icons/lucide": ">=32.0.0 <34.0.0", + "@spartan-ng/brain": "0.0.1-alpha.645", + "clsx": "^2.1.1" } }, "spinner": { diff --git a/libs/helm/package.json b/libs/helm/package.json index 935e01d111..69bce3f025 100644 --- a/libs/helm/package.json +++ b/libs/helm/package.json @@ -14,7 +14,6 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "embla-carousel-angular": "^20.0.0", - "ngx-sonner": ">=3.0.0", "rxjs": "^7.8.0", "tailwind-merge": "^3.3.1" } diff --git a/libs/helm/sonner/src/lib/hlm-toaster.ts b/libs/helm/sonner/src/lib/hlm-toaster.ts index 5f930a5b6d..7348a7819e 100644 --- a/libs/helm/sonner/src/lib/hlm-toaster.ts +++ b/libs/helm/sonner/src/lib/hlm-toaster.ts @@ -1,15 +1,18 @@ import type { BooleanInput, NumberInput } from '@angular/cdk/coercion'; import { ChangeDetectionStrategy, Component, booleanAttribute, computed, input, numberAttribute } from '@angular/core'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideCircleCheck, lucideInfo, lucideLoader2, lucideOctagonX, lucideTriangleAlert } from '@ng-icons/lucide'; +import { BrnSonnerImports, type ToasterProps } from '@spartan-ng/brain/sonner'; import { hlm } from '@spartan-ng/helm/utils'; import type { ClassValue } from 'clsx'; -import { NgxSonnerToaster, type ToasterProps } from 'ngx-sonner'; @Component({ selector: 'hlm-toaster', - imports: [NgxSonnerToaster], + imports: [BrnSonnerImports, NgIcon], + providers: [provideIcons({ lucideCircleCheck, lucideInfo, lucideTriangleAlert, lucideOctagonX, lucideLoader2 })], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - + > + + + + + + + + + + + + + + + + `, }) export class HlmToaster { @@ -51,7 +69,6 @@ export class HlmToaster { }); public readonly toastOptions = input({}); public readonly offset = input(null); - public readonly dir = input('auto'); public readonly userClass = input('', { alias: 'class' }); public readonly userStyle = input>( { diff --git a/libs/tools/src/executors/docs/generate-ui-docs/__snapshots__/executor.spec.ts.snap b/libs/tools/src/executors/docs/generate-ui-docs/__snapshots__/executor.spec.ts.snap index e3b0e9035c..a858ca3fe5 100644 --- a/libs/tools/src/executors/docs/generate-ui-docs/__snapshots__/executor.spec.ts.snap +++ b/libs/tools/src/executors/docs/generate-ui-docs/__snapshots__/executor.spec.ts.snap @@ -4892,8 +4892,124 @@ exports[`generate-ui-docs executor produces stable output 1`] = ` }, }, "sonner": { - "helm": { - "HlmToaster": { + "brain": { + "BrnSonnerIcon": { + "inputs": [ + { + "name": "type", + "required": false, + "type": "ToastTypes", + }, + ], + "models": [], + "outputs": [], + "selector": "brn-sonner-icon", + }, + "BrnSonnerLoader": { + "inputs": [ + { + "name": "isVisible", + "required": true, + "type": "boolean", + }, + ], + "models": [], + "outputs": [], + "selector": "brn-sonner-loader", + }, + "BrnSonnerToast": { + "inputs": [ + { + "name": "toast", + "required": true, + "type": "ToastProps['toast']", + }, + { + "name": "index", + "required": true, + "type": "ToastProps['index']", + }, + { + "name": "expanded", + "required": true, + "type": "ToastProps['expanded']", + }, + { + "name": "invert", + "required": true, + "type": "ToastProps['invert']", + }, + { + "name": "position", + "required": true, + "type": "ToastProps['position']", + }, + { + "name": "visibleToasts", + "required": true, + "type": "ToastProps['visibleToasts']", + }, + { + "name": "expandByDefault", + "required": true, + "type": "ToastProps['expandByDefault']", + }, + { + "name": "closeButton", + "required": true, + "type": "ToastProps['closeButton']", + }, + { + "name": "interacting", + "required": true, + "type": "ToastProps['interacting']", + }, + { + "name": "cancelButtonStyle", + "required": false, + "type": "ToastProps['cancelButtonStyle']", + }, + { + "name": "actionButtonStyle", + "required": false, + "type": "ToastProps['actionButtonStyle']", + }, + { + "name": "duration", + "required": false, + "type": "ToastProps['duration']", + }, + { + "name": "descriptionClass", + "required": false, + "type": "ToastProps['descriptionClass']", + }, + { + "name": "classes", + "required": false, + "type": "ToastProps['classes']", + }, + { + "name": "unstyled", + "required": false, + "type": "ToastProps['unstyled']", + }, + { + "name": "class", + "required": false, + "type": "unknown", + }, + { + "name": "style", + "required": false, + "type": "Record", + }, + ], + "models": [], + "outputs": [], + "selector": "brn-sonner-toast", + }, + "BrnSonnerToaster": { "inputs": [ { "name": "invert", @@ -4951,9 +5067,78 @@ exports[`generate-ui-docs executor produces stable output 1`] = ` "type": "ToasterProps['offset']", }, { - "name": "dir", + "name": "class", + "required": false, + "type": "unknown", + }, + { + "name": "style", + "required": false, + "type": "Record", + }, + ], + "models": [], + "outputs": [], + "selector": "brn-sonner-toaster", + }, + }, + "helm": { + "HlmToaster": { + "inputs": [ + { + "name": "invert", + "required": false, + "type": "ToasterProps['invert']", + }, + { + "name": "theme", + "required": false, + "type": "ToasterProps['theme']", + }, + { + "name": "position", + "required": false, + "type": "ToasterProps['position']", + }, + { + "name": "hotKey", + "required": false, + "type": "ToasterProps['hotkey']", + }, + { + "name": "richColors", + "required": false, + "type": "ToasterProps['richColors']", + }, + { + "name": "expand", + "required": false, + "type": "ToasterProps['expand']", + }, + { + "name": "duration", "required": false, - "type": "ToasterProps['dir']", + "type": "ToasterProps['duration']", + }, + { + "name": "visibleToasts", + "required": false, + "type": "ToasterProps['visibleToasts']", + }, + { + "name": "closeButton", + "required": false, + "type": "ToasterProps['closeButton']", + }, + { + "name": "toastOptions", + "required": false, + "type": "ToasterProps['toastOptions']", + }, + { + "name": "offset", + "required": false, + "type": "ToasterProps['offset']", }, { "name": "class", diff --git a/package.json b/package.json index 96a1dac8ed..8897b15fd3 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "marked-mangle": "1.1.11", "mermaid": "^11.2.1", "ngx-scrollbar": "18.0.0", - "ngx-sonner": "3.1.0", "ngxtension": "^7.0.2", "node-html-parser": "^7.0.1", "ofetch": "^1.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 358295648a..f2bca59e91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,16 +13,16 @@ importers: version: 5.40.0 '@analogjs/content': specifier: ^2.0.3 - version: 2.0.3(wv4djfrzzla3rl6pkuzo5frata) + version: 2.0.3(b4a8513f19cc3490f0ed62c41218e5de) '@analogjs/router': specifier: ^2.0.3 - version: 2.0.3(@analogjs/content@2.0.3(wv4djfrzzla3rl6pkuzo5frata))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/router@20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)) + version: 2.0.3(@analogjs/content@2.0.3(b4a8513f19cc3490f0ed62c41218e5de))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/router@20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)) '@analogjs/trpc': specifier: ~0.3.0 version: 0.3.0(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(isomorphic-fetch@3.0.0(encoding@0.1.13))(superjson@2.2.2) '@analogjs/vite-plugin-angular': specifier: 1.19.4 - version: 1.19.4(@angular-devkit/build-angular@20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa))(@angular/build@20.3.9(d242b6mbg7t4axgmypwndrlnti)) + version: 1.19.4(@angular-devkit/build-angular@20.3.9(05522fc0219b75916c8fe432bbfaf4ae))(@angular/build@20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9)) '@angular/animations': specifier: 20.3.17 version: 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)) @@ -55,7 +55,7 @@ importers: version: 20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@angular/ssr': specifier: 20.3.9 - version: 20.3.9(vw3cxorbagnpnhhn2yathrn6qe) + version: 20.3.9(cfbc5566f5b280c41268f8118965c204) '@fontsource/geist': specifier: ^5.2.5 version: 5.2.8 @@ -67,7 +67,7 @@ importers: version: 32.4.0(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@nx/angular': specifier: 21.6.9 - version: 21.6.9(xubgwdtaadro5fvksmiexaqqsi) + version: 21.6.9(1a93e208044bcb8dc99939291f275aba) '@nx/devkit': specifier: 21.6.9 version: 21.6.9(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))) @@ -137,9 +137,6 @@ importers: ngx-scrollbar: specifier: 18.0.0 version: 18.0.0(@angular/cdk@20.2.14(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - ngx-sonner: - specifier: 3.1.0 - version: 3.1.0(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)) ngxtension: specifier: ^7.0.2 version: 7.0.2(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -191,10 +188,10 @@ importers: devDependencies: '@analogjs/platform': specifier: ^2.0.3 - version: 2.0.3(aysdnfw3363dp72geoqy2obddu) + version: 2.0.3(3d7cd34d6fcf1b9f4cf1f7d58c45c977) '@angular-devkit/build-angular': specifier: 20.3.9 - version: 20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa) + version: 20.3.9(05522fc0219b75916c8fe432bbfaf4ae) '@angular-devkit/core': specifier: 20.3.9 version: 20.3.9(chokidar@4.0.3) @@ -203,7 +200,7 @@ importers: version: 20.3.9(chokidar@4.0.3) '@angular/build': specifier: 20.3.9 - version: 20.3.9(d242b6mbg7t4axgmypwndrlnti) + version: 20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9) '@angular/cli': specifier: ~20.3.0 version: 20.3.17(@types/node@22.6.2)(chokidar@4.0.3) @@ -215,7 +212,7 @@ importers: version: 20.3.17 '@babel/plugin-proposal-private-property-in-object': specifier: ^7.21.11 - version: 7.21.11(@babel/core@7.27.7) + version: 7.21.11(@babel/core@7.28.3) '@commitlint/cli': specifier: ^19.5.0 version: 19.8.1(@types/node@22.6.2)(typescript@5.9.3) @@ -260,7 +257,7 @@ importers: version: 21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(@zkochan/js-yaml@0.0.7)(cypress@14.3.2)(eslint@9.30.0(jiti@2.4.2))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(storybook@9.1.9(@testing-library/dom@10.4.0)(prettier@3.8.1)(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0)))(typescript@5.9.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0)) '@nx/vite': specifier: 21.6.9 - version: 21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(typescript@5.9.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0))(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0))(vitest@2.1.1(@types/node@22.6.2)(@vitest/ui@2.1.1)(jsdom@25.0.1)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)) + version: 21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(typescript@5.9.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0))(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0))(vitest@2.1.1) '@nx/web': specifier: 21.6.9 version: 21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0)) @@ -305,7 +302,7 @@ importers: version: 9.1.9(storybook@9.1.9(@testing-library/dom@10.4.0)(prettier@3.8.1)(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0))) '@storybook/angular': specifier: 9.1.9 - version: 9.1.9(r2qazale777rhog6l2r7ymu4pe) + version: 9.1.9(3d319b7f62ef06596b4c03e303fe81b6) '@swc-node/register': specifier: 1.10.9 version: 1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3) @@ -323,7 +320,7 @@ importers: version: 4.1.13 '@testing-library/angular': specifier: ^17.3.1 - version: 17.4.0(qyqphfusurop7kdnya6mcaudbe) + version: 17.4.0(2f13c451bd488c6007c8d02d67541f56) '@testing-library/jest-dom': specifier: ^6.5.0 version: 6.6.3 @@ -359,7 +356,7 @@ importers: version: 8.46.3(eslint@9.30.0(jiti@2.4.2))(typescript@5.9.3) '@vitest/coverage-v8': specifier: 2.1.1 - version: 2.1.1(vitest@2.1.1(@types/node@22.6.2)(@vitest/ui@2.1.1)(jsdom@25.0.1)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)) + version: 2.1.1(vitest@2.1.1) '@vitest/ui': specifier: 2.1.1 version: 2.1.1(vitest@2.1.1) @@ -419,7 +416,7 @@ importers: version: 29.7.0 jest-preset-angular: specifier: 14.6.2 - version: 14.6.2(5uw4eeksyhrldc6e5vofrefjye) + version: 14.6.2(5853600170e1893ed3b46065385bd77c) jiti: specifier: 2.4.2 version: 2.4.2 @@ -491,7 +488,7 @@ importers: version: 0.2.14 ts-jest: specifier: 29.4.6 - version: 29.4.6(@babel/core@7.27.7)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.27.7))(esbuild@0.24.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.6.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.6.2)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.3)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.3))(esbuild@0.24.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.6.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.6.2)(typescript@5.9.3)))(typescript@5.9.3) ts-morph: specifier: ^25.0.1 version: 25.0.1 @@ -3901,84 +3898,98 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-arm64-gnu@1.1.1': resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.0.1': resolution: {integrity: sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.0.1': resolution: {integrity: sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.0.1': resolution: {integrity: sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.1.1': resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.0.1': resolution: {integrity: sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.0.1': resolution: {integrity: sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.0.1': resolution: {integrity: sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-x64-musl@1.1.1': resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/nice-openharmony-arm64@1.1.1': resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} @@ -4243,21 +4254,25 @@ packages: resolution: {integrity: sha512-1VS38xnAC8iH05A0nnbNn1hi9ypRnEPUfgLL3tPhAwQTWX2DQz4xR/j0NYNcCzL6yBe/JhdKlYoN/LI38lj2UA==} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-musl@21.6.9': resolution: {integrity: sha512-PScHPs0dp+Cc17RvY4Y5wlDXT6xdDlsyhna2JLawodVCyUVArtnbF7whn/VEZKesDD/vAf1avCt4oAjuYS8VXg==} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-gnu@21.6.9': resolution: {integrity: sha512-s8oX6/pLolHH3EyFJPcKITv+rzN/IZuidMCNkGfcr0jYVqrTZcJo8xUEwAQzf6u6J6urOm0bUK3BDuwJLEKESg==} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-musl@21.6.9': resolution: {integrity: sha512-bojpGcscRrnet5N3waeHYnBHW0y6r5tSQ1phnwMjgoBFmWXw+0M+z/f2dfZcTtBmWc7Y/TnzaGb8EenC3a63cQ==} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-win32-arm64-msvc@21.6.9': resolution: {integrity: sha512-cS1bdMiJBs4AcykJ3+vtAdw4RkZLLfXT20o+k07dEskRFADIa5yXdOs2j0qKoe7iCiORKCH+gI/YsPHCyHfV9Q==} @@ -4386,21 +4401,25 @@ packages: resolution: {integrity: sha512-otVbS4zeo3n71zgGLBYRTriDzc0zpruC0WI3ICwjpIk454cLwGV0yzh4jlGYWQJYJk0BRAmXFd3ooKIF+bKBHw==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@1.12.0': resolution: {integrity: sha512-IStQDjIT7Lzmqg1i9wXvPL/NsYsxF24WqaQFS8b8rxra+z0VG7saBOsEnOaa4jcEY8MVpLYabFhTV+fSsA2vnA==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-x64-gnu@1.12.0': resolution: {integrity: sha512-SipT7EVORz8pOQSFwemOm91TpSiBAGmOjG830/o+aLEsvQ4pEy223+SAnCfITh7+AahldYsJnVoIs519jmIlKQ==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@1.12.0': resolution: {integrity: sha512-mGh0XfUzKdn+WFaqPacziNraCWL5znkHRfQVxG9avGS9zb2KC/N1EBbPzFqutDwixGDP54r2gx4q54YCJEZ4iQ==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-wasm32-wasi@1.12.0': resolution: {integrity: sha512-SZN6v7apKmQf/Vwiqb6e/s3Y2Oacw8uW8V2i1AlxtyaEFvnFE0UBn89zq6swEwE3OCajNWs0yPvgAXUMddYc7Q==} @@ -4446,36 +4465,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-wasm@2.5.1': resolution: {integrity: sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==} @@ -4685,111 +4710,133 @@ packages: resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.52.3': resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.44.1': resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.52.3': resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.44.1': resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.52.3': resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.44.1': resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.52.3': resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.3': resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loongarch64-gnu@4.44.1': resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.3': resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.44.1': resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.3': resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.44.1': resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.52.3': resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.44.1': resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.52.3': resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.44.1': resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.3': resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.44.1': resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.52.3': resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.3': resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} @@ -4860,41 +4907,49 @@ packages: resolution: {integrity: sha512-PJ5cHqvrj1bK7jH5DVrdKoR8Fy+p6l9baxXajq/6xWTxP+4YTdEtLsRZnpLMS1Ho2RRpkxDWJn+gdlKuleNioQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rspack/binding-linux-arm64-gnu@1.7.6': resolution: {integrity: sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==} cpu: [arm64] os: [linux] + libc: [glibc] '@rspack/binding-linux-arm64-musl@1.4.1': resolution: {integrity: sha512-cpDz+z3FwVQfK6VYfXQEb0ym6fFIVmvK4y3R/2VAbVGWYVxZB5I6AcSdOWdDnpppHmcHpf+qQFlwhHvbpMMJNQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rspack/binding-linux-arm64-musl@1.7.6': resolution: {integrity: sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==} cpu: [arm64] os: [linux] + libc: [musl] '@rspack/binding-linux-x64-gnu@1.4.1': resolution: {integrity: sha512-jjTx53CpiYWK7fAv5qS8xHEytFK6gLfZRk+0kt2YII6uqez/xQ3SRcboreH8XbJcBoxINBzMNMf5/SeMBZ939A==} cpu: [x64] os: [linux] + libc: [glibc] '@rspack/binding-linux-x64-gnu@1.7.6': resolution: {integrity: sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==} cpu: [x64] os: [linux] + libc: [glibc] '@rspack/binding-linux-x64-musl@1.4.1': resolution: {integrity: sha512-FAyR3Og81Smtr/CnsuTiW4ZCYAPCqeV73lzMKZ9xdVUgM9324ryEgqgX38GZLB5Mo7cvQhv7/fpMeHQo16XQCw==} cpu: [x64] os: [linux] + libc: [musl] '@rspack/binding-linux-x64-musl@1.7.6': resolution: {integrity: sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==} cpu: [x64] os: [linux] + libc: [musl] '@rspack/binding-wasm32-wasi@1.4.1': resolution: {integrity: sha512-3Q1VICIQP4GsaTJEmmwfowQ48NvhlL0CKH88l5+mbji2rBkGx7yR67pPdfCVNjXcCtFoemTYw98eaumJTjC++g==} @@ -5269,24 +5324,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.7.26': resolution: {integrity: sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.7.26': resolution: {integrity: sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.7.26': resolution: {integrity: sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.7.26': resolution: {integrity: sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==} @@ -5406,48 +5465,56 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.13': resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.1.14': resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.13': resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.1.14': resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.13': resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.1.14': resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.13': resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} @@ -6001,41 +6068,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -10478,24 +10553,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -11119,12 +11198,6 @@ packages: '@angular/core': '>=19.0.0' rxjs: '>=7.0.0' - ngx-sonner@3.1.0: - resolution: {integrity: sha512-hN5GolhBeVs0gzPNZi2VPnbv/sM+3+aMGCvIjH33MC8LHz+QIiOUWGw3AbnCH/hQxy08AGwkWHVYy1acvXByQQ==} - peerDependencies: - '@angular/common': '>=19.0.0' - '@angular/core': '>=19.0.0' - ngxtension@7.0.2: resolution: {integrity: sha512-msCX6tVv1WOpxIfKsQnJiDubVi9DnU267N0McfdpwXVUOGZ8BJYoSb17aO64y69DkKziMwPUNJ+LOsO/kmgTUw==} engines: {node: '>=18'} @@ -14910,7 +14983,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.11 '@jridgewell/trace-mapping': 0.3.28 - '@analogjs/content@2.0.3(wv4djfrzzla3rl6pkuzo5frata)': + '@analogjs/content@2.0.3(b4a8513f19cc3490f0ed62c41218e5de)': dependencies: '@angular/common': 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) @@ -14927,9 +15000,9 @@ snapshots: optionalDependencies: '@nx/devkit': 21.6.9(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))) - '@analogjs/platform@2.0.3(aysdnfw3363dp72geoqy2obddu)': + '@analogjs/platform@2.0.3(3d7cd34d6fcf1b9f4cf1f7d58c45c977)': dependencies: - '@analogjs/vite-plugin-angular': 2.0.3(@angular-devkit/build-angular@20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa))(@angular/build@20.3.9(d242b6mbg7t4axgmypwndrlnti)) + '@analogjs/vite-plugin-angular': 2.0.3(@angular-devkit/build-angular@20.3.9(05522fc0219b75916c8fe432bbfaf4ae))(@angular/build@20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9)) '@analogjs/vite-plugin-nitro': 2.0.3(drizzle-orm@0.33.0(@types/react@18.3.23)(postgres@3.4.7)(react@19.1.0))(encoding@0.1.13) marked: 15.0.9 marked-gfm-heading-id: 4.1.2(marked@15.0.9) @@ -14938,9 +15011,9 @@ snapshots: vite: 6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0) vitefu: 1.0.7(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0)) optionalDependencies: - '@nx/angular': 21.6.9(xubgwdtaadro5fvksmiexaqqsi) + '@nx/angular': 21.6.9(1a93e208044bcb8dc99939291f275aba) '@nx/devkit': 21.6.9(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))) - '@nx/vite': 21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(typescript@5.9.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0))(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0))(vitest@2.1.1(@types/node@22.6.2)(@vitest/ui@2.1.1)(jsdom@25.0.1)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)) + '@nx/vite': 21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(typescript@5.9.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0))(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0))(vitest@2.1.1) marked-highlight: 2.2.2(marked@15.0.9) prismjs: 1.30.0 transitivePeerDependencies: @@ -14973,9 +15046,9 @@ snapshots: - uploadthing - xml2js - '@analogjs/router@2.0.3(@analogjs/content@2.0.3(wv4djfrzzla3rl6pkuzo5frata))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/router@20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))': + '@analogjs/router@2.0.3(@analogjs/content@2.0.3(b4a8513f19cc3490f0ed62c41218e5de))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/router@20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))': dependencies: - '@analogjs/content': 2.0.3(wv4djfrzzla3rl6pkuzo5frata) + '@analogjs/content': 2.0.3(b4a8513f19cc3490f0ed62c41218e5de) '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/router': 20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) tslib: 2.8.1 @@ -14990,20 +15063,20 @@ snapshots: superjson: 2.2.2 tslib: 2.8.1 - '@analogjs/vite-plugin-angular@1.19.4(@angular-devkit/build-angular@20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa))(@angular/build@20.3.9(d242b6mbg7t4axgmypwndrlnti))': + '@analogjs/vite-plugin-angular@1.19.4(@angular-devkit/build-angular@20.3.9(05522fc0219b75916c8fe432bbfaf4ae))(@angular/build@20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9))': dependencies: ts-morph: 21.0.1 vfile: 6.0.3 optionalDependencies: - '@angular-devkit/build-angular': 20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa) - '@angular/build': 20.3.9(d242b6mbg7t4axgmypwndrlnti) + '@angular-devkit/build-angular': 20.3.9(05522fc0219b75916c8fe432bbfaf4ae) + '@angular/build': 20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9) - '@analogjs/vite-plugin-angular@2.0.3(@angular-devkit/build-angular@20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa))(@angular/build@20.3.9(d242b6mbg7t4axgmypwndrlnti))': + '@analogjs/vite-plugin-angular@2.0.3(@angular-devkit/build-angular@20.3.9(05522fc0219b75916c8fe432bbfaf4ae))(@angular/build@20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9))': dependencies: ts-morph: 21.0.1 optionalDependencies: - '@angular-devkit/build-angular': 20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa) - '@angular/build': 20.3.9(d242b6mbg7t4axgmypwndrlnti) + '@angular-devkit/build-angular': 20.3.9(05522fc0219b75916c8fe432bbfaf4ae) + '@angular/build': 20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9) '@analogjs/vite-plugin-nitro@2.0.3(drizzle-orm@0.33.0(@types/react@18.3.23)(postgres@3.4.7)(react@19.1.0))(encoding@0.1.13)': dependencies: @@ -15054,13 +15127,13 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa)': + '@angular-devkit/build-angular@20.3.9(05522fc0219b75916c8fe432bbfaf4ae)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.9(chokidar@4.0.3) '@angular-devkit/build-webpack': 0.2003.9(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.9)))(webpack@5.101.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.9)) '@angular-devkit/core': 20.3.9(chokidar@4.0.3) - '@angular/build': 20.3.9(tnl5tasv3zzep54riyy4j6t7bm) + '@angular/build': 20.3.9(08173190124508b6ebfa5b8a67b87dbb) '@angular/compiler-cli': 20.3.17(@angular/compiler@20.3.17)(typescript@5.9.3) '@babel/core': 7.28.3 '@babel/generator': 7.28.3 @@ -15116,7 +15189,7 @@ snapshots: '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/platform-browser': 20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/platform-server': 20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.17)(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) - '@angular/ssr': 20.3.9(vw3cxorbagnpnhhn2yathrn6qe) + '@angular/ssr': 20.3.9(cfbc5566f5b280c41268f8118965c204) esbuild: 0.25.9 jest: 29.7.0(@types/node@22.6.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.6.2)(typescript@5.9.3)) jest-environment-jsdom: 29.7.0 @@ -15264,7 +15337,7 @@ snapshots: '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 - '@angular/build@20.3.9(d242b6mbg7t4axgmypwndrlnti)': + '@angular/build@20.3.9(08173190124508b6ebfa5b8a67b87dbb)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.9(chokidar@4.0.3) @@ -15274,7 +15347,7 @@ snapshots: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 '@inquirer/confirm': 5.1.14(@types/node@22.6.2) - '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0)) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0)) beasties: 0.3.5 browserslist: 4.25.1 esbuild: 0.25.9 @@ -15294,14 +15367,14 @@ snapshots: tinyglobby: 0.2.14 tslib: 2.8.1 typescript: 5.9.3 - vite: 7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0) watchpack: 2.4.4 optionalDependencies: '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/platform-browser': 20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/platform-server': 20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.17)(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) - '@angular/ssr': 20.3.9(vw3cxorbagnpnhhn2yathrn6qe) - less: 4.3.0 + '@angular/ssr': 20.3.9(cfbc5566f5b280c41268f8118965c204) + less: 4.4.0 lmdb: 3.4.2 ng-packagr: 20.3.2(@angular/compiler-cli@20.3.17(@angular/compiler@20.3.17)(typescript@5.9.3))(tailwindcss@4.2.1)(tslib@2.8.1)(typescript@5.9.3) postcss: 8.5.6 @@ -15320,7 +15393,7 @@ snapshots: - tsx - yaml - '@angular/build@20.3.9(tnl5tasv3zzep54riyy4j6t7bm)': + '@angular/build@20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.9(chokidar@4.0.3) @@ -15330,7 +15403,7 @@ snapshots: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 '@inquirer/confirm': 5.1.14(@types/node@22.6.2) - '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0)) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0)) beasties: 0.3.5 browserslist: 4.25.1 esbuild: 0.25.9 @@ -15350,14 +15423,14 @@ snapshots: tinyglobby: 0.2.14 tslib: 2.8.1 typescript: 5.9.3 - vite: 7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0) watchpack: 2.4.4 optionalDependencies: '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/platform-browser': 20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/platform-server': 20.3.17(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.17)(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.17(@angular/animations@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) - '@angular/ssr': 20.3.9(vw3cxorbagnpnhhn2yathrn6qe) - less: 4.4.0 + '@angular/ssr': 20.3.9(cfbc5566f5b280c41268f8118965c204) + less: 4.3.0 lmdb: 3.4.2 ng-packagr: 20.3.2(@angular/compiler-cli@20.3.17(@angular/compiler@20.3.17)(typescript@5.9.3))(tailwindcss@4.2.1)(tslib@2.8.1)(typescript@5.9.3) postcss: 8.5.6 @@ -15488,7 +15561,7 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/ssr@20.3.9(vw3cxorbagnpnhhn2yathrn6qe)': + '@angular/ssr@20.3.9(cfbc5566f5b280c41268f8118965c204)': dependencies: '@angular/common': 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) @@ -15929,13 +16002,13 @@ snapshots: dependencies: '@babel/core': 7.28.3 - '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.27.7)': + '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.27.7 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.7) + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.3) transitivePeerDependencies: - supports-color @@ -15944,21 +16017,45 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -15989,11 +16086,23 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -16004,41 +16113,88 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + optional: true + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -19431,7 +19587,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@nx/angular@21.6.9(xubgwdtaadro5fvksmiexaqqsi)': + '@nx/angular@21.6.9(1a93e208044bcb8dc99939291f275aba)': dependencies: '@angular-devkit/core': 20.3.9(chokidar@4.0.3) '@angular-devkit/schematics': 20.3.9(chokidar@4.0.3) @@ -19455,8 +19611,8 @@ snapshots: tslib: 2.8.1 webpack-merge: 5.10.0 optionalDependencies: - '@angular-devkit/build-angular': 20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa) - '@angular/build': 20.3.9(d242b6mbg7t4axgmypwndrlnti) + '@angular-devkit/build-angular': 20.3.9(05522fc0219b75916c8fe432bbfaf4ae) + '@angular/build': 20.3.9(fdb1ab178c5fa41fa595ceca47c0f8a9) ng-packagr: 20.3.2(@angular/compiler-cli@20.3.17(@angular/compiler@20.3.17)(typescript@5.9.3))(tailwindcss@4.2.1)(tslib@2.8.1)(typescript@5.9.3) transitivePeerDependencies: - '@babel/traverse' @@ -19815,7 +19971,7 @@ snapshots: - typescript - verdaccio - '@nx/vite@21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(typescript@5.9.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0))(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0))(vitest@2.1.1(@types/node@22.6.2)(@vitest/ui@2.1.1)(jsdom@25.0.1)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1))': + '@nx/vite@21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(typescript@5.9.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0))(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0))(vitest@2.1.1)': dependencies: '@nx/devkit': 21.6.9(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))) '@nx/js': 21.6.9(@babel/traverse@7.29.0)(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13))(nx@21.6.9(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.7.26(@swc/helpers@0.5.13)))(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0)) @@ -20730,10 +20886,10 @@ snapshots: storybook: 9.1.9(@testing-library/dom@10.4.0)(prettier@3.8.1)(vite@6.2.1(@types/node@22.6.2)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0)) ts-dedent: 2.2.0 - '@storybook/angular@9.1.9(r2qazale777rhog6l2r7ymu4pe)': + '@storybook/angular@9.1.9(3d319b7f62ef06596b4c03e303fe81b6)': dependencies: '@angular-devkit/architect': 0.2003.17(chokidar@4.0.3) - '@angular-devkit/build-angular': 20.3.9(qbhy5dmjbsl4w6j7j6ftomgipa) + '@angular-devkit/build-angular': 20.3.9(05522fc0219b75916c8fe432bbfaf4ae) '@angular-devkit/core': 20.3.9(chokidar@4.0.3) '@angular/common': 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/compiler': 20.3.17 @@ -21093,7 +21249,7 @@ snapshots: '@tanstack/table-core@8.21.3': {} - '@testing-library/angular@17.4.0(qyqphfusurop7kdnya6mcaudbe)': + '@testing-library/angular@17.4.0(2f13c451bd488c6007c8d02d67541f56)': dependencies: '@angular/animations': 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/common': 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -21886,7 +22042,7 @@ snapshots: dependencies: vite: 7.1.11(@types/node@22.6.2)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.0) - '@vitest/coverage-v8@2.1.1(vitest@2.1.1(@types/node@22.6.2)(@vitest/ui@2.1.1)(jsdom@25.0.1)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.43.1))': + '@vitest/coverage-v8@2.1.1(vitest@2.1.1)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -22567,6 +22723,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@30.2.0(@babel/core@7.28.3): + dependencies: + '@babel/core': 7.28.3 + '@jest/transform': 30.2.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.2.0(@babel/core@7.28.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + optional: true + babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.101.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.9)): dependencies: '@babel/core': 7.28.3 @@ -22719,6 +22889,26 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.7) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.7) + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.3): + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.3) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.3) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.3) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.3) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.3) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.3) + optional: true + babel-preset-jest@29.6.3(@babel/core@7.27.7): dependencies: '@babel/core': 7.27.7 @@ -22731,6 +22921,13 @@ snapshots: babel-plugin-jest-hoist: 30.2.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.27.7) + babel-preset-jest@30.2.0(@babel/core@7.28.3): + dependencies: + '@babel/core': 7.28.3 + babel-plugin-jest-hoist: 30.2.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) + optional: true + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -26533,7 +26730,7 @@ snapshots: optionalDependencies: jest-resolve: 30.2.0 - jest-preset-angular@14.6.2(5uw4eeksyhrldc6e5vofrefjye): + jest-preset-angular@14.6.2(5853600170e1893ed3b46065385bd77c): dependencies: '@angular/compiler-cli': 20.3.17(@angular/compiler@20.3.17)(typescript@5.9.3) '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) @@ -26544,7 +26741,7 @@ snapshots: jest-environment-jsdom: 29.7.0 jest-util: 29.7.0 pretty-format: 29.7.0 - ts-jest: 29.4.6(@babel/core@7.27.7)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.27.7))(esbuild@0.24.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.6.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.6.2)(typescript@5.9.3)))(typescript@5.9.3) + ts-jest: 29.4.6(@babel/core@7.28.3)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.3))(esbuild@0.24.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.6.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.6.2)(typescript@5.9.3)))(typescript@5.9.3) typescript: 5.9.3 optionalDependencies: esbuild: 0.24.2 @@ -27863,12 +28060,6 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - ngx-sonner@3.1.0(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0)): - dependencies: - '@angular/common': 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0) - tslib: 2.8.1 - ngxtension@7.0.2(@angular/common@20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2): dependencies: '@angular/common': 20.3.17(@angular/core@20.3.17(@angular/compiler@20.3.17)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -30756,7 +30947,7 @@ snapshots: ts-dedent@2.2.0: {} - ts-jest@29.4.6(@babel/core@7.27.7)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.27.7))(esbuild@0.24.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.6.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.6.2)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.3))(esbuild@0.24.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.6.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.6.2)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -30770,10 +30961,10 @@ snapshots: typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.27.7 + '@babel/core': 7.28.3 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.27.7) + babel-jest: 30.2.0(@babel/core@7.28.3) esbuild: 0.24.2 jest-util: 29.7.0 diff --git a/tsconfig.base.json b/tsconfig.base.json index ce41e1b835..8f7b36f0b0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -64,6 +64,7 @@ "@spartan-ng/brain/separator": ["libs/brain/separator/src/index.ts"], "@spartan-ng/brain/sheet": ["libs/brain/sheet/src/index.ts"], "@spartan-ng/brain/slider": ["libs/brain/slider/src/index.ts"], + "@spartan-ng/brain/sonner": ["libs/brain/sonner/src/index.ts"], "@spartan-ng/brain/switch": ["libs/brain/switch/src/index.ts"], "@spartan-ng/brain/tabs": ["libs/brain/tabs/src/index.ts"], "@spartan-ng/brain/toggle": ["libs/brain/toggle/src/index.ts"],