@styloviz/notification-center 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StyloViz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @styloviz/notification-center
2
+
3
+ > 6 premium variants: dropdown, drawer, inline, activity feed, compact and grouped — with mark-as-read, dismiss, search, tab filtering and inline actions.
4
+
5
+ Part of the **StyloViz UI Kit** — a premium Angular 21 + Tailwind CSS 4 dashboard component library. Standalone, `OnPush`, strongly typed, dark-mode ready.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @styloviz/core @styloviz/notification-center
11
+ ```
12
+
13
+ Requires `@angular/core` and `@angular/common` >= 21. `@styloviz/core` is a peer dependency shared by every component. Prefer everything at once? `@styloviz/all` installs the whole free tier in one command.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { SvNotificationCenterComponent } from '@styloviz/notification-center';
19
+
20
+ @Component({
21
+ standalone: true,
22
+ imports: [SvNotificationCenterComponent],
23
+ template: `
24
+ <sv-notification-center [notifications]="notifications" />
25
+ `,
26
+ })
27
+ export class DemoComponent {}
28
+ ```
29
+
30
+ ## Inputs
31
+
32
+ | Input | Type | Default | Description |
33
+ | --- | --- | --- | --- |
34
+ | `variant` | `NotificationCenterVariant` | `'dropdown'` | Presentation: dropdown, drawer, inline, feed, compact or grouped. |
35
+ | `notifications` | `NotificationItem[]` | `[]` | The notifications to display. |
36
+ | `maxHeight` | `string` | `'400px'` | Max height of the scrollable list (CSS length). |
37
+ | `tabs` | `string[]` | `['All', 'Unread', 'Mentions', 'System']` | Filter tab labels. |
38
+ | `panelTitle` | `string` | `'Notifications'` | Panel heading text. |
39
+ | `showSearch` | `boolean` | `true` | Show the search box. |
40
+ | `animateBell` | `boolean` | `true` | Animate the bell icon on new notifications. |
41
+
42
+ ## Outputs
43
+
44
+ | Output | Type | Description |
45
+ | --- | --- | --- |
46
+ | `notificationClick` | `NotificationClickEvent` | Emitted when a notification row is clicked. |
47
+ | `actionClick` | `NotificationActionClickEvent` | Emitted when an inline action on a notification is clicked. |
48
+ | `markAllRead` | `void` | Emitted when "mark all as read" is triggered. |
49
+ | `dismiss` | `NotificationDismissEvent` | Emitted when a notification is dismissed. |
50
+
51
+ ## Documentation
52
+
53
+ Full API reference and live demos: https://styloviz.dev/docs/notification-center
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,414 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, PLATFORM_ID, ViewContainerRef, viewChild, input, output, signal, effect, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
4
+
5
+ // ─── Icon registry ────────────────────────────────────────────────────────────
6
+ const TYPE_ICONS = {
7
+ comment: 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
8
+ mention: 'M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207',
9
+ assignment: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4',
10
+ status: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
11
+ invite: 'M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z',
12
+ approval: 'M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z',
13
+ deadline: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
14
+ payment: 'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z',
15
+ security: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
16
+ update: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
17
+ reaction: 'M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
18
+ system: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
19
+ };
20
+ const TYPE_COLORS = {
21
+ comment: 'bg-primary-500',
22
+ mention: 'bg-violet-500',
23
+ assignment: 'bg-sky-500',
24
+ status: 'bg-emerald-500',
25
+ invite: 'bg-teal-500',
26
+ approval: 'bg-amber-500',
27
+ deadline: 'bg-rose-500',
28
+ payment: 'bg-green-600',
29
+ security: 'bg-red-600',
30
+ update: 'bg-indigo-500',
31
+ reaction: 'bg-pink-500',
32
+ system: 'bg-gray-500',
33
+ };
34
+ const TYPE_ACCENT = {
35
+ comment: 'border-primary-400',
36
+ mention: 'border-violet-400',
37
+ assignment: 'border-sky-400',
38
+ status: 'border-emerald-400',
39
+ invite: 'border-teal-400',
40
+ approval: 'border-amber-400',
41
+ deadline: 'border-rose-400',
42
+ payment: 'border-green-500',
43
+ security: 'border-red-500',
44
+ update: 'border-indigo-400',
45
+ reaction: 'border-pink-400',
46
+ system: 'border-gray-400',
47
+ };
48
+ const PRIORITY_COLORS = {
49
+ low: 'bg-gray-400',
50
+ medium: 'bg-amber-400',
51
+ high: 'bg-orange-500',
52
+ urgent: 'bg-red-500',
53
+ };
54
+ function initials(name) {
55
+ return name.split(' ').slice(0, 2).map(p => p[0]).join('').toUpperCase();
56
+ }
57
+ function timeAgo(iso) {
58
+ const diff = (Date.now() - new Date(iso).getTime()) / 1000;
59
+ if (diff < 60)
60
+ return 'just now';
61
+ if (diff < 3600)
62
+ return `${Math.floor(diff / 60)}m ago`;
63
+ if (diff < 86400)
64
+ return `${Math.floor(diff / 3600)}h ago`;
65
+ if (diff < 86400 * 7)
66
+ return `${Math.floor(diff / 86400)}d ago`;
67
+ return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
68
+ }
69
+ function dateLabel(iso) {
70
+ const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000);
71
+ if (diff === 0)
72
+ return 'Today';
73
+ if (diff === 1)
74
+ return 'Yesterday';
75
+ if (diff < 7)
76
+ return 'This week';
77
+ if (diff < 30)
78
+ return 'Earlier this month';
79
+ return 'Older';
80
+ }
81
+ // ─── Component ────────────────────────────────────────────────────────────────
82
+ /**
83
+ * SvNotificationCenterComponent — Premium notification inbox.
84
+ *
85
+ * Selector: `sv-notification-center`
86
+ *
87
+ * Six variants:
88
+ * - `dropdown` Bell + floating panel with tabs, search & actions (default)
89
+ * - `drawer` Bell + full-height right-side slide panel (Slack/Figma style)
90
+ * - `inline` Embedded panel, no trigger — use on dedicated /notifications routes
91
+ * - `feed` Activity-feed style list (GitHub/Jira style)
92
+ * - `compact` Bell + slim dropdown, no tabs/search
93
+ * - `grouped` Bell + dropdown with sticky date-group section headers
94
+ *
95
+ * Floating panels (dropdown, compact, grouped, drawer) are portaled to
96
+ * document.body so they escape every overflow / transform / backdrop-filter
97
+ * ancestor that would otherwise clip or misposition a fixed-positioned panel.
98
+ *
99
+ * @example
100
+ * ```html
101
+ * <sv-notification-center
102
+ * [notifications]="myNotifications"
103
+ * variant="dropdown"
104
+ * (notificationClick)="onNotifClick($event)"
105
+ * (markAllRead)="markAllRead()" />
106
+ * ```
107
+ */
108
+ class SvNotificationCenterComponent {
109
+ platformId = inject(PLATFORM_ID);
110
+ vcr = inject(ViewContainerRef);
111
+ _floatingPanelTpl = viewChild('floatingPanel', ...(ngDevMode ? [{ debugName: "_floatingPanelTpl" }] : /* istanbul ignore next */ []));
112
+ _portalContainer = null;
113
+ _portalView = null;
114
+ /** Trigger element kept for scroll-tracking repositioning. */
115
+ _triggerEl = null;
116
+ _scrollHandler = null;
117
+ _scrollRafId = null;
118
+ // ── Inputs ─────────────────────────────────────────────────────────────────
119
+ /** Presentation: dropdown, drawer, inline, feed, compact or grouped. @default 'dropdown' */
120
+ variant = input('dropdown', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
121
+ /** The notifications to display. */
122
+ notifications = input([], ...(ngDevMode ? [{ debugName: "notifications" }] : /* istanbul ignore next */ []));
123
+ /** Max height of the scrollable list (CSS length). @default '400px' */
124
+ maxHeight = input('400px', ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
125
+ /** Filter tab labels. @default ['All','Unread','Mentions','System'] */
126
+ tabs = input(['All', 'Unread', 'Mentions', 'System'], ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
127
+ /** Panel heading text. @default 'Notifications' */
128
+ panelTitle = input('Notifications', ...(ngDevMode ? [{ debugName: "panelTitle" }] : /* istanbul ignore next */ []));
129
+ /** Show the search box. @default true */
130
+ showSearch = input(true, ...(ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []));
131
+ /** Animate the bell icon on new notifications. @default true */
132
+ animateBell = input(true, ...(ngDevMode ? [{ debugName: "animateBell" }] : /* istanbul ignore next */ []));
133
+ // ── Outputs ────────────────────────────────────────────────────────────────
134
+ /** Emitted when a notification row is clicked. */
135
+ notificationClick = output();
136
+ /** Emitted when an inline action on a notification is clicked. */
137
+ actionClick = output();
138
+ /** Emitted when "mark all as read" is triggered. */
139
+ markAllRead = output();
140
+ /** Emitted when a notification is dismissed. */
141
+ dismiss = output();
142
+ // ── State ──────────────────────────────────────────────────────────────────
143
+ _open = signal(false, ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
144
+ _activeTab = signal('All', ...(ngDevMode ? [{ debugName: "_activeTab" }] : /* istanbul ignore next */ []));
145
+ _search = signal('', ...(ngDevMode ? [{ debugName: "_search" }] : /* istanbul ignore next */ []));
146
+ _items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
147
+ _panelStyle = signal({ right: '0px' }, ...(ngDevMode ? [{ debugName: "_panelStyle" }] : /* istanbul ignore next */ []));
148
+ _clickOutsideHandler = null;
149
+ constructor() {
150
+ effect(() => {
151
+ this._items.set(this.notifications().map(n => ({ ...n })));
152
+ }, { allowSignalWrites: true });
153
+ effect(() => {
154
+ // Clean up portal when variant changes while panel is open
155
+ if (!this._open()) {
156
+ this._destroyPortal();
157
+ }
158
+ });
159
+ }
160
+ ngOnDestroy() {
161
+ this._removeClickOutside();
162
+ this._removeScrollListener();
163
+ this._destroyPortal();
164
+ }
165
+ // ── Computed ───────────────────────────────────────────────────────────────
166
+ unreadCount = computed(() => this._items().filter(n => !n.read).length, ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
167
+ badgeLabel = computed(() => {
168
+ const c = this.unreadCount();
169
+ return c > 99 ? '99+' : c > 0 ? String(c) : '';
170
+ }, ...(ngDevMode ? [{ debugName: "badgeLabel" }] : /* istanbul ignore next */ []));
171
+ filteredItems = computed(() => {
172
+ const tab = this._activeTab();
173
+ const query = this._search().toLowerCase().trim();
174
+ let list = [...this._items()].sort((a, b) => {
175
+ if (a.pinned && !b.pinned)
176
+ return -1;
177
+ if (!a.pinned && b.pinned)
178
+ return 1;
179
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
180
+ });
181
+ if (tab === 'Unread')
182
+ list = list.filter(n => !n.read);
183
+ else if (tab === 'Mentions')
184
+ list = list.filter(n => n.type === 'mention');
185
+ else if (tab === 'System')
186
+ list = list.filter(n => ['system', 'update', 'security'].includes(n.type));
187
+ if (query) {
188
+ list = list.filter(n => n.title.toLowerCase().includes(query) ||
189
+ n.body?.toLowerCase().includes(query) ||
190
+ n.actor?.name.toLowerCase().includes(query) ||
191
+ n.target?.label.toLowerCase().includes(query));
192
+ }
193
+ return list;
194
+ }, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
195
+ groupedItems = computed(() => {
196
+ const map = new Map();
197
+ for (const n of this.filteredItems()) {
198
+ const label = dateLabel(n.timestamp);
199
+ if (!map.has(label))
200
+ map.set(label, []);
201
+ map.get(label).push(n);
202
+ }
203
+ const ORDER = ['Today', 'Yesterday', 'This week', 'Earlier this month', 'Older'];
204
+ return [...map.entries()]
205
+ .sort((a, b) => ORDER.indexOf(a[0]) - ORDER.indexOf(b[0]))
206
+ .map(([label, items]) => ({ label, items }));
207
+ }, ...(ngDevMode ? [{ debugName: "groupedItems" }] : /* istanbul ignore next */ []));
208
+ tabUnread = computed(() => {
209
+ const all = this._items();
210
+ const unread = all.filter(n => !n.read).length;
211
+ const mention = all.filter(n => n.type === 'mention' && !n.read).length;
212
+ const sys = all.filter(n => ['system', 'update', 'security'].includes(n.type) && !n.read).length;
213
+ return { All: unread, Unread: unread, Mentions: mention, System: sys };
214
+ }, ...(ngDevMode ? [{ debugName: "tabUnread" }] : /* istanbul ignore next */ []));
215
+ // ── Panel open / close ─────────────────────────────────────────────────────
216
+ /**
217
+ * Toggles the floating panel. On open, portals the panel to document.body
218
+ * so it is never clipped by overflow:hidden, transform, or backdrop-filter
219
+ * on any ancestor element.
220
+ */
221
+ togglePanel(event) {
222
+ if (this._open()) {
223
+ this._open.set(false);
224
+ this._destroyPortal();
225
+ this._removeClickOutside();
226
+ this._removeScrollListener();
227
+ return;
228
+ }
229
+ if (!isPlatformBrowser(this.platformId))
230
+ return;
231
+ this._triggerEl = event.currentTarget;
232
+ const rect = this._triggerEl.getBoundingClientRect();
233
+ this._panelStyle.set(this._computePosition(rect));
234
+ this._open.set(true);
235
+ // setTimeout allows Angular to process the signal update so the viewChild
236
+ // template ref is available before _createPortal() runs.
237
+ setTimeout(() => {
238
+ this._createPortal();
239
+ this._attachClickOutside();
240
+ this._attachScrollListener();
241
+ }, 0);
242
+ }
243
+ closePanel() {
244
+ this._open.set(false);
245
+ this._destroyPortal();
246
+ this._removeClickOutside();
247
+ this._removeScrollListener();
248
+ }
249
+ // ── Portal helpers ─────────────────────────────────────────────────────────
250
+ /**
251
+ * Computes a viewport-safe position for the floating panel.
252
+ *
253
+ * Horizontal: right-aligned with the trigger's right edge, clamped so the
254
+ * panel never overflows the left viewport edge.
255
+ *
256
+ * Vertical (flip logic):
257
+ * - Prefer BELOW the trigger when there is at least 280 px of clearance.
258
+ * - Flip ABOVE when there is more space above than below.
259
+ * - `maxH` caps the panel so it never clips the opposite viewport edge.
260
+ */
261
+ _computePosition(rect) {
262
+ const GAP = 8;
263
+ const EDGE = 8;
264
+ const vw = window.innerWidth;
265
+ const vh = window.innerHeight;
266
+ const panelW = 416;
267
+ // ── Horizontal ──────────────────────────────────────────────────────────
268
+ const rawRight = vw - rect.right;
269
+ const rightVal = Math.max(EDGE, Math.min(rawRight, vw - panelW - EDGE));
270
+ // ── Vertical (flip) ─────────────────────────────────────────────────────
271
+ const spaceBelow = vh - rect.bottom - GAP - EDGE;
272
+ const spaceAbove = rect.top - GAP - EDGE;
273
+ const minFit = 280; // px — minimum useful panel height
274
+ if (spaceBelow >= minFit || spaceBelow >= spaceAbove) {
275
+ // Show below — anchor top, cap height
276
+ return {
277
+ top: `${rect.bottom + GAP}px`,
278
+ right: `${rightVal}px`,
279
+ maxH: `${Math.max(minFit, spaceBelow)}px`,
280
+ };
281
+ }
282
+ else {
283
+ // Flip above — anchor bottom (panel grows upward), cap height
284
+ return {
285
+ bottom: `${vh - rect.top + GAP}px`,
286
+ right: `${rightVal}px`,
287
+ maxH: `${Math.max(minFit, spaceAbove)}px`,
288
+ };
289
+ }
290
+ }
291
+ /**
292
+ * Appends a portal host <div> to document.body and renders the
293
+ * #floatingPanel template into it via Angular's ViewContainerRef.
294
+ *
295
+ * The host div has no transform / filter / overflow, so position:fixed
296
+ * children are viewport-relative in all browsers.
297
+ */
298
+ _createPortal() {
299
+ const tpl = this._floatingPanelTpl();
300
+ if (!tpl || this._portalContainer)
301
+ return;
302
+ const container = document.createElement('div');
303
+ container.setAttribute('data-nc-portal', '');
304
+ document.body.appendChild(container);
305
+ this._portalContainer = container;
306
+ // EmbeddedViewRef remains in Angular's view/CD tree even though
307
+ // its DOM nodes are moved to document.body.
308
+ const view = this.vcr.createEmbeddedView(tpl);
309
+ view.rootNodes.forEach(node => container.appendChild(node));
310
+ this._portalView = view;
311
+ }
312
+ _destroyPortal() {
313
+ this._portalView?.destroy();
314
+ this._portalView = null;
315
+ this._portalContainer?.remove();
316
+ this._portalContainer = null;
317
+ }
318
+ // ── Scroll / resize tracking ───────────────────────────────────────────────
319
+ /**
320
+ * Recomputes panel position on every scroll / resize so the panel
321
+ * tracks the trigger button as the page scrolls.
322
+ * Uses requestAnimationFrame to throttle updates.
323
+ */
324
+ _attachScrollListener() {
325
+ this._removeScrollListener();
326
+ let pending = false;
327
+ this._scrollHandler = () => {
328
+ if (pending)
329
+ return;
330
+ pending = true;
331
+ this._scrollRafId = requestAnimationFrame(() => {
332
+ pending = false;
333
+ if (this._triggerEl && this._open()) {
334
+ this._panelStyle.set(this._computePosition(this._triggerEl.getBoundingClientRect()));
335
+ }
336
+ });
337
+ };
338
+ // capture:true catches scroll events on any nested scrollable ancestor
339
+ window.addEventListener('scroll', this._scrollHandler, { passive: true, capture: true });
340
+ window.addEventListener('resize', this._scrollHandler, { passive: true });
341
+ }
342
+ _removeScrollListener() {
343
+ if (this._scrollHandler) {
344
+ window.removeEventListener('scroll', this._scrollHandler, true);
345
+ window.removeEventListener('resize', this._scrollHandler);
346
+ this._scrollHandler = null;
347
+ }
348
+ if (this._scrollRafId !== null) {
349
+ cancelAnimationFrame(this._scrollRafId);
350
+ this._scrollRafId = null;
351
+ }
352
+ }
353
+ // ── Click-outside ──────────────────────────────────────────────────────────
354
+ _attachClickOutside() {
355
+ this._removeClickOutside();
356
+ this._clickOutsideHandler = (e) => {
357
+ const target = e.target;
358
+ const inHost = target.closest('sv-notification-center, sv-notification-center');
359
+ const inPortal = target.closest('[data-nc-portal]');
360
+ if (!inHost && !inPortal)
361
+ this.closePanel();
362
+ };
363
+ document.addEventListener('click', this._clickOutsideHandler, true);
364
+ }
365
+ _removeClickOutside() {
366
+ if (this._clickOutsideHandler) {
367
+ document.removeEventListener('click', this._clickOutsideHandler, true);
368
+ this._clickOutsideHandler = null;
369
+ }
370
+ }
371
+ // ── Actions ────────────────────────────────────────────────────────────────
372
+ onNotificationClick(item) {
373
+ this._items.update(list => list.map(n => n.id === item.id ? { ...n, read: true } : n));
374
+ this.notificationClick.emit({ notification: { ...item, read: true } });
375
+ }
376
+ onActionClick(item, actionId, event) {
377
+ event.stopPropagation();
378
+ this.actionClick.emit({ notification: item, actionId });
379
+ }
380
+ onDismiss(item, event) {
381
+ event.stopPropagation();
382
+ this._items.update(list => list.filter(n => n.id !== item.id));
383
+ this.dismiss.emit({ notification: item });
384
+ }
385
+ onMarkAllRead() {
386
+ this._items.update(list => list.map(n => ({ ...n, read: true })));
387
+ this.markAllRead.emit();
388
+ }
389
+ setTab(tab) {
390
+ this._activeTab.set(tab);
391
+ this._search.set('');
392
+ }
393
+ // ── Template helpers ───────────────────────────────────────────────────────
394
+ typeIcon(type) { return TYPE_ICONS[type]; }
395
+ typeColor(type) { return TYPE_COLORS[type]; }
396
+ typeAccent(type) { return TYPE_ACCENT[type]; }
397
+ priorityColor(p) { return PRIORITY_COLORS[p]; }
398
+ initials(name) { return initials(name); }
399
+ timeAgo(iso) { return timeAgo(iso); }
400
+ dateLabel(iso) { return dateLabel(iso); }
401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvNotificationCenterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
402
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvNotificationCenterComponent, isStandalone: true, selector: "sv-notification-center", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, notifications: { classPropertyName: "notifications", publicName: "notifications", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: false, transformFunction: null }, panelTitle: { classPropertyName: "panelTitle", publicName: "panelTitle", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, animateBell: { classPropertyName: "animateBell", publicName: "animateBell", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { notificationClick: "notificationClick", actionClick: "actionClick", markAllRead: "markAllRead", dismiss: "dismiss" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "_floatingPanelTpl", first: true, predicate: ["floatingPanel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- \u2500\u2500\u2500 Notification Center \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<!-- Bell-trigger variants render only the trigger here. -->\n<!-- Floating panels are rendered via a body-level portal (#floatingPanel) -->\n<!-- so they escape every overflow / transform / backdrop-filter ancestor. -->\n\n@switch (variant()) {\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- VARIANT: dropdown \u2014 bell trigger only; panel portaled to body -->\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @case ('dropdown') {\n <div class=\"relative inline-flex\">\n <button\n type=\"button\"\n (click)=\"togglePanel($event)\"\n [attr.aria-expanded]=\"_open()\"\n aria-haspopup=\"true\"\n aria-label=\"Open notifications\"\n class=\"relative inline-flex items-center justify-center w-10 h-10 rounded-xl\n text-gray-500 dark:text-gray-400\n hover:text-gray-700 dark:hover:text-gray-200\n hover:bg-gray-100 dark:hover:bg-gray-800\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n transition-colors duration-150\">\n <svg class=\"w-5 h-5\" [class.animate-bounce]=\"animateBell() && unreadCount() > 0\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"1.8\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\"/>\n </svg>\n @if (badgeLabel()) {\n <span class=\"absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] px-1\n flex items-center justify-center rounded-full\n bg-rose-500 text-white text-[10px] font-bold leading-none\n ring-2 ring-white dark:ring-gray-900\">\n {{ badgeLabel() }}\n </span>\n }\n </button>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- VARIANT: drawer \u2014 bell trigger only; panel portaled to body -->\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @case ('inline') {\n <div class=\"rounded-2xl border border-gray-200 dark:border-gray-700\n bg-white dark:bg-gray-900 shadow-sm overflow-hidden\">\n\n <div class=\"flex items-center justify-between px-5 py-4\n border-b border-gray-100 dark:border-gray-800\">\n <div class=\"flex items-center gap-2.5\">\n <h2 class=\"text-base font-semibold text-gray-900 dark:text-gray-100\">{{ panelTitle() }}</h2>\n @if (unreadCount() > 0) {\n <span class=\"px-2 py-0.5 rounded-full bg-primary-100 dark:bg-primary-900/40\n text-primary-700 dark:text-primary-300 text-xs font-semibold\">\n {{ unreadCount() }}\n </span>\n }\n </div>\n @if (unreadCount() > 0) {\n <button type=\"button\" (click)=\"onMarkAllRead()\"\n class=\"text-xs text-primary-600 dark:text-primary-400 hover:underline focus-visible:outline-none\">\n Mark all read\n </button>\n }\n </div>\n\n @if (showSearch()) {\n <div class=\"px-5 pt-3.5\">\n <div class=\"relative\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"/>\n </svg>\n <input type=\"search\" placeholder=\"Search notifications\u2026\"\n [value]=\"_search()\"\n (input)=\"_search.set($any($event.target).value)\"\n class=\"w-full pl-9 pr-3 py-2.5 text-sm rounded-xl\n bg-gray-50 dark:bg-gray-800\n border border-gray-200 dark:border-gray-700\n text-gray-900 dark:text-gray-100\n placeholder:text-gray-400\n focus:outline-none focus:ring-2 focus:ring-primary-500/40\"/>\n </div>\n </div>\n }\n\n @if (tabs().length > 0) {\n <div class=\"flex gap-0.5 px-5 pt-3 border-b border-gray-100 dark:border-gray-800\">\n @for (tab of tabs(); track tab) {\n <button type=\"button\" (click)=\"setTab(tab)\"\n class=\"relative px-3 py-2 text-sm font-medium rounded-t-lg focus-visible:outline-none transition-colors\"\n [class]=\"_activeTab() === tab\n ? 'text-primary-600 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'\">\n {{ tab }}\n @if (tabUnread()[tab] > 0) {\n <span class=\"ml-1 px-1.5 py-0.5 rounded-full bg-rose-100 dark:bg-rose-900/40 text-rose-600 dark:text-rose-400 text-xs font-bold\">\n {{ tabUnread()[tab] }}\n </span>\n }\n @if (_activeTab() === tab) {\n <span class=\"absolute bottom-0 left-0 right-0 h-0.5 bg-primary-500 rounded-full\"></span>\n }\n </button>\n }\n </div>\n }\n\n <div class=\"overflow-y-auto\" [style.max-height]=\"maxHeight()\">\n @if (filteredItems().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n @for (item of filteredItems(); track item.id) {\n <ng-container *ngTemplateOutlet=\"notifRow; context: { $implicit: item }\"></ng-container>\n }\n }\n </div>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- VARIANT: feed \u2014 activity-feed style list -->\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n}\n\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n PORTAL TEMPLATE \u2014 rendered to document.body on open\n No position:fixed ancestor clipping can reach here.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<ng-template #floatingPanel>\n @switch (variant()) {\n\n <!-- \u2500\u2500\u2500 Dropdown panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @case ('dropdown') {\n <div\n class=\"fixed z-[9999] w-[26rem] max-w-[calc(100vw-1rem)]\n rounded-2xl shadow-2xl shadow-black/15 dark:shadow-black/50\n bg-white dark:bg-gray-900\n border border-gray-200/70 dark:border-gray-700/60\n overflow-hidden flex flex-col\"\n [style.top]=\"_panelStyle().top\"\n [style.bottom]=\"_panelStyle().bottom\"\n [style.right]=\"_panelStyle().right\"\n [style.max-height]=\"_panelStyle().maxH\">\n\n <!-- Header -->\n <div class=\"flex items-center justify-between px-5 py-4\n border-b border-gray-100 dark:border-gray-800\">\n <div class=\"flex items-center gap-2.5\">\n <span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ panelTitle() }}</span>\n @if (unreadCount() > 0) {\n <span class=\"px-2 py-0.5 rounded-full bg-primary-100 dark:bg-primary-900/40\n text-primary-700 dark:text-primary-300 text-[11px] font-semibold\">\n {{ unreadCount() }} new\n </span>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n @if (unreadCount() > 0) {\n <button type=\"button\" (click)=\"onMarkAllRead()\"\n class=\"text-xs text-primary-600 dark:text-primary-400 hover:underline focus-visible:outline-none\">\n Mark all read\n </button>\n }\n <button type=\"button\" (click)=\"closePanel()\" aria-label=\"Close\"\n class=\"w-7 h-7 flex items-center justify-center rounded-lg\n text-gray-400 hover:text-gray-600 dark:hover:text-gray-300\n hover:bg-gray-100 dark:hover:bg-gray-800\n focus-visible:outline-none transition-colors\">\n <svg class=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Search -->\n @if (showSearch()) {\n <div class=\"px-4 pt-3\">\n <div class=\"relative\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"/>\n </svg>\n <input type=\"search\" placeholder=\"Search notifications\u2026\"\n [value]=\"_search()\"\n (input)=\"_search.set($any($event.target).value)\"\n class=\"w-full pl-9 pr-3 py-2 text-sm rounded-xl\n bg-gray-50 dark:bg-gray-800\n border border-gray-200 dark:border-gray-700\n text-gray-900 dark:text-gray-100\n placeholder:text-gray-400 dark:placeholder:text-gray-500\n focus:outline-none focus:ring-2 focus:ring-primary-500/40 focus:border-primary-400\n transition-shadow\"/>\n </div>\n </div>\n }\n\n <!-- Tabs -->\n @if (tabs().length > 0) {\n <div class=\"flex gap-0.5 px-4 pt-3 border-b border-gray-100 dark:border-gray-800\">\n @for (tab of tabs(); track tab) {\n <button type=\"button\" (click)=\"setTab(tab)\"\n class=\"relative px-3 py-2 text-xs font-medium rounded-t-lg\n focus-visible:outline-none transition-colors\"\n [class]=\"_activeTab() === tab\n ? 'text-primary-600 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'\">\n {{ tab }}\n @if (tabUnread()[tab] > 0) {\n <span class=\"ml-1 px-1.5 py-0.5 rounded-full\n bg-rose-100 dark:bg-rose-900/40\n text-rose-600 dark:text-rose-400 text-[10px] font-bold\">\n {{ tabUnread()[tab] }}\n </span>\n }\n @if (_activeTab() === tab) {\n <span class=\"absolute bottom-0 left-0 right-0 h-0.5 bg-primary-500 rounded-full\"></span>\n }\n </button>\n }\n </div>\n }\n\n <!-- List -->\n <div class=\"overflow-y-auto\" [style.max-height]=\"maxHeight()\">\n @if (filteredItems().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n @for (item of filteredItems(); track item.id) {\n <ng-container *ngTemplateOutlet=\"notifRow; context: { $implicit: item }\"></ng-container>\n }\n }\n </div>\n\n <!-- Footer -->\n <div class=\"px-5 py-3 border-t border-gray-100 dark:border-gray-800\n flex items-center justify-between\">\n <span class=\"text-xs text-gray-400\">\n {{ filteredItems().length }} notification{{ filteredItems().length !== 1 ? 's' : '' }}\n </span>\n <button type=\"button\"\n class=\"text-xs font-medium text-primary-600 dark:text-primary-400\n hover:underline focus-visible:outline-none\">\n View all\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500\u2500 Compact panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n }\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Shared: notification row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Used by dropdown / drawer / inline / grouped (via *ngTemplateOutlet).\n Context: { $implicit: NotificationItem }\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #notifRow let-item>\n <div\n (click)=\"onNotificationClick(item)\"\n class=\"group relative flex gap-3.5 px-5 py-4 cursor-pointer\n border-b border-gray-100 dark:border-gray-800/60 last:border-0\n transition-colors duration-100\"\n [class]=\"!item.read\n ? 'bg-primary-50/50 dark:bg-primary-950/20 hover:bg-primary-50/80 dark:hover:bg-primary-950/30'\n : 'hover:bg-gray-50/80 dark:hover:bg-gray-800/50'\">\n\n @if (!item.read) {\n <span class=\"absolute left-0 top-3 bottom-3 w-0.5 rounded-full bg-primary-500\"></span>\n }\n\n <div class=\"relative shrink-0 mt-0.5\">\n @if (item.actor?.avatar) {\n <img [src]=\"item.actor!.avatar\" [alt]=\"item.actor!.name\"\n class=\"w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-gray-900\"/>\n } @else if (item.actor) {\n <div class=\"w-10 h-10 rounded-full flex items-center justify-center\n text-white text-sm font-semibold ring-2 ring-white dark:ring-gray-900\"\n [class]=\"typeColor(item.type)\">\n {{ initials(item.actor.name) }}\n </div>\n } @else {\n <div class=\"w-10 h-10 rounded-xl flex items-center justify-center text-white\"\n [class]=\"typeColor(item.type)\">\n <svg class=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" [attr.d]=\"typeIcon(item.type)\"/>\n </svg>\n </div>\n }\n @if (item.actor) {\n <span class=\"absolute -bottom-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center\n text-white ring-2 ring-white dark:ring-gray-900\"\n [class]=\"typeColor(item.type)\">\n <svg class=\"w-2.5 h-2.5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" [attr.d]=\"typeIcon(item.type)\"/>\n </svg>\n </span>\n }\n </div>\n\n <div class=\"flex-1 min-w-0 pr-4\">\n <div class=\"flex items-start justify-between gap-3 mb-0.5\">\n <p class=\"text-[13.5px] leading-snug text-gray-900 dark:text-gray-100\"\n [class.font-semibold]=\"!item.read\"\n [class.font-normal]=\"item.read\">\n {{ item.title }}\n </p>\n <div class=\"flex items-center gap-1.5 shrink-0 mt-0.5\">\n @if (item.priority && item.priority !== 'low') {\n <span class=\"w-2 h-2 rounded-full shrink-0\" [class]=\"priorityColor(item.priority!)\"></span>\n }\n <span class=\"text-[11px] text-gray-400 whitespace-nowrap\">{{ timeAgo(item.timestamp) }}</span>\n </div>\n </div>\n\n @if (item.body) {\n <p class=\"text-xs text-gray-500 dark:text-gray-400 leading-relaxed line-clamp-2 mt-0.5\">\n {{ item.body }}\n </p>\n }\n\n @if (item.target) {\n <p class=\"text-xs text-primary-600 dark:text-primary-400 mt-1 truncate font-medium\">\n {{ item.target.label }}\n </p>\n }\n\n @if (item.actions && item.actions.length > 0) {\n <div class=\"flex flex-wrap gap-2 mt-2.5\" (click)=\"$event.stopPropagation()\">\n @for (action of item.actions; track action.id) {\n <button type=\"button\"\n (click)=\"onActionClick(item, action.id, $event)\"\n class=\"px-3 py-1.5 rounded-lg text-xs font-medium\n transition-colors focus-visible:outline-none focus-visible:ring-2\"\n [class]=\"action.variant === 'primary'\n ? 'bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500'\n : action.variant === 'danger'\n ? 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50 focus-visible:ring-red-500'\n : 'bg-gray-100 dark:bg-gray-700/80 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus-visible:ring-gray-400'\">\n {{ action.label }}\n </button>\n }\n </div>\n }\n </div>\n\n <button type=\"button\"\n (click)=\"onDismiss(item, $event)\"\n aria-label=\"Dismiss notification\"\n class=\"absolute top-3 right-3 w-6 h-6 flex items-center justify-center rounded-lg\n text-gray-300 dark:text-gray-600\n hover:text-gray-500 dark:hover:text-gray-400\n hover:bg-gray-100 dark:hover:bg-gray-700\n opacity-0 group-hover:opacity-100 transition-opacity\n focus-visible:outline-none focus-visible:opacity-100\">\n <svg class=\"w-3.5 h-3.5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"/>\n </svg>\n </button>\n </div>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Shared: empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #emptyTpl>\n <div class=\"flex flex-col items-center justify-center py-14 text-center\">\n <div class=\"w-12 h-12 rounded-2xl bg-gray-100 dark:bg-gray-800\n flex items-center justify-center mb-3\">\n <svg class=\"w-6 h-6 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\"\n stroke=\"currentColor\" stroke-width=\"1.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\"/>\n </svg>\n </div>\n <p class=\"text-sm font-medium text-gray-600 dark:text-gray-400\">You're all caught up!</p>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 mt-1\">No notifications to show</p>\n </div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
403
+ }
404
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvNotificationCenterComponent, decorators: [{
405
+ type: Component,
406
+ args: [{ selector: 'sv-notification-center', standalone: true, imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- \u2500\u2500\u2500 Notification Center \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<!-- Bell-trigger variants render only the trigger here. -->\n<!-- Floating panels are rendered via a body-level portal (#floatingPanel) -->\n<!-- so they escape every overflow / transform / backdrop-filter ancestor. -->\n\n@switch (variant()) {\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- VARIANT: dropdown \u2014 bell trigger only; panel portaled to body -->\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @case ('dropdown') {\n <div class=\"relative inline-flex\">\n <button\n type=\"button\"\n (click)=\"togglePanel($event)\"\n [attr.aria-expanded]=\"_open()\"\n aria-haspopup=\"true\"\n aria-label=\"Open notifications\"\n class=\"relative inline-flex items-center justify-center w-10 h-10 rounded-xl\n text-gray-500 dark:text-gray-400\n hover:text-gray-700 dark:hover:text-gray-200\n hover:bg-gray-100 dark:hover:bg-gray-800\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n transition-colors duration-150\">\n <svg class=\"w-5 h-5\" [class.animate-bounce]=\"animateBell() && unreadCount() > 0\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"1.8\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\"/>\n </svg>\n @if (badgeLabel()) {\n <span class=\"absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] px-1\n flex items-center justify-center rounded-full\n bg-rose-500 text-white text-[10px] font-bold leading-none\n ring-2 ring-white dark:ring-gray-900\">\n {{ badgeLabel() }}\n </span>\n }\n </button>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- VARIANT: drawer \u2014 bell trigger only; panel portaled to body -->\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @case ('inline') {\n <div class=\"rounded-2xl border border-gray-200 dark:border-gray-700\n bg-white dark:bg-gray-900 shadow-sm overflow-hidden\">\n\n <div class=\"flex items-center justify-between px-5 py-4\n border-b border-gray-100 dark:border-gray-800\">\n <div class=\"flex items-center gap-2.5\">\n <h2 class=\"text-base font-semibold text-gray-900 dark:text-gray-100\">{{ panelTitle() }}</h2>\n @if (unreadCount() > 0) {\n <span class=\"px-2 py-0.5 rounded-full bg-primary-100 dark:bg-primary-900/40\n text-primary-700 dark:text-primary-300 text-xs font-semibold\">\n {{ unreadCount() }}\n </span>\n }\n </div>\n @if (unreadCount() > 0) {\n <button type=\"button\" (click)=\"onMarkAllRead()\"\n class=\"text-xs text-primary-600 dark:text-primary-400 hover:underline focus-visible:outline-none\">\n Mark all read\n </button>\n }\n </div>\n\n @if (showSearch()) {\n <div class=\"px-5 pt-3.5\">\n <div class=\"relative\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"/>\n </svg>\n <input type=\"search\" placeholder=\"Search notifications\u2026\"\n [value]=\"_search()\"\n (input)=\"_search.set($any($event.target).value)\"\n class=\"w-full pl-9 pr-3 py-2.5 text-sm rounded-xl\n bg-gray-50 dark:bg-gray-800\n border border-gray-200 dark:border-gray-700\n text-gray-900 dark:text-gray-100\n placeholder:text-gray-400\n focus:outline-none focus:ring-2 focus:ring-primary-500/40\"/>\n </div>\n </div>\n }\n\n @if (tabs().length > 0) {\n <div class=\"flex gap-0.5 px-5 pt-3 border-b border-gray-100 dark:border-gray-800\">\n @for (tab of tabs(); track tab) {\n <button type=\"button\" (click)=\"setTab(tab)\"\n class=\"relative px-3 py-2 text-sm font-medium rounded-t-lg focus-visible:outline-none transition-colors\"\n [class]=\"_activeTab() === tab\n ? 'text-primary-600 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'\">\n {{ tab }}\n @if (tabUnread()[tab] > 0) {\n <span class=\"ml-1 px-1.5 py-0.5 rounded-full bg-rose-100 dark:bg-rose-900/40 text-rose-600 dark:text-rose-400 text-xs font-bold\">\n {{ tabUnread()[tab] }}\n </span>\n }\n @if (_activeTab() === tab) {\n <span class=\"absolute bottom-0 left-0 right-0 h-0.5 bg-primary-500 rounded-full\"></span>\n }\n </button>\n }\n </div>\n }\n\n <div class=\"overflow-y-auto\" [style.max-height]=\"maxHeight()\">\n @if (filteredItems().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n @for (item of filteredItems(); track item.id) {\n <ng-container *ngTemplateOutlet=\"notifRow; context: { $implicit: item }\"></ng-container>\n }\n }\n </div>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- VARIANT: feed \u2014 activity-feed style list -->\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n}\n\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n PORTAL TEMPLATE \u2014 rendered to document.body on open\n No position:fixed ancestor clipping can reach here.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<ng-template #floatingPanel>\n @switch (variant()) {\n\n <!-- \u2500\u2500\u2500 Dropdown panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @case ('dropdown') {\n <div\n class=\"fixed z-[9999] w-[26rem] max-w-[calc(100vw-1rem)]\n rounded-2xl shadow-2xl shadow-black/15 dark:shadow-black/50\n bg-white dark:bg-gray-900\n border border-gray-200/70 dark:border-gray-700/60\n overflow-hidden flex flex-col\"\n [style.top]=\"_panelStyle().top\"\n [style.bottom]=\"_panelStyle().bottom\"\n [style.right]=\"_panelStyle().right\"\n [style.max-height]=\"_panelStyle().maxH\">\n\n <!-- Header -->\n <div class=\"flex items-center justify-between px-5 py-4\n border-b border-gray-100 dark:border-gray-800\">\n <div class=\"flex items-center gap-2.5\">\n <span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ panelTitle() }}</span>\n @if (unreadCount() > 0) {\n <span class=\"px-2 py-0.5 rounded-full bg-primary-100 dark:bg-primary-900/40\n text-primary-700 dark:text-primary-300 text-[11px] font-semibold\">\n {{ unreadCount() }} new\n </span>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n @if (unreadCount() > 0) {\n <button type=\"button\" (click)=\"onMarkAllRead()\"\n class=\"text-xs text-primary-600 dark:text-primary-400 hover:underline focus-visible:outline-none\">\n Mark all read\n </button>\n }\n <button type=\"button\" (click)=\"closePanel()\" aria-label=\"Close\"\n class=\"w-7 h-7 flex items-center justify-center rounded-lg\n text-gray-400 hover:text-gray-600 dark:hover:text-gray-300\n hover:bg-gray-100 dark:hover:bg-gray-800\n focus-visible:outline-none transition-colors\">\n <svg class=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Search -->\n @if (showSearch()) {\n <div class=\"px-4 pt-3\">\n <div class=\"relative\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"/>\n </svg>\n <input type=\"search\" placeholder=\"Search notifications\u2026\"\n [value]=\"_search()\"\n (input)=\"_search.set($any($event.target).value)\"\n class=\"w-full pl-9 pr-3 py-2 text-sm rounded-xl\n bg-gray-50 dark:bg-gray-800\n border border-gray-200 dark:border-gray-700\n text-gray-900 dark:text-gray-100\n placeholder:text-gray-400 dark:placeholder:text-gray-500\n focus:outline-none focus:ring-2 focus:ring-primary-500/40 focus:border-primary-400\n transition-shadow\"/>\n </div>\n </div>\n }\n\n <!-- Tabs -->\n @if (tabs().length > 0) {\n <div class=\"flex gap-0.5 px-4 pt-3 border-b border-gray-100 dark:border-gray-800\">\n @for (tab of tabs(); track tab) {\n <button type=\"button\" (click)=\"setTab(tab)\"\n class=\"relative px-3 py-2 text-xs font-medium rounded-t-lg\n focus-visible:outline-none transition-colors\"\n [class]=\"_activeTab() === tab\n ? 'text-primary-600 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'\">\n {{ tab }}\n @if (tabUnread()[tab] > 0) {\n <span class=\"ml-1 px-1.5 py-0.5 rounded-full\n bg-rose-100 dark:bg-rose-900/40\n text-rose-600 dark:text-rose-400 text-[10px] font-bold\">\n {{ tabUnread()[tab] }}\n </span>\n }\n @if (_activeTab() === tab) {\n <span class=\"absolute bottom-0 left-0 right-0 h-0.5 bg-primary-500 rounded-full\"></span>\n }\n </button>\n }\n </div>\n }\n\n <!-- List -->\n <div class=\"overflow-y-auto\" [style.max-height]=\"maxHeight()\">\n @if (filteredItems().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n @for (item of filteredItems(); track item.id) {\n <ng-container *ngTemplateOutlet=\"notifRow; context: { $implicit: item }\"></ng-container>\n }\n }\n </div>\n\n <!-- Footer -->\n <div class=\"px-5 py-3 border-t border-gray-100 dark:border-gray-800\n flex items-center justify-between\">\n <span class=\"text-xs text-gray-400\">\n {{ filteredItems().length }} notification{{ filteredItems().length !== 1 ? 's' : '' }}\n </span>\n <button type=\"button\"\n class=\"text-xs font-medium text-primary-600 dark:text-primary-400\n hover:underline focus-visible:outline-none\">\n View all\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500\u2500 Compact panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n }\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Shared: notification row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Used by dropdown / drawer / inline / grouped (via *ngTemplateOutlet).\n Context: { $implicit: NotificationItem }\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #notifRow let-item>\n <div\n (click)=\"onNotificationClick(item)\"\n class=\"group relative flex gap-3.5 px-5 py-4 cursor-pointer\n border-b border-gray-100 dark:border-gray-800/60 last:border-0\n transition-colors duration-100\"\n [class]=\"!item.read\n ? 'bg-primary-50/50 dark:bg-primary-950/20 hover:bg-primary-50/80 dark:hover:bg-primary-950/30'\n : 'hover:bg-gray-50/80 dark:hover:bg-gray-800/50'\">\n\n @if (!item.read) {\n <span class=\"absolute left-0 top-3 bottom-3 w-0.5 rounded-full bg-primary-500\"></span>\n }\n\n <div class=\"relative shrink-0 mt-0.5\">\n @if (item.actor?.avatar) {\n <img [src]=\"item.actor!.avatar\" [alt]=\"item.actor!.name\"\n class=\"w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-gray-900\"/>\n } @else if (item.actor) {\n <div class=\"w-10 h-10 rounded-full flex items-center justify-center\n text-white text-sm font-semibold ring-2 ring-white dark:ring-gray-900\"\n [class]=\"typeColor(item.type)\">\n {{ initials(item.actor.name) }}\n </div>\n } @else {\n <div class=\"w-10 h-10 rounded-xl flex items-center justify-center text-white\"\n [class]=\"typeColor(item.type)\">\n <svg class=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" [attr.d]=\"typeIcon(item.type)\"/>\n </svg>\n </div>\n }\n @if (item.actor) {\n <span class=\"absolute -bottom-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center\n text-white ring-2 ring-white dark:ring-gray-900\"\n [class]=\"typeColor(item.type)\">\n <svg class=\"w-2.5 h-2.5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" [attr.d]=\"typeIcon(item.type)\"/>\n </svg>\n </span>\n }\n </div>\n\n <div class=\"flex-1 min-w-0 pr-4\">\n <div class=\"flex items-start justify-between gap-3 mb-0.5\">\n <p class=\"text-[13.5px] leading-snug text-gray-900 dark:text-gray-100\"\n [class.font-semibold]=\"!item.read\"\n [class.font-normal]=\"item.read\">\n {{ item.title }}\n </p>\n <div class=\"flex items-center gap-1.5 shrink-0 mt-0.5\">\n @if (item.priority && item.priority !== 'low') {\n <span class=\"w-2 h-2 rounded-full shrink-0\" [class]=\"priorityColor(item.priority!)\"></span>\n }\n <span class=\"text-[11px] text-gray-400 whitespace-nowrap\">{{ timeAgo(item.timestamp) }}</span>\n </div>\n </div>\n\n @if (item.body) {\n <p class=\"text-xs text-gray-500 dark:text-gray-400 leading-relaxed line-clamp-2 mt-0.5\">\n {{ item.body }}\n </p>\n }\n\n @if (item.target) {\n <p class=\"text-xs text-primary-600 dark:text-primary-400 mt-1 truncate font-medium\">\n {{ item.target.label }}\n </p>\n }\n\n @if (item.actions && item.actions.length > 0) {\n <div class=\"flex flex-wrap gap-2 mt-2.5\" (click)=\"$event.stopPropagation()\">\n @for (action of item.actions; track action.id) {\n <button type=\"button\"\n (click)=\"onActionClick(item, action.id, $event)\"\n class=\"px-3 py-1.5 rounded-lg text-xs font-medium\n transition-colors focus-visible:outline-none focus-visible:ring-2\"\n [class]=\"action.variant === 'primary'\n ? 'bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500'\n : action.variant === 'danger'\n ? 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50 focus-visible:ring-red-500'\n : 'bg-gray-100 dark:bg-gray-700/80 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus-visible:ring-gray-400'\">\n {{ action.label }}\n </button>\n }\n </div>\n }\n </div>\n\n <button type=\"button\"\n (click)=\"onDismiss(item, $event)\"\n aria-label=\"Dismiss notification\"\n class=\"absolute top-3 right-3 w-6 h-6 flex items-center justify-center rounded-lg\n text-gray-300 dark:text-gray-600\n hover:text-gray-500 dark:hover:text-gray-400\n hover:bg-gray-100 dark:hover:bg-gray-700\n opacity-0 group-hover:opacity-100 transition-opacity\n focus-visible:outline-none focus-visible:opacity-100\">\n <svg class=\"w-3.5 h-3.5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"/>\n </svg>\n </button>\n </div>\n</ng-template>\n\n<!-- \u2500\u2500\u2500 Shared: empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #emptyTpl>\n <div class=\"flex flex-col items-center justify-center py-14 text-center\">\n <div class=\"w-12 h-12 rounded-2xl bg-gray-100 dark:bg-gray-800\n flex items-center justify-center mb-3\">\n <svg class=\"w-6 h-6 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\"\n stroke=\"currentColor\" stroke-width=\"1.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\"/>\n </svg>\n </div>\n <p class=\"text-sm font-medium text-gray-600 dark:text-gray-400\">You're all caught up!</p>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 mt-1\">No notifications to show</p>\n </div>\n</ng-template>\n" }]
407
+ }], ctorParameters: () => [], propDecorators: { _floatingPanelTpl: [{ type: i0.ViewChild, args: ['floatingPanel', { isSignal: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], notifications: [{ type: i0.Input, args: [{ isSignal: true, alias: "notifications", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], tabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabs", required: false }] }], panelTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelTitle", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], animateBell: [{ type: i0.Input, args: [{ isSignal: true, alias: "animateBell", required: false }] }], notificationClick: [{ type: i0.Output, args: ["notificationClick"] }], actionClick: [{ type: i0.Output, args: ["actionClick"] }], markAllRead: [{ type: i0.Output, args: ["markAllRead"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
408
+
409
+ /**
410
+ * Generated bundle index. Do not edit.
411
+ */
412
+
413
+ export { SvNotificationCenterComponent };
414
+ //# sourceMappingURL=styloviz-notification-center.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styloviz-notification-center.mjs","sources":["../../../../projects/notification-center-free/src/lib/notification-center.component.ts","../../../../projects/notification-center-free/src/lib/notification-center.component.html","../../../../projects/notification-center-free/src/styloviz-notification-center.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n EmbeddedViewRef,\n effect,\n inject,\n input,\n OnDestroy,\n output,\n PLATFORM_ID,\n signal,\n TemplateRef,\n ViewContainerRef,\n viewChild,\n} from '@angular/core';\nimport { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';\n\nimport type {\n NotificationActionClickEvent,\n NotificationCenterVariant,\n NotificationClickEvent,\n NotificationDismissEvent,\n NotificationItem,\n NotificationPriority,\n NotificationType,\n} from './notification-center.types';\n\n// ─── Icon registry ────────────────────────────────────────────────────────────\n\nconst TYPE_ICONS: Record<NotificationType, string> = {\n comment: 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',\n mention: 'M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207',\n assignment: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4',\n status: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',\n invite: 'M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z',\n approval: 'M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z',\n deadline: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',\n payment: 'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z',\n security: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',\n update: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',\n reaction: 'M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',\n system: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',\n};\n\nconst TYPE_COLORS: Record<NotificationType, string> = {\n comment: 'bg-primary-500',\n mention: 'bg-violet-500',\n assignment: 'bg-sky-500',\n status: 'bg-emerald-500',\n invite: 'bg-teal-500',\n approval: 'bg-amber-500',\n deadline: 'bg-rose-500',\n payment: 'bg-green-600',\n security: 'bg-red-600',\n update: 'bg-indigo-500',\n reaction: 'bg-pink-500',\n system: 'bg-gray-500',\n};\n\nconst TYPE_ACCENT: Record<NotificationType, string> = {\n comment: 'border-primary-400',\n mention: 'border-violet-400',\n assignment: 'border-sky-400',\n status: 'border-emerald-400',\n invite: 'border-teal-400',\n approval: 'border-amber-400',\n deadline: 'border-rose-400',\n payment: 'border-green-500',\n security: 'border-red-500',\n update: 'border-indigo-400',\n reaction: 'border-pink-400',\n system: 'border-gray-400',\n};\n\nconst PRIORITY_COLORS: Record<NotificationPriority, string> = {\n low: 'bg-gray-400',\n medium: 'bg-amber-400',\n high: 'bg-orange-500',\n urgent: 'bg-red-500',\n};\n\nfunction initials(name: string): string {\n return name.split(' ').slice(0, 2).map(p => p[0]).join('').toUpperCase();\n}\n\nfunction timeAgo(iso: string): string {\n const diff = (Date.now() - new Date(iso).getTime()) / 1000;\n if (diff < 60) return 'just now';\n if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;\n if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;\n if (diff < 86400 * 7) return `${Math.floor(diff / 86400)}d ago`;\n return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\nfunction dateLabel(iso: string): string {\n const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000);\n if (diff === 0) return 'Today';\n if (diff === 1) return 'Yesterday';\n if (diff < 7) return 'This week';\n if (diff < 30) return 'Earlier this month';\n return 'Older';\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * SvNotificationCenterComponent — Premium notification inbox.\n *\n * Selector: `sv-notification-center`\n *\n * Six variants:\n * - `dropdown` Bell + floating panel with tabs, search & actions (default)\n * - `drawer` Bell + full-height right-side slide panel (Slack/Figma style)\n * - `inline` Embedded panel, no trigger — use on dedicated /notifications routes\n * - `feed` Activity-feed style list (GitHub/Jira style)\n * - `compact` Bell + slim dropdown, no tabs/search\n * - `grouped` Bell + dropdown with sticky date-group section headers\n *\n * Floating panels (dropdown, compact, grouped, drawer) are portaled to\n * document.body so they escape every overflow / transform / backdrop-filter\n * ancestor that would otherwise clip or misposition a fixed-positioned panel.\n *\n * @example\n * ```html\n * <sv-notification-center\n * [notifications]=\"myNotifications\"\n * variant=\"dropdown\"\n * (notificationClick)=\"onNotifClick($event)\"\n * (markAllRead)=\"markAllRead()\" />\n * ```\n */\n@Component({\n selector: 'sv-notification-center',\n standalone: true,\n imports: [NgTemplateOutlet],\n templateUrl: './notification-center.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { class: 'block' },\n})\nexport class SvNotificationCenterComponent implements OnDestroy {\n private readonly platformId = inject(PLATFORM_ID);\n private readonly vcr = inject(ViewContainerRef);\n\n private readonly _floatingPanelTpl = viewChild<TemplateRef<unknown>>('floatingPanel');\n\n private _portalContainer: HTMLElement | null = null;\n private _portalView: EmbeddedViewRef<unknown> | null = null;\n\n /** Trigger element kept for scroll-tracking repositioning. */\n private _triggerEl: HTMLElement | null = null;\n private _scrollHandler: (() => void) | null = null;\n private _scrollRafId: number | null = null;\n\n // ── Inputs ─────────────────────────────────────────────────────────────────\n\n /** Presentation: dropdown, drawer, inline, feed, compact or grouped. @default 'dropdown' */\n readonly variant = input<NotificationCenterVariant>('dropdown');\n /** The notifications to display. */\n readonly notifications = input<NotificationItem[]>([]);\n /** Max height of the scrollable list (CSS length). @default '400px' */\n readonly maxHeight = input<string>('400px');\n /** Filter tab labels. @default ['All','Unread','Mentions','System'] */\n readonly tabs = input<string[]>(['All', 'Unread', 'Mentions', 'System']);\n /** Panel heading text. @default 'Notifications' */\n readonly panelTitle = input<string>('Notifications');\n /** Show the search box. @default true */\n readonly showSearch = input<boolean>(true);\n /** Animate the bell icon on new notifications. @default true */\n readonly animateBell = input<boolean>(true);\n\n // ── Outputs ────────────────────────────────────────────────────────────────\n\n /** Emitted when a notification row is clicked. */\n readonly notificationClick = output<NotificationClickEvent>();\n /** Emitted when an inline action on a notification is clicked. */\n readonly actionClick = output<NotificationActionClickEvent>();\n /** Emitted when \"mark all as read\" is triggered. */\n readonly markAllRead = output<void>();\n /** Emitted when a notification is dismissed. */\n readonly dismiss = output<NotificationDismissEvent>();\n\n // ── State ──────────────────────────────────────────────────────────────────\n\n readonly _open = signal(false);\n readonly _activeTab = signal('All');\n readonly _search = signal('');\n readonly _items = signal<NotificationItem[]>([]);\n readonly _panelStyle = signal<{\n top?: string;\n bottom?: string;\n right: string;\n maxH?: string;\n }>({ right: '0px' });\n\n private _clickOutsideHandler: ((e: Event) => void) | null = null;\n\n constructor() {\n effect(() => {\n this._items.set(this.notifications().map(n => ({ ...n })));\n }, { allowSignalWrites: true });\n\n effect(() => {\n // Clean up portal when variant changes while panel is open\n if (!this._open()) {\n this._destroyPortal();\n }\n });\n }\n\n ngOnDestroy(): void {\n this._removeClickOutside();\n this._removeScrollListener();\n this._destroyPortal();\n }\n\n // ── Computed ───────────────────────────────────────────────────────────────\n\n readonly unreadCount = computed(() => this._items().filter(n => !n.read).length);\n\n readonly badgeLabel = computed(() => {\n const c = this.unreadCount();\n return c > 99 ? '99+' : c > 0 ? String(c) : '';\n });\n\n readonly filteredItems = computed(() => {\n const tab = this._activeTab();\n const query = this._search().toLowerCase().trim();\n let list = [...this._items()].sort((a, b) => {\n if (a.pinned && !b.pinned) return -1;\n if (!a.pinned && b.pinned) return 1;\n return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();\n });\n if (tab === 'Unread') list = list.filter(n => !n.read);\n else if (tab === 'Mentions') list = list.filter(n => n.type === 'mention');\n else if (tab === 'System') list = list.filter(n => ['system','update','security'].includes(n.type));\n if (query) {\n list = list.filter(n =>\n n.title.toLowerCase().includes(query) ||\n n.body?.toLowerCase().includes(query) ||\n n.actor?.name.toLowerCase().includes(query) ||\n n.target?.label.toLowerCase().includes(query)\n );\n }\n return list;\n });\n\n readonly groupedItems = computed(() => {\n const map = new Map<string, NotificationItem[]>();\n for (const n of this.filteredItems()) {\n const label = dateLabel(n.timestamp);\n if (!map.has(label)) map.set(label, []);\n map.get(label)!.push(n);\n }\n const ORDER = ['Today', 'Yesterday', 'This week', 'Earlier this month', 'Older'];\n return [...map.entries()]\n .sort((a, b) => ORDER.indexOf(a[0]) - ORDER.indexOf(b[0]))\n .map(([label, items]) => ({ label, items }));\n });\n\n readonly tabUnread = computed<Record<string, number>>(() => {\n const all = this._items();\n const unread = all.filter(n => !n.read).length;\n const mention = all.filter(n => n.type === 'mention' && !n.read).length;\n const sys = all.filter(n => ['system','update','security'].includes(n.type) && !n.read).length;\n return { All: unread, Unread: unread, Mentions: mention, System: sys };\n });\n\n // ── Panel open / close ─────────────────────────────────────────────────────\n\n /**\n * Toggles the floating panel. On open, portals the panel to document.body\n * so it is never clipped by overflow:hidden, transform, or backdrop-filter\n * on any ancestor element.\n */\n togglePanel(event: MouseEvent): void {\n if (this._open()) {\n this._open.set(false);\n this._destroyPortal();\n this._removeClickOutside();\n this._removeScrollListener();\n return;\n }\n\n if (!isPlatformBrowser(this.platformId)) return;\n\n this._triggerEl = event.currentTarget as HTMLElement;\n const rect = this._triggerEl.getBoundingClientRect();\n this._panelStyle.set(this._computePosition(rect));\n this._open.set(true);\n\n // setTimeout allows Angular to process the signal update so the viewChild\n // template ref is available before _createPortal() runs.\n setTimeout(() => {\n this._createPortal();\n this._attachClickOutside();\n this._attachScrollListener();\n }, 0);\n }\n\n closePanel(): void {\n this._open.set(false);\n this._destroyPortal();\n this._removeClickOutside();\n this._removeScrollListener();\n }\n\n // ── Portal helpers ─────────────────────────────────────────────────────────\n\n /**\n * Computes a viewport-safe position for the floating panel.\n *\n * Horizontal: right-aligned with the trigger's right edge, clamped so the\n * panel never overflows the left viewport edge.\n *\n * Vertical (flip logic):\n * - Prefer BELOW the trigger when there is at least 280 px of clearance.\n * - Flip ABOVE when there is more space above than below.\n * - `maxH` caps the panel so it never clips the opposite viewport edge.\n */\n private _computePosition(rect: DOMRect): {\n top?: string;\n bottom?: string;\n right: string;\n maxH?: string;\n } {\n const GAP = 8;\n const EDGE = 8;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const panelW = 416;\n\n // ── Horizontal ──────────────────────────────────────────────────────────\n const rawRight = vw - rect.right;\n const rightVal = Math.max(EDGE, Math.min(rawRight, vw - panelW - EDGE));\n\n // ── Vertical (flip) ─────────────────────────────────────────────────────\n const spaceBelow = vh - rect.bottom - GAP - EDGE;\n const spaceAbove = rect.top - GAP - EDGE;\n const minFit = 280; // px — minimum useful panel height\n\n if (spaceBelow >= minFit || spaceBelow >= spaceAbove) {\n // Show below — anchor top, cap height\n return {\n top: `${rect.bottom + GAP}px`,\n right: `${rightVal}px`,\n maxH: `${Math.max(minFit, spaceBelow)}px`,\n };\n } else {\n // Flip above — anchor bottom (panel grows upward), cap height\n return {\n bottom: `${vh - rect.top + GAP}px`,\n right: `${rightVal}px`,\n maxH: `${Math.max(minFit, spaceAbove)}px`,\n };\n }\n }\n\n /**\n * Appends a portal host <div> to document.body and renders the\n * #floatingPanel template into it via Angular's ViewContainerRef.\n *\n * The host div has no transform / filter / overflow, so position:fixed\n * children are viewport-relative in all browsers.\n */\n private _createPortal(): void {\n const tpl = this._floatingPanelTpl();\n if (!tpl || this._portalContainer) return;\n\n const container = document.createElement('div');\n container.setAttribute('data-nc-portal', '');\n document.body.appendChild(container);\n this._portalContainer = container;\n\n // EmbeddedViewRef remains in Angular's view/CD tree even though\n // its DOM nodes are moved to document.body.\n const view = this.vcr.createEmbeddedView(tpl);\n view.rootNodes.forEach(node => container.appendChild(node));\n this._portalView = view;\n }\n\n private _destroyPortal(): void {\n this._portalView?.destroy();\n this._portalView = null;\n this._portalContainer?.remove();\n this._portalContainer = null;\n }\n\n // ── Scroll / resize tracking ───────────────────────────────────────────────\n\n /**\n * Recomputes panel position on every scroll / resize so the panel\n * tracks the trigger button as the page scrolls.\n * Uses requestAnimationFrame to throttle updates.\n */\n private _attachScrollListener(): void {\n this._removeScrollListener();\n let pending = false;\n this._scrollHandler = () => {\n if (pending) return;\n pending = true;\n this._scrollRafId = requestAnimationFrame(() => {\n pending = false;\n if (this._triggerEl && this._open()) {\n this._panelStyle.set(this._computePosition(this._triggerEl.getBoundingClientRect()));\n }\n });\n };\n // capture:true catches scroll events on any nested scrollable ancestor\n window.addEventListener('scroll', this._scrollHandler, { passive: true, capture: true });\n window.addEventListener('resize', this._scrollHandler, { passive: true });\n }\n\n private _removeScrollListener(): void {\n if (this._scrollHandler) {\n window.removeEventListener('scroll', this._scrollHandler, true);\n window.removeEventListener('resize', this._scrollHandler);\n this._scrollHandler = null;\n }\n if (this._scrollRafId !== null) {\n cancelAnimationFrame(this._scrollRafId);\n this._scrollRafId = null;\n }\n }\n\n // ── Click-outside ──────────────────────────────────────────────────────────\n\n private _attachClickOutside(): void {\n this._removeClickOutside();\n this._clickOutsideHandler = (e: Event) => {\n const target = e.target as Element;\n const inHost = target.closest('sv-notification-center, sv-notification-center');\n const inPortal = target.closest('[data-nc-portal]');\n if (!inHost && !inPortal) this.closePanel();\n };\n document.addEventListener('click', this._clickOutsideHandler, true);\n }\n\n private _removeClickOutside(): void {\n if (this._clickOutsideHandler) {\n document.removeEventListener('click', this._clickOutsideHandler, true);\n this._clickOutsideHandler = null;\n }\n }\n\n // ── Actions ────────────────────────────────────────────────────────────────\n\n onNotificationClick(item: NotificationItem): void {\n this._items.update(list => list.map(n => n.id === item.id ? { ...n, read: true } : n));\n this.notificationClick.emit({ notification: { ...item, read: true } });\n }\n\n onActionClick(item: NotificationItem, actionId: string, event: Event): void {\n event.stopPropagation();\n this.actionClick.emit({ notification: item, actionId });\n }\n\n onDismiss(item: NotificationItem, event: Event): void {\n event.stopPropagation();\n this._items.update(list => list.filter(n => n.id !== item.id));\n this.dismiss.emit({ notification: item });\n }\n\n onMarkAllRead(): void {\n this._items.update(list => list.map(n => ({ ...n, read: true })));\n this.markAllRead.emit();\n }\n\n setTab(tab: string): void {\n this._activeTab.set(tab);\n this._search.set('');\n }\n\n // ── Template helpers ───────────────────────────────────────────────────────\n\n typeIcon(type: NotificationType): string { return TYPE_ICONS[type]; }\n typeColor(type: NotificationType): string { return TYPE_COLORS[type]; }\n typeAccent(type: NotificationType): string { return TYPE_ACCENT[type]; }\n priorityColor(p: NotificationPriority): string { return PRIORITY_COLORS[p]; }\n initials(name: string): string { return initials(name); }\n timeAgo(iso: string): string { return timeAgo(iso); }\n dateLabel(iso: string): string { return dateLabel(iso); }\n}\n","<!-- ─── Notification Center ────────────────────────────────────────────────── -->\n<!-- Bell-trigger variants render only the trigger here. -->\n<!-- Floating panels are rendered via a body-level portal (#floatingPanel) -->\n<!-- so they escape every overflow / transform / backdrop-filter ancestor. -->\n\n@switch (variant()) {\n\n <!-- ═══════════════════════════════════════════════════════════════════════ -->\n <!-- VARIANT: dropdown — bell trigger only; panel portaled to body -->\n <!-- ═══════════════════════════════════════════════════════════════════════ -->\n @case ('dropdown') {\n <div class=\"relative inline-flex\">\n <button\n type=\"button\"\n (click)=\"togglePanel($event)\"\n [attr.aria-expanded]=\"_open()\"\n aria-haspopup=\"true\"\n aria-label=\"Open notifications\"\n class=\"relative inline-flex items-center justify-center w-10 h-10 rounded-xl\n text-gray-500 dark:text-gray-400\n hover:text-gray-700 dark:hover:text-gray-200\n hover:bg-gray-100 dark:hover:bg-gray-800\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n transition-colors duration-150\">\n <svg class=\"w-5 h-5\" [class.animate-bounce]=\"animateBell() && unreadCount() > 0\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"1.8\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\"/>\n </svg>\n @if (badgeLabel()) {\n <span class=\"absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] px-1\n flex items-center justify-center rounded-full\n bg-rose-500 text-white text-[10px] font-bold leading-none\n ring-2 ring-white dark:ring-gray-900\">\n {{ badgeLabel() }}\n </span>\n }\n </button>\n </div>\n }\n\n <!-- ═══════════════════════════════════════════════════════════════════════ -->\n <!-- VARIANT: drawer — bell trigger only; panel portaled to body -->\n <!-- ═══════════════════════════════════════════════════════════════════════ -->\n @case ('inline') {\n <div class=\"rounded-2xl border border-gray-200 dark:border-gray-700\n bg-white dark:bg-gray-900 shadow-sm overflow-hidden\">\n\n <div class=\"flex items-center justify-between px-5 py-4\n border-b border-gray-100 dark:border-gray-800\">\n <div class=\"flex items-center gap-2.5\">\n <h2 class=\"text-base font-semibold text-gray-900 dark:text-gray-100\">{{ panelTitle() }}</h2>\n @if (unreadCount() > 0) {\n <span class=\"px-2 py-0.5 rounded-full bg-primary-100 dark:bg-primary-900/40\n text-primary-700 dark:text-primary-300 text-xs font-semibold\">\n {{ unreadCount() }}\n </span>\n }\n </div>\n @if (unreadCount() > 0) {\n <button type=\"button\" (click)=\"onMarkAllRead()\"\n class=\"text-xs text-primary-600 dark:text-primary-400 hover:underline focus-visible:outline-none\">\n Mark all read\n </button>\n }\n </div>\n\n @if (showSearch()) {\n <div class=\"px-5 pt-3.5\">\n <div class=\"relative\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"/>\n </svg>\n <input type=\"search\" placeholder=\"Search notifications…\"\n [value]=\"_search()\"\n (input)=\"_search.set($any($event.target).value)\"\n class=\"w-full pl-9 pr-3 py-2.5 text-sm rounded-xl\n bg-gray-50 dark:bg-gray-800\n border border-gray-200 dark:border-gray-700\n text-gray-900 dark:text-gray-100\n placeholder:text-gray-400\n focus:outline-none focus:ring-2 focus:ring-primary-500/40\"/>\n </div>\n </div>\n }\n\n @if (tabs().length > 0) {\n <div class=\"flex gap-0.5 px-5 pt-3 border-b border-gray-100 dark:border-gray-800\">\n @for (tab of tabs(); track tab) {\n <button type=\"button\" (click)=\"setTab(tab)\"\n class=\"relative px-3 py-2 text-sm font-medium rounded-t-lg focus-visible:outline-none transition-colors\"\n [class]=\"_activeTab() === tab\n ? 'text-primary-600 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'\">\n {{ tab }}\n @if (tabUnread()[tab] > 0) {\n <span class=\"ml-1 px-1.5 py-0.5 rounded-full bg-rose-100 dark:bg-rose-900/40 text-rose-600 dark:text-rose-400 text-xs font-bold\">\n {{ tabUnread()[tab] }}\n </span>\n }\n @if (_activeTab() === tab) {\n <span class=\"absolute bottom-0 left-0 right-0 h-0.5 bg-primary-500 rounded-full\"></span>\n }\n </button>\n }\n </div>\n }\n\n <div class=\"overflow-y-auto\" [style.max-height]=\"maxHeight()\">\n @if (filteredItems().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n @for (item of filteredItems(); track item.id) {\n <ng-container *ngTemplateOutlet=\"notifRow; context: { $implicit: item }\"></ng-container>\n }\n }\n </div>\n </div>\n }\n\n <!-- ═══════════════════════════════════════════════════════════════════════ -->\n <!-- VARIANT: feed — activity-feed style list -->\n <!-- ═══════════════════════════════════════════════════════════════════════ -->\n}\n\n<!-- ═══════════════════════════════════════════════════════════════════════════\n PORTAL TEMPLATE — rendered to document.body on open\n No position:fixed ancestor clipping can reach here.\n ═══════════════════════════════════════════════════════════════════════════ -->\n<ng-template #floatingPanel>\n @switch (variant()) {\n\n <!-- ─── Dropdown panel ──────────────────────────────────────────────── -->\n @case ('dropdown') {\n <div\n class=\"fixed z-[9999] w-[26rem] max-w-[calc(100vw-1rem)]\n rounded-2xl shadow-2xl shadow-black/15 dark:shadow-black/50\n bg-white dark:bg-gray-900\n border border-gray-200/70 dark:border-gray-700/60\n overflow-hidden flex flex-col\"\n [style.top]=\"_panelStyle().top\"\n [style.bottom]=\"_panelStyle().bottom\"\n [style.right]=\"_panelStyle().right\"\n [style.max-height]=\"_panelStyle().maxH\">\n\n <!-- Header -->\n <div class=\"flex items-center justify-between px-5 py-4\n border-b border-gray-100 dark:border-gray-800\">\n <div class=\"flex items-center gap-2.5\">\n <span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100\">{{ panelTitle() }}</span>\n @if (unreadCount() > 0) {\n <span class=\"px-2 py-0.5 rounded-full bg-primary-100 dark:bg-primary-900/40\n text-primary-700 dark:text-primary-300 text-[11px] font-semibold\">\n {{ unreadCount() }} new\n </span>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n @if (unreadCount() > 0) {\n <button type=\"button\" (click)=\"onMarkAllRead()\"\n class=\"text-xs text-primary-600 dark:text-primary-400 hover:underline focus-visible:outline-none\">\n Mark all read\n </button>\n }\n <button type=\"button\" (click)=\"closePanel()\" aria-label=\"Close\"\n class=\"w-7 h-7 flex items-center justify-center rounded-lg\n text-gray-400 hover:text-gray-600 dark:hover:text-gray-300\n hover:bg-gray-100 dark:hover:bg-gray-800\n focus-visible:outline-none transition-colors\">\n <svg class=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Search -->\n @if (showSearch()) {\n <div class=\"px-4 pt-3\">\n <div class=\"relative\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"/>\n </svg>\n <input type=\"search\" placeholder=\"Search notifications…\"\n [value]=\"_search()\"\n (input)=\"_search.set($any($event.target).value)\"\n class=\"w-full pl-9 pr-3 py-2 text-sm rounded-xl\n bg-gray-50 dark:bg-gray-800\n border border-gray-200 dark:border-gray-700\n text-gray-900 dark:text-gray-100\n placeholder:text-gray-400 dark:placeholder:text-gray-500\n focus:outline-none focus:ring-2 focus:ring-primary-500/40 focus:border-primary-400\n transition-shadow\"/>\n </div>\n </div>\n }\n\n <!-- Tabs -->\n @if (tabs().length > 0) {\n <div class=\"flex gap-0.5 px-4 pt-3 border-b border-gray-100 dark:border-gray-800\">\n @for (tab of tabs(); track tab) {\n <button type=\"button\" (click)=\"setTab(tab)\"\n class=\"relative px-3 py-2 text-xs font-medium rounded-t-lg\n focus-visible:outline-none transition-colors\"\n [class]=\"_activeTab() === tab\n ? 'text-primary-600 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'\">\n {{ tab }}\n @if (tabUnread()[tab] > 0) {\n <span class=\"ml-1 px-1.5 py-0.5 rounded-full\n bg-rose-100 dark:bg-rose-900/40\n text-rose-600 dark:text-rose-400 text-[10px] font-bold\">\n {{ tabUnread()[tab] }}\n </span>\n }\n @if (_activeTab() === tab) {\n <span class=\"absolute bottom-0 left-0 right-0 h-0.5 bg-primary-500 rounded-full\"></span>\n }\n </button>\n }\n </div>\n }\n\n <!-- List -->\n <div class=\"overflow-y-auto\" [style.max-height]=\"maxHeight()\">\n @if (filteredItems().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n @for (item of filteredItems(); track item.id) {\n <ng-container *ngTemplateOutlet=\"notifRow; context: { $implicit: item }\"></ng-container>\n }\n }\n </div>\n\n <!-- Footer -->\n <div class=\"px-5 py-3 border-t border-gray-100 dark:border-gray-800\n flex items-center justify-between\">\n <span class=\"text-xs text-gray-400\">\n {{ filteredItems().length }} notification{{ filteredItems().length !== 1 ? 's' : '' }}\n </span>\n <button type=\"button\"\n class=\"text-xs font-medium text-primary-600 dark:text-primary-400\n hover:underline focus-visible:outline-none\">\n View all\n </button>\n </div>\n </div>\n }\n\n <!-- ─── Compact panel ───────────────────────────────────────────────── -->\n }\n</ng-template>\n\n<!-- ─── Shared: notification row ─────────────────────────────────────────────\n Used by dropdown / drawer / inline / grouped (via *ngTemplateOutlet).\n Context: { $implicit: NotificationItem }\n ──────────────────────────────────────────────────────────────────────── -->\n<ng-template #notifRow let-item>\n <div\n (click)=\"onNotificationClick(item)\"\n class=\"group relative flex gap-3.5 px-5 py-4 cursor-pointer\n border-b border-gray-100 dark:border-gray-800/60 last:border-0\n transition-colors duration-100\"\n [class]=\"!item.read\n ? 'bg-primary-50/50 dark:bg-primary-950/20 hover:bg-primary-50/80 dark:hover:bg-primary-950/30'\n : 'hover:bg-gray-50/80 dark:hover:bg-gray-800/50'\">\n\n @if (!item.read) {\n <span class=\"absolute left-0 top-3 bottom-3 w-0.5 rounded-full bg-primary-500\"></span>\n }\n\n <div class=\"relative shrink-0 mt-0.5\">\n @if (item.actor?.avatar) {\n <img [src]=\"item.actor!.avatar\" [alt]=\"item.actor!.name\"\n class=\"w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-gray-900\"/>\n } @else if (item.actor) {\n <div class=\"w-10 h-10 rounded-full flex items-center justify-center\n text-white text-sm font-semibold ring-2 ring-white dark:ring-gray-900\"\n [class]=\"typeColor(item.type)\">\n {{ initials(item.actor.name) }}\n </div>\n } @else {\n <div class=\"w-10 h-10 rounded-xl flex items-center justify-center text-white\"\n [class]=\"typeColor(item.type)\">\n <svg class=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" [attr.d]=\"typeIcon(item.type)\"/>\n </svg>\n </div>\n }\n @if (item.actor) {\n <span class=\"absolute -bottom-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center\n text-white ring-2 ring-white dark:ring-gray-900\"\n [class]=\"typeColor(item.type)\">\n <svg class=\"w-2.5 h-2.5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" [attr.d]=\"typeIcon(item.type)\"/>\n </svg>\n </span>\n }\n </div>\n\n <div class=\"flex-1 min-w-0 pr-4\">\n <div class=\"flex items-start justify-between gap-3 mb-0.5\">\n <p class=\"text-[13.5px] leading-snug text-gray-900 dark:text-gray-100\"\n [class.font-semibold]=\"!item.read\"\n [class.font-normal]=\"item.read\">\n {{ item.title }}\n </p>\n <div class=\"flex items-center gap-1.5 shrink-0 mt-0.5\">\n @if (item.priority && item.priority !== 'low') {\n <span class=\"w-2 h-2 rounded-full shrink-0\" [class]=\"priorityColor(item.priority!)\"></span>\n }\n <span class=\"text-[11px] text-gray-400 whitespace-nowrap\">{{ timeAgo(item.timestamp) }}</span>\n </div>\n </div>\n\n @if (item.body) {\n <p class=\"text-xs text-gray-500 dark:text-gray-400 leading-relaxed line-clamp-2 mt-0.5\">\n {{ item.body }}\n </p>\n }\n\n @if (item.target) {\n <p class=\"text-xs text-primary-600 dark:text-primary-400 mt-1 truncate font-medium\">\n {{ item.target.label }}\n </p>\n }\n\n @if (item.actions && item.actions.length > 0) {\n <div class=\"flex flex-wrap gap-2 mt-2.5\" (click)=\"$event.stopPropagation()\">\n @for (action of item.actions; track action.id) {\n <button type=\"button\"\n (click)=\"onActionClick(item, action.id, $event)\"\n class=\"px-3 py-1.5 rounded-lg text-xs font-medium\n transition-colors focus-visible:outline-none focus-visible:ring-2\"\n [class]=\"action.variant === 'primary'\n ? 'bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500'\n : action.variant === 'danger'\n ? 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50 focus-visible:ring-red-500'\n : 'bg-gray-100 dark:bg-gray-700/80 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus-visible:ring-gray-400'\">\n {{ action.label }}\n </button>\n }\n </div>\n }\n </div>\n\n <button type=\"button\"\n (click)=\"onDismiss(item, $event)\"\n aria-label=\"Dismiss notification\"\n class=\"absolute top-3 right-3 w-6 h-6 flex items-center justify-center rounded-lg\n text-gray-300 dark:text-gray-600\n hover:text-gray-500 dark:hover:text-gray-400\n hover:bg-gray-100 dark:hover:bg-gray-700\n opacity-0 group-hover:opacity-100 transition-opacity\n focus-visible:outline-none focus-visible:opacity-100\">\n <svg class=\"w-3.5 h-3.5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"/>\n </svg>\n </button>\n </div>\n</ng-template>\n\n<!-- ─── Shared: empty state ──────────────────────────────────────────────── -->\n<ng-template #emptyTpl>\n <div class=\"flex flex-col items-center justify-center py-14 text-center\">\n <div class=\"w-12 h-12 rounded-2xl bg-gray-100 dark:bg-gray-800\n flex items-center justify-center mb-3\">\n <svg class=\"w-6 h-6 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\"\n stroke=\"currentColor\" stroke-width=\"1.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\"/>\n </svg>\n </div>\n <p class=\"text-sm font-medium text-gray-600 dark:text-gray-400\">You're all caught up!</p>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 mt-1\">No notifications to show</p>\n </div>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@styloviz/notification-center",
3
+ "version": "0.1.1",
4
+ "description": "6 premium variants: dropdown, drawer, inline, activity feed, compact and grouped — with mark-as-read, dismiss, search, tab filtering and inline actions.",
5
+ "license": "MIT",
6
+ "author": "sazzad-bs23",
7
+ "homepage": "https://styloviz.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rhossain/angular-tailwind-dashboard-components.git",
11
+ "directory": "projects/notification-center-free"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/rhossain/angular-tailwind-dashboard-components/issues"
15
+ },
16
+ "peerDependencies": {
17
+ "@angular/common": ">=21.0.0",
18
+ "@angular/core": ">=21.0.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "engines": {
24
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
25
+ },
26
+ "sideEffects": false,
27
+ "module": "fesm2022/styloviz-notification-center.mjs",
28
+ "typings": "types/styloviz-notification-center.d.ts",
29
+ "exports": {
30
+ "./package.json": {
31
+ "default": "./package.json"
32
+ },
33
+ ".": {
34
+ "types": "./types/styloviz-notification-center.d.ts",
35
+ "default": "./fesm2022/styloviz-notification-center.mjs"
36
+ }
37
+ },
38
+ "type": "module",
39
+ "dependencies": {
40
+ "tslib": "^2.3.0"
41
+ }
42
+ }
@@ -0,0 +1,193 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { OnDestroy } from '@angular/core';
3
+
4
+ /**
5
+ * Notification Center — type definitions
6
+ *
7
+ * Six variants cover the most common global-market notification UI patterns:
8
+ * 'dropdown' – Bell trigger → floating panel with tabs & search (default)
9
+ * 'drawer' – Bell trigger → full-height right-side slide-in panel
10
+ * 'inline' – Panel embedded directly in the page (dedicated route)
11
+ * 'feed' – Activity-feed: actor + verb + target (GitHub/Jira style)
12
+ * 'compact' – Bell trigger → minimal slim list, no tabs/search
13
+ * 'grouped' – Bell trigger → dropdown with sticky date-group headers
14
+ */
15
+ type NotificationCenterVariant = 'dropdown' | 'inline';
16
+ /** Semantic category — drives the icon and colour of the type badge. */
17
+ type NotificationType = 'comment' | 'mention' | 'assignment' | 'status' | 'invite' | 'approval' | 'deadline' | 'payment' | 'security' | 'update' | 'reaction' | 'system';
18
+ type NotificationPriority = 'low' | 'medium' | 'high' | 'urgent';
19
+ interface NotificationAction {
20
+ readonly id: string;
21
+ readonly label: string;
22
+ /** @default 'secondary' */
23
+ readonly variant?: 'primary' | 'secondary' | 'danger';
24
+ }
25
+ interface NotificationActor {
26
+ readonly id: string;
27
+ readonly name: string;
28
+ readonly avatar?: string;
29
+ readonly color?: string;
30
+ }
31
+ interface NotificationTarget {
32
+ readonly label: string;
33
+ readonly href?: string;
34
+ }
35
+ interface NotificationItem {
36
+ readonly id: string;
37
+ readonly type: NotificationType;
38
+ readonly title: string;
39
+ readonly body?: string;
40
+ readonly priority?: NotificationPriority;
41
+ readonly timestamp: string;
42
+ read: boolean;
43
+ readonly actor?: NotificationActor;
44
+ readonly target?: NotificationTarget;
45
+ readonly actions?: NotificationAction[];
46
+ readonly category?: string;
47
+ readonly pinned?: boolean;
48
+ }
49
+ interface NotificationClickEvent {
50
+ readonly notification: NotificationItem;
51
+ }
52
+ interface NotificationActionClickEvent {
53
+ readonly notification: NotificationItem;
54
+ readonly actionId: string;
55
+ }
56
+ interface NotificationDismissEvent {
57
+ readonly notification: NotificationItem;
58
+ }
59
+
60
+ /**
61
+ * SvNotificationCenterComponent — Premium notification inbox.
62
+ *
63
+ * Selector: `sv-notification-center`
64
+ *
65
+ * Six variants:
66
+ * - `dropdown` Bell + floating panel with tabs, search & actions (default)
67
+ * - `drawer` Bell + full-height right-side slide panel (Slack/Figma style)
68
+ * - `inline` Embedded panel, no trigger — use on dedicated /notifications routes
69
+ * - `feed` Activity-feed style list (GitHub/Jira style)
70
+ * - `compact` Bell + slim dropdown, no tabs/search
71
+ * - `grouped` Bell + dropdown with sticky date-group section headers
72
+ *
73
+ * Floating panels (dropdown, compact, grouped, drawer) are portaled to
74
+ * document.body so they escape every overflow / transform / backdrop-filter
75
+ * ancestor that would otherwise clip or misposition a fixed-positioned panel.
76
+ *
77
+ * @example
78
+ * ```html
79
+ * <sv-notification-center
80
+ * [notifications]="myNotifications"
81
+ * variant="dropdown"
82
+ * (notificationClick)="onNotifClick($event)"
83
+ * (markAllRead)="markAllRead()" />
84
+ * ```
85
+ */
86
+ declare class SvNotificationCenterComponent implements OnDestroy {
87
+ private readonly platformId;
88
+ private readonly vcr;
89
+ private readonly _floatingPanelTpl;
90
+ private _portalContainer;
91
+ private _portalView;
92
+ /** Trigger element kept for scroll-tracking repositioning. */
93
+ private _triggerEl;
94
+ private _scrollHandler;
95
+ private _scrollRafId;
96
+ /** Presentation: dropdown, drawer, inline, feed, compact or grouped. @default 'dropdown' */
97
+ readonly variant: _angular_core.InputSignal<NotificationCenterVariant>;
98
+ /** The notifications to display. */
99
+ readonly notifications: _angular_core.InputSignal<NotificationItem[]>;
100
+ /** Max height of the scrollable list (CSS length). @default '400px' */
101
+ readonly maxHeight: _angular_core.InputSignal<string>;
102
+ /** Filter tab labels. @default ['All','Unread','Mentions','System'] */
103
+ readonly tabs: _angular_core.InputSignal<string[]>;
104
+ /** Panel heading text. @default 'Notifications' */
105
+ readonly panelTitle: _angular_core.InputSignal<string>;
106
+ /** Show the search box. @default true */
107
+ readonly showSearch: _angular_core.InputSignal<boolean>;
108
+ /** Animate the bell icon on new notifications. @default true */
109
+ readonly animateBell: _angular_core.InputSignal<boolean>;
110
+ /** Emitted when a notification row is clicked. */
111
+ readonly notificationClick: _angular_core.OutputEmitterRef<NotificationClickEvent>;
112
+ /** Emitted when an inline action on a notification is clicked. */
113
+ readonly actionClick: _angular_core.OutputEmitterRef<NotificationActionClickEvent>;
114
+ /** Emitted when "mark all as read" is triggered. */
115
+ readonly markAllRead: _angular_core.OutputEmitterRef<void>;
116
+ /** Emitted when a notification is dismissed. */
117
+ readonly dismiss: _angular_core.OutputEmitterRef<NotificationDismissEvent>;
118
+ readonly _open: _angular_core.WritableSignal<boolean>;
119
+ readonly _activeTab: _angular_core.WritableSignal<string>;
120
+ readonly _search: _angular_core.WritableSignal<string>;
121
+ readonly _items: _angular_core.WritableSignal<NotificationItem[]>;
122
+ readonly _panelStyle: _angular_core.WritableSignal<{
123
+ top?: string;
124
+ bottom?: string;
125
+ right: string;
126
+ maxH?: string;
127
+ }>;
128
+ private _clickOutsideHandler;
129
+ constructor();
130
+ ngOnDestroy(): void;
131
+ readonly unreadCount: _angular_core.Signal<number>;
132
+ readonly badgeLabel: _angular_core.Signal<string>;
133
+ readonly filteredItems: _angular_core.Signal<NotificationItem[]>;
134
+ readonly groupedItems: _angular_core.Signal<{
135
+ label: string;
136
+ items: NotificationItem[];
137
+ }[]>;
138
+ readonly tabUnread: _angular_core.Signal<Record<string, number>>;
139
+ /**
140
+ * Toggles the floating panel. On open, portals the panel to document.body
141
+ * so it is never clipped by overflow:hidden, transform, or backdrop-filter
142
+ * on any ancestor element.
143
+ */
144
+ togglePanel(event: MouseEvent): void;
145
+ closePanel(): void;
146
+ /**
147
+ * Computes a viewport-safe position for the floating panel.
148
+ *
149
+ * Horizontal: right-aligned with the trigger's right edge, clamped so the
150
+ * panel never overflows the left viewport edge.
151
+ *
152
+ * Vertical (flip logic):
153
+ * - Prefer BELOW the trigger when there is at least 280 px of clearance.
154
+ * - Flip ABOVE when there is more space above than below.
155
+ * - `maxH` caps the panel so it never clips the opposite viewport edge.
156
+ */
157
+ private _computePosition;
158
+ /**
159
+ * Appends a portal host <div> to document.body and renders the
160
+ * #floatingPanel template into it via Angular's ViewContainerRef.
161
+ *
162
+ * The host div has no transform / filter / overflow, so position:fixed
163
+ * children are viewport-relative in all browsers.
164
+ */
165
+ private _createPortal;
166
+ private _destroyPortal;
167
+ /**
168
+ * Recomputes panel position on every scroll / resize so the panel
169
+ * tracks the trigger button as the page scrolls.
170
+ * Uses requestAnimationFrame to throttle updates.
171
+ */
172
+ private _attachScrollListener;
173
+ private _removeScrollListener;
174
+ private _attachClickOutside;
175
+ private _removeClickOutside;
176
+ onNotificationClick(item: NotificationItem): void;
177
+ onActionClick(item: NotificationItem, actionId: string, event: Event): void;
178
+ onDismiss(item: NotificationItem, event: Event): void;
179
+ onMarkAllRead(): void;
180
+ setTab(tab: string): void;
181
+ typeIcon(type: NotificationType): string;
182
+ typeColor(type: NotificationType): string;
183
+ typeAccent(type: NotificationType): string;
184
+ priorityColor(p: NotificationPriority): string;
185
+ initials(name: string): string;
186
+ timeAgo(iso: string): string;
187
+ dateLabel(iso: string): string;
188
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvNotificationCenterComponent, never>;
189
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvNotificationCenterComponent, "sv-notification-center", never, { "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "notifications": { "alias": "notifications"; "required": false; "isSignal": true; }; "maxHeight": { "alias": "maxHeight"; "required": false; "isSignal": true; }; "tabs": { "alias": "tabs"; "required": false; "isSignal": true; }; "panelTitle": { "alias": "panelTitle"; "required": false; "isSignal": true; }; "showSearch": { "alias": "showSearch"; "required": false; "isSignal": true; }; "animateBell": { "alias": "animateBell"; "required": false; "isSignal": true; }; }, { "notificationClick": "notificationClick"; "actionClick": "actionClick"; "markAllRead": "markAllRead"; "dismiss": "dismiss"; }, never, never, true, never>;
190
+ }
191
+
192
+ export { SvNotificationCenterComponent };
193
+ export type { NotificationAction, NotificationActionClickEvent, NotificationCenterVariant, NotificationClickEvent, NotificationDismissEvent, NotificationItem, NotificationPriority, NotificationType };