chore: update Angular cache and TypeScript build info
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
import { Component, ChangeDetectionStrategy, input, output, signal, computed, inject, effect } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CdkAccordionModule } from '@angular/cdk/accordion';
|
||||
import { GraphConfig } from '../../app/graph/graph-settings.types';
|
||||
import { GraphSettingsService } from '../../app/graph/graph-settings.service';
|
||||
import { GraphFiltersSectionComponent } from '../../app/graph/ui/sections/filters-section.component';
|
||||
import { GraphGroupsSectionComponent } from '../../app/graph/ui/sections/groups-section.component';
|
||||
import { GraphDisplaySectionComponent } from '../../app/graph/ui/sections/display-section.component';
|
||||
import { GraphForcesSectionComponent } from '../../app/graph/ui/sections/forces-section.component';
|
||||
|
||||
/** Section identifier type */
|
||||
export type SectionId = 'filters' | 'groups' | 'display' | 'forces';
|
||||
|
||||
/** Section definition */
|
||||
interface AccordionSection {
|
||||
id: SectionId;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Headless accordion component using @angular/cdk/accordion
|
||||
* Replaces PrimeNG accordion with CDK + Tailwind styling
|
||||
* Features:
|
||||
* - Independent open/close per section
|
||||
* - Smooth CSS grid animation (0fr → 1fr)
|
||||
* - A11y support (keyboard, aria-expanded)
|
||||
* - Persistent state via GraphSettingsService
|
||||
* - Dark/Light mode support
|
||||
* - Conditional rendering for performance
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ov-graph-settings-accordion',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
CdkAccordionModule,
|
||||
GraphFiltersSectionComponent,
|
||||
GraphGroupsSectionComponent,
|
||||
GraphDisplaySectionComponent,
|
||||
GraphForcesSectionComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<cdk-accordion class="accordion-container" [multi]="true">
|
||||
@for (section of sections(); track section.id) {
|
||||
<cdk-accordion-item
|
||||
#accordionItem="cdkAccordionItem"
|
||||
[expanded]="isOpen(section.id)"
|
||||
(expandedChange)="onExpandedChange(section.id, $event)"
|
||||
class="accordion-item"
|
||||
[class.accordion-item-expanded]="accordionItem.expanded">
|
||||
|
||||
<!-- Header -->
|
||||
<button
|
||||
type="button"
|
||||
class="accordion-header"
|
||||
[attr.aria-expanded]="accordionItem.expanded"
|
||||
[attr.aria-controls]="'panel-' + section.id"
|
||||
(click)="accordionItem.toggle()">
|
||||
|
||||
<span class="header-content">
|
||||
<!-- Icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="header-icon"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
[innerHTML]="section.icon">
|
||||
</svg>
|
||||
|
||||
<!-- Title -->
|
||||
<span class="header-title">{{ section.title }}</span>
|
||||
</span>
|
||||
|
||||
<!-- Chevron -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="chevron-icon"
|
||||
[class.chevron-expanded]="accordionItem.expanded"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Content Panel with Grid Animation -->
|
||||
<div
|
||||
class="accordion-panel"
|
||||
[class.accordion-panel-expanded]="accordionItem.expanded"
|
||||
[attr.id]="'panel-' + section.id"
|
||||
role="region"
|
||||
[attr.aria-labelledby]="'header-' + section.id">
|
||||
|
||||
<div class="accordion-panel-inner">
|
||||
@if (accordionItem.expanded) {
|
||||
@switch (section.id) {
|
||||
@case ('filters') {
|
||||
<app-graph-filters-section
|
||||
[config]="config()"
|
||||
(configChange)="configChange.emit($event)">
|
||||
</app-graph-filters-section>
|
||||
}
|
||||
@case ('groups') {
|
||||
<app-graph-groups-section
|
||||
[config]="config()"
|
||||
(configChange)="configChange.emit($event)">
|
||||
</app-graph-groups-section>
|
||||
}
|
||||
@case ('display') {
|
||||
<app-graph-display-section
|
||||
[config]="config()"
|
||||
(configChange)="configChange.emit($event)"
|
||||
(animateRequested)="animateRequested.emit()">
|
||||
</app-graph-display-section>
|
||||
}
|
||||
@case ('forces') {
|
||||
<app-graph-forces-section
|
||||
[config]="config()"
|
||||
(configChange)="configChange.emit($event)">
|
||||
</app-graph-forces-section>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</cdk-accordion-item>
|
||||
}
|
||||
</cdk-accordion>
|
||||
|
||||
<!-- Optional: Collapse All / Expand All Toggle -->
|
||||
@if (showCollapseToggle()) {
|
||||
<div class="collapse-toggle-container">
|
||||
<button
|
||||
type="button"
|
||||
class="collapse-toggle-button"
|
||||
(click)="toggleAll()">
|
||||
{{ allExpanded() ? 'Collapse all' : 'Expand all' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
/* Container */
|
||||
.accordion-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
/* Accordion Item */
|
||||
.accordion-item {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgba(113, 113, 122, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.accordion-item:hover {
|
||||
border-color: rgba(113, 113, 122, 0.5);
|
||||
}
|
||||
|
||||
.accordion-item-expanded {
|
||||
border-color: rgba(59, 130, 246, 0.45);
|
||||
box-shadow: 0 12px 24px -16px rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
/* Dark mode - Item */
|
||||
:host-context(.dark) .accordion-item {
|
||||
border-color: rgba(63, 63, 70, 0.5);
|
||||
background-color: rgba(24, 24, 27, 0.5);
|
||||
}
|
||||
|
||||
:host-context(.dark) .accordion-item:hover {
|
||||
border-color: rgba(82, 82, 91, 0.6);
|
||||
}
|
||||
|
||||
:host-context(.dark) .accordion-item-expanded {
|
||||
border-color: rgba(96, 165, 250, 0.6);
|
||||
box-shadow: 0 12px 28px -12px rgba(2, 132, 199, 0.35);
|
||||
}
|
||||
|
||||
/* Header Button */
|
||||
.accordion-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.95rem 1.2rem;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9) 0%, rgba(248, 250, 252, 0.6) 100%);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.accordion-header:hover {
|
||||
background: rgba(241, 245, 249, 0.9);
|
||||
}
|
||||
|
||||
.accordion-header:focus-visible {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Dark mode - Header */
|
||||
:host-context(.dark) .accordion-header {
|
||||
color: #f8fafc;
|
||||
background: linear-gradient(180deg, rgba(30, 41, 59, 0.95) 0%, rgba(30, 41, 59, 0.75) 100%);
|
||||
}
|
||||
|
||||
:host-context(.dark) .accordion-header:hover {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
}
|
||||
|
||||
/* Header Content */
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Chevron Icon */
|
||||
.chevron-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease-out;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.chevron-expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
:host-context(.dark) .chevron-icon {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Panel with Grid Animation */
|
||||
.accordion-panel {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
overflow: hidden;
|
||||
transition: grid-template-rows 0.2s ease-out;
|
||||
}
|
||||
|
||||
.accordion-panel-expanded {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.accordion-panel-inner {
|
||||
min-height: 0;
|
||||
padding: 0 1.25rem;
|
||||
}
|
||||
|
||||
.accordion-panel-expanded .accordion-panel-inner {
|
||||
padding: 1.1rem 1.25rem 1.25rem;
|
||||
}
|
||||
|
||||
/* Dark mode - Panel */
|
||||
:host-context(.dark) .accordion-panel {
|
||||
background-color: rgba(15, 23, 42, 0.9);
|
||||
}
|
||||
|
||||
/* Collapse Toggle */
|
||||
.collapse-toggle-container {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.collapse-toggle-button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(113, 113, 122, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.collapse-toggle-button:hover {
|
||||
color: #374151;
|
||||
border-color: rgba(113, 113, 122, 0.5);
|
||||
background: rgba(249, 250, 251, 0.5);
|
||||
}
|
||||
|
||||
:host-context(.dark) .collapse-toggle-button {
|
||||
color: #9ca3af;
|
||||
border-color: rgba(82, 82, 91, 0.5);
|
||||
}
|
||||
|
||||
:host-context(.dark) .collapse-toggle-button:hover {
|
||||
color: #d1d5db;
|
||||
border-color: rgba(113, 113, 122, 0.6);
|
||||
background: rgba(39, 39, 42, 0.5);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class GraphSettingsAccordionComponent {
|
||||
/** Graph configuration input */
|
||||
config = input.required<GraphConfig>();
|
||||
|
||||
/** Configuration change output */
|
||||
configChange = output<Partial<GraphConfig>>();
|
||||
|
||||
/** Animate requested output */
|
||||
animateRequested = output<void>();
|
||||
|
||||
/** Show collapse/expand all toggle */
|
||||
showCollapseToggle = input<boolean>(false);
|
||||
|
||||
private settingsService = inject(GraphSettingsService);
|
||||
|
||||
/** Section definitions with icons */
|
||||
sections = signal<AccordionSection[]>([
|
||||
{
|
||||
id: 'filters',
|
||||
title: 'Filters',
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />'
|
||||
},
|
||||
{
|
||||
id: 'groups',
|
||||
title: 'Groups',
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />'
|
||||
},
|
||||
{
|
||||
id: 'display',
|
||||
title: 'Display',
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />'
|
||||
},
|
||||
{
|
||||
id: 'forces',
|
||||
title: 'Forces',
|
||||
icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />'
|
||||
}
|
||||
]);
|
||||
|
||||
/** Open sections set */
|
||||
private openSectionsSet = signal<Set<SectionId>>(new Set(['filters']));
|
||||
|
||||
/** Computed: Check if all sections are expanded */
|
||||
allExpanded = computed(() => {
|
||||
return this.openSectionsSet().size === this.sections().length;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Initialize from GraphSettingsService on startup
|
||||
effect(() => {
|
||||
const currentConfig = this.config();
|
||||
const openSet = new Set<SectionId>();
|
||||
|
||||
// Map collapse flags to open state (inverted logic)
|
||||
if (!currentConfig['collapse-filter']) {
|
||||
openSet.add('filters');
|
||||
}
|
||||
if (!currentConfig['collapse-color-groups']) {
|
||||
openSet.add('groups');
|
||||
}
|
||||
if (!currentConfig['collapse-display']) {
|
||||
openSet.add('display');
|
||||
}
|
||||
if (!currentConfig['collapse-forces']) {
|
||||
openSet.add('forces');
|
||||
}
|
||||
|
||||
this.openSectionsSet.set(openSet);
|
||||
}, { allowSignalWrites: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a section is open
|
||||
*/
|
||||
isOpen(id: SectionId): boolean {
|
||||
return this.openSectionsSet().has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle section expand/collapse
|
||||
*/
|
||||
onExpandedChange(id: SectionId, expanded: boolean): void {
|
||||
const newSet = new Set(this.openSectionsSet());
|
||||
|
||||
if (expanded) {
|
||||
newSet.add(id);
|
||||
} else {
|
||||
newSet.delete(id);
|
||||
}
|
||||
|
||||
this.openSectionsSet.set(newSet);
|
||||
|
||||
// Persist to GraphSettingsService
|
||||
this.persistState(id, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle all sections
|
||||
*/
|
||||
toggleAll(): void {
|
||||
const shouldExpandAll = !this.allExpanded();
|
||||
const sections = this.sections();
|
||||
|
||||
if (shouldExpandAll) {
|
||||
// Expand all
|
||||
const allIds = new Set(sections.map(s => s.id));
|
||||
this.openSectionsSet.set(allIds);
|
||||
|
||||
// Persist all as expanded
|
||||
sections.forEach(section => {
|
||||
this.persistState(section.id, true);
|
||||
});
|
||||
} else {
|
||||
// Collapse all
|
||||
this.openSectionsSet.set(new Set());
|
||||
|
||||
// Persist all as collapsed
|
||||
sections.forEach(section => {
|
||||
this.persistState(section.id, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist section state to GraphSettingsService
|
||||
*/
|
||||
private persistState(id: SectionId, expanded: boolean): void {
|
||||
// Map section ID to collapse flag key (inverted logic)
|
||||
const collapseKey = this.getCollapseKey(id);
|
||||
|
||||
if (collapseKey) {
|
||||
this.settingsService.save({
|
||||
[collapseKey]: !expanded
|
||||
} as Partial<GraphConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collapse key for section ID
|
||||
*/
|
||||
private getCollapseKey(id: SectionId): keyof GraphConfig | null {
|
||||
switch (id) {
|
||||
case 'filters':
|
||||
return 'collapse-filter';
|
||||
case 'groups':
|
||||
return 'collapse-color-groups';
|
||||
case 'display':
|
||||
return 'collapse-display';
|
||||
case 'forces':
|
||||
return 'collapse-forces';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user