import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; export type BannerTone = 'info' | 'warn' | 'accent' | 'pos' | 'neg'; const BANNER_ICON: Record = { info: 'M12 3l1.6 5L19 9.6l-5 1.6L12 16l-1.6-4.8L5 9.6l5.4-1.6z', warn: 'M18 8a6 6 0 00-12 0c0 7-3 9-3 9h18s-3-2-3-9', accent: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z', pos: 'M22 11.08V12a10 10 0 11-5.93-9.14M22 4L12 14.01l-3-3', neg: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', }; @Component({ selector: 'vsp-banner', template: `
@if (!ic.childElementCount) { }
@if (dismissible) { }
`, }) export class VspBanner { @Input() tone: BannerTone = 'info'; @Input() dismissible = false; @Output() dismiss = new EventEmitter(); get cls(): string { return 'ui-banner ' + this.tone; } get iconPath(): string { return BANNER_ICON[this.tone]; } } @Component({ selector: 'vsp-empty-state', template: `
@if (!ic.childElementCount) { }
{{ title }}
@if (desc) {

{{ desc }}

}
`, }) export class VspEmptyState { @Input() title?: string; @Input() desc?: string; @Input() compact = false; } export interface AccordionItem { title: string; body: string; } @Component({ selector: 'vsp-accordion', template: `
@for (it of items; track $index; let i = $index) {
{{ it.body }}
}
`, }) export class VspAccordion implements OnInit { @Input() items: AccordionItem[] = []; @Input() multiple = false; @Input() defaultOpen: number[] = []; /** Controlled open indices. Omit for uncontrolled. */ @Input() open?: number[]; @Output() openChange = new EventEmitter(); private internal = new Set(); ngOnInit(): void { this.internal = new Set(this.defaultOpen); } private get openSet(): Set { return this.open !== undefined ? new Set(this.open) : this.internal; } toggle(i: number): void { const cur = this.openSet; const n = new Set(this.multiple ? cur : []); if (cur.has(i)) n.delete(i); else n.add(i); if (this.open === undefined) this.internal = n; this.openChange.emit([...n]); } itemCls(i: number): string { return 'ui-acc-item' + (this.openSet.has(i) ? ' open' : ''); } }