@memberjunction/ng-explorer-core 6.2.0-edge.5 → 6.2.0-edge.7

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.
Files changed (76) hide show
  1. package/README.md +7 -2
  2. package/dist/app-routing.module.d.ts.map +1 -1
  3. package/dist/app-routing.module.js +22 -4
  4. package/dist/app-routing.module.js.map +1 -1
  5. package/dist/generated/lazy-feature-config.d.ts +1 -1
  6. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  7. package/dist/generated/lazy-feature-config.js +16 -2
  8. package/dist/generated/lazy-feature-config.js.map +1 -1
  9. package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js +2 -2
  10. package/dist/lib/office/office-connect.component.d.ts +19 -0
  11. package/dist/lib/office/office-connect.component.d.ts.map +1 -0
  12. package/dist/lib/office/office-connect.component.js +80 -0
  13. package/dist/lib/office/office-connect.component.js.map +1 -0
  14. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +7 -0
  15. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
  16. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +20 -8
  17. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
  18. package/dist/lib/resource-wrappers/dashboard-class-resolution.d.ts +14 -0
  19. package/dist/lib/resource-wrappers/dashboard-class-resolution.d.ts.map +1 -0
  20. package/dist/lib/resource-wrappers/dashboard-class-resolution.js +21 -0
  21. package/dist/lib/resource-wrappers/dashboard-class-resolution.js.map +1 -0
  22. package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
  23. package/dist/lib/resource-wrappers/dashboard-resource.component.js +3 -2
  24. package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
  25. package/dist/lib/resource-wrappers/record-form-mode.d.ts +44 -0
  26. package/dist/lib/resource-wrappers/record-form-mode.d.ts.map +1 -0
  27. package/dist/lib/resource-wrappers/record-form-mode.js +47 -0
  28. package/dist/lib/resource-wrappers/record-form-mode.js.map +1 -0
  29. package/dist/lib/resource-wrappers/record-resource.component.d.ts +56 -1
  30. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  31. package/dist/lib/resource-wrappers/record-resource.component.js +124 -9
  32. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  33. package/dist/lib/resource-wrappers/standard-form-create.d.ts +4 -0
  34. package/dist/lib/resource-wrappers/standard-form-create.d.ts.map +1 -0
  35. package/dist/lib/resource-wrappers/standard-form-create.js +6 -0
  36. package/dist/lib/resource-wrappers/standard-form-create.js.map +1 -0
  37. package/dist/lib/resource-wrappers/view-resource.component.d.ts +18 -1
  38. package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
  39. package/dist/lib/resource-wrappers/view-resource.component.js +105 -39
  40. package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
  41. package/dist/lib/shell/components/dialogs/app-access-dialog.component.js +2 -2
  42. package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts.map +1 -1
  43. package/dist/lib/shell/components/record-open/record-origin-crumb.component.js +12 -8
  44. package/dist/lib/shell/components/record-open/record-origin-crumb.component.js.map +1 -1
  45. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +6 -0
  46. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  47. package/dist/lib/shell/components/tabs/component-cache-manager.js +18 -3
  48. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  49. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +2 -0
  50. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  51. package/dist/lib/shell/components/tabs/tab-container.component.js +38 -10
  52. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  53. package/dist/lib/shell/shell-chrome-policy.d.ts +6 -0
  54. package/dist/lib/shell/shell-chrome-policy.d.ts.map +1 -1
  55. package/dist/lib/shell/shell-chrome-policy.js +1 -0
  56. package/dist/lib/shell/shell-chrome-policy.js.map +1 -1
  57. package/dist/lib/shell/shell.component.d.ts +23 -0
  58. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  59. package/dist/lib/shell/shell.component.js +283 -71
  60. package/dist/lib/shell/shell.component.js.map +1 -1
  61. package/dist/lib/single-list-detail/single-list-detail.component.js +2 -2
  62. package/dist/lib/single-list-detail/single-list-detail.component.js.map +1 -1
  63. package/dist/lib/single-record/single-record.component.d.ts +27 -2
  64. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  65. package/dist/lib/single-record/single-record.component.js +54 -8
  66. package/dist/lib/single-record/single-record.component.js.map +1 -1
  67. package/dist/lib/user-notifications/user-notifications.component.d.ts +24 -2
  68. package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
  69. package/dist/lib/user-notifications/user-notifications.component.js +139 -24
  70. package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
  71. package/dist/lib/user-profile/user-profile.component.js +2 -2
  72. package/dist/module.d.ts +64 -63
  73. package/dist/module.d.ts.map +1 -1
  74. package/dist/module.js +3 -0
  75. package/dist/module.js.map +1 -1
  76. package/package.json +58 -57
