Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions docs/components-api/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,39 @@

# ExtraTabs

> ✅ **Реализован**: `ExtraTabsComponent` + `ExtraTabItemComponent` (`@cdek-it/angular-ui-kit`) соответствуют спецификации.

Унифицированный навигационный компонент вкладок (может использоваться как меню или как переключатель панелей). Вкладки передаются как дочерние элементы `ExtraTabItem`; активная вкладка управляется через привязку значения.

| Свойство | Описание | Типизация |
| ------------ | ----------------------------------------------------------------- | ---------------------- |
| `value` | значение активной вкладки; совпадает с `name` одного из `ExtraTabItem` | `string \| undefined` |
| `scrollable` | горизонтальная прокрутка списка вкладок с кнопками навигации | `boolean` |
| `lazy` | ленивая инициализация панелей — содержимое рендерится при первом открытии | `boolean` |

`scrollable`/`lazy` — полезные пропы сверх первоначального черновика спеки, уже реализованы и
добавлены сюда по итогам доработки (см. чеклист в корневом `CLAUDE.md`, п.3). Если `value` не
задан — активной становится первая вкладка.

# События

| Событие | Описание | Типизация |
| ------- | --------------------------------------- | --------------------------------- |
| `valueChange` | срабатывает при переключении вкладки | `(value: string \| undefined) => void` |

# ExtraTabItem

Отдельная вкладка: заголовок (подпись, иконка, значок) + содержимое панели.
Отдельная вкладка: заголовок (подпись, иконка, значок) + содержимое панели (через content
projection — дочернее содержимое `<extra-tab-item>` и есть тело панели).

| Свойство | Описание | Типизация |
| --------------- | ------------------------ | --------- |
| `name` | название вкладки; также служит уникальным идентификатором для связи заголовка и панели | `string` |
| `icon` | класс иконки tabler icon | `string` |
| `badge` | значок в конце вкладки | `string` |
| `badgeSeverity` | цветовая схема значка | `'primary' \| 'success' \| 'info' \| 'warning' \| 'danger'` |
| `disabled` | вкладка недоступна | `boolean` |

| Свойство | Описание | Типизация |
| ---------- | ------------------------ | --------- |
| `name` | название вкладки | `string` |
| `icon` | класс иконки tabler icon | `string` |
| `badge` | значок в конце вкладки | `string` |
| `disabled` | вкладка недоступна | `boolean` |
`badgeSeverity` — полезный проп сверх черновика спеки, уже реализован и добавлен сюда по итогам
доработки (см. чеклист в корневом `CLAUDE.md`, п.3); использует шкалу `ExtraBadgeSeverity`
(`docs/naming-rules.md`), а не сырые значения PrimeNG.
4 changes: 1 addition & 3 deletions src/lib/components/tabs/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export * from './tabs.component';



export * from './tab-item.component';
25 changes: 25 additions & 0 deletions src/lib/components/tabs/tab-item.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewChild } from '@angular/core';
import { ExtraBadgeSeverity } from '@cdek-it/angular-ui-kit/components/badge';

/**
* Данные и проекция контента одной вкладки. Не рендерит p-tab/p-tabpanel сам — PrimeNG-теги
* внедряют родителя через DI (pcTabs), поэтому их обязан объявлять ExtraTabsComponent в своём
* собственном шаблоне (см. CLAUDE.md, «Ловушка: PrimeNG-компонент с DI-зависимостью от родителя
* внутри слота»). ExtraTabItemComponent — только держатель @Input()'ов и TemplateRef на контент.
*/
@Component({
selector: 'extra-tab-item',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { style: 'display: contents' },
template: `<ng-template #bodyTpl><ng-content /></ng-template>`
})
export class ExtraTabItemComponent {
@Input({ required: true }) name!: string;
@Input() icon = '';
@Input() badge = '';
@Input() badgeSeverity: ExtraBadgeSeverity = 'primary';
@Input() disabled = false;

@ViewChild('bodyTpl', { static: true }) bodyTpl!: TemplateRef<unknown>;
}
76 changes: 50 additions & 26 deletions src/lib/components/tabs/tabs.component.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,73 @@
import { Component, Input } from '@angular/core';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChildren,
EventEmitter,
inject,
Input,
Output,
QueryList
} from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs';
import { Badge } from 'primeng/badge';
import { ExtraBadgeComponent } from '@cdek-it/angular-ui-kit/components/badge';
import { ExtraTabItemComponent } from './tab-item.component';

export interface ExtraTabItem {
value: string;
label: string;
icon?: string;
disabled?: boolean;
badge?: string;
badgeSeverity?: 'success' | 'info' | 'warn' | 'danger' | 'secondary' | 'contrast';
content?: string;
}
export type ExtraTabsValueChangeEvent = string | undefined;

@Component({
selector: 'extra-tabs',
standalone: true,
imports: [Tabs, TabList, Tab, TabPanels, TabPanel, Badge],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [Tabs, TabList, Tab, TabPanels, TabPanel, ExtraBadgeComponent, NgTemplateOutlet],
template: `
<p-tabs [value]="value" [scrollable]="scrollable" [lazy]="lazy" (valueChange)="value = $event">
<p-tabs [value]="value" [scrollable]="scrollable" [lazy]="lazy" (valueChange)="onValueChange($event)">
<p-tablist>
@for (tab of tabs; track tab.value) {
<p-tab [value]="tab.value" [disabled]="tab.disabled || false">
@if (tab.icon) {
<i class="text-xl" [class]="tab.icon"></i>
@for (item of items; track item.name) {
<p-tab [value]="item.name" [disabled]="item.disabled">
@if (item.icon) {
<i class="text-xl" [class]="item.icon"></i>
}
<span>{{ tab.label }}</span>
@if (tab.badge) {
<p-badge [value]="tab.badge" [severity]="tab.badgeSeverity || 'success'"></p-badge>
<span>{{ item.name }}</span>
@if (item.badge) {
<extra-badge [value]="item.badge" [severity]="item.badgeSeverity"></extra-badge>
}
</p-tab>
}
</p-tablist>
<p-tabpanels>
@for (tab of tabs; track tab.value) {
<p-tabpanel [value]="tab.value">
<p class="m-0">{{ tab.content }}</p>
@for (item of items; track item.name) {
<p-tabpanel [value]="item.name">
<ng-container [ngTemplateOutlet]="item.bodyTpl"></ng-container>
</p-tabpanel>
}
</p-tabpanels>
</p-tabs>
`
})
export class ExtraTabsComponent {
@Input() value: string | number | undefined = '0';
@Input() tabs: ExtraTabItem[] = [];
export class ExtraTabsComponent implements AfterContentInit {
private readonly _cdr = inject(ChangeDetectorRef);

@ContentChildren(ExtraTabItemComponent) items!: QueryList<ExtraTabItemComponent>;

@Input() value: string | undefined;
@Input() scrollable = false;
@Input() lazy = false;

@Output() valueChange = new EventEmitter<ExtraTabsValueChangeEvent>();

ngAfterContentInit(): void {
if (this.value === undefined) {
this.value = this.items.first?.name;
this._cdr.markForCheck();
}
this.items.changes.subscribe(() => this._cdr.markForCheck());
}

onValueChange(newValue: string | number | undefined): void {
this.value = newValue as string | undefined;
this.valueChange.emit(this.value);
}
}
Loading
Loading