codexly-ui 0.11.0 → 0.11.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.
@@ -9603,6 +9603,13 @@ class ClxTabsComponent {
9603
9603
  _clxColor = computed(() => this.clxColor() ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "_clxColor" }] : /* istanbul ignore next */ []));
9604
9604
  _panels = contentChildren(ClxTabDirective, ...(ngDevMode ? [{ debugName: "_panels" }] : /* istanbul ignore next */ []));
9605
9605
  _underlineClass = computed(() => resolveColor(this._clxColor()).bg, ...(ngDevMode ? [{ debugName: "_underlineClass" }] : /* istanbul ignore next */ []));
9606
+ _badgeClass(tabId) {
9607
+ const active = this.activeTab() === tabId;
9608
+ const t = resolveColor(this._clxColor());
9609
+ return active
9610
+ ? `ml-0.5 px-1.5 py-0.5 rounded-full text-[10px] font-bold leading-none tabular-nums ${t.bgSubtle} ${t.textSubtle}`
9611
+ : 'ml-0.5 px-1.5 py-0.5 rounded-full text-[10px] font-bold leading-none tabular-nums bg-clx-surface-2 text-clx-text-subtle';
9612
+ }
9606
9613
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9607
9614
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxTabsComponent, isStandalone: true, selector: "clx-tabs", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: true, transformFunction: null }, clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeTab: "activeTabChange" }, queries: [{ propertyName: "_panels", predicate: ClxTabDirective, isSignal: true }], ngImport: i0, template: `
9608
9615
  <!-- Tab bar -->
@@ -9625,6 +9632,9 @@ class ClxTabsComponent {
9625
9632
  <span clx-icon [name]="tab.icon" size="xs" class="shrink-0"></span>
9626
9633
  }
9627
9634
  {{ tab.label }}
9635
+ @if (tab.badge !== undefined) {
9636
+ <span [class]="_badgeClass(tab.id)">{{ tab.badge }}</span>
9637
+ }
9628
9638
  </button>
9629
9639
  @if (activeTab() === tab.id) {
9630
9640
  <span class="absolute bottom-0 left-0 right-0 h-0.5 rounded-full {{ _underlineClass() }}"></span>
@@ -9666,6 +9676,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
9666
9676
  <span clx-icon [name]="tab.icon" size="xs" class="shrink-0"></span>
9667
9677
  }
9668
9678
  {{ tab.label }}
9679
+ @if (tab.badge !== undefined) {
9680
+ <span [class]="_badgeClass(tab.id)">{{ tab.badge }}</span>
9681
+ }
9669
9682
  </button>
9670
9683
  @if (activeTab() === tab.id) {
9671
9684
  <span class="absolute bottom-0 left-0 right-0 h-0.5 rounded-full {{ _underlineClass() }}"></span>
@@ -11272,29 +11285,60 @@ const NOTIFICATION_SEVERITY_ICON = {
11272
11285
  error: 'error',
11273
11286
  };
11274
11287
 
11275
- /** "Dumb" panel: renders whatever list of items it's given via `items()` and emits intent
11276
- * (itemClick/markRead/markAllRead/delete/deleteAllRead) outward it never fetches, persists,
11277
- * or connects to a real-time source itself. Callers (e.g. codexly-app's NotificationService,
11278
- * backed by SSE) own that and just feed the current list in. Meant to be projected inside
11279
- * clx-drawer's body, same as any other content. */
11288
+ /** Self-contained bell trigger + floating panel same overlay pattern as clx-profile (CDK
11289
+ * Overlay anchored to the trigger, no page-level backdrop), not a full-height clx-drawer. It's
11290
+ * "dumb" beyond that: renders whatever `items()` it's given and emits intent (itemClick/
11291
+ * markRead/delete/...) outward. Callers (e.g. a NotificationService backed by SSE) own fetching
11292
+ * and persistence this component never calls an API itself. */
11280
11293
  class ClxNotificationComponent {
11281
11294
  _themeSvc = inject(ClxThemeService);
11282
11295
  items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
11283
11296
  color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
11284
- size = input(undefined, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
11297
+ /** Optional third tab that filters items by ClxNotificationItem.category e.g.
11298
+ * `{ id: 'orders', label: 'Órdenes' }`. Omit for just Todas/Sin leer. */
11299
+ categoryTab = input(undefined, ...(ngDevMode ? [{ debugName: "categoryTab" }] : /* istanbul ignore next */ []));
11285
11300
  itemClick = output();
11286
11301
  markRead = output();
11287
11302
  markAllRead = output();
11288
11303
  delete = output();
11289
11304
  deleteAllRead = output();
11290
- _filter = signal('all', ...(ngDevMode ? [{ debugName: "_filter" }] : /* istanbul ignore next */ []));
11291
11305
  _style = this._themeSvc.notificationStyle;
11292
11306
  _color = computed(() => this.color() ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
11293
- _resolvedSize = computed(() => this.size() ?? this._style().size, ...(ngDevMode ? [{ debugName: "_resolvedSize" }] : /* istanbul ignore next */ []));
11294
- _sizeTokens = computed(() => NOTIFICATION_SIZE_MAP[this._resolvedSize()], ...(ngDevMode ? [{ debugName: "_sizeTokens" }] : /* istanbul ignore next */ []));
11307
+ // ── CDK Overlay ─────────────────────────────────────────────────────────────
11308
+ _sso = inject(ScrollStrategyOptions);
11309
+ _scrollStrategy = this._sso.reposition();
11310
+ _positions = [
11311
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 8 },
11312
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -8 },
11313
+ ];
11314
+ _isOpen = signal(false, ...(ngDevMode ? [{ debugName: "_isOpen" }] : /* istanbul ignore next */ []));
11315
+ _filter = signal('all', ...(ngDevMode ? [{ debugName: "_filter" }] : /* istanbul ignore next */ []));
11316
+ _ringClass = computed(() => {
11317
+ const t = resolveColor(this._color());
11318
+ return `focus-visible:${t.ringBase}`;
11319
+ }, ...(ngDevMode ? [{ debugName: "_ringClass" }] : /* istanbul ignore next */ []));
11320
+ _panelClass = computed(() => `w-96 bg-clx-surface shadow-xl border border-clx-border-soft overflow-hidden mt-2 flex flex-col ${resolveContainerRadius(this._style().radius)}`, ...(ngDevMode ? [{ debugName: "_panelClass" }] : /* istanbul ignore next */ []));
11295
11321
  _unreadCount = computed(() => this.items().filter(i => !i.read).length, ...(ngDevMode ? [{ debugName: "_unreadCount" }] : /* istanbul ignore next */ []));
11296
11322
  _readCount = computed(() => this.items().filter(i => i.read).length, ...(ngDevMode ? [{ debugName: "_readCount" }] : /* istanbul ignore next */ []));
11297
- _visibleItems = computed(() => this._filter() === 'unread' ? this.items().filter(i => !i.read) : this.items(), ...(ngDevMode ? [{ debugName: "_visibleItems" }] : /* istanbul ignore next */ []));
11323
+ _tabs = computed(() => {
11324
+ const tabs = [
11325
+ { id: 'all', label: 'Todas', badge: this.items().length || undefined },
11326
+ { id: 'unread', label: 'Sin leer', badge: this._unreadCount() || undefined },
11327
+ ];
11328
+ const category = this.categoryTab();
11329
+ if (category)
11330
+ tabs.push({ id: category.id, label: category.label });
11331
+ return tabs;
11332
+ }, ...(ngDevMode ? [{ debugName: "_tabs" }] : /* istanbul ignore next */ []));
11333
+ _visibleItems = computed(() => {
11334
+ const f = this._filter();
11335
+ if (f === 'unread')
11336
+ return this.items().filter(i => !i.read);
11337
+ const category = this.categoryTab();
11338
+ if (category && f === category.id)
11339
+ return this.items().filter(i => i.category === category.id);
11340
+ return this.items();
11341
+ }, ...(ngDevMode ? [{ debugName: "_visibleItems" }] : /* istanbul ignore next */ []));
11298
11342
  _groups = computed(() => {
11299
11343
  const byLabel = new Map();
11300
11344
  for (const item of this._visibleItems()) {
@@ -11307,10 +11351,9 @@ class ClxNotificationComponent {
11307
11351
  }
11308
11352
  return Array.from(byLabel, ([label, items]) => ({ label, items }));
11309
11353
  }, ...(ngDevMode ? [{ debugName: "_groups" }] : /* istanbul ignore next */ []));
11310
- _wrapClass = computed(() => `flex flex-col ${resolveContainerRadius(this._style().radius)}`, ...(ngDevMode ? [{ debugName: "_wrapClass" }] : /* istanbul ignore next */ []));
11311
11354
  _groupLabelClass = computed(() => {
11312
11355
  const w = this._style().groupLabelWeight;
11313
- return `px-1 pt-3 pb-1.5 text-[11px] uppercase tracking-wide text-clx-text-subtle font-[${w}]`;
11356
+ return `px-2 pt-3 pb-1.5 text-[11px] uppercase tracking-wide text-clx-text-subtle font-[${w}]`;
11314
11357
  }, ...(ngDevMode ? [{ debugName: "_groupLabelClass" }] : /* istanbul ignore next */ []));
11315
11358
  _linkClass = computed(() => resolveColor(this._color()).textSubtle, ...(ngDevMode ? [{ debugName: "_linkClass" }] : /* istanbul ignore next */ []));
11316
11359
  _unreadRailClass = computed(() => resolveColor(this._style().unreadColor).bg, ...(ngDevMode ? [{ debugName: "_unreadRailClass" }] : /* istanbul ignore next */ []));
@@ -11322,7 +11365,7 @@ class ClxNotificationComponent {
11322
11365
  }
11323
11366
  _iconWrapClass(item) {
11324
11367
  const c = resolveColor(this._severityColor(item.severity));
11325
- return `flex items-center justify-center rounded-lg shrink-0 ${c.bgSubtle} ${c.textSubtle} ${this._sizeTokens().iconWrap}`;
11368
+ return `flex items-center justify-center rounded-lg shrink-0 w-8.5 h-8.5 mt-0.5 ${c.bgSubtle} ${c.textSubtle}`;
11326
11369
  }
11327
11370
  _defaultIcon(severity) {
11328
11371
  return NOTIFICATION_SEVERITY_ICON[severity];
@@ -11332,11 +11375,11 @@ class ClxNotificationComponent {
11332
11375
  if (!item.read)
11333
11376
  this.markRead.emit(item.id);
11334
11377
  }
11335
- _countBadgeClass(tab) {
11336
- const active = this._filter() === tab;
11337
- return active
11338
- ? 'ml-0.5 text-[10px] font-bold tabular-nums'
11339
- : 'ml-0.5 text-[10px] font-bold tabular-nums text-clx-text-subtle';
11378
+ _toggle() { this._isOpen.update(v => !v); }
11379
+ _close() { this._isOpen.set(false); }
11380
+ _onKeydown(event) {
11381
+ if (event.key === 'Escape')
11382
+ this._close();
11340
11383
  }
11341
11384
  _severityColor(severity) {
11342
11385
  return this._style().severityColors[severity];
@@ -11358,199 +11401,244 @@ class ClxNotificationComponent {
11358
11401
  return new Date(iso).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
11359
11402
  }
11360
11403
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxNotificationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11361
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxNotificationComponent, isStandalone: true, selector: "clx-notification", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", markRead: "markRead", markAllRead: "markAllRead", delete: "delete", deleteAllRead: "deleteAllRead" }, ngImport: i0, template: `
11362
- <div [class]="_wrapClass()">
11363
- <!-- Filter -->
11364
- <div class="flex items-center gap-2 px-1 pb-3">
11365
- <button clx-button type="button" size="xs" shape="pill"
11366
- [variant]="_filter() === 'all' ? 'solid' : 'ghost'"
11367
- [color]="_filter() === 'all' ? _color() : 'slate'"
11368
- (click)="_filter.set('all')">
11369
- Todas
11370
- @if (items().length) {
11371
- <span [class]="_countBadgeClass('all')">{{ items().length }}</span>
11372
- }
11373
- </button>
11374
- <button clx-button type="button" size="xs" shape="pill"
11375
- [variant]="_filter() === 'unread' ? 'solid' : 'ghost'"
11376
- [color]="_filter() === 'unread' ? _color() : 'slate'"
11377
- (click)="_filter.set('unread')">
11378
- Sin leer
11379
- @if (_unreadCount() > 0) {
11380
- <span [class]="_countBadgeClass('unread')">{{ _unreadCount() }}</span>
11381
- }
11382
- </button>
11383
- <div class="flex-1"></div>
11384
- @if (_unreadCount() > 0) {
11385
- <button type="button" class="text-xs font-semibold shrink-0" [class]="_linkClass()"
11386
- (click)="markAllRead.emit()">
11387
- Marcar todo leído
11388
- </button>
11389
- }
11390
- @if (_readCount() > 0) {
11391
- <button type="button" class="text-xs font-semibold shrink-0 text-clx-text-subtle hover:text-red-500"
11392
- (click)="deleteAllRead.emit()">
11393
- Eliminar leídas
11394
- </button>
11395
- }
11396
- </div>
11404
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxNotificationComponent, isStandalone: true, selector: "clx-notification", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, categoryTab: { classPropertyName: "categoryTab", publicName: "categoryTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", markRead: "markRead", markAllRead: "markAllRead", delete: "delete", deleteAllRead: "deleteAllRead" }, host: { classAttribute: "relative inline-flex items-center" }, ngImport: i0, template: `
11405
+ <!-- Trigger -->
11406
+ <button
11407
+ #origin="cdkOverlayOrigin"
11408
+ cdkOverlayOrigin
11409
+ type="button"
11410
+ aria-label="Notificaciones"
11411
+ class="relative flex items-center justify-center w-9 h-9 rounded-full text-clx-text-muted hover:bg-clx-surface-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 cursor-pointer"
11412
+ [class]="_ringClass()"
11413
+ (click)="_toggle()">
11414
+ <span clx-icon name="notifications" size="sm"></span>
11415
+ @if (_unreadCount() > 0) {
11416
+ <span clx-badge [color]="_style().unreadColor" variant="solid" size="xs" shape="circle"
11417
+ class="absolute -top-0.5 -right-0.5">{{ _unreadCount() > 9 ? '9+' : _unreadCount() }}</span>
11418
+ }
11419
+ </button>
11397
11420
 
11398
- <!-- List -->
11399
- @if (_groups().length) {
11400
- <div class="flex flex-col">
11401
- @for (group of _groups(); track group.label) {
11402
- <p [class]="_groupLabelClass()">{{ group.label }}</p>
11403
- @for (item of group.items; track item.id) {
11404
- <div
11405
- class="group relative flex items-start gap-3 px-1 rounded-lg cursor-pointer hover:bg-clx-surface-2 {{ _sizeTokens().row }}"
11406
- role="button"
11407
- tabindex="0"
11408
- (click)="_open(item)"
11409
- (keydown.enter)="_open(item)">
11410
-
11411
- <span class="absolute left-0 top-1.5 bottom-1.5 w-[3px] rounded-full {{ item.read ? '' : _unreadRailClass() }}"></span>
11412
-
11413
- <span [class]="_iconWrapClass(item)">
11414
- <span clx-icon [name]="item.icon || _defaultIcon(item.severity)" [size]="_sizeTokens().iconSize"></span>
11415
- </span>
11421
+ <!-- Overlay panel -->
11422
+ <ng-template
11423
+ cdkConnectedOverlay
11424
+ [cdkConnectedOverlayOrigin]="origin"
11425
+ [cdkConnectedOverlayOpen]="_isOpen()"
11426
+ [cdkConnectedOverlayPositions]="_positions"
11427
+ [cdkConnectedOverlayScrollStrategy]="_scrollStrategy"
11428
+ [cdkConnectedOverlayPush]="true"
11429
+ (overlayOutsideClick)="_close()"
11430
+ (overlayKeydown)="_onKeydown($event)">
11416
11431
 
11417
- <div class="flex-1 min-w-0 pl-1.5">
11418
- <p class="flex items-baseline gap-1.5 {{ _sizeTokens().title }}" [class]="_titleClass(item)">
11419
- @if (!item.read) {
11420
- <span [class]="_unreadDotClass()"></span>
11432
+ <div [class]="_panelClass()">
11433
+ <!-- Header -->
11434
+ <div class="flex items-center justify-between gap-3 px-4 pt-4 pb-1">
11435
+ <p class="text-base font-semibold text-clx-text-label">Notificaciones</p>
11436
+ <div class="flex items-center gap-3">
11437
+ @if (_unreadCount() > 0) {
11438
+ <button type="button" class="text-xs font-semibold shrink-0" [class]="_linkClass()"
11439
+ (click)="markAllRead.emit()">
11440
+ Marcar todo leído
11441
+ </button>
11442
+ }
11443
+ <button type="button" class="w-6 h-6 flex items-center justify-center rounded-md text-clx-text-muted hover:bg-clx-surface-2"
11444
+ aria-label="Cerrar" (click)="_close()">
11445
+ <span clx-icon name="close" size="xs"></span>
11446
+ </button>
11447
+ </div>
11448
+ </div>
11449
+
11450
+ <!-- Tabs -->
11451
+ <div class="px-4">
11452
+ <clx-tabs [tabs]="_tabs()" [(activeTab)]="_filter" [clxColor]="_color()" />
11453
+ </div>
11454
+
11455
+ <!-- List -->
11456
+ <div class="max-h-105 overflow-y-auto px-2 pb-2">
11457
+ @if (_groups().length) {
11458
+ @for (group of _groups(); track group.label) {
11459
+ <p [class]="_groupLabelClass()">{{ group.label }}</p>
11460
+ @for (item of group.items; track item.id) {
11461
+ <div
11462
+ class="group relative flex items-start gap-3 pl-3 pr-2 rounded-lg cursor-pointer hover:bg-clx-surface-2 py-2.5"
11463
+ role="button"
11464
+ tabindex="0"
11465
+ (click)="_open(item)"
11466
+ (keydown.enter)="_open(item)">
11467
+
11468
+ <span class="absolute left-0.5 top-1 bottom-1 w-0.75 rounded-full {{ item.read ? '' : _unreadRailClass() }}"></span>
11469
+
11470
+ <span [class]="_iconWrapClass(item)">
11471
+ <span clx-icon [name]="item.icon || _defaultIcon(item.severity)" size="sm"></span>
11472
+ </span>
11473
+
11474
+ <div class="flex-1 min-w-0">
11475
+ <p class="flex items-baseline gap-1.5 text-sm" [class]="_titleClass(item)">
11476
+ @if (!item.read) {
11477
+ <span [class]="_unreadDotClass()"></span>
11478
+ }
11479
+ <span class="truncate">{{ item.title }}</span>
11480
+ </p>
11481
+ @if (item.detail) {
11482
+ <p class="mt-0.5 text-sm line-clamp-2" [class]="_detailClass()">{{ item.detail }}</p>
11421
11483
  }
11422
- <span class="truncate">{{ item.title }}</span>
11423
- </p>
11424
- @if (item.detail) {
11425
- <p class="mt-0.5 line-clamp-2 {{ _sizeTokens().detail }}" [class]="_detailClass()">{{ item.detail }}</p>
11426
- }
11427
- </div>
11484
+ <p class="mt-1 text-xs text-clx-text-subtle tabular-nums">{{ _timeOf(item.createdAt) }}</p>
11485
+ </div>
11428
11486
 
11429
- <div class="flex flex-col items-end gap-1 shrink-0 pl-2">
11430
- <span class="text-clx-text-subtle tabular-nums {{ _sizeTokens().time }}">{{ _timeOf(item.createdAt) }}</span>
11431
11487
  <button type="button"
11432
- class="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity rounded-md p-0.5 text-clx-text-subtle hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950"
11488
+ class="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity shrink-0 rounded-md p-1 text-clx-text-subtle hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950"
11433
11489
  aria-label="Eliminar notificación"
11434
11490
  (click)="$event.stopPropagation(); delete.emit(item.id)">
11435
11491
  <span clx-icon name="close" size="xs"></span>
11436
11492
  </button>
11437
11493
  </div>
11438
- </div>
11494
+ }
11439
11495
  }
11496
+ } @else {
11497
+ <div class="flex flex-col items-center justify-center gap-2 py-14 text-center">
11498
+ <span clx-icon name="notifications_off" size="xl" class="text-clx-text-subtle"></span>
11499
+ <p class="text-sm font-medium text-clx-text-label">
11500
+ {{ _filter() === 'unread' ? 'No tienes notificaciones sin leer' : 'No hay notificaciones' }}
11501
+ </p>
11502
+ </div>
11440
11503
  }
11441
11504
  </div>
11442
- } @else {
11443
- <div class="flex flex-col items-center justify-center gap-2 py-14 text-center">
11444
- <span clx-icon name="notifications_off" size="xl" class="text-clx-text-subtle"></span>
11445
- <p class="text-sm font-medium text-clx-text-label">
11446
- {{ _filter() === 'unread' ? 'No tienes notificaciones sin leer' : 'No hay notificaciones' }}
11447
- </p>
11448
- </div>
11449
- }
11450
- </div>
11451
- `, isInline: true, dependencies: [{ kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11505
+
11506
+ @if (_readCount() > 0) {
11507
+ <div class="flex justify-end px-4 py-2.5 border-t border-clx-border-soft">
11508
+ <button type="button" class="text-xs font-semibold text-clx-text-subtle hover:text-red-500"
11509
+ (click)="deleteAllRead.emit()">
11510
+ Eliminar leídas
11511
+ </button>
11512
+ </div>
11513
+ }
11514
+ </div>
11515
+ </ng-template>
11516
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned"] }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTab"], outputs: ["activeTabChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11452
11517
  }
11453
11518
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxNotificationComponent, decorators: [{
11454
11519
  type: Component,
11455
11520
  args: [{
11456
11521
  selector: 'clx-notification',
11457
11522
  standalone: true,
11458
- imports: [ClxButtonComponent, ClxIconComponent],
11523
+ imports: [OverlayModule, ClxIconComponent, ClxBadgeComponent, ClxTabsComponent],
11459
11524
  template: `
11460
- <div [class]="_wrapClass()">
11461
- <!-- Filter -->
11462
- <div class="flex items-center gap-2 px-1 pb-3">
11463
- <button clx-button type="button" size="xs" shape="pill"
11464
- [variant]="_filter() === 'all' ? 'solid' : 'ghost'"
11465
- [color]="_filter() === 'all' ? _color() : 'slate'"
11466
- (click)="_filter.set('all')">
11467
- Todas
11468
- @if (items().length) {
11469
- <span [class]="_countBadgeClass('all')">{{ items().length }}</span>
11470
- }
11471
- </button>
11472
- <button clx-button type="button" size="xs" shape="pill"
11473
- [variant]="_filter() === 'unread' ? 'solid' : 'ghost'"
11474
- [color]="_filter() === 'unread' ? _color() : 'slate'"
11475
- (click)="_filter.set('unread')">
11476
- Sin leer
11477
- @if (_unreadCount() > 0) {
11478
- <span [class]="_countBadgeClass('unread')">{{ _unreadCount() }}</span>
11479
- }
11480
- </button>
11481
- <div class="flex-1"></div>
11482
- @if (_unreadCount() > 0) {
11483
- <button type="button" class="text-xs font-semibold shrink-0" [class]="_linkClass()"
11484
- (click)="markAllRead.emit()">
11485
- Marcar todo leído
11486
- </button>
11487
- }
11488
- @if (_readCount() > 0) {
11489
- <button type="button" class="text-xs font-semibold shrink-0 text-clx-text-subtle hover:text-red-500"
11490
- (click)="deleteAllRead.emit()">
11491
- Eliminar leídas
11492
- </button>
11493
- }
11494
- </div>
11525
+ <!-- Trigger -->
11526
+ <button
11527
+ #origin="cdkOverlayOrigin"
11528
+ cdkOverlayOrigin
11529
+ type="button"
11530
+ aria-label="Notificaciones"
11531
+ class="relative flex items-center justify-center w-9 h-9 rounded-full text-clx-text-muted hover:bg-clx-surface-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 cursor-pointer"
11532
+ [class]="_ringClass()"
11533
+ (click)="_toggle()">
11534
+ <span clx-icon name="notifications" size="sm"></span>
11535
+ @if (_unreadCount() > 0) {
11536
+ <span clx-badge [color]="_style().unreadColor" variant="solid" size="xs" shape="circle"
11537
+ class="absolute -top-0.5 -right-0.5">{{ _unreadCount() > 9 ? '9+' : _unreadCount() }}</span>
11538
+ }
11539
+ </button>
11495
11540
 
11496
- <!-- List -->
11497
- @if (_groups().length) {
11498
- <div class="flex flex-col">
11499
- @for (group of _groups(); track group.label) {
11500
- <p [class]="_groupLabelClass()">{{ group.label }}</p>
11501
- @for (item of group.items; track item.id) {
11502
- <div
11503
- class="group relative flex items-start gap-3 px-1 rounded-lg cursor-pointer hover:bg-clx-surface-2 {{ _sizeTokens().row }}"
11504
- role="button"
11505
- tabindex="0"
11506
- (click)="_open(item)"
11507
- (keydown.enter)="_open(item)">
11508
-
11509
- <span class="absolute left-0 top-1.5 bottom-1.5 w-[3px] rounded-full {{ item.read ? '' : _unreadRailClass() }}"></span>
11510
-
11511
- <span [class]="_iconWrapClass(item)">
11512
- <span clx-icon [name]="item.icon || _defaultIcon(item.severity)" [size]="_sizeTokens().iconSize"></span>
11513
- </span>
11541
+ <!-- Overlay panel -->
11542
+ <ng-template
11543
+ cdkConnectedOverlay
11544
+ [cdkConnectedOverlayOrigin]="origin"
11545
+ [cdkConnectedOverlayOpen]="_isOpen()"
11546
+ [cdkConnectedOverlayPositions]="_positions"
11547
+ [cdkConnectedOverlayScrollStrategy]="_scrollStrategy"
11548
+ [cdkConnectedOverlayPush]="true"
11549
+ (overlayOutsideClick)="_close()"
11550
+ (overlayKeydown)="_onKeydown($event)">
11551
+
11552
+ <div [class]="_panelClass()">
11553
+ <!-- Header -->
11554
+ <div class="flex items-center justify-between gap-3 px-4 pt-4 pb-1">
11555
+ <p class="text-base font-semibold text-clx-text-label">Notificaciones</p>
11556
+ <div class="flex items-center gap-3">
11557
+ @if (_unreadCount() > 0) {
11558
+ <button type="button" class="text-xs font-semibold shrink-0" [class]="_linkClass()"
11559
+ (click)="markAllRead.emit()">
11560
+ Marcar todo leído
11561
+ </button>
11562
+ }
11563
+ <button type="button" class="w-6 h-6 flex items-center justify-center rounded-md text-clx-text-muted hover:bg-clx-surface-2"
11564
+ aria-label="Cerrar" (click)="_close()">
11565
+ <span clx-icon name="close" size="xs"></span>
11566
+ </button>
11567
+ </div>
11568
+ </div>
11569
+
11570
+ <!-- Tabs -->
11571
+ <div class="px-4">
11572
+ <clx-tabs [tabs]="_tabs()" [(activeTab)]="_filter" [clxColor]="_color()" />
11573
+ </div>
11514
11574
 
11515
- <div class="flex-1 min-w-0 pl-1.5">
11516
- <p class="flex items-baseline gap-1.5 {{ _sizeTokens().title }}" [class]="_titleClass(item)">
11517
- @if (!item.read) {
11518
- <span [class]="_unreadDotClass()"></span>
11575
+ <!-- List -->
11576
+ <div class="max-h-105 overflow-y-auto px-2 pb-2">
11577
+ @if (_groups().length) {
11578
+ @for (group of _groups(); track group.label) {
11579
+ <p [class]="_groupLabelClass()">{{ group.label }}</p>
11580
+ @for (item of group.items; track item.id) {
11581
+ <div
11582
+ class="group relative flex items-start gap-3 pl-3 pr-2 rounded-lg cursor-pointer hover:bg-clx-surface-2 py-2.5"
11583
+ role="button"
11584
+ tabindex="0"
11585
+ (click)="_open(item)"
11586
+ (keydown.enter)="_open(item)">
11587
+
11588
+ <span class="absolute left-0.5 top-1 bottom-1 w-0.75 rounded-full {{ item.read ? '' : _unreadRailClass() }}"></span>
11589
+
11590
+ <span [class]="_iconWrapClass(item)">
11591
+ <span clx-icon [name]="item.icon || _defaultIcon(item.severity)" size="sm"></span>
11592
+ </span>
11593
+
11594
+ <div class="flex-1 min-w-0">
11595
+ <p class="flex items-baseline gap-1.5 text-sm" [class]="_titleClass(item)">
11596
+ @if (!item.read) {
11597
+ <span [class]="_unreadDotClass()"></span>
11598
+ }
11599
+ <span class="truncate">{{ item.title }}</span>
11600
+ </p>
11601
+ @if (item.detail) {
11602
+ <p class="mt-0.5 text-sm line-clamp-2" [class]="_detailClass()">{{ item.detail }}</p>
11519
11603
  }
11520
- <span class="truncate">{{ item.title }}</span>
11521
- </p>
11522
- @if (item.detail) {
11523
- <p class="mt-0.5 line-clamp-2 {{ _sizeTokens().detail }}" [class]="_detailClass()">{{ item.detail }}</p>
11524
- }
11525
- </div>
11604
+ <p class="mt-1 text-xs text-clx-text-subtle tabular-nums">{{ _timeOf(item.createdAt) }}</p>
11605
+ </div>
11526
11606
 
11527
- <div class="flex flex-col items-end gap-1 shrink-0 pl-2">
11528
- <span class="text-clx-text-subtle tabular-nums {{ _sizeTokens().time }}">{{ _timeOf(item.createdAt) }}</span>
11529
11607
  <button type="button"
11530
- class="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity rounded-md p-0.5 text-clx-text-subtle hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950"
11608
+ class="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity shrink-0 rounded-md p-1 text-clx-text-subtle hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950"
11531
11609
  aria-label="Eliminar notificación"
11532
11610
  (click)="$event.stopPropagation(); delete.emit(item.id)">
11533
11611
  <span clx-icon name="close" size="xs"></span>
11534
11612
  </button>
11535
11613
  </div>
11536
- </div>
11614
+ }
11537
11615
  }
11616
+ } @else {
11617
+ <div class="flex flex-col items-center justify-center gap-2 py-14 text-center">
11618
+ <span clx-icon name="notifications_off" size="xl" class="text-clx-text-subtle"></span>
11619
+ <p class="text-sm font-medium text-clx-text-label">
11620
+ {{ _filter() === 'unread' ? 'No tienes notificaciones sin leer' : 'No hay notificaciones' }}
11621
+ </p>
11622
+ </div>
11538
11623
  }
11539
11624
  </div>
11540
- } @else {
11541
- <div class="flex flex-col items-center justify-center gap-2 py-14 text-center">
11542
- <span clx-icon name="notifications_off" size="xl" class="text-clx-text-subtle"></span>
11543
- <p class="text-sm font-medium text-clx-text-label">
11544
- {{ _filter() === 'unread' ? 'No tienes notificaciones sin leer' : 'No hay notificaciones' }}
11545
- </p>
11546
- </div>
11547
- }
11548
- </div>
11625
+
11626
+ @if (_readCount() > 0) {
11627
+ <div class="flex justify-end px-4 py-2.5 border-t border-clx-border-soft">
11628
+ <button type="button" class="text-xs font-semibold text-clx-text-subtle hover:text-red-500"
11629
+ (click)="deleteAllRead.emit()">
11630
+ Eliminar leídas
11631
+ </button>
11632
+ </div>
11633
+ }
11634
+ </div>
11635
+ </ng-template>
11549
11636
  `,
11550
11637
  encapsulation: ViewEncapsulation.None,
11551
11638
  changeDetection: ChangeDetectionStrategy.OnPush,
11639
+ host: { class: 'relative inline-flex items-center' },
11552
11640
  }]
11553
- }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], markRead: [{ type: i0.Output, args: ["markRead"] }], markAllRead: [{ type: i0.Output, args: ["markAllRead"] }], delete: [{ type: i0.Output, args: ["delete"] }], deleteAllRead: [{ type: i0.Output, args: ["deleteAllRead"] }] } });
11641
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], categoryTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "categoryTab", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], markRead: [{ type: i0.Output, args: ["markRead"] }], markAllRead: [{ type: i0.Output, args: ["markAllRead"] }], delete: [{ type: i0.Output, args: ["delete"] }], deleteAllRead: [{ type: i0.Output, args: ["deleteAllRead"] }] } });
11554
11642
 
11555
11643
  // ─── CDK overlay token (carries the resolved ref into the component) ──────────
11556
11644
  const CLX_ALERT_OPTIONS = new InjectionToken('CLX_ALERT_OPTIONS');