@@ -1,4 +1,4 @@
1
- import { Component, ChangeDetectorRef, inject } from '@angular/core';
1
+ import { Component, ChangeDetectorRef, NgZone, inject } from '@angular/core';
2
2
  import { SharedService } from '@memberjunction/ng-shared';
3
3
  import { MJUserNotificationEntity, UserInfoEngine } from '@memberjunction/core-entities';
4
4
  import { CompositeKey } from '@memberjunction/core';
@@ -368,6 +368,7 @@ export class UserNotificationsComponent extends BaseAngularComponent {
368
368
  navigationService;
369
369
  appManager;
370
370
  cdr = inject(ChangeDetectorRef);
371
+ ngZone = inject(NgZone, { optional: true });
371
372
  RadioSelected = 'All';
372
373
  /** @deprecated Use {@link RadioSelected}. */
373
374
  get radioSelected() {
@@ -436,13 +437,34 @@ export class UserNotificationsComponent extends BaseAngularComponent {
436
437
  async ngOnInit() {
437
438
  this.loadNotificationTypes();
438
439
  // Refresh the snapshot relative-time labels between change-detection passes.
439
- this.relativeTimer = setInterval(() => {
440
- for (const vm of this.vmCache.values()) {
441
- vm.Relative = this.relativeTime(vm.N.__mj_CreatedAt);
442
- }
443
- this.groupsKey = ''; // day buckets can shift at midnight
444
- this.cdr.markForCheck();
445
- }, RELATIVE_REFRESH_MS);
440
+ // Run outside NgZone so the 60s timer only triggers Angular CD when a label actually changes.
441
+ const startTimer = () => {
442
+ this.relativeTimer = setInterval(() => {
443
+ let anyChanged = false;
444
+ for (const vm of this.vmCache.values()) {
445
+ const next = this.relativeTime(vm.N.__mj_CreatedAt);
446
+ if (vm.Relative !== next) {
447
+ vm.Relative = next;
448
+ anyChanged = true;
449
+ }
450
+ }
451
+ this.groupsKey = ''; // day buckets can shift at midnight
452
+ if (anyChanged) {
453
+ if (this.ngZone) {
454
+ this.ngZone.run(() => this.cdr.markForCheck());
455
+ }
456
+ else {
457
+ this.cdr.markForCheck();
458
+ }
459
+ }
460
+ }, RELATIVE_REFRESH_MS);
461
+ };
462
+ if (this.ngZone) {
463
+ this.ngZone.runOutsideAngular(startTimer);
464
+ }
465
+ else {
466
+ startTimer();
467
+ }
446
468
  }
447
469
  ngAfterViewInit() {
448
470
  this.SharedService.InvokeManualResize(); // make sure the notifications component is sized correctly
@@ -468,17 +490,38 @@ export class UserNotificationsComponent extends BaseAngularComponent {
468
490
  // ========================================================================
469
491
  // Data shaping (filter → view models → day groups)
470
492
  // ========================================================================
493
+ notificationsToShowCache = [];
494
+ notificationsToShowKey = '';
495
+ unreadNotificationsCache = [];
496
+ unreadNotificationsKey = '';
497
+ readNotificationsCache = [];
498
+ readNotificationsKey = '';
499
+ computeAllNotificationsSignature() {
500
+ const all = this.AllNotifications;
501
+ let unreadCount = 0;
502
+ for (let i = 0; i < all.length; i++) {
503
+ if (all[i].Unread)
504
+ unreadCount++;
505
+ }
506
+ const baseKey = `${all.length}|${unreadCount}|${all[0]?.ID ?? ''}|${all[all.length - 1]?.ID ?? ''}`;
507
+ return { all, unreadCount, baseKey };
508
+ }
471
509
  get NotificationsToShow() {
510
+ const { all, baseKey } = this.computeAllNotificationsSignature();
511
+ const key = `${baseKey}|${this.RadioSelected}|${this.SelectedTypeFilter ?? ''}|${this.CurrentFilter}`;
512
+ if (key === this.notificationsToShowKey) {
513
+ return this.notificationsToShowCache;
514
+ }
472
515
  let temp = [];
473
516
  switch (this.RadioSelected) {
474
517
  case 'All':
475
- temp = this.AllNotifications;
518
+ temp = all;
476
519
  break;
477
520
  case 'Unread':
478
- temp = this.AllNotifications.filter(n => n.Unread);
521
+ temp = all.filter(n => n.Unread);
479
522
  break;
480
523
  case 'Read':
481
- temp = this.AllNotifications.filter(n => !n.Unread);
524
+ temp = all.filter(n => !n.Unread);
482
525
  break;
483
526
  }
484
527
  // Apply type filter if selected
@@ -486,24 +529,26 @@ export class UserNotificationsComponent extends BaseAngularComponent {
486
529
  temp = temp.filter(n => UUIDsEqual(n.NotificationTypeID, this.SelectedTypeFilter));
487
530
  }
488
531
  // Apply text filter if it is not empty
489
- if (this.CurrentFilter.trim().length > 0) {
532
+ const filterLower = this.CurrentFilter.trim().toLowerCase();
533
+ if (filterLower.length > 0) {
490
534
  // check for inclusion of filter value in title or message
491
- temp = temp.filter(n => n.Title?.toLowerCase().includes(this.CurrentFilter.trim().toLowerCase()) ||
492
- n.Message?.toLowerCase().includes(this.CurrentFilter.trim().toLowerCase()));
535
+ temp = temp.filter(n => n.Title?.toLowerCase().includes(filterLower) ||
536
+ n.Message?.toLowerCase().includes(filterLower));
493
537
  }
538
+ this.notificationsToShowKey = key;
539
+ this.notificationsToShowCache = temp;
494
540
  return temp;
495
541
  }
496
542
  /**
497
543
  * The filtered notifications as render-ready day groups. Memoized on a composite
498
- * key of the inputs so the getter stays cheap across change-detection passes and
499
- * the (time-sensitive) VM snapshots never rebuild mid-pass.
544
+ * key of the inputs BEFORE filtering `AllNotifications`, so the getter stays $O(1)$
545
+ * across change-detection passes and the (time-sensitive) VM snapshots never rebuild mid-pass.
500
546
  */
501
547
  get Groups() {
502
- const list = this.NotificationsToShow;
503
- const unread = list.filter(n => n.Unread).length;
504
- const key = `${list.length}|${unread}|${this.RadioSelected}|${this.SelectedTypeFilter}|${this.CurrentFilter}|${list[0]?.ID ?? ''}|${list[list.length - 1]?.ID ?? ''}`;
548
+ const { baseKey } = this.computeAllNotificationsSignature();
549
+ const key = `${baseKey}|${this.RadioSelected}|${this.SelectedTypeFilter ?? ''}|${this.CurrentFilter}`;
505
550
  if (key !== this.groupsKey) {
506
- this.groups = this.buildGroups(list);
551
+ this.groups = this.buildGroups(this.NotificationsToShow);
507
552
  this.groupsKey = key;
508
553
  }
509
554
  return this.groups;
@@ -651,6 +696,9 @@ export class UserNotificationsComponent extends BaseAngularComponent {
651
696
  if (typeName === 'meet-room') {
652
697
  return true;
653
698
  }
699
+ if (this.parseUserRoutineConfig(notification)) {
700
+ return true;
701
+ }
654
702
  }
655
703
  const info = this.NotificationUrl(notification);
656
704
  return (info !== null && info.urlParts && info.urlParts.length > 0);
@@ -718,10 +766,20 @@ export class UserNotificationsComponent extends BaseAngularComponent {
718
766
  return SharedService.UserNotifications;
719
767
  }
720
768
  get UnreadNotifications() {
721
- return this.AllNotifications.filter(n => n.Unread);
769
+ const { all, baseKey } = this.computeAllNotificationsSignature();
770
+ if (baseKey !== this.unreadNotificationsKey) {
771
+ this.unreadNotificationsKey = baseKey;
772
+ this.unreadNotificationsCache = all.filter(n => n.Unread);
773
+ }
774
+ return this.unreadNotificationsCache;
722
775
  }
723
776
  get ReadNotifications() {
724
- return this.AllNotifications.filter(n => !n.Unread);
777
+ const { all, baseKey } = this.computeAllNotificationsSignature();
778
+ if (baseKey !== this.readNotificationsKey) {
779
+ this.readNotificationsKey = baseKey;
780
+ this.readNotificationsCache = all.filter(n => !n.Unread);
781
+ }
782
+ return this.readNotificationsCache;
725
783
  }
726
784
  SelectReadOption(option) {
727
785
  this.RadioSelected = option;
@@ -824,6 +882,9 @@ export class UserNotificationsComponent extends BaseAngularComponent {
824
882
  if (this.navigateToMeetRoom(notification)) {
825
883
  return;
826
884
  }
885
+ if (this.navigateToUserRoutine(notification)) {
886
+ return;
887
+ }
827
888
  if (this.navigateToHandoffConsole(notification)) {
828
889
  return;
829
890
  }
@@ -1009,9 +1070,63 @@ export class UserNotificationsComponent extends BaseAngularComponent {
1009
1070
  navConfig['versionNumber'] = config.versionNumber;
1010
1071
  if (config.taskId)
1011
1072
  navConfig['taskId'] = config.taskId;
1012
- this.navigationService.OpenNavItemByName('Conversations', navConfig, chatApp.ID);
1073
+ // The Chat tab is usually already open and gets reused; a reused tab reads its configuration
1074
+ // only at init, so the same values go in the query params, which it does react to.
1075
+ this.navigationService.OpenNavItemByName('Conversations', navConfig, chatApp.ID, { queryParams: { ...navConfig } });
1076
+ return true;
1077
+ }
1078
+ /** The routine-run config of a notification, or null when it is not one (or names no routine). */
1079
+ parseUserRoutineConfig(notification) {
1080
+ if (!notification.ResourceConfiguration || notification.ResourceConfiguration.trim().length === 0) {
1081
+ return null;
1082
+ }
1083
+ const config = SafeJSONParse(notification.ResourceConfiguration);
1084
+ if (!config || config.type?.trim().toLowerCase() !== 'userroutine' || !config.routineId) {
1085
+ return null;
1086
+ }
1087
+ return config;
1088
+ }
1089
+ /**
1090
+ * Opens what a routine run produced: its reply, in the routine's own conversation (the
1091
+ * `{ type:'UserRoutine', routineId, runId, conversationId? }` ResourceConfiguration). That
1092
+ * conversation is application-scoped, so the chat sidebar never lists it; the click deep-links
1093
+ * into the Chat app's Conversations item. A notification written before the config carried the
1094
+ * conversation reads it off the routine; a routine with no conversation opens in the Routines
1095
+ * app. Returns false (not handled) when the notification is not a routine run.
1096
+ */
1097
+ navigateToUserRoutine(notification) {
1098
+ const config = this.parseUserRoutineConfig(notification);
1099
+ if (!config) {
1100
+ return false;
1101
+ }
1102
+ void this.openUserRoutineOutput(config);
1013
1103
  return true;
1014
1104
  }
1105
+ async openUserRoutineOutput(config) {
1106
+ const conversationId = config.conversationId ?? await this.loadRoutineConversationId(config.routineId);
1107
+ const chatApp = conversationId ? this.appManager.GetAppByName('Chat') : null;
1108
+ if (conversationId && chatApp) {
1109
+ // Query params as well as configuration: an already-open Chat tab is reused and reacts
1110
+ // only to a query-param change (its configuration is read once, at init).
1111
+ await this.navigationService.OpenNavItemByName('Conversations', { conversationId }, chatApp.ID, { queryParams: { conversationId } });
1112
+ return;
1113
+ }
1114
+ const routinesApp = this.appManager.GetAppByName('Routines');
1115
+ if (routinesApp) {
1116
+ await this.navigationService.OpenNavItemByName('My Routines', { routine: config.routineId }, routinesApp.ID);
1117
+ return;
1118
+ }
1119
+ MJNotificationService.Instance.CreateSimpleNotification('The Routines app is not installed, so this routine cannot be opened', 'error', 4000);
1120
+ }
1121
+ /** The routine's ConversationID, or null when the routine cannot be read or has none. */
1122
+ async loadRoutineConversationId(routineId) {
1123
+ const p = this.ProviderToUse;
1124
+ const routine = await p.GetEntityObject('MJ: User Routines', p.CurrentUser);
1125
+ if (!await routine.Load(routineId)) {
1126
+ return null;
1127
+ }
1128
+ return routine.ConversationID ?? null;
1129
+ }
1015
1130
  GetNotificationType(typeId) {
1016
1131
  if (!typeId)
1017
1132
  return null;
@@ -1084,5 +1199,5 @@ export class UserNotificationsComponent extends BaseAngularComponent {
1084
1199
  type: Component,
1085
1200
  args: [{ standalone: false, selector: 'app-user-notifications', template: "<div class=\"notifications-container\">\n <!-- Header Section -->\n <div class=\"notifications-header\">\n <div class=\"header-top\">\n <div class=\"header-title\">\n <div class=\"title-icon\">\n <i class=\"fa-solid fa-bell\"></i>\n </div>\n <div class=\"title-text\">\n <h1>Notifications</h1>\n @if (UnreadNotifications.length > 0) {\n <span class=\"title-subtitle\">{{UnreadNotifications.length}} unread</span>\n } @else if (AllNotifications.length > 0) {\n <span class=\"title-subtitle\">All caught up</span>\n }\n </div>\n </div>\n\n @if (AllNotifications.length > 0) {\n <div class=\"header-actions\">\n @if (UnreadNotifications.length > 0) {\n <button class=\"action-btn action-btn-ghost\"\n type=\"button\"\n (click)=\"markAllAsRead()\"\n aria-label=\"Mark all notifications as read\">\n <i class=\"fa-solid fa-check-double\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Mark All Read</span>\n </button>\n }\n @if (ReadNotifications.length > 0) {\n <button class=\"action-btn action-btn-secondary\"\n type=\"button\"\n (click)=\"markAllAsUnread()\"\n aria-label=\"Mark all notifications as unread\">\n <i class=\"fa-solid fa-envelope\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Mark All Unread</span>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Filters Section -->\n @if (AllNotifications.length > 0) {\n <div class=\"filters-section\">\n <div class=\"filters-slider\">\n <div class=\"slider-content\">\n <!-- Read-status pills -->\n <div class=\"header-stats\" role=\"group\" aria-label=\"Filter notifications by read status\">\n <button class=\"stat-pill\"\n type=\"button\"\n [class.active]=\"radioSelected === 'All'\"\n (click)=\"selectReadOption('All')\"\n [attr.aria-pressed]=\"radioSelected === 'All'\"\n aria-label=\"Show all notifications\">\n <span class=\"stat-label\">All</span>\n <span class=\"stat-count\">{{AllNotifications.length}}</span>\n </button>\n\n @if (UnreadNotifications.length > 0 || ReadNotifications.length === 0) {\n <button class=\"stat-pill\"\n type=\"button\"\n [class.active]=\"radioSelected === 'Unread'\"\n [class.has-unread]=\"UnreadNotifications.length > 0\"\n (click)=\"selectReadOption('Unread')\"\n [attr.aria-pressed]=\"radioSelected === 'Unread'\"\n aria-label=\"Show unread notifications\">\n <span class=\"stat-label\">Unread</span>\n <span class=\"stat-count\" [class.unread-count]=\"UnreadNotifications.length > 0\">\n {{UnreadNotifications.length}}\n </span>\n </button>\n }\n\n @if (ReadNotifications.length > 0 || UnreadNotifications.length === 0) {\n <button class=\"stat-pill\"\n type=\"button\"\n [class.active]=\"radioSelected === 'Read'\"\n (click)=\"selectReadOption('Read')\"\n [attr.aria-pressed]=\"radioSelected === 'Read'\"\n aria-label=\"Show read notifications\">\n <span class=\"stat-label\">Read</span>\n <span class=\"stat-count\">{{ReadNotifications.length}}</span>\n </button>\n }\n </div>\n\n <!-- Type filter -->\n @if (notificationTypes.length > 0) {\n <div class=\"filter-item type-filter-slider\">\n <select class=\"type-select\"\n [(ngModel)]=\"selectedTypeFilter\"\n (change)=\"onTypeFilterChange(selectedTypeFilter)\"\n aria-label=\"Filter by notification type\">\n <option [value]=\"null\">All Types</option>\n @for (type of notificationTypes; track type.ID) {\n <option [value]=\"type.ID\">{{type.Name}}</option>\n }\n </select>\n </div>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"search-section\">\n <div class=\"search-container\">\n <i class=\"fa-solid fa-magnifying-glass search-icon\" aria-hidden=\"true\"></i>\n <input type=\"text\"\n class=\"search-input\"\n placeholder=\"Search notifications...\"\n [value]=\"currentFilter\"\n (input)=\"onFilterChanged($any($event.target).value)\"\n aria-label=\"Search notifications\" />\n </div>\n </div>\n\n @if (NotificationsToShow.length !== AllNotifications.length) {\n <div class=\"filter-info\">\n <span class=\"filter-badge\">\n <i class=\"fa-solid fa-filter\" aria-hidden=\"true\"></i>\n Showing {{NotificationsToShow.length}} of {{AllNotifications.length}}\n </span>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Notifications Content -->\n <div class=\"notifications-content\">\n @if (NotificationsToShow.length === 0 && AllNotifications.length === 0) {\n <mj-empty-state\n Variant=\"success\"\n Icon=\"fa-solid fa-bell-slash\"\n Title=\"No Notifications\"\n Message=\"You're all caught up! New notifications will appear here.\" />\n } @else if (NotificationsToShow.length === 0) {\n <mj-empty-state\n Variant=\"no-results\"\n Icon=\"fa-solid fa-filter-circle-xmark\"\n Title=\"No Matching Notifications\"\n Message=\"Try adjusting your filters to see more notifications.\" />\n } @else {\n <!-- Day-grouped notification list -->\n <div class=\"notifications-list\">\n @for (group of Groups; track group.Key) {\n <div class=\"group-label\">{{group.Label}}</div>\n\n @for (vm of group.Items; track vm.N.ID) {\n <article class=\"notification-card\"\n [class.unread]=\"vm.N.Unread\"\n [class.expanded]=\"IsExpanded(vm)\"\n [class.clickable]=\"vm.Clickable\"\n role=\"article\"\n [attr.aria-label]=\"vm.N.Title\">\n\n <!-- Unread Indicator Bar -->\n <div class=\"card-indicator\" aria-hidden=\"true\"></div>\n\n <!-- Type Icon -->\n <div class=\"card-icon\" aria-hidden=\"true\">\n <i [class]=\"'fa-solid ' + getTypeIcon(vm.N)\"\n [style.color]=\"getTypeColor(vm.N)\"></i>\n </div>\n\n <!-- Content -->\n <div class=\"card-content\" (click)=\"notificationClicked(vm.N)\">\n <div class=\"card-header\">\n <h3 class=\"card-title\" [class.card-title-unread]=\"vm.N.Unread\">\n {{vm.N.Title}}\n </h3>\n <span class=\"card-time\" [title]=\"(vm.N.__mj_CreatedAt | date:'medium') ?? ''\">\n @if (vm.Relative) {\n {{vm.Relative}}\n } @else {\n <time [attr.datetime]=\"vm.N.__mj_CreatedAt\">{{vm.N.__mj_CreatedAt | date:'MMM d, h:mm a'}}</time>\n }\n </span>\n </div>\n\n <div class=\"card-meta\">\n <span class=\"card-type\">{{getTypeName(vm.N)}}</span>\n @if (vm.Clickable) {\n <span class=\"card-open-hint\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\" aria-hidden=\"true\"></i> Open\n </span>\n }\n </div>\n\n @if (!IsExpanded(vm)) {\n <p class=\"card-message\">{{vm.Preview}}</p>\n } @else {\n <div class=\"card-rich\" (click)=\"$event.stopPropagation()\">\n @switch (vm.Kind) {\n @case ('html') {\n <!-- Angular sanitizes on [innerHTML] bind; chrome tags were stripped at classification -->\n <div class=\"card-rich-html\" [innerHTML]=\"vm.Body\"></div>\n }\n @case ('markdown') {\n <mj-markdown class=\"card-rich-md\" [data]=\"vm.Body\" [enableMermaid]=\"false\"></mj-markdown>\n }\n @default {\n <p class=\"card-message card-message-full\">{{vm.N.Message}}</p>\n }\n }\n </div>\n }\n\n @if (vm.Expandable) {\n <button class=\"card-expand-btn\" type=\"button\"\n (click)=\"ToggleExpanded(vm, $event)\"\n [attr.aria-expanded]=\"IsExpanded(vm)\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!IsExpanded(vm)\" [class.fa-chevron-up]=\"IsExpanded(vm)\" aria-hidden=\"true\"></i>\n {{ IsExpanded(vm) ? 'Show less' : 'Show more' }}\n </button>\n }\n </div>\n\n <!-- Actions -->\n <div class=\"card-actions\">\n @if (vm.N.Unread) {\n <button class=\"card-action-btn\"\n type=\"button\"\n (click)=\"markAsRead(vm.N, true, null)\"\n [attr.aria-label]=\"'Mark ' + vm.N.Title + ' as read'\"\n title=\"Mark as Read\">\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n <span class=\"action-text\">Read</span>\n </button>\n } @else {\n <button class=\"card-action-btn\"\n type=\"button\"\n (click)=\"markAsRead(vm.N, false, null)\"\n [attr.aria-label]=\"'Mark ' + vm.N.Title + ' as unread'\"\n title=\"Mark as Unread\">\n <i class=\"fa-solid fa-envelope\" aria-hidden=\"true\"></i>\n <span class=\"action-text\">Unread</span>\n </button>\n }\n </div>\n </article>\n }\n }\n </div>\n }\n </div>\n</div>\n", styles: ["/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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 User Notifications \u2014 day-grouped cards with rich (HTML/Markdown) content,\n expand/collapse, and token-driven theming (light + dark for free).\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 */\n\n:host {\n display: block;\n height: 100%;\n}\n\n.notifications-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-page);\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.notifications-header {\n flex-shrink: 0;\n padding: 20px 24px 12px 24px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.header-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 14px;\n}\n\n.header-title {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n\n.title-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-lg, 12px);\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 16px;\n}\n\n.title-text {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.title-text h1 {\n margin: 0;\n font-size: 20px;\n font-weight: 700;\n color: var(--mj-text-primary);\n line-height: 1.2;\n}\n\n.title-subtitle {\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n\n.header-actions {\n display: flex;\n gap: 8px;\n flex-shrink: 0;\n}\n\n.action-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 7px 12px;\n border-radius: var(--mj-radius-md, 8px);\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.15s, color 0.15s, border-color 0.15s;\n}\n\n.action-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.action-btn-ghost {\n border-color: color-mix(in srgb, var(--mj-brand-primary) 35%, transparent);\n color: var(--mj-brand-primary);\n}\n\n.action-btn-ghost:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n/* \u2500\u2500 Filters \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.filters-section {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\n.filters-slider {\n min-width: 0;\n}\n\n.slider-content {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.header-stats {\n display: inline-flex;\n gap: 4px;\n padding: 3px;\n border-radius: var(--mj-radius-full, 999px);\n background: var(--mj-bg-surface-sunken);\n}\n\n.stat-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 12px;\n border: none;\n border-radius: var(--mj-radius-full, 999px);\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.15s, color 0.15s;\n}\n\n.stat-pill:hover {\n color: var(--mj-text-primary);\n}\n\n.stat-pill.active {\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.stat-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n padding: 0 5px;\n border-radius: var(--mj-radius-full, 999px);\n background: color-mix(in srgb, var(--mj-text-muted) 14%, transparent);\n color: var(--mj-text-secondary);\n font-size: 10px;\n font-weight: 700;\n}\n\n.stat-count.unread-count {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n color: var(--mj-brand-primary);\n}\n\n.type-select {\n padding: 7px 10px;\n border-radius: var(--mj-radius-md, 8px);\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.search-section {\n flex: 1 1 220px;\n min-width: 180px;\n}\n\n.search-container {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.search-icon {\n position: absolute;\n left: 12px;\n color: var(--mj-text-muted);\n font-size: 12px;\n pointer-events: none;\n}\n\n.search-input {\n width: 100%;\n padding: 8px 12px 8px 32px;\n border-radius: var(--mj-radius-md, 8px);\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 13px;\n}\n\n.search-input:focus {\n outline: none;\n border-color: var(--mj-border-focus);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 12%, transparent);\n}\n\n.filter-info {\n flex-basis: 100%;\n}\n\n.filter-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n border-radius: var(--mj-radius-full, 999px);\n background: color-mix(in srgb, var(--mj-status-info) 10%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n font-size: 11px;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Content / list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.notifications-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n padding: 16px 24px 32px 24px;\n}\n\n.notifications-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n max-width: 960px;\n margin: 0 auto;\n}\n\n.group-label {\n position: sticky;\n top: -16px;\n z-index: 1;\n padding: 10px 2px 4px 2px;\n background: var(--mj-bg-page);\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--mj-text-muted);\n}\n\n.group-label:not(:first-child) {\n margin-top: 8px;\n}\n\n/* \u2500\u2500 Card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.notification-card {\n position: relative;\n display: flex;\n gap: 12px;\n padding: 14px 16px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 12px);\n background: var(--mj-bg-surface);\n transition: border-color 0.15s, box-shadow 0.15s, background-color 0.15s;\n overflow: hidden;\n}\n\n.notification-card:hover {\n border-color: var(--mj-border-strong);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.notification-card.unread {\n background: color-mix(in srgb, var(--mj-brand-primary) 3%, var(--mj-bg-surface));\n}\n\n.card-indicator {\n display: none;\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 3px;\n background: var(--mj-brand-primary);\n border-radius: 0 2px 2px 0;\n}\n\n.notification-card.unread .card-indicator {\n display: block;\n}\n\n.card-icon {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border-radius: var(--mj-radius-md, 10px);\n background: var(--mj-bg-surface-sunken);\n font-size: 14px;\n}\n\n.card-content {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.notification-card.clickable .card-content {\n cursor: pointer;\n}\n\n.card-header {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 12px;\n}\n\n.card-title {\n margin: 0;\n font-size: 14px;\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow-wrap: anywhere;\n}\n\n.card-title-unread {\n font-weight: 700;\n}\n\n.card-time {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n}\n\n.card-meta {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 3px 0 6px 0;\n}\n\n.card-type {\n display: inline-flex;\n padding: 2px 8px;\n border-radius: var(--mj-radius-full, 999px);\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-secondary);\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n.card-open-hint {\n display: none;\n align-items: center;\n gap: 4px;\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.notification-card.clickable:hover .card-open-hint {\n display: inline-flex;\n}\n\n.card-message {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--mj-text-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n\n.card-message-full {\n display: block;\n -webkit-line-clamp: unset;\n white-space: pre-wrap;\n}\n\n/* \u2500\u2500 Rich (expanded) content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.card-rich {\n margin-top: 4px;\n padding: 12px 14px;\n border: 1px solid var(--mj-border-subtle);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-card);\n cursor: default;\n}\n\n/* Sanitized foreign markup: keep it inside the card no matter what it brought along */\n.card-rich-html {\n font-size: 13px;\n line-height: 1.55;\n color: var(--mj-text-primary);\n overflow-x: auto;\n}\n\n.card-rich-html img {\n max-width: 100%;\n height: auto;\n}\n\n.card-rich-html table {\n max-width: 100%;\n border-collapse: collapse;\n}\n\n.card-rich-html td,\n.card-rich-html th {\n padding: 4px 8px;\n border: 1px solid var(--mj-border-subtle);\n}\n\n.card-rich-html a {\n color: var(--mj-text-link);\n}\n\n.card-rich-html h1,\n.card-rich-html h2,\n.card-rich-html h3 {\n margin: 0.4em 0 0.3em 0;\n font-size: 1.05em;\n}\n\n.card-rich-md {\n display: block;\n font-size: 13px;\n}\n\n.card-expand-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: 8px;\n padding: 4px 10px;\n border: none;\n border-radius: var(--mj-radius-full, 999px);\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n}\n\n.card-expand-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n/* \u2500\u2500 Card actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.card-actions {\n flex-shrink: 0;\n display: flex;\n align-items: flex-start;\n}\n\n.card-action-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid transparent;\n border-radius: var(--mj-radius-md, 8px);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: 11px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.15s, color 0.15s;\n}\n\n.card-action-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n border-color: var(--mj-border-default);\n}\n\n/* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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@media (max-width: 720px) {\n .notifications-header {\n padding: 14px 14px 10px 14px;\n }\n\n .notifications-content {\n padding: 12px 12px 24px 12px;\n }\n\n .btn-text,\n .action-text {\n display: none;\n }\n\n .card-header {\n flex-direction: column;\n gap: 2px;\n }\n\n .card-time {\n order: -1;\n }\n}\n"] }]
1086
1201
  }], () => [{ type: i1.SharedService }, { type: i1.NavigationService }, { type: i2.ApplicationManager }], null); })();
1087
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserNotificationsComponent, { className: "UserNotificationsComponent", filePath: "src/lib/user-notifications/user-notifications.component.ts", lineNumber: 93 }); })();
1202
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserNotificationsComponent, { className: "UserNotificationsComponent", filePath: "src/lib/user-notifications/user-notifications.component.ts", lineNumber: 104 }); })();
1088
1203
  //# sourceMappingURL=user-notifications.component.js.map