import { Component, EventEmitter, Input, Output } from '@angular/core'; import { VspIcon } from './icon.component'; /* ===================== Setting row ===================== */ /** A labelled row (title + optional description) with a trailing control slot. */ @Component({ selector: 'vsp-setting-row', template: `
{{ title }}
@if (desc) {
{{ desc }}
}
`, }) export class VspSettingRow { @Input() title?: string; @Input() desc?: string; @Input() last = false; } /* ===================== Vertical tabs ===================== */ export type VerticalTabItem = | string | { value: string; label: string; icon?: string; badge?: string | number }; /** Vertical tab strip — a settings-style left rail. */ @Component({ selector: 'vsp-vertical-tabs', imports: [VspIcon], template: `
@for (t of tabs; track id(t)) { }
`, }) export class VspVerticalTabs { @Input() tabs: VerticalTabItem[] = []; @Input() value?: string; @Output() valueChange = new EventEmitter(); id(t: VerticalTabItem): string { return typeof t === 'string' ? t : t.value; } lbl(t: VerticalTabItem): string { return typeof t === 'string' ? t : t.label; } ico(t: VerticalTabItem): string | undefined { return typeof t === 'object' ? t.icon : undefined; } bdg(t: VerticalTabItem): string | number | undefined { return typeof t === 'object' ? t.badge : undefined; } cls(t: VerticalTabItem): string { return 'ui-vtab' + (this.value === this.id(t) ? ' on' : ''); } pick(v: string): void { this.value = v; this.valueChange.emit(v); } } /* ===================== Nav item ===================== */ /** A sidebar nav row — renders as an `` when `href` is set, else a ` }`, }) export class VspNavItem { @Input() icon?: string; @Input() label?: string; @Input() active = false; @Input() badge?: string | number; @Input() sub = false; @Input() href?: string; @Input() disabled = false; get cls(): string { return 'ui-nav-item' + (this.active ? ' on' : '') + (this.disabled ? ' disabled' : ''); } } /* ===================== Nav group ===================== */ /** A collapsible sidebar group; nested `vsp-nav-item`s project into the body. */ @Component({ selector: 'vsp-nav-group', imports: [VspIcon], template: `
`, }) export class VspNavGroup { @Input() icon?: string; @Input() label?: string; @Input() set defaultOpen(v: boolean) { this.open = v; } open = false; } /* ===================== Input affix ===================== */ /** Text input wrapped with an optional leading icon, prefix, and trailing unit. */ @Component({ selector: 'vsp-input-affix', imports: [VspIcon], template: `
@if (leadingIcon) { } @if (prefix) { {{ prefix }} } @if (unit) { {{ unit }} }
`, }) export class VspInputAffix { @Input() value = ''; @Output() valueChange = new EventEmitter(); @Input() leadingIcon?: string; @Input() prefix?: string; @Input() unit?: string; @Input() wrapClass?: string; @Input() placeholder?: string; @Input() type = 'text'; @Input() disabled = false; /** Common pass-through input attributes (React spreads the full set). */ @Input() name?: string; @Input() inputId?: string; @Input() inputmode?: string; @Input() autocomplete?: string; @Input() min?: string | number; @Input() max?: string | number; @Input() step?: string | number; @Input() readonly = false; @Input() required = false; @Input() ariaLabel?: string; onInput(e: Event): void { this.value = (e.target as HTMLInputElement).value; this.valueChange.emit(this.value); } }