Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
@if (mobile()) {
<div class="flex items-center gap-1 px-3 py-2" data-testid="lens-switcher-mobile">
@for (lens of lenses(); track lens.id) {
@let isActive = activeLensId() === lens.id;
<button
type="button"
(click)="setLens(lens.id)"
[attr.data-testid]="lens.testId + '-mobile'"
class="flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors"
[ngClass]="activeLens() === lens.id ? 'bg-indigo-900 text-white' : 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'">
<i [ngClass]="activeLens() === lens.id ? lens.activeIcon : lens.icon" class="text-sm"></i>
[ngClass]="isActive ? 'bg-indigo-900 text-white' : 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'">
<i [ngClass]="isActive ? lens.activeIcon : lens.icon" class="text-sm"></i>
<span>{{ lens.shortLabel }}</span>
</button>
}
Expand Down Expand Up @@ -65,7 +66,7 @@
<!-- Lens Buttons -->
<div class="flex flex-col items-center gap-3 flex-1">
@for (lens of lenses(); track lens.id) {
@let isActive = activeLens() === lens.id;
@let isActive = activeLensId() === lens.id;
<button
type="button"
(click)="setLens(lens.id)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT

import { NgClass } from '@angular/common';
import { afterNextRender, Component, computed, inject, input, signal, viewChild } from '@angular/core';
import { afterNextRender, Component, computed, inject, input, signal, Signal, viewChild } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { AvatarComponent } from '@components/avatar/avatar.component';
import { ButtonComponent } from '@components/button/button.component';
Expand Down Expand Up @@ -35,7 +35,13 @@ export class LensSwitcherComponent {
public readonly mobile = input<boolean>(false);

protected readonly activeLens = this.lensService.activeLens;
protected readonly lenses = this.lensService.availableLenses;
protected readonly lenses = this.lensService.displayLenses;
protected readonly isHybrid = this.lensService.isHybridPersona;
// Hybrid personas merge the 'project' button with the 'foundation' lens state — both map to 'project' for highlighting.
protected readonly activeLensId: Signal<Lens> = computed(() => {
const active = this.activeLens();
return this.isHybrid() && active === 'foundation' ? 'project' : active;
});
protected readonly user = this.userService.user;
protected readonly insightsUrl = buildInsightsUrl();
protected readonly userMenu = viewChild<Popover>('userMenu');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,22 @@
</div>
</div>

<div class="flex flex-col gap-0.5 grow items-start relative min-w-0 overflow-hidden">
<div class="flex flex-col gap-1.5 grow items-start relative min-w-0 overflow-hidden">
<p class="sidebar-project-name" [attr.title]="displayName()">{{ displayName() }}</p>
<span class="text-xs font-normal text-gray-400 leading-none">{{ lensTypeLabel() }}</span>
<div class="flex items-center gap-1 w-full min-w-0 text-left">
<span class="text-xs font-normal text-gray-400 leading-none truncate">{{ lensTypeLabel() }}</span>
@if (selectedRoleLabel()) {
<span class="text-xs font-normal text-gray-400 leading-none shrink-0" aria-hidden="true">・</span>
<span
role="img"
[attr.aria-label]="'Role: ' + selectedRoleLabel()"
[pTooltip]="selectedRoleLabel()"
tooltipPosition="top"
class="inline-flex items-center justify-center size-5 rounded-full bg-white border border-gray-200 text-gray-700 shrink-0">
<i [ngClass]="selectedRoleIcon()" class="text-[10px]" aria-hidden="true"></i>
</span>
}
</div>
</div>

<i class="fa-light fa-angles-up-down shrink-0 text-gray-400" aria-hidden="true"></i>
Expand All @@ -34,21 +47,42 @@
(onHide)="onPopoverHide()"
data-testid="project-selector-panel">
<div class="p-1">
<!-- Search input — no background or border -->
<div class="relative mb-2">
<i class="fa-light fa-search absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"></i>
<i class="fa-light fa-search absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"></i>
<input
[pAutoFocus]="true"
pInputText
type="text"
[formControl]="searchControl"
[placeholder]="searchPlaceholder()"
class="pl-10 h-9 w-full text-sm bg-gray-50"
class="pl-10 h-9 w-full text-sm !border-0 !bg-transparent !shadow-none focus:!ring-0"
data-testid="project-search-input" />
</div>

<!-- Tab switcher — hybrid mode only. These are filter buttons, not an ARIA tabs widget, so we
use aria-pressed instead of role="tab" to avoid promising tab-panel semantics we don't ship. -->
@if (hybridMode()) {
<div class="flex gap-1 mb-5 bg-gray-100 rounded-full p-1" role="group" aria-label="Filter results">
@for (tab of selectorTabs; track tab) {
<button
type="button"
[attr.aria-pressed]="activeTab() === tab"
(click)="activeTab.set(tab)"
class="flex-1 text-sm font-medium py-1 px-3 rounded-full transition-colors capitalize"
[class.bg-white]="activeTab() === tab"
[class.shadow-sm]="activeTab() === tab"
[class.text-gray-900]="activeTab() === tab"
[class.text-gray-500]="activeTab() !== tab">
{{ tab === 'all' ? 'All' : tab === 'foundations' ? 'Foundations' : 'Projects' }}
</button>
}
</div>
}

<div class="max-h-[400px] overflow-y-auto">
@for (item of items(); track item.uid; let i = $index) {
<!-- Auto-load sentinel 8 items from the end — shifts each page so lfxOnRender re-fires. -->
@for (displayItem of displayedItems(); track displayItem.item.uid; let i = $index) {
<!-- Auto-load sentinel 8 items from the end -->
@if (i === autoLoadTriggerIndex() && hasMore()) {
@defer (on viewport) {
<div lfxOnRender (rendered)="loadMore()" class="h-0 w-full" aria-hidden="true"></div>
Expand All @@ -57,28 +91,50 @@
}
}

<button
type="button"
class="flex items-center gap-3 p-2 w-full text-left hover:bg-gray-100 rounded-lg transition-colors"
(click)="selectItem(item, popover)"
[attr.data-testid]="'lens-item-' + item.slug">
<div class="bg-gray-100 overflow-clip rounded-lg shrink-0 size-9 border border-gray-200">
<div class="flex items-center justify-center size-full p-1.5">
@if (item.logoUrl) {
<img [alt]="item.name" class="w-full h-full object-contain" [src]="item.logoUrl" />
} @else {
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.96369 15.9789V7.98987H0V19.9504H11.9372V15.9789H3.96369Z" fill="#0094FF" />
<path d="M19.8645 0H0V6.00333H3.96369V4.01761H15.9009V15.9781H13.919V19.9495H19.8645V0Z" fill="#003778" />
</svg>
<div
class="mb-1"
[class.pl-5]="displayItem.isNested"
[class.border-l-2]="displayItem.isNested"
[class.border-gray-100]="displayItem.isNested"
[class.ml-[18px]]="displayItem.isNested">
<button
type="button"
class="flex items-center gap-3 p-2 w-full text-left rounded-lg transition-colors"
[class.bg-blue-50]="displayItem.isSelected"
[class.hover:bg-gray-100]="!displayItem.isSelected"
(click)="selectItem(displayItem.item, popover)"
[attr.data-testid]="'lens-item-' + displayItem.item.slug"
[attr.data-selected]="displayItem.isSelected">
<div class="bg-gray-100 overflow-clip rounded-lg shrink-0 size-9 border border-gray-200">
<div class="flex items-center justify-center size-full p-1.5">
@if (displayItem.item.logoUrl) {
<img [alt]="displayItem.item.name" class="w-full h-full object-contain" [src]="displayItem.item.logoUrl" />
} @else {
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.96369 15.9789V7.98987H0V19.9504H11.9372V15.9789H3.96369Z" class="fill-blue-500" />
<path d="M19.8645 0H0V6.00333H3.96369V4.01761H15.9009V15.9781H13.919V19.9495H19.8645V0Z" class="fill-blue-800" />
</svg>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
</div>
</div>

<div class="flex-1 min-w-0">
<p class="font-medium text-sm text-gray-900 mb-0 truncate">{{ displayItem.item.name || displayItem.item.slug }}</p>
@if (displayItem.roleLabel) {
<p class="text-xs text-gray-400 leading-none mt-0.5 truncate">
<i [ngClass]="displayItem.roleIcon" class="mr-1" aria-hidden="true"></i>{{ displayItem.roleLabel }}
</p>
}
</div>
</div>

<div class="flex-1 min-w-0">
<p class="font-medium text-sm text-gray-900 mb-0">{{ item.name || item.slug }}</p>
</div>
</button>
<i
class="fa-light fa-check text-blue-500 shrink-0 transition-opacity"
[class.opacity-100]="displayItem.isSelected"
[class.opacity-0]="!displayItem.isSelected"
[attr.aria-label]="displayItem.isSelected ? 'Selected' : null"
[attr.aria-hidden]="!displayItem.isSelected"></i>
</button>
</div>
} @empty {
@if (!loading()) {
<div class="text-center py-8 text-gray-500 text-sm">{{ emptyMessage() }}</div>
Expand Down
Loading
Loading