@lifecorner/angular-ui-kit 3.18.0 → 3.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/lifecorner-angular-ui-kit-drawer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-drawer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-menu.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-menu.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs +31 -8
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-toast.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-toast.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/theme-contrast.spec.ts +69 -0
- package/types/lifecorner-angular-ui-kit-table.d.ts +18 -2
|
@@ -172,7 +172,7 @@ var DrawerComponent = class DrawerComponent {
|
|
|
172
172
|
outputs: { closed: "closed" },
|
|
173
173
|
host: { listeners: { "document:keydown": "onKeydown($event)" } },
|
|
174
174
|
ngImport: i0,
|
|
175
|
-
template: "@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n",
|
|
175
|
+
template: "@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n",
|
|
176
176
|
styles: ["@charset \"UTF-8\";.lc-drawer{position:fixed;inset:0;z-index:1100;pointer-events:none}.lc-drawer__overlay{position:absolute;inset:0;background:#0000;transition:background .25s ease;pointer-events:auto;cursor:pointer}.lc-drawer__overlay--visible{background:#00000073}.lc-drawer__panel{position:absolute;top:0;bottom:0;display:flex;flex-direction:column;background-color:var(--color-surface);background-image:var(--gradient-surface);box-shadow:var(--elevation-4);pointer-events:auto;transition:transform .25s cubic-bezier(.4,0,.2,1);max-width:90vw;overflow:hidden}.lc-drawer__panel--right{right:0;transform:translate(100%);border-left:1px solid var(--color-border-strong)}.lc-drawer__panel--left{left:0;transform:translate(-100%);border-right:1px solid var(--color-border-strong)}.lc-drawer__panel--open{transform:translate(0)}.lc-drawer__header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--color-divider);flex-shrink:0}.lc-drawer__heading{margin:0;font-size:1rem;font-weight:600;color:var(--color-text-primary);letter-spacing:-.01em}.lc-drawer__close{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;background:none;border:none;border-radius:var(--border-radius-md, .375rem);color:var(--color-text-tertiary);cursor:pointer;transition:background .15s,color .15s}.lc-drawer__close:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-drawer__close:focus-visible{outline:2px solid var(--color-primary-500, #208497);outline-offset:2px}.lc-drawer__close--floating{position:absolute;top:.75rem;right:.75rem;z-index:1}.lc-drawer__body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch}@media(max-width:640px){.lc-drawer__panel{max-width:100vw;width:100vw!important}}\n"],
|
|
177
177
|
dependencies: [
|
|
178
178
|
{
|
|
@@ -218,7 +218,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
218
218
|
imports: [A11yModule, IconComponent],
|
|
219
219
|
viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],
|
|
220
220
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
221
|
-
template: "@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n",
|
|
221
|
+
template: "@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n",
|
|
222
222
|
styles: ["@charset \"UTF-8\";.lc-drawer{position:fixed;inset:0;z-index:1100;pointer-events:none}.lc-drawer__overlay{position:absolute;inset:0;background:#0000;transition:background .25s ease;pointer-events:auto;cursor:pointer}.lc-drawer__overlay--visible{background:#00000073}.lc-drawer__panel{position:absolute;top:0;bottom:0;display:flex;flex-direction:column;background-color:var(--color-surface);background-image:var(--gradient-surface);box-shadow:var(--elevation-4);pointer-events:auto;transition:transform .25s cubic-bezier(.4,0,.2,1);max-width:90vw;overflow:hidden}.lc-drawer__panel--right{right:0;transform:translate(100%);border-left:1px solid var(--color-border-strong)}.lc-drawer__panel--left{left:0;transform:translate(-100%);border-right:1px solid var(--color-border-strong)}.lc-drawer__panel--open{transform:translate(0)}.lc-drawer__header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--color-divider);flex-shrink:0}.lc-drawer__heading{margin:0;font-size:1rem;font-weight:600;color:var(--color-text-primary);letter-spacing:-.01em}.lc-drawer__close{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;background:none;border:none;border-radius:var(--border-radius-md, .375rem);color:var(--color-text-tertiary);cursor:pointer;transition:background .15s,color .15s}.lc-drawer__close:hover{background:var(--color-surface-hover);color:var(--color-text-primary)}.lc-drawer__close:focus-visible{outline:2px solid var(--color-primary-500, #208497);outline-offset:2px}.lc-drawer__close--floating{position:absolute;top:.75rem;right:.75rem;z-index:1}.lc-drawer__body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch}@media(max-width:640px){.lc-drawer__panel{max-width:100vw;width:100vw!important}}\n"]
|
|
223
223
|
}]
|
|
224
224
|
}],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-drawer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.ts","../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n untracked,\n inject,\n HostListener,\n OnDestroy,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type DrawerPosition = 'left' | 'right';\nexport type DrawerSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Duration of the slide-out transition (see drawer.component.scss). */\nconst CLOSE_ANIMATION_MS = 250;\n\n/**\n * Drawer component for slide-out overlay panels.\n *\n * Features:\n * - Slide-in from left or right\n * - Size presets (sm, md, lg, xl)\n * - Optional backdrop overlay with click-to-close\n * - Close on Escape key support\n * - Heading text display\n * - Content projection for arbitrary body content\n * - Accessible with ARIA dialog role, focus trap and focus restore\n *\n * @example\n * ```html\n * <lc-drawer [open]=\"showPanel\" position=\"right\" size=\"md\"\n * heading=\"Settings\" (closed)=\"showPanel = false\">\n * <p>Any content goes here</p>\n * </lc-drawer>\n * ```\n */\n@Component({\n selector: 'lc-drawer',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './drawer.component.html',\n styleUrl: './drawer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DrawerComponent implements OnDestroy {\n private static nextId = 0;\n\n /** Whether the drawer is open. */\n readonly open = input<boolean>(false);\n\n /** Slide-in direction. */\n readonly position = input<DrawerPosition>('right');\n\n /** Predefined width (sm=320, md=400, lg=520, xl=640). */\n readonly size = input<DrawerSize>('md');\n\n /** Optional heading shown in the drawer header. */\n readonly heading = input<string>('');\n\n /** Whether the overlay backdrop is shown. */\n readonly hasOverlay = input<boolean>(true);\n\n /** Whether clicking the overlay closes the drawer. */\n readonly closeOnOverlayClick = input<boolean>(true);\n\n /** Whether pressing Escape closes the drawer. */\n readonly closeOnEscape = input<boolean>(true);\n\n /** ARIA label for assistive technology. */\n readonly ariaLabel = input<string>('');\n\n /** Accessible name of the close button. */\n readonly closeAriaLabel = input('Close drawer');\n\n /** Emitted when the drawer should close. */\n readonly closed = output<void>();\n\n /** Internal visibility — stays true during the slide-out animation. */\n protected _visible = signal(false);\n\n /** Width token → CSS. */\n protected readonly widthValue = computed(() => {\n const map: Record<DrawerSize, string> = {\n sm: '320px',\n md: '400px',\n lg: '520px',\n xl: '640px',\n };\n return map[this.size()];\n });\n\n protected readonly panelClasses = computed(() => {\n const cls = ['lc-drawer__panel', `lc-drawer__panel--${this.position()}`];\n if (this.open()) cls.push('lc-drawer__panel--open');\n return cls.join(' ');\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly drawerId = `lc-drawer-${++DrawerComponent.nextId}`;\n private closeTimer?: ReturnType<typeof setTimeout>;\n private originalOverflow?: string;\n\n constructor() {\n effect(() => {\n const isOpen = this.open();\n untracked(() => {\n if (isOpen) {\n // Re-opening within the close animation must cancel the pending hide,\n // otherwise the stale timer hides the drawer and unlocks scroll while\n // `open()` is still true.\n this.clearCloseTimer();\n this._visible.set(true);\n this.lockScroll();\n this.overlayStack.push(this.drawerId);\n } else if (this._visible()) {\n this.overlayStack.remove(this.drawerId);\n // Keep the panel mounted until the slide-out transition has finished\n this.closeTimer = setTimeout(() => {\n this.closeTimer = undefined;\n this._visible.set(false);\n this.unlockScroll();\n }, CLOSE_ANIMATION_MS);\n }\n });\n });\n }\n\n ngOnDestroy(): void {\n // Navigating away while open must not leave the page scroll-locked\n this.clearCloseTimer();\n this.overlayStack.remove(this.drawerId);\n this.unlockScroll();\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (menu, popover, …)\n * sits above this drawer — the click belongs to that layer.\n */\n protected onOverlayClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n if (this.closeOnOverlayClick()) {\n this.close();\n }\n }\n\n /** Handle close action. */\n protected close(): void {\n this.closed.emit();\n }\n\n /**\n * Escape closes the drawer only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this.open()) return;\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.close();\n }\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow !== undefined) return;\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow === undefined) return;\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n private clearCloseTimer(): void {\n if (this.closeTimer !== undefined) {\n clearTimeout(this.closeTimer);\n this.closeTimer = undefined;\n }\n }\n}\n","@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-drawer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.ts","../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n untracked,\n inject,\n HostListener,\n OnDestroy,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type DrawerPosition = 'left' | 'right';\nexport type DrawerSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Duration of the slide-out transition (see drawer.component.scss). */\nconst CLOSE_ANIMATION_MS = 250;\n\n/**\n * Drawer component for slide-out overlay panels.\n *\n * Features:\n * - Slide-in from left or right\n * - Size presets (sm, md, lg, xl)\n * - Optional backdrop overlay with click-to-close\n * - Close on Escape key support\n * - Heading text display\n * - Content projection for arbitrary body content\n * - Accessible with ARIA dialog role, focus trap and focus restore\n *\n * @example\n * ```html\n * <lc-drawer [open]=\"showPanel\" position=\"right\" size=\"md\"\n * heading=\"Settings\" (closed)=\"showPanel = false\">\n * <p>Any content goes here</p>\n * </lc-drawer>\n * ```\n */\n@Component({\n selector: 'lc-drawer',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './drawer.component.html',\n styleUrl: './drawer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DrawerComponent implements OnDestroy {\n private static nextId = 0;\n\n /** Whether the drawer is open. */\n readonly open = input<boolean>(false);\n\n /** Slide-in direction. */\n readonly position = input<DrawerPosition>('right');\n\n /** Predefined width (sm=320, md=400, lg=520, xl=640). */\n readonly size = input<DrawerSize>('md');\n\n /** Optional heading shown in the drawer header. */\n readonly heading = input<string>('');\n\n /** Whether the overlay backdrop is shown. */\n readonly hasOverlay = input<boolean>(true);\n\n /** Whether clicking the overlay closes the drawer. */\n readonly closeOnOverlayClick = input<boolean>(true);\n\n /** Whether pressing Escape closes the drawer. */\n readonly closeOnEscape = input<boolean>(true);\n\n /** ARIA label for assistive technology. */\n readonly ariaLabel = input<string>('');\n\n /** Accessible name of the close button. */\n readonly closeAriaLabel = input('Close drawer');\n\n /** Emitted when the drawer should close. */\n readonly closed = output<void>();\n\n /** Internal visibility — stays true during the slide-out animation. */\n protected _visible = signal(false);\n\n /** Width token → CSS. */\n protected readonly widthValue = computed(() => {\n const map: Record<DrawerSize, string> = {\n sm: '320px',\n md: '400px',\n lg: '520px',\n xl: '640px',\n };\n return map[this.size()];\n });\n\n protected readonly panelClasses = computed(() => {\n const cls = ['lc-drawer__panel', `lc-drawer__panel--${this.position()}`];\n if (this.open()) cls.push('lc-drawer__panel--open');\n return cls.join(' ');\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly drawerId = `lc-drawer-${++DrawerComponent.nextId}`;\n private closeTimer?: ReturnType<typeof setTimeout>;\n private originalOverflow?: string;\n\n constructor() {\n effect(() => {\n const isOpen = this.open();\n untracked(() => {\n if (isOpen) {\n // Re-opening within the close animation must cancel the pending hide,\n // otherwise the stale timer hides the drawer and unlocks scroll while\n // `open()` is still true.\n this.clearCloseTimer();\n this._visible.set(true);\n this.lockScroll();\n this.overlayStack.push(this.drawerId);\n } else if (this._visible()) {\n this.overlayStack.remove(this.drawerId);\n // Keep the panel mounted until the slide-out transition has finished\n this.closeTimer = setTimeout(() => {\n this.closeTimer = undefined;\n this._visible.set(false);\n this.unlockScroll();\n }, CLOSE_ANIMATION_MS);\n }\n });\n });\n }\n\n ngOnDestroy(): void {\n // Navigating away while open must not leave the page scroll-locked\n this.clearCloseTimer();\n this.overlayStack.remove(this.drawerId);\n this.unlockScroll();\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (menu, popover, …)\n * sits above this drawer — the click belongs to that layer.\n */\n protected onOverlayClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n if (this.closeOnOverlayClick()) {\n this.close();\n }\n }\n\n /** Handle close action. */\n protected close(): void {\n this.closed.emit();\n }\n\n /**\n * Escape closes the drawer only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this.open()) return;\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.close();\n }\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow !== undefined) return;\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow === undefined) return;\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n private clearCloseTimer(): void {\n if (this.closeTimer !== undefined) {\n clearTimeout(this.closeTimer);\n this.closeTimer = undefined;\n }\n }\n}\n","@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n"],"mappings":";;;;;;;AAsBA,MAAM,qBAAqB;AA+B3B,IAAa,kBAAb,MAAa,gBAAe;CAClB,OAAO,SAAS;CAGf,OAAO,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAG5B,WAAW,MAAsB,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGzC,OAAO,MAAkB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAG9B,UAAU,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAG3B,aAAa,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,sBAAsB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,sBAAA,CAAA,+BAAA,CAAA,CAAA;CAG1C,gBAAgB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAGpC,YAAY,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG7B,iBAAiB,MAAM,gBAAe,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAGtC,SAAS,OAAM;CAGd,WAAW,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGf,aAAa,eAAc;EAO5C,OAAO;GALL,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EAEC,EAAI,KAAK,KAAI;CACtB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,eAAe,eAAc;EAC9C,MAAM,MAAM,CAAC,oBAAoB,qBAAqB,KAAK,SAAQ,GAAI;EACvE,IAAI,KAAK,KAAI,GAAI,IAAI,KAAK,wBAAwB;EAClD,OAAO,IAAI,KAAK,GAAG;CACrB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEe,eAAe,OAAO,mBAAmB;CAEzC,WAAW,aAAa,EAAE,gBAAgB;CACnD;CACA;CAER,cAAA;EACE,aAAY;GACV,MAAM,SAAS,KAAK,KAAI;GACxB,gBAAe;IACb,IAAI,QAAQ;KAIV,KAAK,gBAAe;KACpB,KAAK,SAAS,IAAI,IAAI;KACtB,KAAK,WAAU;KACf,KAAK,aAAa,KAAK,KAAK,QAAQ;IACtC,OAAO,IAAI,KAAK,SAAQ,GAAI;KAC1B,KAAK,aAAa,OAAO,KAAK,QAAQ;KAEtC,KAAK,aAAa,iBAAgB;MAChC,KAAK,aAAa;MAClB,KAAK,SAAS,IAAI,KAAK;MACvB,KAAK,aAAY;KACnB,GAAG,kBAAkB;IACvB;GACF,CAAC;EACH,CAAC;CACH;CAEA,cAAW;EAET,KAAK,gBAAe;EACpB,KAAK,aAAa,OAAO,KAAK,QAAQ;EACtC,KAAK,aAAY;CACnB;CAMU,eAAe,OAAiB;EACxC,IAAI,CAAC,KAAK,aAAa,MAAM,KAAK,UAAU,KAAK,GAAG;EACpD,IAAI,KAAK,oBAAmB,GAC1B,KAAK,MAAK;CAEd;CAGU,QAAK;EACb,KAAK,OAAO,KAAI;CAClB;CAOU,UAAU,OAAoB;EACtC,IAAI,MAAM,QAAQ,YAAY,CAAC,KAAK,KAAI,GAAI;EAC5C,IAAI,CAAC,KAAK,aAAa,MAAM,KAAK,UAAU,KAAK,GAAG;EACpD,MAAM,gBAAe;EACrB,IAAI,KAAK,cAAa,GACpB,KAAK,MAAK;CAEd;CAEQ,aAAU;EAChB,IAAI,OAAO,aAAa,eAAe,KAAK,qBAAqB,QAAW;EAC5E,KAAK,mBAAmB,SAAS,KAAK,MAAM;EAC5C,SAAS,KAAK,MAAM,WAAW;CACjC;CAEQ,eAAY;EAClB,IAAI,OAAO,aAAa,eAAe,KAAK,qBAAqB,QAAW;EAC5E,SAAS,KAAK,MAAM,WAAW,KAAK;EACpC,KAAK,mBAAmB;CAC1B;CAEQ,kBAAe;EACrB,IAAI,KAAK,eAAe,QAAW;GACjC,aAAa,KAAK,UAAU;GAC5B,KAAK,aAAa;EACpB;CACF;CA1IW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAe,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAf,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAe,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,qBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA,EAAA,QAAA,SAAA;EAAA,MAAA,EAAA,WAAA,EAAA,oBAAA,oBAAA,EAAA;EAAA,UAAA;EAAA,UCrD5B;EAuDA,QAAA,CAAA,ixDAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MDRY;GAAU;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,gBAAA,yBAAA;IAAA,UAAA,CAAA,cAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAa,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;EAAA,eACpB,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;EAAC,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAKrC,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAe,YAAA,CAAA;EAT3B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAW,YACT;GAAI,SACP,CAAC,YAAY,aAAa;GAAC,eACrB,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;GAAC,iBAG/B,wBAAwB;GAAM,UAAA;GAAA,QAAA,CAAA,ixDAAA;EAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiH9C,MAAA;GAAa,MAAA,CAAA,oBAAoB,CAAC,QAAQ,CAAC"}
|
|
@@ -210,7 +210,7 @@ var MenuComponent = class MenuComponent {
|
|
|
210
210
|
}],
|
|
211
211
|
ngImport: i0,
|
|
212
212
|
template: "<div class=\"lc-menu\">\n <!-- Trigger slot -->\n <div class=\"lc-menu__trigger\">\n <ng-content select=\"[trigger]\" />\n </div>\n\n <!-- Dropdown menu -->\n @if (isOpen()) {\n <div\n class=\"lc-menu__dropdown\"\n [class.lc-menu__dropdown--bottom-left]=\"position() === 'bottom-left'\"\n [class.lc-menu__dropdown--bottom-right]=\"position() === 'bottom-right'\"\n [class.lc-menu__dropdown--top-left]=\"position() === 'top-left'\"\n [class.lc-menu__dropdown--top-right]=\"position() === 'top-right'\"\n [class.lc-menu__dropdown--sm]=\"size() === 'sm'\"\n [class.lc-menu__dropdown--lg]=\"size() === 'lg'\"\n [style.min-width]=\"minWidth()\"\n [style.max-width]=\"maxWidth() || null\"\n [class.lc-menu__dropdown--capped]=\"!!maxWidth()\"\n >\n <!-- Optional header slot (outside the menu role: it is not an item) -->\n <ng-content select=\"[header]\" />\n\n <!-- Menu items -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -- keydown bubbles from the focused menu items (roving tabindex) -->\n <div\n class=\"lc-menu__items\"\n role=\"menu\"\n [id]=\"panelId\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n @for (item of items(); track item.id; let i = $index) {\n @if (item.href) {\n <!-- Link item -->\n <a\n #menuItem\n role=\"menuitem\"\n [href]=\"item.href\"\n [target]=\"item.target || '_self'\"\n [attr.rel]=\"item.target === '_blank' ? 'noopener noreferrer' : null\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </a>\n } @else {\n <!-- Button item -->\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </button>\n }\n\n @if (item.dividerAfter) {\n <hr class=\"lc-menu__divider\" />\n }\n }\n </div>\n\n <!-- Optional footer slot -->\n <ng-content select=\"[footer]\" />\n </div>\n }\n</div>\n",
|
|
213
|
-
styles: [".lc-menu{position:relative;display:inline-block}.lc-menu__trigger{display:inline-block}.lc-menu__dropdown{position:absolute;background-color:var(--color-surface);background-image:var(--gradient-surface);border:1px solid var(--color-border-strong);border-radius:var(--border-radius-lg, .5rem);box-shadow:var(--elevation-4);padding:var(--lc-density-padding-xs, var(--spacing-2)) 0;z-index:9999;animation:slideDown .15s ease-out}.lc-menu__dropdown--bottom-left{top:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--bottom-right{top:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--top-left{bottom:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--top-right{bottom:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--capped{width:max-content}.lc-menu__item{display:flex;align-items:center;gap:var(--spacing-3);width:100%;padding:var(--lc-density-padding-sm, var(--spacing-3)) var(--lc-density-padding-md, var(--spacing-4));background:transparent;border:none;text-align:left;text-decoration:none;color:var(--color-text-primary);font-size:var(--font-size-base);font-family:var(--font-family-base);cursor:pointer;transition:background-color .2s ease}.lc-menu__item:hover:not(.lc-menu__item--disabled){background-color:var(--color-surface-hover)}.lc-menu__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px;background-color:var(--color-surface-hover)}.lc-menu__item--danger{color:var(--color-error)}.lc-menu__item--danger:hover:not(.lc-menu__item--
|
|
213
|
+
styles: [".lc-menu{position:relative;display:inline-block}.lc-menu__trigger{display:inline-block}.lc-menu__dropdown{position:absolute;background-color:var(--color-surface);background-image:var(--gradient-surface);border:1px solid var(--color-border-strong);border-radius:var(--border-radius-lg, .5rem);box-shadow:var(--elevation-4);padding:var(--lc-density-padding-xs, var(--spacing-2)) 0;z-index:9999;animation:slideDown .15s ease-out}.lc-menu__dropdown--bottom-left{top:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--bottom-right{top:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--top-left{bottom:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--top-right{bottom:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--capped{width:max-content}.lc-menu__item{display:flex;align-items:center;gap:var(--spacing-3);width:100%;padding:var(--lc-density-padding-sm, var(--spacing-3)) var(--lc-density-padding-md, var(--spacing-4));background:transparent;border:none;text-align:left;text-decoration:none;color:var(--color-text-primary);font-size:var(--font-size-base);font-family:var(--font-family-base);cursor:pointer;transition:background-color .2s ease}.lc-menu__item:hover:not(.lc-menu__item--disabled){background-color:var(--color-surface-hover)}.lc-menu__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px;background-color:var(--color-surface-hover)}.lc-menu__item--danger{color:var(--color-text-error)}.lc-menu__item--danger:hover:not(.lc-menu__item--disabled){background-color:var(--color-error-subtle)}.lc-menu__item--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.lc-menu__item-content{display:flex;flex-direction:column;gap:var(--lc-density-gap-xs, var(--spacing-1));flex:1}.lc-menu__item-label{font-size:var(--font-size-base);font-weight:var(--font-weight-medium);line-height:var(--line-height-tight)}.lc-menu__item-subtitle{font-size:var(--font-size-sm);color:var(--color-text-secondary);line-height:var(--line-height-tight)}.lc-menu__item-badge{padding:var(--spacing-1) var(--spacing-2);background-color:var(--color-primary-100);color:var(--color-primary-700);font-size:var(--font-size-xs);font-weight:var(--font-weight-semibold);border-radius:var(--border-radius-full);line-height:1}.lc-menu__divider{margin:var(--spacing-2) 0;border:none;border-top:1px solid var(--color-divider)}.lc-menu__dropdown--sm{padding:var(--spacing-1, .25rem) 0}.lc-menu__dropdown--sm .lc-menu__item{gap:var(--lc-density-gap-sm, var(--spacing-2));padding:var(--lc-density-padding-xs, var(--spacing-2)) var(--lc-density-padding-sm, var(--spacing-3));font-size:var(--font-size-sm)}.lc-menu__dropdown--sm .lc-menu__item-label{font-size:var(--font-size-sm)}.lc-menu__dropdown--sm .lc-menu__item-subtitle{font-size:var(--font-size-xs)}.lc-menu__dropdown--sm .lc-menu__item-badge{font-size:.625rem;padding:1px var(--spacing-1)}.lc-menu__dropdown--sm .lc-menu__divider{margin:var(--spacing-1) 0}.lc-menu__dropdown--lg .lc-menu__item{gap:var(--lc-density-gap-md, var(--spacing-4));padding:var(--lc-density-padding-md, var(--spacing-4)) var(--spacing-5);font-size:var(--font-size-lg)}.lc-menu__dropdown--lg .lc-menu__item-label{font-size:var(--font-size-lg)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}\n"],
|
|
214
214
|
dependencies: [{
|
|
215
215
|
kind: "component",
|
|
216
216
|
type: IconComponent,
|
|
@@ -241,7 +241,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
241
241
|
imports: [IconComponent],
|
|
242
242
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
243
243
|
template: "<div class=\"lc-menu\">\n <!-- Trigger slot -->\n <div class=\"lc-menu__trigger\">\n <ng-content select=\"[trigger]\" />\n </div>\n\n <!-- Dropdown menu -->\n @if (isOpen()) {\n <div\n class=\"lc-menu__dropdown\"\n [class.lc-menu__dropdown--bottom-left]=\"position() === 'bottom-left'\"\n [class.lc-menu__dropdown--bottom-right]=\"position() === 'bottom-right'\"\n [class.lc-menu__dropdown--top-left]=\"position() === 'top-left'\"\n [class.lc-menu__dropdown--top-right]=\"position() === 'top-right'\"\n [class.lc-menu__dropdown--sm]=\"size() === 'sm'\"\n [class.lc-menu__dropdown--lg]=\"size() === 'lg'\"\n [style.min-width]=\"minWidth()\"\n [style.max-width]=\"maxWidth() || null\"\n [class.lc-menu__dropdown--capped]=\"!!maxWidth()\"\n >\n <!-- Optional header slot (outside the menu role: it is not an item) -->\n <ng-content select=\"[header]\" />\n\n <!-- Menu items -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -- keydown bubbles from the focused menu items (roving tabindex) -->\n <div\n class=\"lc-menu__items\"\n role=\"menu\"\n [id]=\"panelId\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n @for (item of items(); track item.id; let i = $index) {\n @if (item.href) {\n <!-- Link item -->\n <a\n #menuItem\n role=\"menuitem\"\n [href]=\"item.href\"\n [target]=\"item.target || '_self'\"\n [attr.rel]=\"item.target === '_blank' ? 'noopener noreferrer' : null\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </a>\n } @else {\n <!-- Button item -->\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </button>\n }\n\n @if (item.dividerAfter) {\n <hr class=\"lc-menu__divider\" />\n }\n }\n </div>\n\n <!-- Optional footer slot -->\n <ng-content select=\"[footer]\" />\n </div>\n }\n</div>\n",
|
|
244
|
-
styles: [".lc-menu{position:relative;display:inline-block}.lc-menu__trigger{display:inline-block}.lc-menu__dropdown{position:absolute;background-color:var(--color-surface);background-image:var(--gradient-surface);border:1px solid var(--color-border-strong);border-radius:var(--border-radius-lg, .5rem);box-shadow:var(--elevation-4);padding:var(--lc-density-padding-xs, var(--spacing-2)) 0;z-index:9999;animation:slideDown .15s ease-out}.lc-menu__dropdown--bottom-left{top:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--bottom-right{top:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--top-left{bottom:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--top-right{bottom:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--capped{width:max-content}.lc-menu__item{display:flex;align-items:center;gap:var(--spacing-3);width:100%;padding:var(--lc-density-padding-sm, var(--spacing-3)) var(--lc-density-padding-md, var(--spacing-4));background:transparent;border:none;text-align:left;text-decoration:none;color:var(--color-text-primary);font-size:var(--font-size-base);font-family:var(--font-family-base);cursor:pointer;transition:background-color .2s ease}.lc-menu__item:hover:not(.lc-menu__item--disabled){background-color:var(--color-surface-hover)}.lc-menu__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px;background-color:var(--color-surface-hover)}.lc-menu__item--danger{color:var(--color-error)}.lc-menu__item--danger:hover:not(.lc-menu__item--
|
|
244
|
+
styles: [".lc-menu{position:relative;display:inline-block}.lc-menu__trigger{display:inline-block}.lc-menu__dropdown{position:absolute;background-color:var(--color-surface);background-image:var(--gradient-surface);border:1px solid var(--color-border-strong);border-radius:var(--border-radius-lg, .5rem);box-shadow:var(--elevation-4);padding:var(--lc-density-padding-xs, var(--spacing-2)) 0;z-index:9999;animation:slideDown .15s ease-out}.lc-menu__dropdown--bottom-left{top:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--bottom-right{top:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--top-left{bottom:calc(100% + var(--spacing-2));left:0}.lc-menu__dropdown--top-right{bottom:calc(100% + var(--spacing-2));right:0}.lc-menu__dropdown--capped{width:max-content}.lc-menu__item{display:flex;align-items:center;gap:var(--spacing-3);width:100%;padding:var(--lc-density-padding-sm, var(--spacing-3)) var(--lc-density-padding-md, var(--spacing-4));background:transparent;border:none;text-align:left;text-decoration:none;color:var(--color-text-primary);font-size:var(--font-size-base);font-family:var(--font-family-base);cursor:pointer;transition:background-color .2s ease}.lc-menu__item:hover:not(.lc-menu__item--disabled){background-color:var(--color-surface-hover)}.lc-menu__item:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-2px;background-color:var(--color-surface-hover)}.lc-menu__item--danger{color:var(--color-text-error)}.lc-menu__item--danger:hover:not(.lc-menu__item--disabled){background-color:var(--color-error-subtle)}.lc-menu__item--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.lc-menu__item-content{display:flex;flex-direction:column;gap:var(--lc-density-gap-xs, var(--spacing-1));flex:1}.lc-menu__item-label{font-size:var(--font-size-base);font-weight:var(--font-weight-medium);line-height:var(--line-height-tight)}.lc-menu__item-subtitle{font-size:var(--font-size-sm);color:var(--color-text-secondary);line-height:var(--line-height-tight)}.lc-menu__item-badge{padding:var(--spacing-1) var(--spacing-2);background-color:var(--color-primary-100);color:var(--color-primary-700);font-size:var(--font-size-xs);font-weight:var(--font-weight-semibold);border-radius:var(--border-radius-full);line-height:1}.lc-menu__divider{margin:var(--spacing-2) 0;border:none;border-top:1px solid var(--color-divider)}.lc-menu__dropdown--sm{padding:var(--spacing-1, .25rem) 0}.lc-menu__dropdown--sm .lc-menu__item{gap:var(--lc-density-gap-sm, var(--spacing-2));padding:var(--lc-density-padding-xs, var(--spacing-2)) var(--lc-density-padding-sm, var(--spacing-3));font-size:var(--font-size-sm)}.lc-menu__dropdown--sm .lc-menu__item-label{font-size:var(--font-size-sm)}.lc-menu__dropdown--sm .lc-menu__item-subtitle{font-size:var(--font-size-xs)}.lc-menu__dropdown--sm .lc-menu__item-badge{font-size:.625rem;padding:1px var(--spacing-1)}.lc-menu__dropdown--sm .lc-menu__divider{margin:var(--spacing-1) 0}.lc-menu__dropdown--lg .lc-menu__item{gap:var(--lc-density-gap-md, var(--spacing-4));padding:var(--lc-density-padding-md, var(--spacing-4)) var(--spacing-5);font-size:var(--font-size-lg)}.lc-menu__dropdown--lg .lc-menu__item-label{font-size:var(--font-size-lg)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}\n"]
|
|
245
245
|
}]
|
|
246
246
|
}],
|
|
247
247
|
ctorParameters: () => [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-menu.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/menu/menu.component.ts","../../../../libs/angular-ui-kit/src/lib/menu/menu.component.html"],"sourcesContent":["import {\n Component,\n input,\n output,\n signal,\n effect,\n untracked,\n afterNextRender,\n afterRenderEffect,\n viewChildren,\n ChangeDetectionStrategy,\n HostListener,\n ElementRef,\n Injector,\n Renderer2,\n OnDestroy,\n inject,\n} from '@angular/core';\nimport { IconComponent } from '@lifecorner/angular-ui-kit/icon';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport interface MenuItem {\n id: string;\n label: string;\n icon?: string;\n iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n href?: string;\n target?: '_blank' | '_self' | '_parent' | '_top';\n variant?: 'default' | 'danger';\n dividerAfter?: boolean;\n disabled?: boolean;\n metadata?: {\n subtitle?: string;\n badge?: string;\n };\n}\n\n/** Natively focusable elements a projected trigger may be or contain. */\nconst INTERACTIVE_SELECTOR = 'button, a[href], input, select, textarea, [tabindex]';\n\n/**\n * Menu Component - Dropdown menu for navigation and actions\n *\n * Features:\n * - Customizable menu items with icons\n * - Support for links and buttons\n * - Dividers between menu sections\n * - Optional subtitle/metadata for items\n * - Danger variant for destructive actions\n * - Click outside to close (top-most overlay only)\n * - Keyboard navigation: focus moves into the menu on open, Arrow keys /\n * Home / End move between items (roving tabindex), Enter / Space activate,\n * Escape closes and returns focus to the trigger, Tab closes\n * - ARIA menu semantics (`aria-haspopup` / `aria-expanded` / `aria-controls`\n * on the projected trigger, `role=\"menu\"` / `role=\"menuitem\"` on the panel)\n * - OnPush change detection for performance\n *\n * @example\n * ```html\n * <lc-menu\n * [items]=\"menuItems\"\n * [isOpen]=\"isMenuOpen\"\n * (itemClick)=\"handleMenuClick($event)\"\n * (closed)=\"isMenuOpen = false\"\n * >\n * <button trigger>Open Menu</button>\n * <div header>User Profile</div>\n * </lc-menu>\n * ```\n */\n@Component({\n selector: 'lc-menu',\n standalone: true,\n imports: [IconComponent],\n templateUrl: './menu.component.html',\n styleUrl: './menu.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class MenuComponent implements OnDestroy {\n private static nextId = 0;\n /** Identifies this instance in the overlay stack. */\n private readonly menuId = `lc-menu-${++MenuComponent.nextId}`;\n\n readonly items = input<MenuItem[]>([]);\n readonly isOpen = input(false);\n readonly position = input<'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'>('bottom-right');\n readonly size = input<'sm' | 'md' | 'lg'>('md');\n readonly minWidth = input('220px');\n /**\n * Maximum width of the panel (any CSS width, e.g. `min(18rem, calc(100vw - 2rem))`).\n * Empty leaves it unbounded. Long item labels and header lines wrap within it.\n */\n readonly maxWidth = input('');\n\n readonly itemClick = output<MenuItem>();\n readonly closed = output<void>();\n\n /** Id of the `role=\"menu\"` element, referenced by the trigger's `aria-controls`. */\n protected readonly panelId = `${this.menuId}-panel`;\n\n /** Index (into `items()`) of the item that currently owns the roving tabindex. */\n protected readonly activeIndex = signal(-1);\n\n private readonly itemElements = viewChildren<ElementRef<HTMLElement>>('menuItem');\n private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly renderer = inject(Renderer2);\n private readonly injector = inject(Injector);\n private readonly overlayStack = inject(OverlayStackService);\n\n constructor() {\n // Whether the menu has been through its first open-state evaluation; a menu\n // that is already open when it renders (demo pages) must not steal focus.\n let initialized = false;\n effect(() => {\n const open = this.isOpen();\n untracked(() => {\n if (open) {\n this.overlayStack.push(this.menuId);\n this.activeIndex.set(this.firstEnabledIndex());\n // Move focus into the menu once the panel has rendered\n if (initialized) {\n afterNextRender(() => this.focusActiveItem(), { injector: this.injector });\n }\n } else {\n this.overlayStack.remove(this.menuId);\n this.restoreFocus();\n this.activeIndex.set(-1);\n }\n initialized = true;\n });\n });\n\n // Mirror the open state onto the projected trigger so assistive tech knows\n // it opens a menu (the trigger is caller-supplied, so we set the\n // attributes rather than bind them).\n afterRenderEffect(() => {\n const open = this.isOpen();\n const trigger = this.triggerElement();\n if (!trigger) return;\n this.renderer.setAttribute(trigger, 'aria-haspopup', 'menu');\n this.renderer.setAttribute(trigger, 'aria-expanded', String(open));\n if (open) {\n this.renderer.setAttribute(trigger, 'aria-controls', this.panelId);\n } else {\n this.renderer.removeAttribute(trigger, 'aria-controls');\n }\n });\n }\n\n ngOnDestroy(): void {\n this.overlayStack.remove(this.menuId);\n }\n\n /**\n * Handle menu item click\n */\n onItemClick(item: MenuItem, event: Event): void {\n if (item.disabled) {\n event.preventDefault();\n return;\n }\n\n // Links navigate natively; button items only emit\n if (!item.href) {\n event.preventDefault();\n }\n\n this.itemClick.emit(item);\n }\n\n /**\n * Close the menu\n */\n close(): void {\n this.closed.emit();\n }\n\n /**\n * Handle click outside to close menu — only while the menu is the top-most\n * overlay, so a click inside a layer above it (a modal opened from a menu\n * item) is left alone.\n */\n @HostListener('document:click', ['$event'])\n onDocumentClick(event: MouseEvent): void {\n if (!this.isOpen()) {\n return;\n }\n\n const target = event.target;\n if (target instanceof Node && this.elementRef.nativeElement.contains(target)) {\n return;\n }\n if (!this.overlayStack.claim(this.menuId, event)) return;\n this.close();\n }\n\n /**\n * Escape closes the menu (top-most overlay only) and returns focus to the\n * trigger; the event is consumed so overlays underneath stay open.\n */\n @HostListener('document:keydown.escape', ['$event'])\n onEscapeKey(event: Event): void {\n if (!this.isOpen()) return;\n if (!this.overlayStack.claim(this.menuId, event)) return;\n event.stopPropagation();\n this.triggerElement()?.focus();\n this.close();\n }\n\n /** Keyboard navigation inside the `role=\"menu\"` panel. */\n protected onPanelKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n this.focusIndex(this.nextEnabledIndex(this.activeIndex(), 1));\n break;\n case 'ArrowUp':\n event.preventDefault();\n this.focusIndex(this.nextEnabledIndex(this.activeIndex(), -1));\n break;\n case 'Home':\n event.preventDefault();\n this.focusIndex(this.firstEnabledIndex());\n break;\n case 'End':\n event.preventDefault();\n this.focusIndex(this.nextEnabledIndex(0, -1));\n break;\n case ' ': {\n // Buttons activate on Space natively, links only on Enter\n const target = event.target as HTMLElement | null;\n if (target?.matches('a[role=\"menuitem\"]')) {\n event.preventDefault();\n target.click();\n }\n break;\n }\n case 'Tab':\n // Focus leaves the menu — let it move on naturally, but close the menu\n this.close();\n break;\n }\n }\n\n /** Roving tabindex: only the active item is in the tab sequence. */\n protected tabindexFor(index: number, item: MenuItem): number {\n return !item.disabled && index === this.activeIndex() ? 0 : -1;\n }\n\n private focusIndex(index: number): void {\n if (index < 0) return;\n this.activeIndex.set(index);\n this.focusActiveItem();\n }\n\n private focusActiveItem(): void {\n this.itemElements()[this.activeIndex()]?.nativeElement.focus();\n }\n\n private firstEnabledIndex(): number {\n return this.items().findIndex((item) => !item.disabled);\n }\n\n /** Index of the next enabled item in `direction`, wrapping around; -1 if none. */\n private nextEnabledIndex(from: number, direction: 1 | -1): number {\n const items = this.items();\n for (let step = 1; step <= items.length; step++) {\n const index = (from + direction * step + items.length) % items.length;\n if (!items[index]?.disabled) return index;\n }\n return -1;\n }\n\n /**\n * The element that carries the menu-button ARIA attributes and receives\n * focus back: the projected `[trigger]` node itself when it is natively\n * interactive, otherwise the first interactive element inside it (e.g. the\n * `<button>` inside an `<lc-button trigger>`).\n */\n private triggerElement(): HTMLElement | null {\n const slot = this.elementRef.nativeElement.querySelector<HTMLElement>('.lc-menu__trigger [trigger]');\n if (!slot) return null;\n return slot.matches(INTERACTIVE_SELECTOR)\n ? slot\n : (slot.querySelector<HTMLElement>(INTERACTIVE_SELECTOR) ?? slot);\n }\n\n /**\n * Closing removes the panel from the DOM; if focus is inside it (an item was\n * activated by keyboard) it would silently drop to `<body>`, so send it back\n * to the trigger.\n */\n private restoreFocus(): void {\n if (typeof document === 'undefined') return;\n const panel = this.elementRef.nativeElement.querySelector<HTMLElement>('.lc-menu__dropdown');\n if (panel?.contains(document.activeElement)) {\n this.triggerElement()?.focus();\n }\n }\n}\n","<div class=\"lc-menu\">\n <!-- Trigger slot -->\n <div class=\"lc-menu__trigger\">\n <ng-content select=\"[trigger]\" />\n </div>\n\n <!-- Dropdown menu -->\n @if (isOpen()) {\n <div\n class=\"lc-menu__dropdown\"\n [class.lc-menu__dropdown--bottom-left]=\"position() === 'bottom-left'\"\n [class.lc-menu__dropdown--bottom-right]=\"position() === 'bottom-right'\"\n [class.lc-menu__dropdown--top-left]=\"position() === 'top-left'\"\n [class.lc-menu__dropdown--top-right]=\"position() === 'top-right'\"\n [class.lc-menu__dropdown--sm]=\"size() === 'sm'\"\n [class.lc-menu__dropdown--lg]=\"size() === 'lg'\"\n [style.min-width]=\"minWidth()\"\n [style.max-width]=\"maxWidth() || null\"\n [class.lc-menu__dropdown--capped]=\"!!maxWidth()\"\n >\n <!-- Optional header slot (outside the menu role: it is not an item) -->\n <ng-content select=\"[header]\" />\n\n <!-- Menu items -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -- keydown bubbles from the focused menu items (roving tabindex) -->\n <div\n class=\"lc-menu__items\"\n role=\"menu\"\n [id]=\"panelId\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n @for (item of items(); track item.id; let i = $index) {\n @if (item.href) {\n <!-- Link item -->\n <a\n #menuItem\n role=\"menuitem\"\n [href]=\"item.href\"\n [target]=\"item.target || '_self'\"\n [attr.rel]=\"item.target === '_blank' ? 'noopener noreferrer' : null\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </a>\n } @else {\n <!-- Button item -->\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </button>\n }\n\n @if (item.dividerAfter) {\n <hr class=\"lc-menu__divider\" />\n }\n }\n </div>\n\n <!-- Optional footer slot -->\n <ng-content select=\"[footer]\" />\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-menu.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/menu/menu.component.ts","../../../../libs/angular-ui-kit/src/lib/menu/menu.component.html"],"sourcesContent":["import {\n Component,\n input,\n output,\n signal,\n effect,\n untracked,\n afterNextRender,\n afterRenderEffect,\n viewChildren,\n ChangeDetectionStrategy,\n HostListener,\n ElementRef,\n Injector,\n Renderer2,\n OnDestroy,\n inject,\n} from '@angular/core';\nimport { IconComponent } from '@lifecorner/angular-ui-kit/icon';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport interface MenuItem {\n id: string;\n label: string;\n icon?: string;\n iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n href?: string;\n target?: '_blank' | '_self' | '_parent' | '_top';\n variant?: 'default' | 'danger';\n dividerAfter?: boolean;\n disabled?: boolean;\n metadata?: {\n subtitle?: string;\n badge?: string;\n };\n}\n\n/** Natively focusable elements a projected trigger may be or contain. */\nconst INTERACTIVE_SELECTOR = 'button, a[href], input, select, textarea, [tabindex]';\n\n/**\n * Menu Component - Dropdown menu for navigation and actions\n *\n * Features:\n * - Customizable menu items with icons\n * - Support for links and buttons\n * - Dividers between menu sections\n * - Optional subtitle/metadata for items\n * - Danger variant for destructive actions\n * - Click outside to close (top-most overlay only)\n * - Keyboard navigation: focus moves into the menu on open, Arrow keys /\n * Home / End move between items (roving tabindex), Enter / Space activate,\n * Escape closes and returns focus to the trigger, Tab closes\n * - ARIA menu semantics (`aria-haspopup` / `aria-expanded` / `aria-controls`\n * on the projected trigger, `role=\"menu\"` / `role=\"menuitem\"` on the panel)\n * - OnPush change detection for performance\n *\n * @example\n * ```html\n * <lc-menu\n * [items]=\"menuItems\"\n * [isOpen]=\"isMenuOpen\"\n * (itemClick)=\"handleMenuClick($event)\"\n * (closed)=\"isMenuOpen = false\"\n * >\n * <button trigger>Open Menu</button>\n * <div header>User Profile</div>\n * </lc-menu>\n * ```\n */\n@Component({\n selector: 'lc-menu',\n standalone: true,\n imports: [IconComponent],\n templateUrl: './menu.component.html',\n styleUrl: './menu.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class MenuComponent implements OnDestroy {\n private static nextId = 0;\n /** Identifies this instance in the overlay stack. */\n private readonly menuId = `lc-menu-${++MenuComponent.nextId}`;\n\n readonly items = input<MenuItem[]>([]);\n readonly isOpen = input(false);\n readonly position = input<'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'>('bottom-right');\n readonly size = input<'sm' | 'md' | 'lg'>('md');\n readonly minWidth = input('220px');\n /**\n * Maximum width of the panel (any CSS width, e.g. `min(18rem, calc(100vw - 2rem))`).\n * Empty leaves it unbounded. Long item labels and header lines wrap within it.\n */\n readonly maxWidth = input('');\n\n readonly itemClick = output<MenuItem>();\n readonly closed = output<void>();\n\n /** Id of the `role=\"menu\"` element, referenced by the trigger's `aria-controls`. */\n protected readonly panelId = `${this.menuId}-panel`;\n\n /** Index (into `items()`) of the item that currently owns the roving tabindex. */\n protected readonly activeIndex = signal(-1);\n\n private readonly itemElements = viewChildren<ElementRef<HTMLElement>>('menuItem');\n private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly renderer = inject(Renderer2);\n private readonly injector = inject(Injector);\n private readonly overlayStack = inject(OverlayStackService);\n\n constructor() {\n // Whether the menu has been through its first open-state evaluation; a menu\n // that is already open when it renders (demo pages) must not steal focus.\n let initialized = false;\n effect(() => {\n const open = this.isOpen();\n untracked(() => {\n if (open) {\n this.overlayStack.push(this.menuId);\n this.activeIndex.set(this.firstEnabledIndex());\n // Move focus into the menu once the panel has rendered\n if (initialized) {\n afterNextRender(() => this.focusActiveItem(), { injector: this.injector });\n }\n } else {\n this.overlayStack.remove(this.menuId);\n this.restoreFocus();\n this.activeIndex.set(-1);\n }\n initialized = true;\n });\n });\n\n // Mirror the open state onto the projected trigger so assistive tech knows\n // it opens a menu (the trigger is caller-supplied, so we set the\n // attributes rather than bind them).\n afterRenderEffect(() => {\n const open = this.isOpen();\n const trigger = this.triggerElement();\n if (!trigger) return;\n this.renderer.setAttribute(trigger, 'aria-haspopup', 'menu');\n this.renderer.setAttribute(trigger, 'aria-expanded', String(open));\n if (open) {\n this.renderer.setAttribute(trigger, 'aria-controls', this.panelId);\n } else {\n this.renderer.removeAttribute(trigger, 'aria-controls');\n }\n });\n }\n\n ngOnDestroy(): void {\n this.overlayStack.remove(this.menuId);\n }\n\n /**\n * Handle menu item click\n */\n onItemClick(item: MenuItem, event: Event): void {\n if (item.disabled) {\n event.preventDefault();\n return;\n }\n\n // Links navigate natively; button items only emit\n if (!item.href) {\n event.preventDefault();\n }\n\n this.itemClick.emit(item);\n }\n\n /**\n * Close the menu\n */\n close(): void {\n this.closed.emit();\n }\n\n /**\n * Handle click outside to close menu — only while the menu is the top-most\n * overlay, so a click inside a layer above it (a modal opened from a menu\n * item) is left alone.\n */\n @HostListener('document:click', ['$event'])\n onDocumentClick(event: MouseEvent): void {\n if (!this.isOpen()) {\n return;\n }\n\n const target = event.target;\n if (target instanceof Node && this.elementRef.nativeElement.contains(target)) {\n return;\n }\n if (!this.overlayStack.claim(this.menuId, event)) return;\n this.close();\n }\n\n /**\n * Escape closes the menu (top-most overlay only) and returns focus to the\n * trigger; the event is consumed so overlays underneath stay open.\n */\n @HostListener('document:keydown.escape', ['$event'])\n onEscapeKey(event: Event): void {\n if (!this.isOpen()) return;\n if (!this.overlayStack.claim(this.menuId, event)) return;\n event.stopPropagation();\n this.triggerElement()?.focus();\n this.close();\n }\n\n /** Keyboard navigation inside the `role=\"menu\"` panel. */\n protected onPanelKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n this.focusIndex(this.nextEnabledIndex(this.activeIndex(), 1));\n break;\n case 'ArrowUp':\n event.preventDefault();\n this.focusIndex(this.nextEnabledIndex(this.activeIndex(), -1));\n break;\n case 'Home':\n event.preventDefault();\n this.focusIndex(this.firstEnabledIndex());\n break;\n case 'End':\n event.preventDefault();\n this.focusIndex(this.nextEnabledIndex(0, -1));\n break;\n case ' ': {\n // Buttons activate on Space natively, links only on Enter\n const target = event.target as HTMLElement | null;\n if (target?.matches('a[role=\"menuitem\"]')) {\n event.preventDefault();\n target.click();\n }\n break;\n }\n case 'Tab':\n // Focus leaves the menu — let it move on naturally, but close the menu\n this.close();\n break;\n }\n }\n\n /** Roving tabindex: only the active item is in the tab sequence. */\n protected tabindexFor(index: number, item: MenuItem): number {\n return !item.disabled && index === this.activeIndex() ? 0 : -1;\n }\n\n private focusIndex(index: number): void {\n if (index < 0) return;\n this.activeIndex.set(index);\n this.focusActiveItem();\n }\n\n private focusActiveItem(): void {\n this.itemElements()[this.activeIndex()]?.nativeElement.focus();\n }\n\n private firstEnabledIndex(): number {\n return this.items().findIndex((item) => !item.disabled);\n }\n\n /** Index of the next enabled item in `direction`, wrapping around; -1 if none. */\n private nextEnabledIndex(from: number, direction: 1 | -1): number {\n const items = this.items();\n for (let step = 1; step <= items.length; step++) {\n const index = (from + direction * step + items.length) % items.length;\n if (!items[index]?.disabled) return index;\n }\n return -1;\n }\n\n /**\n * The element that carries the menu-button ARIA attributes and receives\n * focus back: the projected `[trigger]` node itself when it is natively\n * interactive, otherwise the first interactive element inside it (e.g. the\n * `<button>` inside an `<lc-button trigger>`).\n */\n private triggerElement(): HTMLElement | null {\n const slot = this.elementRef.nativeElement.querySelector<HTMLElement>('.lc-menu__trigger [trigger]');\n if (!slot) return null;\n return slot.matches(INTERACTIVE_SELECTOR)\n ? slot\n : (slot.querySelector<HTMLElement>(INTERACTIVE_SELECTOR) ?? slot);\n }\n\n /**\n * Closing removes the panel from the DOM; if focus is inside it (an item was\n * activated by keyboard) it would silently drop to `<body>`, so send it back\n * to the trigger.\n */\n private restoreFocus(): void {\n if (typeof document === 'undefined') return;\n const panel = this.elementRef.nativeElement.querySelector<HTMLElement>('.lc-menu__dropdown');\n if (panel?.contains(document.activeElement)) {\n this.triggerElement()?.focus();\n }\n }\n}\n","<div class=\"lc-menu\">\n <!-- Trigger slot -->\n <div class=\"lc-menu__trigger\">\n <ng-content select=\"[trigger]\" />\n </div>\n\n <!-- Dropdown menu -->\n @if (isOpen()) {\n <div\n class=\"lc-menu__dropdown\"\n [class.lc-menu__dropdown--bottom-left]=\"position() === 'bottom-left'\"\n [class.lc-menu__dropdown--bottom-right]=\"position() === 'bottom-right'\"\n [class.lc-menu__dropdown--top-left]=\"position() === 'top-left'\"\n [class.lc-menu__dropdown--top-right]=\"position() === 'top-right'\"\n [class.lc-menu__dropdown--sm]=\"size() === 'sm'\"\n [class.lc-menu__dropdown--lg]=\"size() === 'lg'\"\n [style.min-width]=\"minWidth()\"\n [style.max-width]=\"maxWidth() || null\"\n [class.lc-menu__dropdown--capped]=\"!!maxWidth()\"\n >\n <!-- Optional header slot (outside the menu role: it is not an item) -->\n <ng-content select=\"[header]\" />\n\n <!-- Menu items -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -- keydown bubbles from the focused menu items (roving tabindex) -->\n <div\n class=\"lc-menu__items\"\n role=\"menu\"\n [id]=\"panelId\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n @for (item of items(); track item.id; let i = $index) {\n @if (item.href) {\n <!-- Link item -->\n <a\n #menuItem\n role=\"menuitem\"\n [href]=\"item.href\"\n [target]=\"item.target || '_self'\"\n [attr.rel]=\"item.target === '_blank' ? 'noopener noreferrer' : null\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </a>\n } @else {\n <!-- Button item -->\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n class=\"lc-menu__item\"\n [class.lc-menu__item--danger]=\"item.variant === 'danger'\"\n [class.lc-menu__item--disabled]=\"item.disabled\"\n [attr.tabindex]=\"tabindexFor(i, item)\"\n [disabled]=\"item.disabled\"\n (click)=\"onItemClick(item, $event)\"\n >\n @if (item.icon) {\n <lc-icon [name]=\"item.icon\" [size]=\"item.iconSize || 'sm'\" [decorative]=\"true\" />\n }\n <span class=\"lc-menu__item-content\">\n <span class=\"lc-menu__item-label\">{{ item.label }}</span>\n @if (item.metadata?.subtitle; as subtitle) {\n <span class=\"lc-menu__item-subtitle\">{{ subtitle }}</span>\n }\n </span>\n @if (item.metadata?.badge; as badge) {\n <span class=\"lc-menu__item-badge\">{{ badge }}</span>\n }\n </button>\n }\n\n @if (item.dividerAfter) {\n <hr class=\"lc-menu__divider\" />\n }\n }\n </div>\n\n <!-- Optional footer slot -->\n <ng-content select=\"[footer]\" />\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
@@ -354,7 +354,7 @@ var NotificationCenterComponent = class NotificationCenterComponent {
|
|
|
354
354
|
clearAll: "clearAll"
|
|
355
355
|
},
|
|
356
356
|
ngImport: i0,
|
|
357
|
-
template: "<div class=\"lc-nc\">\n <!-- Header -->\n <div class=\"lc-nc__header\">\n <div class=\"lc-nc__header-left\">\n <h3 class=\"lc-nc__title\">{{ title() }}</h3>\n @if (unreadCount() > 0) {\n <span class=\"lc-nc__badge\">{{ unreadCount() }}</span>\n }\n </div>\n <div class=\"lc-nc__header-actions\">\n @if (unreadCount() > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn\" (click)=\"onMarkAllRead()\">{{ markAllReadLabel() }}</button>\n }\n @if (notifications().length > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn lc-nc__action-btn--danger\" (click)=\"onClearAll()\">{{ clearAllLabel() }}</button>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"lc-nc__search\">\n <lc-search-input\n class=\"lc-nc__search-field\"\n [placeholder]=\"searchPlaceholder()\"\n [debounceMs]=\"0\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n\n <!-- Filters -->\n @if (showFilter()) {\n <div class=\"lc-nc__filters\">\n <button\n type=\"button\"\n class=\"lc-nc__filter\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'all'\"\n [attr.aria-pressed]=\"activeFilter() === 'all'\"\n (click)=\"setFilter('all')\"\n >{{ filterAllLabel() }} ({{ filterCounts().all }})</button>\n @if (filterCounts().error > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--error\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'error'\"\n [attr.aria-pressed]=\"activeFilter() === 'error'\"\n (click)=\"setFilter('error')\"\n >{{ filterErrorLabel() }} ({{ filterCounts().error }})</button>\n }\n @if (filterCounts().warning > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--warning\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'warning'\"\n [attr.aria-pressed]=\"activeFilter() === 'warning'\"\n (click)=\"setFilter('warning')\"\n >{{ filterWarningLabel() }} ({{ filterCounts().warning }})</button>\n }\n @if (filterCounts().success > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--success\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'success'\"\n [attr.aria-pressed]=\"activeFilter() === 'success'\"\n (click)=\"setFilter('success')\"\n >{{ filterSuccessLabel() }} ({{ filterCounts().success }})</button>\n }\n @if (filterCounts().info > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--info\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'info'\"\n [attr.aria-pressed]=\"activeFilter() === 'info'\"\n (click)=\"setFilter('info')\"\n >{{ filterInfoLabel() }} ({{ filterCounts().info }})</button>\n }\n </div>\n }\n\n <!-- Notification list -->\n <div class=\"lc-nc__list\" [style.max-height]=\"maxHeight()\">\n @if (!hasNotifications()) {\n <div class=\"lc-nc__empty\">\n <lc-icon name=\"bell\" size=\"lg\" class=\"lc-nc__empty-icon\"></lc-icon>\n <span class=\"lc-nc__empty-text\">{{ emptyMessage() }}</span>\n </div>\n } @else {\n @for (group of groupedNotifications(); track group.category ?? '') {\n <div class=\"lc-nc__group\">\n @if (group.category !== null) {\n <div class=\"lc-nc__group-header\">{{ group.category }}</div>\n }\n <!-- The whole row is a mouse target; the title is a real button so\n keyboard and screen-reader users can activate it (its click bubbles\n to the row), and the dismiss/action buttons stop propagation. -->\n @for (n of group.items; track n.id) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- row is a mouse target; its title is a real button for keyboard users -->\n <div\n class=\"lc-nc__item\"\n [class.lc-nc__item--unread]=\"!n.read\"\n [class.lc-nc__item--urgent]=\"n.priority === 'urgent'\"\n (click)=\"onClick(n)\"\n >\n <lc-icon [name]=\"getTypeIcon(n.type)\" size=\"sm\" class=\"lc-nc__item-icon\" [class]=\"'lc-nc__item-icon lc-nc__item-icon--' + (n.type || 'info')\"></lc-icon>\n <div class=\"lc-nc__item-body\">\n <div class=\"lc-nc__item-header\">\n <button type=\"button\" class=\"lc-nc__item-title\">{{ n.title }}</button>\n @if (showPriority() && n.priority && n.priority !== 'normal') {\n <span [class]=\"'lc-nc__item-priority lc-nc__item-priority--' + n.priority\">\n {{ getPriorityLabel(n.priority) }}\n </span>\n }\n </div>\n @if (n.message) {\n <p class=\"lc-nc__item-message\">{{ n.message }}</p>\n }\n <div class=\"lc-nc__item-footer\">\n @if (showTimestamp()) {\n <span class=\"lc-nc__item-time\">{{ formatTime(n.timestamp) }}</span>\n }\n @if (n.category) {\n <span class=\"lc-nc__item-category\">{{ n.category }}</span>\n }\n @if (n.actionLabel) {\n <button type=\"button\" class=\"lc-nc__item-action\" (click)=\"onAction(n, $event)\">{{ n.actionLabel }}</button>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"lc-nc__item-dismiss\"\n (click)=\"onDismiss(n.id, $event)\"\n [attr.aria-label]=\"dismissAriaLabel() + ' ' + n.title\"\n >×</button>\n </div>\n }\n </div>\n }\n }\n </div>\n</div>\n",
|
|
357
|
+
template: "<div class=\"lc-nc\">\n <!-- Header -->\n <div class=\"lc-nc__header\">\n <div class=\"lc-nc__header-left\">\n <h3 class=\"lc-nc__title\">{{ title() }}</h3>\n @if (unreadCount() > 0) {\n <span class=\"lc-nc__badge\">{{ unreadCount() }}</span>\n }\n </div>\n <div class=\"lc-nc__header-actions\">\n @if (unreadCount() > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn\" (click)=\"onMarkAllRead()\">{{ markAllReadLabel() }}</button>\n }\n @if (notifications().length > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn lc-nc__action-btn--danger\" (click)=\"onClearAll()\">{{ clearAllLabel() }}</button>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"lc-nc__search\">\n <lc-search-input\n class=\"lc-nc__search-field\"\n [placeholder]=\"searchPlaceholder()\"\n [debounceMs]=\"0\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n\n <!-- Filters -->\n @if (showFilter()) {\n <div class=\"lc-nc__filters\">\n <button\n type=\"button\"\n class=\"lc-nc__filter\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'all'\"\n [attr.aria-pressed]=\"activeFilter() === 'all'\"\n (click)=\"setFilter('all')\"\n >{{ filterAllLabel() }} ({{ filterCounts().all }})</button>\n @if (filterCounts().error > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--error\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'error'\"\n [attr.aria-pressed]=\"activeFilter() === 'error'\"\n (click)=\"setFilter('error')\"\n >{{ filterErrorLabel() }} ({{ filterCounts().error }})</button>\n }\n @if (filterCounts().warning > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--warning\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'warning'\"\n [attr.aria-pressed]=\"activeFilter() === 'warning'\"\n (click)=\"setFilter('warning')\"\n >{{ filterWarningLabel() }} ({{ filterCounts().warning }})</button>\n }\n @if (filterCounts().success > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--success\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'success'\"\n [attr.aria-pressed]=\"activeFilter() === 'success'\"\n (click)=\"setFilter('success')\"\n >{{ filterSuccessLabel() }} ({{ filterCounts().success }})</button>\n }\n @if (filterCounts().info > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--info\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'info'\"\n [attr.aria-pressed]=\"activeFilter() === 'info'\"\n (click)=\"setFilter('info')\"\n >{{ filterInfoLabel() }} ({{ filterCounts().info }})</button>\n }\n </div>\n }\n\n <!-- Notification list -->\n <div class=\"lc-nc__list\" [style.max-height]=\"maxHeight()\">\n @if (!hasNotifications()) {\n <div class=\"lc-nc__empty\">\n <lc-icon name=\"bell\" size=\"lg\" class=\"lc-nc__empty-icon\" [decorative]=\"true\"></lc-icon>\n <span class=\"lc-nc__empty-text\">{{ emptyMessage() }}</span>\n </div>\n } @else {\n @for (group of groupedNotifications(); track group.category ?? '') {\n <div class=\"lc-nc__group\">\n @if (group.category !== null) {\n <div class=\"lc-nc__group-header\">{{ group.category }}</div>\n }\n <!-- The whole row is a mouse target; the title is a real button so\n keyboard and screen-reader users can activate it (its click bubbles\n to the row), and the dismiss/action buttons stop propagation. -->\n @for (n of group.items; track n.id) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- row is a mouse target; its title is a real button for keyboard users -->\n <div\n class=\"lc-nc__item\"\n [class.lc-nc__item--unread]=\"!n.read\"\n [class.lc-nc__item--urgent]=\"n.priority === 'urgent'\"\n (click)=\"onClick(n)\"\n >\n <lc-icon [name]=\"getTypeIcon(n.type)\" size=\"sm\" class=\"lc-nc__item-icon\" [class]=\"'lc-nc__item-icon lc-nc__item-icon--' + (n.type || 'info')\" [decorative]=\"true\"></lc-icon>\n <div class=\"lc-nc__item-body\">\n <div class=\"lc-nc__item-header\">\n <button type=\"button\" class=\"lc-nc__item-title\">{{ n.title }}</button>\n @if (showPriority() && n.priority && n.priority !== 'normal') {\n <span [class]=\"'lc-nc__item-priority lc-nc__item-priority--' + n.priority\">\n {{ getPriorityLabel(n.priority) }}\n </span>\n }\n </div>\n @if (n.message) {\n <p class=\"lc-nc__item-message\">{{ n.message }}</p>\n }\n <div class=\"lc-nc__item-footer\">\n @if (showTimestamp()) {\n <span class=\"lc-nc__item-time\">{{ formatTime(n.timestamp) }}</span>\n }\n @if (n.category) {\n <span class=\"lc-nc__item-category\">{{ n.category }}</span>\n }\n @if (n.actionLabel) {\n <button type=\"button\" class=\"lc-nc__item-action\" (click)=\"onAction(n, $event)\">{{ n.actionLabel }}</button>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"lc-nc__item-dismiss\"\n (click)=\"onDismiss(n.id, $event)\"\n [attr.aria-label]=\"dismissAriaLabel() + ' ' + n.title\"\n >×</button>\n </div>\n }\n </div>\n }\n }\n </div>\n</div>\n",
|
|
358
358
|
styles: [":host{display:block}.lc-nc{border:1px solid var(--color-divider);border-radius:var(--border-radius-lg);background-color:var(--color-surface);background-image:var(--gradient-surface);overflow:hidden}.lc-nc__header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--color-divider)}.lc-nc__header-left{display:flex;align-items:center;gap:8px}.lc-nc__title{margin:0;font-size:var(--typography-font-size-base, 1rem);font-weight:600;color:var(--color-text-primary)}.lc-nc__badge{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border-radius:var(--border-radius-full);background:var(--color-error-default);color:#fff;font-size:.6875rem;font-weight:600}.lc-nc__header-actions{display:flex;gap:8px}.lc-nc__action-btn{border:none;background:none;padding:4px 8px;border-radius:var(--border-radius-sm);font-size:.75rem;font-weight:500;cursor:pointer;color:var(--color-primary-600);transition:background .15s}.lc-nc__action-btn:hover{background:var(--color-primary-50)}.lc-nc__action-btn--danger{color:var(--color-error-default)}.lc-nc__action-btn--danger:hover{background:#9d0e0e29}.lc-nc__search{padding:8px 16px;border-bottom:1px solid var(--color-divider)}.lc-nc__search-field{width:100%;display:block}.lc-nc__filters{display:flex;gap:4px;padding:8px 16px;border-bottom:1px solid var(--color-divider);overflow-x:auto}.lc-nc__filter{border:1px solid var(--color-divider);background:transparent;padding:4px 10px;border-radius:var(--border-radius-full);font-size:.6875rem;font-weight:500;cursor:pointer;white-space:nowrap;color:var(--color-text-secondary);transition:all .15s}.lc-nc__filter:hover{background:var(--color-surface-2)}.lc-nc__filter--active{background:var(--color-primary-500);color:#fff;border-color:var(--color-primary-500)}.lc-nc__filter--error.lc-nc__filter--active{background:var(--color-error-default);border-color:var(--color-error-default)}.lc-nc__filter--warning.lc-nc__filter--active{background:var(--color-warning-default);border-color:var(--color-warning-default)}.lc-nc__filter--success.lc-nc__filter--active{background:var(--color-success-default);border-color:var(--color-success-default)}.lc-nc__filter--info.lc-nc__filter--active{background:var(--color-info-default, #3b82f6);border-color:var(--color-info-default, #3b82f6)}.lc-nc__list{overflow-y:auto}.lc-nc__group-header{padding:6px 16px;font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-tertiary);background:var(--color-surface-2);border-bottom:1px solid var(--color-divider)}.lc-nc__item{display:flex;align-items:flex-start;gap:10px;padding:10px 16px;border-bottom:1px solid var(--color-divider);cursor:pointer;transition:background .15s}.lc-nc__item:hover{background:var(--color-surface-2)}.lc-nc__item:last-child{border-bottom:none}.lc-nc__item--unread,.lc-nc__item--unread:hover{background:var(--color-surface-selected)}.lc-nc__item--urgent{border-left:3px solid var(--color-error)}.lc-nc__item-icon{flex-shrink:0;margin-top:2px}.lc-nc__item-icon--info{color:var(--color-info-default, #3b82f6)}.lc-nc__item-icon--success{color:var(--color-success-default, #22c55e)}.lc-nc__item-icon--warning{color:var(--color-warning-default, #f59e0b)}.lc-nc__item-icon--error{color:var(--color-error-default, #ef4444)}.lc-nc__item-body{flex:1;min-width:0}.lc-nc__item-header{display:flex;align-items:center;gap:6px}.lc-nc__item-title{display:block;min-width:0;max-width:100%;padding:0;border:none;background:none;font:inherit;font-size:var(--typography-font-size-sm, .875rem);font-weight:500;text-align:left;color:var(--color-text-primary);cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lc-nc__item-title:focus-visible{outline:none;border-radius:var(--border-radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 20%,transparent)}.lc-nc__item--unread .lc-nc__item-title{font-weight:600}.lc-nc__item-priority{font-size:.625rem;font-weight:600;padding:1px 5px;border-radius:var(--border-radius-sm);flex-shrink:0;text-transform:uppercase}.lc-nc__item-priority--high{background:#e1a0402e;color:var(--color-warning)}.lc-nc__item-priority--urgent{background:#9d0e0e2e;color:var(--color-error)}.lc-nc__item-priority--low{background:var(--color-surface-hover);color:var(--color-text-tertiary)}.lc-nc__item-message{margin:2px 0 0;font-size:.75rem;color:var(--color-text-tertiary);line-height:1.4;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.lc-nc__item-footer{display:flex;align-items:center;gap:8px;margin-top:4px}.lc-nc__item-time{font-size:.6875rem;color:var(--color-text-tertiary)}.lc-nc__item-category{font-size:.625rem;padding:1px 5px;border-radius:var(--border-radius-sm);background:var(--color-surface-hover);color:var(--color-text-tertiary)}.lc-nc__item-action{border:none;background:none;padding:0;font-size:.6875rem;font-weight:600;color:var(--color-primary-600);cursor:pointer}.lc-nc__item-action:hover{text-decoration:underline}.lc-nc__item-dismiss{flex-shrink:0;width:24px;height:24px;border:none;border-radius:var(--border-radius-sm);background:transparent;cursor:pointer;font-size:16px;color:var(--color-text-tertiary);display:flex;align-items:center;justify-content:center;transition:background .1s,color .1s;opacity:0}.lc-nc__item:hover .lc-nc__item-dismiss,.lc-nc__item:focus-within .lc-nc__item-dismiss{opacity:1}.lc-nc__item-dismiss:hover{background:var(--color-surface-hover);color:var(--color-text-secondary)}.lc-nc__item-dismiss:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 20%,transparent)}.lc-nc__empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:40px 16px;color:var(--color-text-tertiary)}.lc-nc__empty-icon{font-size:32px;opacity:.5}.lc-nc__empty-text{font-size:var(--typography-font-size-sm, .875rem)}\n"],
|
|
359
359
|
dependencies: [{
|
|
360
360
|
kind: "component",
|
|
@@ -417,7 +417,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
417
417
|
ICON_CIRCLE_X
|
|
418
418
|
])],
|
|
419
419
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
420
|
-
template: "<div class=\"lc-nc\">\n <!-- Header -->\n <div class=\"lc-nc__header\">\n <div class=\"lc-nc__header-left\">\n <h3 class=\"lc-nc__title\">{{ title() }}</h3>\n @if (unreadCount() > 0) {\n <span class=\"lc-nc__badge\">{{ unreadCount() }}</span>\n }\n </div>\n <div class=\"lc-nc__header-actions\">\n @if (unreadCount() > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn\" (click)=\"onMarkAllRead()\">{{ markAllReadLabel() }}</button>\n }\n @if (notifications().length > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn lc-nc__action-btn--danger\" (click)=\"onClearAll()\">{{ clearAllLabel() }}</button>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"lc-nc__search\">\n <lc-search-input\n class=\"lc-nc__search-field\"\n [placeholder]=\"searchPlaceholder()\"\n [debounceMs]=\"0\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n\n <!-- Filters -->\n @if (showFilter()) {\n <div class=\"lc-nc__filters\">\n <button\n type=\"button\"\n class=\"lc-nc__filter\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'all'\"\n [attr.aria-pressed]=\"activeFilter() === 'all'\"\n (click)=\"setFilter('all')\"\n >{{ filterAllLabel() }} ({{ filterCounts().all }})</button>\n @if (filterCounts().error > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--error\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'error'\"\n [attr.aria-pressed]=\"activeFilter() === 'error'\"\n (click)=\"setFilter('error')\"\n >{{ filterErrorLabel() }} ({{ filterCounts().error }})</button>\n }\n @if (filterCounts().warning > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--warning\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'warning'\"\n [attr.aria-pressed]=\"activeFilter() === 'warning'\"\n (click)=\"setFilter('warning')\"\n >{{ filterWarningLabel() }} ({{ filterCounts().warning }})</button>\n }\n @if (filterCounts().success > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--success\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'success'\"\n [attr.aria-pressed]=\"activeFilter() === 'success'\"\n (click)=\"setFilter('success')\"\n >{{ filterSuccessLabel() }} ({{ filterCounts().success }})</button>\n }\n @if (filterCounts().info > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--info\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'info'\"\n [attr.aria-pressed]=\"activeFilter() === 'info'\"\n (click)=\"setFilter('info')\"\n >{{ filterInfoLabel() }} ({{ filterCounts().info }})</button>\n }\n </div>\n }\n\n <!-- Notification list -->\n <div class=\"lc-nc__list\" [style.max-height]=\"maxHeight()\">\n @if (!hasNotifications()) {\n <div class=\"lc-nc__empty\">\n <lc-icon name=\"bell\" size=\"lg\" class=\"lc-nc__empty-icon\"></lc-icon>\n <span class=\"lc-nc__empty-text\">{{ emptyMessage() }}</span>\n </div>\n } @else {\n @for (group of groupedNotifications(); track group.category ?? '') {\n <div class=\"lc-nc__group\">\n @if (group.category !== null) {\n <div class=\"lc-nc__group-header\">{{ group.category }}</div>\n }\n <!-- The whole row is a mouse target; the title is a real button so\n keyboard and screen-reader users can activate it (its click bubbles\n to the row), and the dismiss/action buttons stop propagation. -->\n @for (n of group.items; track n.id) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- row is a mouse target; its title is a real button for keyboard users -->\n <div\n class=\"lc-nc__item\"\n [class.lc-nc__item--unread]=\"!n.read\"\n [class.lc-nc__item--urgent]=\"n.priority === 'urgent'\"\n (click)=\"onClick(n)\"\n >\n <lc-icon [name]=\"getTypeIcon(n.type)\" size=\"sm\" class=\"lc-nc__item-icon\" [class]=\"'lc-nc__item-icon lc-nc__item-icon--' + (n.type || 'info')\"></lc-icon>\n <div class=\"lc-nc__item-body\">\n <div class=\"lc-nc__item-header\">\n <button type=\"button\" class=\"lc-nc__item-title\">{{ n.title }}</button>\n @if (showPriority() && n.priority && n.priority !== 'normal') {\n <span [class]=\"'lc-nc__item-priority lc-nc__item-priority--' + n.priority\">\n {{ getPriorityLabel(n.priority) }}\n </span>\n }\n </div>\n @if (n.message) {\n <p class=\"lc-nc__item-message\">{{ n.message }}</p>\n }\n <div class=\"lc-nc__item-footer\">\n @if (showTimestamp()) {\n <span class=\"lc-nc__item-time\">{{ formatTime(n.timestamp) }}</span>\n }\n @if (n.category) {\n <span class=\"lc-nc__item-category\">{{ n.category }}</span>\n }\n @if (n.actionLabel) {\n <button type=\"button\" class=\"lc-nc__item-action\" (click)=\"onAction(n, $event)\">{{ n.actionLabel }}</button>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"lc-nc__item-dismiss\"\n (click)=\"onDismiss(n.id, $event)\"\n [attr.aria-label]=\"dismissAriaLabel() + ' ' + n.title\"\n >×</button>\n </div>\n }\n </div>\n }\n }\n </div>\n</div>\n",
|
|
420
|
+
template: "<div class=\"lc-nc\">\n <!-- Header -->\n <div class=\"lc-nc__header\">\n <div class=\"lc-nc__header-left\">\n <h3 class=\"lc-nc__title\">{{ title() }}</h3>\n @if (unreadCount() > 0) {\n <span class=\"lc-nc__badge\">{{ unreadCount() }}</span>\n }\n </div>\n <div class=\"lc-nc__header-actions\">\n @if (unreadCount() > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn\" (click)=\"onMarkAllRead()\">{{ markAllReadLabel() }}</button>\n }\n @if (notifications().length > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn lc-nc__action-btn--danger\" (click)=\"onClearAll()\">{{ clearAllLabel() }}</button>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"lc-nc__search\">\n <lc-search-input\n class=\"lc-nc__search-field\"\n [placeholder]=\"searchPlaceholder()\"\n [debounceMs]=\"0\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n\n <!-- Filters -->\n @if (showFilter()) {\n <div class=\"lc-nc__filters\">\n <button\n type=\"button\"\n class=\"lc-nc__filter\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'all'\"\n [attr.aria-pressed]=\"activeFilter() === 'all'\"\n (click)=\"setFilter('all')\"\n >{{ filterAllLabel() }} ({{ filterCounts().all }})</button>\n @if (filterCounts().error > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--error\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'error'\"\n [attr.aria-pressed]=\"activeFilter() === 'error'\"\n (click)=\"setFilter('error')\"\n >{{ filterErrorLabel() }} ({{ filterCounts().error }})</button>\n }\n @if (filterCounts().warning > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--warning\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'warning'\"\n [attr.aria-pressed]=\"activeFilter() === 'warning'\"\n (click)=\"setFilter('warning')\"\n >{{ filterWarningLabel() }} ({{ filterCounts().warning }})</button>\n }\n @if (filterCounts().success > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--success\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'success'\"\n [attr.aria-pressed]=\"activeFilter() === 'success'\"\n (click)=\"setFilter('success')\"\n >{{ filterSuccessLabel() }} ({{ filterCounts().success }})</button>\n }\n @if (filterCounts().info > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--info\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'info'\"\n [attr.aria-pressed]=\"activeFilter() === 'info'\"\n (click)=\"setFilter('info')\"\n >{{ filterInfoLabel() }} ({{ filterCounts().info }})</button>\n }\n </div>\n }\n\n <!-- Notification list -->\n <div class=\"lc-nc__list\" [style.max-height]=\"maxHeight()\">\n @if (!hasNotifications()) {\n <div class=\"lc-nc__empty\">\n <lc-icon name=\"bell\" size=\"lg\" class=\"lc-nc__empty-icon\" [decorative]=\"true\"></lc-icon>\n <span class=\"lc-nc__empty-text\">{{ emptyMessage() }}</span>\n </div>\n } @else {\n @for (group of groupedNotifications(); track group.category ?? '') {\n <div class=\"lc-nc__group\">\n @if (group.category !== null) {\n <div class=\"lc-nc__group-header\">{{ group.category }}</div>\n }\n <!-- The whole row is a mouse target; the title is a real button so\n keyboard and screen-reader users can activate it (its click bubbles\n to the row), and the dismiss/action buttons stop propagation. -->\n @for (n of group.items; track n.id) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- row is a mouse target; its title is a real button for keyboard users -->\n <div\n class=\"lc-nc__item\"\n [class.lc-nc__item--unread]=\"!n.read\"\n [class.lc-nc__item--urgent]=\"n.priority === 'urgent'\"\n (click)=\"onClick(n)\"\n >\n <lc-icon [name]=\"getTypeIcon(n.type)\" size=\"sm\" class=\"lc-nc__item-icon\" [class]=\"'lc-nc__item-icon lc-nc__item-icon--' + (n.type || 'info')\" [decorative]=\"true\"></lc-icon>\n <div class=\"lc-nc__item-body\">\n <div class=\"lc-nc__item-header\">\n <button type=\"button\" class=\"lc-nc__item-title\">{{ n.title }}</button>\n @if (showPriority() && n.priority && n.priority !== 'normal') {\n <span [class]=\"'lc-nc__item-priority lc-nc__item-priority--' + n.priority\">\n {{ getPriorityLabel(n.priority) }}\n </span>\n }\n </div>\n @if (n.message) {\n <p class=\"lc-nc__item-message\">{{ n.message }}</p>\n }\n <div class=\"lc-nc__item-footer\">\n @if (showTimestamp()) {\n <span class=\"lc-nc__item-time\">{{ formatTime(n.timestamp) }}</span>\n }\n @if (n.category) {\n <span class=\"lc-nc__item-category\">{{ n.category }}</span>\n }\n @if (n.actionLabel) {\n <button type=\"button\" class=\"lc-nc__item-action\" (click)=\"onAction(n, $event)\">{{ n.actionLabel }}</button>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"lc-nc__item-dismiss\"\n (click)=\"onDismiss(n.id, $event)\"\n [attr.aria-label]=\"dismissAriaLabel() + ' ' + n.title\"\n >×</button>\n </div>\n }\n </div>\n }\n }\n </div>\n</div>\n",
|
|
421
421
|
styles: [":host{display:block}.lc-nc{border:1px solid var(--color-divider);border-radius:var(--border-radius-lg);background-color:var(--color-surface);background-image:var(--gradient-surface);overflow:hidden}.lc-nc__header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--color-divider)}.lc-nc__header-left{display:flex;align-items:center;gap:8px}.lc-nc__title{margin:0;font-size:var(--typography-font-size-base, 1rem);font-weight:600;color:var(--color-text-primary)}.lc-nc__badge{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border-radius:var(--border-radius-full);background:var(--color-error-default);color:#fff;font-size:.6875rem;font-weight:600}.lc-nc__header-actions{display:flex;gap:8px}.lc-nc__action-btn{border:none;background:none;padding:4px 8px;border-radius:var(--border-radius-sm);font-size:.75rem;font-weight:500;cursor:pointer;color:var(--color-primary-600);transition:background .15s}.lc-nc__action-btn:hover{background:var(--color-primary-50)}.lc-nc__action-btn--danger{color:var(--color-error-default)}.lc-nc__action-btn--danger:hover{background:#9d0e0e29}.lc-nc__search{padding:8px 16px;border-bottom:1px solid var(--color-divider)}.lc-nc__search-field{width:100%;display:block}.lc-nc__filters{display:flex;gap:4px;padding:8px 16px;border-bottom:1px solid var(--color-divider);overflow-x:auto}.lc-nc__filter{border:1px solid var(--color-divider);background:transparent;padding:4px 10px;border-radius:var(--border-radius-full);font-size:.6875rem;font-weight:500;cursor:pointer;white-space:nowrap;color:var(--color-text-secondary);transition:all .15s}.lc-nc__filter:hover{background:var(--color-surface-2)}.lc-nc__filter--active{background:var(--color-primary-500);color:#fff;border-color:var(--color-primary-500)}.lc-nc__filter--error.lc-nc__filter--active{background:var(--color-error-default);border-color:var(--color-error-default)}.lc-nc__filter--warning.lc-nc__filter--active{background:var(--color-warning-default);border-color:var(--color-warning-default)}.lc-nc__filter--success.lc-nc__filter--active{background:var(--color-success-default);border-color:var(--color-success-default)}.lc-nc__filter--info.lc-nc__filter--active{background:var(--color-info-default, #3b82f6);border-color:var(--color-info-default, #3b82f6)}.lc-nc__list{overflow-y:auto}.lc-nc__group-header{padding:6px 16px;font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-tertiary);background:var(--color-surface-2);border-bottom:1px solid var(--color-divider)}.lc-nc__item{display:flex;align-items:flex-start;gap:10px;padding:10px 16px;border-bottom:1px solid var(--color-divider);cursor:pointer;transition:background .15s}.lc-nc__item:hover{background:var(--color-surface-2)}.lc-nc__item:last-child{border-bottom:none}.lc-nc__item--unread,.lc-nc__item--unread:hover{background:var(--color-surface-selected)}.lc-nc__item--urgent{border-left:3px solid var(--color-error)}.lc-nc__item-icon{flex-shrink:0;margin-top:2px}.lc-nc__item-icon--info{color:var(--color-info-default, #3b82f6)}.lc-nc__item-icon--success{color:var(--color-success-default, #22c55e)}.lc-nc__item-icon--warning{color:var(--color-warning-default, #f59e0b)}.lc-nc__item-icon--error{color:var(--color-error-default, #ef4444)}.lc-nc__item-body{flex:1;min-width:0}.lc-nc__item-header{display:flex;align-items:center;gap:6px}.lc-nc__item-title{display:block;min-width:0;max-width:100%;padding:0;border:none;background:none;font:inherit;font-size:var(--typography-font-size-sm, .875rem);font-weight:500;text-align:left;color:var(--color-text-primary);cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lc-nc__item-title:focus-visible{outline:none;border-radius:var(--border-radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 20%,transparent)}.lc-nc__item--unread .lc-nc__item-title{font-weight:600}.lc-nc__item-priority{font-size:.625rem;font-weight:600;padding:1px 5px;border-radius:var(--border-radius-sm);flex-shrink:0;text-transform:uppercase}.lc-nc__item-priority--high{background:#e1a0402e;color:var(--color-warning)}.lc-nc__item-priority--urgent{background:#9d0e0e2e;color:var(--color-error)}.lc-nc__item-priority--low{background:var(--color-surface-hover);color:var(--color-text-tertiary)}.lc-nc__item-message{margin:2px 0 0;font-size:.75rem;color:var(--color-text-tertiary);line-height:1.4;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.lc-nc__item-footer{display:flex;align-items:center;gap:8px;margin-top:4px}.lc-nc__item-time{font-size:.6875rem;color:var(--color-text-tertiary)}.lc-nc__item-category{font-size:.625rem;padding:1px 5px;border-radius:var(--border-radius-sm);background:var(--color-surface-hover);color:var(--color-text-tertiary)}.lc-nc__item-action{border:none;background:none;padding:0;font-size:.6875rem;font-weight:600;color:var(--color-primary-600);cursor:pointer}.lc-nc__item-action:hover{text-decoration:underline}.lc-nc__item-dismiss{flex-shrink:0;width:24px;height:24px;border:none;border-radius:var(--border-radius-sm);background:transparent;cursor:pointer;font-size:16px;color:var(--color-text-tertiary);display:flex;align-items:center;justify-content:center;transition:background .1s,color .1s;opacity:0}.lc-nc__item:hover .lc-nc__item-dismiss,.lc-nc__item:focus-within .lc-nc__item-dismiss{opacity:1}.lc-nc__item-dismiss:hover{background:var(--color-surface-hover);color:var(--color-text-secondary)}.lc-nc__item-dismiss:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 20%,transparent)}.lc-nc__empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:40px 16px;color:var(--color-text-tertiary)}.lc-nc__empty-icon{font-size:32px;opacity:.5}.lc-nc__empty-text{font-size:var(--typography-font-size-sm, .875rem)}\n"]
|
|
422
422
|
}]
|
|
423
423
|
}],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-notification-center.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/notification-center/notification-center.component.ts","../../../../libs/angular-ui-kit/src/lib/notification-center/notification-center.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n signal,\n computed,\n output,\n} from '@angular/core';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n} from '@lifecorner/angular-ui-kit/icons';\nimport { SearchInputComponent } from '@lifecorner/angular-ui-kit/search-input';\n\nexport type NotificationType = 'info' | 'success' | 'warning' | 'error';\nexport type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';\n\nexport interface Notification {\n id: string;\n title: string;\n message?: string;\n type?: NotificationType;\n priority?: NotificationPriority;\n timestamp: Date;\n read?: boolean;\n icon?: string;\n category?: string;\n actionLabel?: string;\n actionUrl?: string;\n}\n\nconst TYPE_ICONS: Record<NotificationType, string> = {\n info: 'information-circle',\n success: 'check-circle',\n warning: 'exclamation-triangle',\n error: 'x-circle',\n};\n\nconst PRIORITY_LABELS: Record<NotificationPriority, string> = {\n low: 'Low',\n normal: 'Normal',\n high: 'High',\n urgent: 'Urgent',\n};\n\n@Component({\n selector: 'lc-notification-center',\n standalone: true,\n imports: [IconComponent, SearchInputComponent],\n viewProviders: [ɵprovideKitIcons([ICON_INFORMATION_CIRCLE, ICON_CHECK_CIRCLE, ICON_EXCLAMATION_TRIANGLE, ICON_CIRCLE_X])],\n templateUrl: './notification-center.component.html',\n styleUrls: ['./notification-center.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class NotificationCenterComponent {\n readonly notifications = input.required<Notification[]>();\n readonly title = input('Notifications');\n readonly showFilter = input(true);\n readonly showTimestamp = input(true);\n readonly showPriority = input(false);\n readonly emptyMessage = input('No notifications');\n readonly maxHeight = input('480px');\n readonly groupByCategory = input(false);\n /** Locale used for absolute dates (older than a week). */\n readonly dateLocale = input('de-DE');\n\n /** Label of the mark-all-read action. */\n readonly markAllReadLabel = input('Mark all read');\n\n /** Label of the clear-all action. */\n readonly clearAllLabel = input('Clear all');\n\n /** Placeholder of the search field. */\n readonly searchPlaceholder = input('Search notifications…');\n\n /** Label of the \"all\" filter (the count follows in parentheses). */\n readonly filterAllLabel = input('All');\n\n /** Label of the error filter. */\n readonly filterErrorLabel = input('Error');\n\n /** Label of the warning filter. */\n readonly filterWarningLabel = input('Warning');\n\n /** Label of the success filter. */\n readonly filterSuccessLabel = input('Success');\n\n /** Label of the info filter. */\n readonly filterInfoLabel = input('Info');\n\n /** Accessible name of a dismiss button, followed by the notification's title. */\n readonly dismissAriaLabel = input('Dismiss notification:');\n\n /** Group header for notifications without `category` (`groupByCategory`). */\n readonly otherCategoryLabel = input('Other');\n\n /** Priority badge for `low` (`showPriority`). */\n readonly lowPriorityLabel = input('Low');\n\n /** Priority badge for `high` (`showPriority`). */\n readonly highPriorityLabel = input('High');\n\n /** Priority badge for `urgent` (`showPriority`). */\n readonly urgentPriorityLabel = input('Urgent');\n\n /** Timestamp of notifications younger than a minute. */\n readonly justNowLabel = input('Just now');\n\n /** Timestamp under an hour; `{n}` is replaced by the minutes. */\n readonly minutesAgoLabel = input('{n}m ago');\n\n /** Timestamp under a day; `{n}` is replaced by the hours. */\n readonly hoursAgoLabel = input('{n}h ago');\n\n /** Timestamp under a week; `{n}` is replaced by the days (older ones show the date in `dateLocale`). */\n readonly daysAgoLabel = input('{n}d ago');\n\n readonly notificationClick = output<Notification>();\n readonly notificationDismiss = output<string>();\n readonly notificationAction = output<Notification>();\n readonly markAllRead = output<void>();\n readonly clearAll = output<void>();\n\n protected activeFilter = signal<NotificationType | 'all'>('all');\n protected searchQuery = signal('');\n\n protected readonly unreadCount = computed(() =>\n this.notifications().filter(n => !n.read).length\n );\n\n protected readonly filteredNotifications = computed(() => {\n let items = this.notifications();\n const filter = this.activeFilter();\n const query = this.searchQuery().toLowerCase().trim();\n\n if (filter !== 'all') {\n items = items.filter(n => n.type === filter);\n }\n\n if (query) {\n items = items.filter(\n n =>\n n.title.toLowerCase().includes(query) ||\n n.message?.toLowerCase().includes(query) ||\n n.category?.toLowerCase().includes(query)\n );\n }\n\n // Copy before sorting: without a filter `items` is the caller's array and\n // `sort` would reorder it in place\n return [...items].sort((a, b) => {\n const prio = this.priorityWeight(b.priority) - this.priorityWeight(a.priority);\n if (prio !== 0) return prio;\n return b.timestamp.getTime() - a.timestamp.getTime();\n });\n });\n\n /**\n * The list as rendered: one group per category, or a single group without a\n * category (`null`, no header) when grouping is off.\n */\n protected readonly groupedNotifications = computed((): { category: string | null; items: Notification[] }[] => {\n const items = this.filteredNotifications();\n if (!this.groupByCategory()) return [{ category: null, items }];\n const map = new Map<string, Notification[]>();\n for (const n of items) {\n const cat = n.category || this.otherCategoryLabel();\n if (!map.has(cat)) map.set(cat, []);\n map.get(cat)!.push(n);\n }\n return Array.from(map.entries()).map(([category, items]) => ({ category, items }));\n });\n\n protected readonly filterCounts = computed(() => {\n const items = this.notifications();\n return {\n all: items.length,\n info: items.filter(n => n.type === 'info').length,\n success: items.filter(n => n.type === 'success').length,\n warning: items.filter(n => n.type === 'warning').length,\n error: items.filter(n => n.type === 'error').length,\n };\n });\n\n protected readonly hasNotifications = computed(() => this.filteredNotifications().length > 0);\n\n protected getTypeIcon(type?: NotificationType): string {\n return type ? TYPE_ICONS[type] : 'information-circle';\n }\n\n protected getPriorityLabel(priority?: NotificationPriority): string {\n switch (priority) {\n case 'low':\n return this.lowPriorityLabel();\n case 'high':\n return this.highPriorityLabel();\n case 'urgent':\n return this.urgentPriorityLabel();\n default:\n return priority ? PRIORITY_LABELS[priority] : '';\n }\n }\n\n protected formatTime(date: Date): string {\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return this.justNowLabel();\n if (mins < 60) return this.minutesAgoLabel().replace('{n}', String(mins));\n const hours = Math.floor(mins / 60);\n if (hours < 24) return this.hoursAgoLabel().replace('{n}', String(hours));\n const days = Math.floor(hours / 24);\n if (days < 7) return this.daysAgoLabel().replace('{n}', String(days));\n return date.toLocaleDateString(this.dateLocale(), { day: '2-digit', month: '2-digit', year: 'numeric' });\n }\n\n protected setFilter(filter: NotificationType | 'all'): void {\n this.activeFilter.set(filter);\n }\n\n protected onSearch(query: string): void {\n this.searchQuery.set(query);\n }\n\n protected onClick(notification: Notification): void {\n this.notificationClick.emit(notification);\n }\n\n protected onDismiss(id: string, event: Event): void {\n event.stopPropagation();\n this.notificationDismiss.emit(id);\n }\n\n protected onAction(notification: Notification, event: Event): void {\n event.stopPropagation();\n this.notificationAction.emit(notification);\n }\n\n protected onMarkAllRead(): void {\n this.markAllRead.emit();\n }\n\n protected onClearAll(): void {\n this.clearAll.emit();\n }\n\n private priorityWeight(p?: NotificationPriority): number {\n switch (p) {\n case 'urgent': return 4;\n case 'high': return 3;\n case 'normal': return 2;\n case 'low': return 1;\n default: return 2;\n }\n }\n}\n","<div class=\"lc-nc\">\n <!-- Header -->\n <div class=\"lc-nc__header\">\n <div class=\"lc-nc__header-left\">\n <h3 class=\"lc-nc__title\">{{ title() }}</h3>\n @if (unreadCount() > 0) {\n <span class=\"lc-nc__badge\">{{ unreadCount() }}</span>\n }\n </div>\n <div class=\"lc-nc__header-actions\">\n @if (unreadCount() > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn\" (click)=\"onMarkAllRead()\">{{ markAllReadLabel() }}</button>\n }\n @if (notifications().length > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn lc-nc__action-btn--danger\" (click)=\"onClearAll()\">{{ clearAllLabel() }}</button>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"lc-nc__search\">\n <lc-search-input\n class=\"lc-nc__search-field\"\n [placeholder]=\"searchPlaceholder()\"\n [debounceMs]=\"0\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n\n <!-- Filters -->\n @if (showFilter()) {\n <div class=\"lc-nc__filters\">\n <button\n type=\"button\"\n class=\"lc-nc__filter\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'all'\"\n [attr.aria-pressed]=\"activeFilter() === 'all'\"\n (click)=\"setFilter('all')\"\n >{{ filterAllLabel() }} ({{ filterCounts().all }})</button>\n @if (filterCounts().error > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--error\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'error'\"\n [attr.aria-pressed]=\"activeFilter() === 'error'\"\n (click)=\"setFilter('error')\"\n >{{ filterErrorLabel() }} ({{ filterCounts().error }})</button>\n }\n @if (filterCounts().warning > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--warning\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'warning'\"\n [attr.aria-pressed]=\"activeFilter() === 'warning'\"\n (click)=\"setFilter('warning')\"\n >{{ filterWarningLabel() }} ({{ filterCounts().warning }})</button>\n }\n @if (filterCounts().success > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--success\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'success'\"\n [attr.aria-pressed]=\"activeFilter() === 'success'\"\n (click)=\"setFilter('success')\"\n >{{ filterSuccessLabel() }} ({{ filterCounts().success }})</button>\n }\n @if (filterCounts().info > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--info\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'info'\"\n [attr.aria-pressed]=\"activeFilter() === 'info'\"\n (click)=\"setFilter('info')\"\n >{{ filterInfoLabel() }} ({{ filterCounts().info }})</button>\n }\n </div>\n }\n\n <!-- Notification list -->\n <div class=\"lc-nc__list\" [style.max-height]=\"maxHeight()\">\n @if (!hasNotifications()) {\n <div class=\"lc-nc__empty\">\n <lc-icon name=\"bell\" size=\"lg\" class=\"lc-nc__empty-icon\"></lc-icon>\n <span class=\"lc-nc__empty-text\">{{ emptyMessage() }}</span>\n </div>\n } @else {\n @for (group of groupedNotifications(); track group.category ?? '') {\n <div class=\"lc-nc__group\">\n @if (group.category !== null) {\n <div class=\"lc-nc__group-header\">{{ group.category }}</div>\n }\n <!-- The whole row is a mouse target; the title is a real button so\n keyboard and screen-reader users can activate it (its click bubbles\n to the row), and the dismiss/action buttons stop propagation. -->\n @for (n of group.items; track n.id) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- row is a mouse target; its title is a real button for keyboard users -->\n <div\n class=\"lc-nc__item\"\n [class.lc-nc__item--unread]=\"!n.read\"\n [class.lc-nc__item--urgent]=\"n.priority === 'urgent'\"\n (click)=\"onClick(n)\"\n >\n <lc-icon [name]=\"getTypeIcon(n.type)\" size=\"sm\" class=\"lc-nc__item-icon\" [class]=\"'lc-nc__item-icon lc-nc__item-icon--' + (n.type || 'info')\"></lc-icon>\n <div class=\"lc-nc__item-body\">\n <div class=\"lc-nc__item-header\">\n <button type=\"button\" class=\"lc-nc__item-title\">{{ n.title }}</button>\n @if (showPriority() && n.priority && n.priority !== 'normal') {\n <span [class]=\"'lc-nc__item-priority lc-nc__item-priority--' + n.priority\">\n {{ getPriorityLabel(n.priority) }}\n </span>\n }\n </div>\n @if (n.message) {\n <p class=\"lc-nc__item-message\">{{ n.message }}</p>\n }\n <div class=\"lc-nc__item-footer\">\n @if (showTimestamp()) {\n <span class=\"lc-nc__item-time\">{{ formatTime(n.timestamp) }}</span>\n }\n @if (n.category) {\n <span class=\"lc-nc__item-category\">{{ n.category }}</span>\n }\n @if (n.actionLabel) {\n <button type=\"button\" class=\"lc-nc__item-action\" (click)=\"onAction(n, $event)\">{{ n.actionLabel }}</button>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"lc-nc__item-dismiss\"\n (click)=\"onDismiss(n.id, $event)\"\n [attr.aria-label]=\"dismissAriaLabel() + ' ' + n.title\"\n >×</button>\n </div>\n }\n </div>\n }\n }\n </div>\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-notification-center.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/notification-center/notification-center.component.ts","../../../../libs/angular-ui-kit/src/lib/notification-center/notification-center.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n signal,\n computed,\n output,\n} from '@angular/core';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n} from '@lifecorner/angular-ui-kit/icons';\nimport { SearchInputComponent } from '@lifecorner/angular-ui-kit/search-input';\n\nexport type NotificationType = 'info' | 'success' | 'warning' | 'error';\nexport type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';\n\nexport interface Notification {\n id: string;\n title: string;\n message?: string;\n type?: NotificationType;\n priority?: NotificationPriority;\n timestamp: Date;\n read?: boolean;\n icon?: string;\n category?: string;\n actionLabel?: string;\n actionUrl?: string;\n}\n\nconst TYPE_ICONS: Record<NotificationType, string> = {\n info: 'information-circle',\n success: 'check-circle',\n warning: 'exclamation-triangle',\n error: 'x-circle',\n};\n\nconst PRIORITY_LABELS: Record<NotificationPriority, string> = {\n low: 'Low',\n normal: 'Normal',\n high: 'High',\n urgent: 'Urgent',\n};\n\n@Component({\n selector: 'lc-notification-center',\n standalone: true,\n imports: [IconComponent, SearchInputComponent],\n viewProviders: [ɵprovideKitIcons([ICON_INFORMATION_CIRCLE, ICON_CHECK_CIRCLE, ICON_EXCLAMATION_TRIANGLE, ICON_CIRCLE_X])],\n templateUrl: './notification-center.component.html',\n styleUrls: ['./notification-center.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class NotificationCenterComponent {\n readonly notifications = input.required<Notification[]>();\n readonly title = input('Notifications');\n readonly showFilter = input(true);\n readonly showTimestamp = input(true);\n readonly showPriority = input(false);\n readonly emptyMessage = input('No notifications');\n readonly maxHeight = input('480px');\n readonly groupByCategory = input(false);\n /** Locale used for absolute dates (older than a week). */\n readonly dateLocale = input('de-DE');\n\n /** Label of the mark-all-read action. */\n readonly markAllReadLabel = input('Mark all read');\n\n /** Label of the clear-all action. */\n readonly clearAllLabel = input('Clear all');\n\n /** Placeholder of the search field. */\n readonly searchPlaceholder = input('Search notifications…');\n\n /** Label of the \"all\" filter (the count follows in parentheses). */\n readonly filterAllLabel = input('All');\n\n /** Label of the error filter. */\n readonly filterErrorLabel = input('Error');\n\n /** Label of the warning filter. */\n readonly filterWarningLabel = input('Warning');\n\n /** Label of the success filter. */\n readonly filterSuccessLabel = input('Success');\n\n /** Label of the info filter. */\n readonly filterInfoLabel = input('Info');\n\n /** Accessible name of a dismiss button, followed by the notification's title. */\n readonly dismissAriaLabel = input('Dismiss notification:');\n\n /** Group header for notifications without `category` (`groupByCategory`). */\n readonly otherCategoryLabel = input('Other');\n\n /** Priority badge for `low` (`showPriority`). */\n readonly lowPriorityLabel = input('Low');\n\n /** Priority badge for `high` (`showPriority`). */\n readonly highPriorityLabel = input('High');\n\n /** Priority badge for `urgent` (`showPriority`). */\n readonly urgentPriorityLabel = input('Urgent');\n\n /** Timestamp of notifications younger than a minute. */\n readonly justNowLabel = input('Just now');\n\n /** Timestamp under an hour; `{n}` is replaced by the minutes. */\n readonly minutesAgoLabel = input('{n}m ago');\n\n /** Timestamp under a day; `{n}` is replaced by the hours. */\n readonly hoursAgoLabel = input('{n}h ago');\n\n /** Timestamp under a week; `{n}` is replaced by the days (older ones show the date in `dateLocale`). */\n readonly daysAgoLabel = input('{n}d ago');\n\n readonly notificationClick = output<Notification>();\n readonly notificationDismiss = output<string>();\n readonly notificationAction = output<Notification>();\n readonly markAllRead = output<void>();\n readonly clearAll = output<void>();\n\n protected activeFilter = signal<NotificationType | 'all'>('all');\n protected searchQuery = signal('');\n\n protected readonly unreadCount = computed(() =>\n this.notifications().filter(n => !n.read).length\n );\n\n protected readonly filteredNotifications = computed(() => {\n let items = this.notifications();\n const filter = this.activeFilter();\n const query = this.searchQuery().toLowerCase().trim();\n\n if (filter !== 'all') {\n items = items.filter(n => n.type === filter);\n }\n\n if (query) {\n items = items.filter(\n n =>\n n.title.toLowerCase().includes(query) ||\n n.message?.toLowerCase().includes(query) ||\n n.category?.toLowerCase().includes(query)\n );\n }\n\n // Copy before sorting: without a filter `items` is the caller's array and\n // `sort` would reorder it in place\n return [...items].sort((a, b) => {\n const prio = this.priorityWeight(b.priority) - this.priorityWeight(a.priority);\n if (prio !== 0) return prio;\n return b.timestamp.getTime() - a.timestamp.getTime();\n });\n });\n\n /**\n * The list as rendered: one group per category, or a single group without a\n * category (`null`, no header) when grouping is off.\n */\n protected readonly groupedNotifications = computed((): { category: string | null; items: Notification[] }[] => {\n const items = this.filteredNotifications();\n if (!this.groupByCategory()) return [{ category: null, items }];\n const map = new Map<string, Notification[]>();\n for (const n of items) {\n const cat = n.category || this.otherCategoryLabel();\n if (!map.has(cat)) map.set(cat, []);\n map.get(cat)!.push(n);\n }\n return Array.from(map.entries()).map(([category, items]) => ({ category, items }));\n });\n\n protected readonly filterCounts = computed(() => {\n const items = this.notifications();\n return {\n all: items.length,\n info: items.filter(n => n.type === 'info').length,\n success: items.filter(n => n.type === 'success').length,\n warning: items.filter(n => n.type === 'warning').length,\n error: items.filter(n => n.type === 'error').length,\n };\n });\n\n protected readonly hasNotifications = computed(() => this.filteredNotifications().length > 0);\n\n protected getTypeIcon(type?: NotificationType): string {\n return type ? TYPE_ICONS[type] : 'information-circle';\n }\n\n protected getPriorityLabel(priority?: NotificationPriority): string {\n switch (priority) {\n case 'low':\n return this.lowPriorityLabel();\n case 'high':\n return this.highPriorityLabel();\n case 'urgent':\n return this.urgentPriorityLabel();\n default:\n return priority ? PRIORITY_LABELS[priority] : '';\n }\n }\n\n protected formatTime(date: Date): string {\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return this.justNowLabel();\n if (mins < 60) return this.minutesAgoLabel().replace('{n}', String(mins));\n const hours = Math.floor(mins / 60);\n if (hours < 24) return this.hoursAgoLabel().replace('{n}', String(hours));\n const days = Math.floor(hours / 24);\n if (days < 7) return this.daysAgoLabel().replace('{n}', String(days));\n return date.toLocaleDateString(this.dateLocale(), { day: '2-digit', month: '2-digit', year: 'numeric' });\n }\n\n protected setFilter(filter: NotificationType | 'all'): void {\n this.activeFilter.set(filter);\n }\n\n protected onSearch(query: string): void {\n this.searchQuery.set(query);\n }\n\n protected onClick(notification: Notification): void {\n this.notificationClick.emit(notification);\n }\n\n protected onDismiss(id: string, event: Event): void {\n event.stopPropagation();\n this.notificationDismiss.emit(id);\n }\n\n protected onAction(notification: Notification, event: Event): void {\n event.stopPropagation();\n this.notificationAction.emit(notification);\n }\n\n protected onMarkAllRead(): void {\n this.markAllRead.emit();\n }\n\n protected onClearAll(): void {\n this.clearAll.emit();\n }\n\n private priorityWeight(p?: NotificationPriority): number {\n switch (p) {\n case 'urgent': return 4;\n case 'high': return 3;\n case 'normal': return 2;\n case 'low': return 1;\n default: return 2;\n }\n }\n}\n","<div class=\"lc-nc\">\n <!-- Header -->\n <div class=\"lc-nc__header\">\n <div class=\"lc-nc__header-left\">\n <h3 class=\"lc-nc__title\">{{ title() }}</h3>\n @if (unreadCount() > 0) {\n <span class=\"lc-nc__badge\">{{ unreadCount() }}</span>\n }\n </div>\n <div class=\"lc-nc__header-actions\">\n @if (unreadCount() > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn\" (click)=\"onMarkAllRead()\">{{ markAllReadLabel() }}</button>\n }\n @if (notifications().length > 0) {\n <button type=\"button\" class=\"lc-nc__action-btn lc-nc__action-btn--danger\" (click)=\"onClearAll()\">{{ clearAllLabel() }}</button>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"lc-nc__search\">\n <lc-search-input\n class=\"lc-nc__search-field\"\n [placeholder]=\"searchPlaceholder()\"\n [debounceMs]=\"0\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n\n <!-- Filters -->\n @if (showFilter()) {\n <div class=\"lc-nc__filters\">\n <button\n type=\"button\"\n class=\"lc-nc__filter\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'all'\"\n [attr.aria-pressed]=\"activeFilter() === 'all'\"\n (click)=\"setFilter('all')\"\n >{{ filterAllLabel() }} ({{ filterCounts().all }})</button>\n @if (filterCounts().error > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--error\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'error'\"\n [attr.aria-pressed]=\"activeFilter() === 'error'\"\n (click)=\"setFilter('error')\"\n >{{ filterErrorLabel() }} ({{ filterCounts().error }})</button>\n }\n @if (filterCounts().warning > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--warning\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'warning'\"\n [attr.aria-pressed]=\"activeFilter() === 'warning'\"\n (click)=\"setFilter('warning')\"\n >{{ filterWarningLabel() }} ({{ filterCounts().warning }})</button>\n }\n @if (filterCounts().success > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--success\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'success'\"\n [attr.aria-pressed]=\"activeFilter() === 'success'\"\n (click)=\"setFilter('success')\"\n >{{ filterSuccessLabel() }} ({{ filterCounts().success }})</button>\n }\n @if (filterCounts().info > 0) {\n <button\n type=\"button\"\n class=\"lc-nc__filter lc-nc__filter--info\"\n [class.lc-nc__filter--active]=\"activeFilter() === 'info'\"\n [attr.aria-pressed]=\"activeFilter() === 'info'\"\n (click)=\"setFilter('info')\"\n >{{ filterInfoLabel() }} ({{ filterCounts().info }})</button>\n }\n </div>\n }\n\n <!-- Notification list -->\n <div class=\"lc-nc__list\" [style.max-height]=\"maxHeight()\">\n @if (!hasNotifications()) {\n <div class=\"lc-nc__empty\">\n <lc-icon name=\"bell\" size=\"lg\" class=\"lc-nc__empty-icon\" [decorative]=\"true\"></lc-icon>\n <span class=\"lc-nc__empty-text\">{{ emptyMessage() }}</span>\n </div>\n } @else {\n @for (group of groupedNotifications(); track group.category ?? '') {\n <div class=\"lc-nc__group\">\n @if (group.category !== null) {\n <div class=\"lc-nc__group-header\">{{ group.category }}</div>\n }\n <!-- The whole row is a mouse target; the title is a real button so\n keyboard and screen-reader users can activate it (its click bubbles\n to the row), and the dismiss/action buttons stop propagation. -->\n @for (n of group.items; track n.id) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- row is a mouse target; its title is a real button for keyboard users -->\n <div\n class=\"lc-nc__item\"\n [class.lc-nc__item--unread]=\"!n.read\"\n [class.lc-nc__item--urgent]=\"n.priority === 'urgent'\"\n (click)=\"onClick(n)\"\n >\n <lc-icon [name]=\"getTypeIcon(n.type)\" size=\"sm\" class=\"lc-nc__item-icon\" [class]=\"'lc-nc__item-icon lc-nc__item-icon--' + (n.type || 'info')\" [decorative]=\"true\"></lc-icon>\n <div class=\"lc-nc__item-body\">\n <div class=\"lc-nc__item-header\">\n <button type=\"button\" class=\"lc-nc__item-title\">{{ n.title }}</button>\n @if (showPriority() && n.priority && n.priority !== 'normal') {\n <span [class]=\"'lc-nc__item-priority lc-nc__item-priority--' + n.priority\">\n {{ getPriorityLabel(n.priority) }}\n </span>\n }\n </div>\n @if (n.message) {\n <p class=\"lc-nc__item-message\">{{ n.message }}</p>\n }\n <div class=\"lc-nc__item-footer\">\n @if (showTimestamp()) {\n <span class=\"lc-nc__item-time\">{{ formatTime(n.timestamp) }}</span>\n }\n @if (n.category) {\n <span class=\"lc-nc__item-category\">{{ n.category }}</span>\n }\n @if (n.actionLabel) {\n <button type=\"button\" class=\"lc-nc__item-action\" (click)=\"onAction(n, $event)\">{{ n.actionLabel }}</button>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"lc-nc__item-dismiss\"\n (click)=\"onDismiss(n.id, $event)\"\n [attr.aria-label]=\"dismissAriaLabel() + ' ' + n.title\"\n >×</button>\n </div>\n }\n </div>\n }\n }\n </div>\n</div>\n"],"mappings":";;;;;AAkCA,MAAM,aAA+C;CACnD,MAAM;CACN,SAAS;CACT,SAAS;CACT,OAAO;AACR;AAED,MAAM,kBAAwD;CAC5D,KAAK;CACL,QAAQ;CACR,MAAM;CACN,QAAQ;AACT;AAWD,IAAa,8BAAb,MAAa,4BAA2B;CAC7B,gBAAgB,MAAM,SAA0B,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAChD,QAAQ,MAAM,iBAAgB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAC9B,aAAa,MAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CACxB,gBAAgB,MAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAC3B,eAAe,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAC3B,eAAe,MAAM,oBAAmB,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CACxC,YAAY,MAAM,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAC1B,kBAAkB,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAE9B,aAAa,MAAM,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAG3B,mBAAmB,MAAM,iBAAgB,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAGzC,gBAAgB,MAAM,aAAY,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAGlC,oBAAoB,MAAM,yBAAwB,GAAA,YAAA,CAAA,EAAA,WAAA,oBAAA,CAAA,+BAAA,CAAA,CAAA;CAGlD,iBAAiB,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAG7B,mBAAmB,MAAM,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,qBAAqB,MAAM,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;CAGrC,qBAAqB,MAAM,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;CAGrC,kBAAkB,MAAM,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAG/B,mBAAmB,MAAM,yBAAwB,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAGjD,qBAAqB,MAAM,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;CAGnC,mBAAmB,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAG/B,oBAAoB,MAAM,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,oBAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,sBAAsB,MAAM,UAAS,GAAA,YAAA,CAAA,EAAA,WAAA,sBAAA,CAAA,+BAAA,CAAA,CAAA;CAGrC,eAAe,MAAM,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAGhC,kBAAkB,MAAM,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAGnC,gBAAgB,MAAM,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,eAAe,MAAM,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEhC,oBAAoB,OAAM;CAC1B,sBAAsB,OAAM;CAC5B,qBAAqB,OAAM;CAC3B,cAAc,OAAM;CACpB,WAAW,OAAM;CAEhB,eAAe,OAAiC,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CACtD,cAAc,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAEf,cAAc,eAC/B,KAAK,cAAa,CAAE,CAAC,QAAO,MAAK,CAAC,EAAE,IAAI,CAAC,CAAC,QAC3C,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAEkB,wBAAwB,eAAc;EACvD,IAAI,QAAQ,KAAK,cAAa;EAC9B,MAAM,SAAS,KAAK,aAAY;EAChC,MAAM,QAAQ,KAAK,YAAW,CAAE,CAAC,YAAW,CAAE,CAAC,KAAI;EAEnD,IAAI,WAAW,OACb,QAAQ,MAAM,QAAO,MAAK,EAAE,SAAS,MAAM;EAG7C,IAAI,OACF,QAAQ,MAAM,QACZ,MACE,EAAE,MAAM,YAAW,CAAE,CAAC,SAAS,KAAK,KACpC,EAAE,SAAS,YAAW,CAAE,CAAC,SAAS,KAAK,KACvC,EAAE,UAAU,YAAW,CAAE,CAAC,SAAS,KAAK,CAAC;EAM/C,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAK;GAC9B,MAAM,OAAO,KAAK,eAAe,EAAE,QAAQ,IAAI,KAAK,eAAe,EAAE,QAAQ;GAC7E,IAAI,SAAS,GAAG,OAAO;GACvB,OAAO,EAAE,UAAU,QAAO,IAAK,EAAE,UAAU,QAAO;EACpD,CAAC;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,wBAAA,CAAA,+BAAA,CAAA,CAAA;CAMiB,uBAAuB,eAAoE;EAC5G,MAAM,QAAQ,KAAK,sBAAqB;EACxC,IAAI,CAAC,KAAK,gBAAe,GAAI,OAAO,CAAC;GAAE,UAAU;GAAM;EAAK,CAAE;EAC9D,MAAM,sBAAM,IAAI,IAAG;EACnB,KAAK,MAAM,KAAK,OAAO;GACrB,MAAM,MAAM,EAAE,YAAY,KAAK,mBAAkB;GACjD,IAAI,CAAC,IAAI,IAAI,GAAG,GAAG,IAAI,IAAI,KAAK,CAAA,CAAE;GAClC,IAAI,IAAI,GAAG,CAAE,CAAC,KAAK,CAAC;EACtB;EACA,OAAO,MAAM,KAAK,IAAI,QAAO,CAAE,CAAC,CAAC,KAAK,CAAC,UAAU,YAAY;GAAE;GAAU;EAAK,EAAG;CACnF,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,uBAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,eAAe,eAAc;EAC9C,MAAM,QAAQ,KAAK,cAAa;EAChC,OAAO;GACL,KAAK,MAAM;GACX,MAAM,MAAM,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,CAAC;GAC3C,SAAS,MAAM,QAAO,MAAK,EAAE,SAAS,SAAS,CAAC,CAAC;GACjD,SAAS,MAAM,QAAO,MAAK,EAAE,SAAS,SAAS,CAAC,CAAC;GACjD,OAAO,MAAM,QAAO,MAAK,EAAE,SAAS,OAAO,CAAC,CAAC;EAC9C;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,mBAAmB,eAAe,KAAK,sBAAqB,CAAE,CAAC,SAAS,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAEnF,YAAY,MAAuB;EAC3C,OAAO,OAAO,WAAW,QAAQ;CACnC;CAEU,iBAAiB,UAA+B;EACxD,QAAQ,UAAR;GACE,KAAK,OACH,OAAO,KAAK,iBAAgB;GAC9B,KAAK,QACH,OAAO,KAAK,kBAAiB;GAC/B,KAAK,UACH,OAAO,KAAK,oBAAmB;GACjC,SACE,OAAO,WAAW,gBAAgB,YAAY;EAClD;CACF;CAEU,WAAW,MAAU;EAE7B,MAAM,wBAAO,IADG,KACH,EAAA,CAAI,QAAO,IAAK,KAAK,QAAO;EACzC,MAAM,OAAO,KAAK,MAAM,OAAO,GAAK;EACpC,IAAI,OAAO,GAAG,OAAO,KAAK,aAAY;EACtC,IAAI,OAAO,IAAI,OAAO,KAAK,gBAAe,CAAE,CAAC,QAAQ,OAAO,OAAO,IAAI,CAAC;EACxE,MAAM,QAAQ,KAAK,MAAM,OAAO,EAAE;EAClC,IAAI,QAAQ,IAAI,OAAO,KAAK,cAAa,CAAE,CAAC,QAAQ,OAAO,OAAO,KAAK,CAAC;EACxE,MAAM,OAAO,KAAK,MAAM,QAAQ,EAAE;EAClC,IAAI,OAAO,GAAG,OAAO,KAAK,aAAY,CAAE,CAAC,QAAQ,OAAO,OAAO,IAAI,CAAC;EACpE,OAAO,KAAK,mBAAmB,KAAK,WAAU,GAAI;GAAE,KAAK;GAAW,OAAO;GAAW,MAAM;EAAS,CAAE;CACzG;CAEU,UAAU,QAAgC;EAClD,KAAK,aAAa,IAAI,MAAM;CAC9B;CAEU,SAAS,OAAa;EAC9B,KAAK,YAAY,IAAI,KAAK;CAC5B;CAEU,QAAQ,cAA0B;EAC1C,KAAK,kBAAkB,KAAK,YAAY;CAC1C;CAEU,UAAU,IAAY,OAAY;EAC1C,MAAM,gBAAe;EACrB,KAAK,oBAAoB,KAAK,EAAE;CAClC;CAEU,SAAS,cAA4B,OAAY;EACzD,MAAM,gBAAe;EACrB,KAAK,mBAAmB,KAAK,YAAY;CAC3C;CAEU,gBAAa;EACrB,KAAK,YAAY,KAAI;CACvB;CAEU,aAAU;EAClB,KAAK,SAAS,KAAI;CACpB;CAEQ,eAAe,GAAwB;EAC7C,QAAQ,GAAR;GACE,KAAK,UAAU,OAAO;GACtB,KAAK,QAAQ,OAAO;GACpB,KAAK,UAAU,OAAO;GACtB,KAAK,OAAO,OAAO;GACnB,SAAS,OAAO;EAClB;CACF;CAxMW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAA2B,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAA3B,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAA2B,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,kBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,mBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,kBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,oBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,oBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,kBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,oBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,kBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,mBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,qBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA;GAAA,mBAAA;GAAA,qBAAA;GAAA,oBAAA;GAAA,aAAA;GAAA,UAAA;EAAA;EAAA,UAAA;EAAA,UCzDxC;EA4IA,QAAA,CAAA,2xLAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MDzFY;GAAa,UAAA;GAAA,QAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;GAAA;EAAA,GAAA;GAAA,MAAA;GAAA,MAAE;GAAoB,UAAA;GAAA,QAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;GAAA;GAAA,SAAA;IAAA;IAAA;IAAA;GAAA;EAAA,CAAA;EAAA,eAC9B,CAAC,iBAAiB;GAAC;GAAyB;GAAmB;GAA2B;EAAa,CAAC,CAAC;EAAC,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAK9G,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAA2B,YAAA,CAAA;EATvC,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAwB,YACtB;GAAI,SACP,CAAC,eAAe,oBAAoB;GAAC,eAC/B,CAAC,iBAAiB;IAAC;IAAyB;IAAmB;IAA2B;GAAa,CAAC,CAAC;GAAC,iBAGxG,wBAAwB;GAAM,UAAA;GAAA,QAAA,CAAA,2xLAAA;EAAA,CAAA"}
|