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' : '');
}
}