import { Component, EventEmitter, Input, Output } from '@angular/core'; import { VspIcon } from './icon.component'; export type TabItem = string | { value: string; label: string; count?: number; disabled?: boolean }; export type BreadcrumbItem = string | { label: string; href?: string; icon?: string }; const BC_ELL = { ellipsis: true }; @Component({ selector: 'vsp-tabs', template: `
@for (t of tabs; track id(t)) { }
`, }) export class VspTabs { @Input() tabs: TabItem[] = []; @Input() value?: string; @Output() valueChange = new EventEmitter(); id(t: TabItem): string { return typeof t === 'string' ? t : t.value; } lbl(t: TabItem): string { return typeof t === 'string' ? t : t.label; } cnt(t: TabItem): number | undefined { return typeof t === 'object' ? t.count : undefined; } dis(t: TabItem): boolean | undefined { return typeof t === 'object' ? t.disabled : undefined; } cls(t: TabItem): string { return 'ui-tab' + (this.value === this.id(t) ? ' on' : ''); } pick(v: string): void { this.value = v; this.valueChange.emit(v); } } @Component({ selector: 'vsp-breadcrumb', imports: [VspIcon], template: ``, }) export class VspBreadcrumb { @Input() items: BreadcrumbItem[] = []; @Input() maxItems?: number; readonly ELL = BC_ELL; get display(): unknown[] { if (this.maxItems && this.items.length > this.maxItems) return [ this.items[0], this.ELL, ...this.items.slice(this.items.length - (this.maxItems - 1)), ]; return this.items; } isEll(it: unknown): boolean { return it === this.ELL; } private obj(it: unknown): { label: string; href?: string; icon?: string } | null { return typeof it === 'object' && it !== this.ELL ? (it as { label: string; href?: string; icon?: string }) : null; } lbl(it: unknown): string { return this.obj(it)?.label ?? (it as string); } hrefOf(it: unknown): string | undefined { return this.obj(it)?.href; } iconOf(it: unknown): string | undefined { return this.obj(it)?.icon; } } interface PageItem { gap: boolean; n: number; } @Component({ selector: 'vsp-pagination', template: `
@for (item of nums; track $index) { @if (item.gap) { } @else { } }
`, }) export class VspPagination { @Input() page = 0; @Output() pageChange = new EventEmitter(); @Input() pages = 1; get nums(): PageItem[] { const r: PageItem[] = []; for (let i = 0; i < this.pages; i++) { if (i === 0 || i === this.pages - 1 || Math.abs(i - this.page) <= 1) r.push({ gap: false, n: i }); else if (!r[r.length - 1]?.gap) r.push({ gap: true, n: -1 }); } return r; } numCls(n: number): string { return 'btn btn-sm ' + (n === this.page ? 'btn-primary' : 'btn-subtle'); } go(p: number): void { this.page = p; this.pageChange.emit(p); } } export type StepStatus = 'done' | 'active' | 'pending' | 'error'; export type StepperItem = string | { label: string; status?: StepStatus }; @Component({ selector: 'vsp-stepper', template: `
@for (s of steps; track $index; let i = $index) { @if (i > 0) {
} @if (onStepClick) { } @else {
@if (statusOf(i, s) === 'done') { } @else if (statusOf(i, s) === 'error') { ! } @else { {{ i + 1 }} } {{ lbl(s) }}
} }
`, }) export class VspStepper { @Input() steps: StepperItem[] = []; @Input() current = 0; @Input() onStepClick?: (i: number) => void; @Input() orientation: 'horizontal' | 'vertical' = 'horizontal'; lbl(s: StepperItem): string { return typeof s === 'object' ? s.label : s; } statusOf(i: number, s: StepperItem): StepStatus { if (typeof s === 'object' && s.status) return s.status; return i < this.current ? 'done' : i === this.current ? 'active' : 'pending'; } barCls(i: number): string { return 'ui-step-bar' + (i <= this.current ? ' done' : ''); } stepCls(i: number, s: StepperItem): string { return 'ui-step ' + this.statusOf(i, s) + (this.onStepClick ? ' clickable' : ''); } }