feat: tree grouping for list and cards views (matching table view)

This commit is contained in:
Agent Zero
2026-07-28 15:26:46 +02:00
parent dbf804f0e3
commit 33aae769e4
@@ -475,10 +475,48 @@ export function ContactList({
</li> </li>
); );
// Group header renderer for list view
const renderListGroupHeader = (group: GroupedContacts) => {
const isExpanded = expandedGroups.has(group.key);
return (
<div
key={`group-header-${group.key}`}
className="flex items-center gap-2 px-3 py-2 bg-secondary-50 border-b border-secondary-200"
>
<button
onClick={() => toggleGroup(group.key)}
className="flex items-center justify-center w-5 h-5 hover:bg-secondary-200 rounded transition-colors flex-shrink-0"
aria-label={isExpanded ? 'Collapse group' : 'Expand group'}
>
{isExpanded ? (
<ChevronDown className="w-3.5 h-3.5" />
) : (
<ChevronRight className="w-3.5 h-3.5" />
)}
</button>
<span className="font-medium text-sm text-secondary-700 truncate">{group.label}</span>
<span className="text-secondary-400 text-xs flex-shrink-0">({group.contacts.length})</span>
</div>
);
};
return ( return (
<div className="flex flex-col h-full" data-testid="contact-list-view"> <div className="flex flex-col h-full" data-testid="contact-list-view">
<div ref={scrollRef} className="flex-1 overflow-y-auto"> <div ref={scrollRef} className="flex-1 overflow-y-auto">
{shouldVirtualize ? ( {isGrouped && groupedContacts ? (
<div>
{groupedContacts.map((group) => (
<div key={group.key}>
{renderListGroupHeader(group)}
{expandedGroups.has(group.key) && (
<ul className="divide-y divide-secondary-100" role="list">
{group.contacts.map((contact) => renderContactItem(contact))}
</ul>
)}
</div>
))}
</div>
) : shouldVirtualize ? (
<ul className="divide-y divide-secondary-100" role="list" style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}> <ul className="divide-y divide-secondary-100" role="list" style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => { {rowVirtualizer.getVirtualItems().map((virtualRow) => {
const contact = contacts[virtualRow.index]; const contact = contacts[virtualRow.index];
@@ -826,10 +864,48 @@ export function ContactList({
</div> </div>
); );
// Group header renderer for cards view
const renderCardsGroupHeader = (group: GroupedContacts) => {
const isExpanded = expandedGroups.has(group.key);
return (
<div
key={`group-header-${group.key}`}
className="flex items-center gap-2 px-3 py-2 bg-secondary-50 border-b border-secondary-200 mb-3 sticky top-0 z-10"
>
<button
onClick={() => toggleGroup(group.key)}
className="flex items-center justify-center w-5 h-5 hover:bg-secondary-200 rounded transition-colors flex-shrink-0"
aria-label={isExpanded ? 'Collapse group' : 'Expand group'}
>
{isExpanded ? (
<ChevronDown className="w-3.5 h-3.5" />
) : (
<ChevronRight className="w-3.5 h-3.5" />
)}
</button>
<span className="font-medium text-sm text-secondary-700 truncate">{group.label}</span>
<span className="text-secondary-400 text-xs flex-shrink-0">({group.contacts.length})</span>
</div>
);
};
return ( return (
<div className="flex flex-col h-full" data-testid="contact-cards-view"> <div className="flex flex-col h-full" data-testid="contact-cards-view">
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3"> <div ref={scrollRef} className="flex-1 overflow-y-auto p-3">
{shouldVirtualize ? ( {isGrouped && groupedContacts ? (
<div className="space-y-4">
{groupedContacts.map((group) => (
<div key={group.key}>
{renderCardsGroupHeader(group)}
{expandedGroups.has(group.key) && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{group.contacts.map((contact) => renderContactCard(contact))}
</div>
)}
</div>
))}
</div>
) : shouldVirtualize ? (
<div style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}> <div style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3" style={{ position: 'absolute', top: 0, left: 0, right: 0 }}> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3" style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => { {rowVirtualizer.getVirtualItems().map((virtualRow) => {