From a686e2354cba9880299d2e9abc2b1fc76964e4c6 Mon Sep 17 00:00:00 2001 From: "ak.dmitriev" Date: Mon, 5 Oct 2026 01:41:03 +0700 Subject: [PATCH] API.tabs --- docs/components-api/tabs.md | 38 ++++- src/lib/components/tabs/public_api.ts | 4 +- src/lib/components/tabs/tab-item.component.ts | 25 +++ src/lib/components/tabs/tabs.component.ts | 76 ++++++--- src/lib/components/tabs/tabs.figma.md | 157 ++++++++---------- .../tabs/examples/tabs-default.component.ts | 21 +++ .../examples/tabs-with-badge.component.ts | 49 +++--- .../examples/tabs-with-disabled.component.ts | 49 +++--- src/stories/components/tabs/tabs.stories.ts | 95 +++++------ 9 files changed, 286 insertions(+), 228 deletions(-) create mode 100644 src/lib/components/tabs/tab-item.component.ts create mode 100644 src/stories/components/tabs/examples/tabs-default.component.ts diff --git a/docs/components-api/tabs.md b/docs/components-api/tabs.md index 78561407..a4427761 100644 --- a/docs/components-api/tabs.md +++ b/docs/components-api/tabs.md @@ -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 — дочернее содержимое `` и есть тело панели). + +| Свойство | Описание | Типизация | +| --------------- | ------------------------ | --------- | +| `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. diff --git a/src/lib/components/tabs/public_api.ts b/src/lib/components/tabs/public_api.ts index 26af794e..443b7749 100644 --- a/src/lib/components/tabs/public_api.ts +++ b/src/lib/components/tabs/public_api.ts @@ -1,4 +1,2 @@ export * from './tabs.component'; - - - +export * from './tab-item.component'; diff --git a/src/lib/components/tabs/tab-item.component.ts b/src/lib/components/tabs/tab-item.component.ts new file mode 100644 index 00000000..1ebdb3f5 --- /dev/null +++ b/src/lib/components/tabs/tab-item.component.ts @@ -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: `` +}) +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; +} diff --git a/src/lib/components/tabs/tabs.component.ts b/src/lib/components/tabs/tabs.component.ts index 35dcccd0..2de90d34 100644 --- a/src/lib/components/tabs/tabs.component.ts +++ b/src/lib/components/tabs/tabs.component.ts @@ -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: ` - + - @for (tab of tabs; track tab.value) { - - @if (tab.icon) { - + @for (item of items; track item.name) { + + @if (item.icon) { + } - {{ tab.label }} - @if (tab.badge) { - + {{ item.name }} + @if (item.badge) { + } } - @for (tab of tabs; track tab.value) { - -

{{ tab.content }}

+ @for (item of items; track item.name) { + + }
` }) -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; + + @Input() value: string | undefined; @Input() scrollable = false; @Input() lazy = false; + + @Output() valueChange = new EventEmitter(); + + 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); + } } diff --git a/src/lib/components/tabs/tabs.figma.md b/src/lib/components/tabs/tabs.figma.md index 43da5aa1..0ce018e7 100644 --- a/src/lib/components/tabs/tabs.figma.md +++ b/src/lib/components/tabs/tabs.figma.md @@ -10,35 +10,60 @@ figma: componentKey: 'ef7d7ac9ae94d9df8efe08ae85751d59c91779c3' name: '' status: stable -updated: '2026-06-22' +updated: '2026-10-03' --- ## Overview -`ExtraTabsComponent` — компонент вкладок, организующий контент в несколько переключаемых разделов. Оборачивает PrimeNG `p-tabs` (`p-tablist` + `p-tabpanels`) и предлагает декларативный API: список вкладок задаётся одним массивом `tabs` типа `ExtraTabItem[]`, а не отдельными под-компонентами в шаблоне. - -Компонент соответствует Figma-компоненту `` (nodeId `884:7324`, библиотека «UI Kit (DS) v2.0»). Figma-свойство `show-nav-buttons` соответствует Angular-входу `scrollable` (навигационные стрелки появляются при горизонтальной прокрутке списка вкладок). +`ExtraTabsComponent` — компонент вкладок, организующий контент в несколько переключаемых разделов. +Оборачивает PrimeNG `p-tabs` (`p-tablist` + `p-tabpanels`). В отличие от более ранней версии (массив +`tabs: ExtraTabItem[]` с текстовым `content`), вкладки задаются декларативно как дочерние компоненты +`` — по спеке (`docs/components-api/tabs.md`: «Вкладки передаются как дочерние +элементы `ExtraTabItem`»), содержимое панели — через `` внутри каждого ``. + +Компонент соответствует Figma-компоненту `` (nodeId `884:7324`, библиотека «UI Kit (DS) +v2.0»). Figma-свойство `show-nav-buttons` соответствует Angular-входу `scrollable`. + +### ⚠️ Архитектура: почему `ExtraTabItem` не рендерит `p-tab`/`p-tabpanel` сам + +PrimeNG `Tab` и `TabPanel` внедряют родителя через DI (`pcTabs: Tabs`, инжектится по +`forwardRef(() => Tabs)`). Angular резолвит зависимости спроецированного контента по месту его +**объявления** у вызывающего кода, а не по месту фактического рендера — поэтому если `p-tab`/ +`p-tabpanel` рендерить в шаблоне ОТДЕЛЬНОГО `ExtraTabItemComponent`, а сам компонент передавать как +`` в `ExtraTabsComponent`, DI ломается: `NG0201: No provider found for Tabs`. Та же +ловушка у `p-accordion-panel`/`Accordion` и `p-step`/`Stepper` (см. корневой `CLAUDE.md`, раздел +«Ловушка: PrimeNG-компонент с DI-зависимостью от родителя внутри слота»). + +Починка: `ExtraTabItemComponent` — чистый держатель данных (`name`/`icon`/`badge`/`badgeSeverity`/ +`disabled`) и `TemplateRef` на свой `` (`@ViewChild('bodyTpl') bodyTpl`). Сам он никакой +PrimeNG-тег не рендерит. `ExtraTabsComponent` собирает детей через +`@ContentChildren(ExtraTabItemComponent) items` и рендерит РЕАЛЬНЫЕ `p-tab`/`p-tabpanel` в своём +собственном шаблоне через `@for`, подставляя тело панели через +`[ngTemplateOutlet]="item.bodyTpl"`. Так все PrimeNG-теги объявлены в одном месте — в шаблоне +`ExtraTabsComponent` — и DI резолвится корректно. ## Props mapping +### ExtraTabs + | Свойство | Тип | По умолчанию | Описание | |----------|-----|--------------|---------| -| `value` | `string \| number \| undefined` | `'0'` | Значение активной вкладки; совпадает с `value` одного из элементов `tabs` | -| `tabs` | `ExtraTabItem[]` | `[]` | Массив описаний вкладок (см. структуру ниже в Slots) | +| `value` | `string \| undefined` | первая вкладка | Значение активной вкладки; совпадает с `name` одного из `extra-tab-item` | | `scrollable` | `boolean` | `false` | Горизонтальная прокрутка списка вкладок — соответствует Figma-свойству `show-nav-buttons` | | `lazy` | `boolean` | `false` | Ленивая инициализация панелей: содержимое рендерится при первом открытии | +| `(valueChange)` | `EventEmitter` | — | Событие при переключении вкладки; вместе с `value` поддерживает `[(value)]` | + +### ExtraTabItem -Элемент массива `ExtraTabItem`: +| Свойство | Тип | По умолчанию | Описание | +|----------|-----|--------------|---------| +| `name` | `string` (обязателен) | — | Текст заголовка вкладки; также уникальный идентификатор, связывающий заголовок и панель | +| `icon` | `string` | `''` | CSS-класс иконки заголовка; доступные иконки — [icons.md](../../figma-code-connect/icons.md) | +| `badge` | `string` | `''` | Текст бейджа в заголовке вкладки | +| `badgeSeverity` | `ExtraBadgeSeverity` | `'primary'` | Цветовая схема бейджа — делегирована `ExtraBadge`, не сырые значения PrimeNG | +| `disabled` | `boolean` | `false` | Отключённое состояние вкладки | -| Поле | Тип | Описание | -|------|-----|---------| -| `value` | `string` | Уникальный идентификатор вкладки; связывает заголовок и панель | -| `label` | `string` | Текст заголовка вкладки | -| `icon` | `string` | CSS-класс иконки заголовка; доступные иконки — [icons.md](../../figma-code-connect/icons.md) | -| `disabled` | `boolean` | Отключённое состояние вкладки | -| `badge` | `string` | Текст бейджа в заголовке вкладки | -| `badgeSeverity` | `'success' \| 'info' \| 'warn' \| 'danger' \| 'secondary' \| 'contrast'` | Цветовая схема бейджа; цвета соответствуют токенам [tokens.md](../../figma-code-connect/tokens.md) | -| `content` | `string` | Текстовое содержимое панели вкладки | +Содержимое панели — не проп, а `` внутри `` (см. Slots). ## Variants @@ -47,18 +72,11 @@ updated: '2026-06-22' Figma: ``, show-nav-buttons=false — nodeId `884:7324` ```html - -``` - -```ts -import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; - -// В компоненте: -tabs: ExtraTabItem[] = [ - { value: '0', label: 'Информация', content: 'Содержимое первой вкладки' }, - { value: '1', label: 'Параметры', content: 'Содержимое второй вкладки' }, - { value: '2', label: 'История', content: 'Содержимое третьей вкладки' }, -]; + + Содержимое первой вкладки + Содержимое второй вкладки + Содержимое третьей вкладки + ``` ### Прокручиваемые вкладки (scrollable) @@ -66,92 +84,63 @@ tabs: ExtraTabItem[] = [ Figma: ``, show-nav-buttons=true ```html - + + ... + ... + ``` ### Вкладки с иконками Figma: ``, change-layout=иконка + текст -Иконки заголовков задаются полем `icon` элемента `ExtraTabItem`; доступные иконки — [icons.md](../../figma-code-connect/icons.md). - ```html - -``` - -```ts -import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; - -// В компоненте (CSS-классы иконок — см. icons.md): -tabs: ExtraTabItem[] = [ - { value: '0', label: 'Профиль', icon: 'ti ti-user', content: 'Данные профиля' }, - { value: '1', label: 'Настройки', icon: 'ti ti-settings', content: 'Параметры аккаунта' }, -]; + + Данные профиля + Параметры аккаунта + ``` ### Вкладки с бейджами -Бейдж задаётся полем `badge` (и опционально `badgeSeverity`); цвета бейджа соответствуют токенам [tokens.md](../../figma-code-connect/tokens.md). - ```html - -``` - -```ts -import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; - -// В компоненте: -tabs: ExtraTabItem[] = [ - { value: '0', label: 'Входящие', badge: '99+', badgeSeverity: 'danger', content: 'Список входящих' }, - { value: '1', label: 'Архив', badge: '5', content: 'Архивные записи' }, -]; + + Список входящих + Архивные записи + ``` ### С отключённой вкладкой -Отключение задаётся полем `disabled` элемента `ExtraTabItem`. - ```html - + + Активная вкладка + Эта вкладка отключена + ``` -```ts -import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; - -// В компоненте: -tabs: ExtraTabItem[] = [ - { value: '0', label: 'Доступно', content: 'Активная вкладка' }, - { value: '1', label: 'Недоступно', disabled: true, content: 'Эта вкладка отключена' }, -]; -``` - -## Slots - -Компонент не использует `` и структурные директивы. Структура вкладок (заголовки и панели) описывается декларативно через массив `tabs`, который во внутреннем шаблоне разворачивается в PrimeNG-разметку: - -| Уровень | PrimeNG-элемент | Источник данных | -|---------|-----------------|-----------------| -| Контейнер | `p-tabs` | входы `value`, `scrollable`, `lazy` | -| Список заголовков | `p-tablist` → `p-tab` (на каждый элемент `tabs`) | поля `label`, `icon`, `badge`, `disabled` элемента `ExtraTabItem` | -| Панели контента | `p-tabpanels` → `p-tabpanel` (на каждый элемент `tabs`) | поле `content` элемента `ExtraTabItem` | +## Слоты (content projection) -Связь заголовка и панели обеспечивается полем `value`: совпадающие значения в `p-tab` и `p-tabpanel` объединяют их в одну вкладку. +| Слот | Описание | +|------|---------| +| `content` (на `extra-tab-item`) | Содержимое панели — обычный ``, без отдельной директивы | ## Related -- [Иконки](../../figma-code-connect/icons.md) — CSS-классы иконок для поля `icon` -- [Токены](../../figma-code-connect/tokens.md) — цветовые токены бейджей вкладок +- [ExtraAccordion](../accordion/accordion.figma.md) — та же DI-ловушка и тот же паттерн починки (панели вместо вкладок) +- [Иконки](../../figma-code-connect/icons.md) — CSS-классы иконок для `icon` +- [ExtraBadge](../badge/badge.figma.md) — `badgeSeverity` - [Conventions](../../figma-code-connect/conventions.md) — соглашения маппинга Figma → Angular ## Do / Don't **Do:** -- Задавайте уникальный `value` каждому элементу `tabs` и используйте то же значение во входе `value` для выбора активной вкладки по умолчанию. +- Задавайте уникальный `name` каждому `extra-tab-item` и используйте то же значение в `[value]`/`[(value)]` на `extra-tabs` для выбора активной вкладки. - Включайте `[scrollable]="true"`, когда вкладок много и они не помещаются по ширине контейнера. -- Используйте `[lazy]="true"` для тяжёлого содержимого панелей, чтобы откладывать их инициализацию до первого открытия. +- Используйте `[lazy]="true"` для тяжёлого содержимого панелей. - Для иконок заголовков используйте CSS-классы из справочника [icons.md](../../figma-code-connect/icons.md). **Don't:** -- Не дублируйте значения `value` у разных элементов `tabs` — это нарушит связь заголовка и панели. +- Не дублируйте `name` у разных `extra-tab-item` — это нарушит связь заголовка и панели. +- Не рендерьте `p-tab`/`p-tabpanel` самостоятельно в обход `extra-tabs`/`extra-tab-item` — сломается DI (см. раздел «Архитектура» выше). - Не инлайньте CSS-классы иконок вручную в обход [icons.md](../../figma-code-connect/icons.md). -- Не задавайте во входе `value` идентификатор, которого нет ни в одном элементе `tabs` — активная вкладка не будет выбрана. diff --git a/src/stories/components/tabs/examples/tabs-default.component.ts b/src/stories/components/tabs/examples/tabs-default.component.ts new file mode 100644 index 00000000..25831935 --- /dev/null +++ b/src/stories/components/tabs/examples/tabs-default.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ExtraTabsComponent } from '../../../../lib/components/tabs/tabs.component'; +import { ExtraTabItemComponent } from '../../../../lib/components/tabs/tab-item.component'; + +@Component({ + selector: 'app-tabs-default', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ExtraTabsComponent, ExtraTabItemComponent], + template: ` + + Данные профиля + Параметры аккаунта + Настройки уведомлений + + ` +}) +export class TabsDefaultComponent { + @Input() scrollable = false; + @Input() lazy = false; +} diff --git a/src/stories/components/tabs/examples/tabs-with-badge.component.ts b/src/stories/components/tabs/examples/tabs-with-badge.component.ts index 2b070056..9c8830fe 100644 --- a/src/stories/components/tabs/examples/tabs-with-badge.component.ts +++ b/src/stories/components/tabs/examples/tabs-with-badge.component.ts @@ -1,28 +1,21 @@ import { Component } from '@angular/core'; import { StoryObj } from '@storybook/angular'; -import { ExtraTabsComponent, ExtraTabItem } from '../../../../lib/components/tabs/tabs.component'; - -const template = ` -
- -
-`; -const styles = ''; +import { ExtraTabsComponent } from '../../../../lib/components/tabs/tabs.component'; +import { ExtraTabItemComponent } from '../../../../lib/components/tabs/tab-item.component'; @Component({ selector: 'app-tabs-with-badge', standalone: true, - imports: [ExtraTabsComponent], - template, - styles + imports: [ExtraTabsComponent, ExtraTabItemComponent], + template: ` + + Список входящих + Отправленные сообщения + Архивные записи + + ` }) -export class TabsWithBadgeComponent { - tabs: ExtraTabItem[] = [ - { value: '0', label: 'Tab 1', icon: 'ti ti-user', badge: '99+', content: 'Tab 1 Content' }, - { value: '1', label: 'Tab 2', icon: 'ti ti-settings', badge: '5', content: 'Tab 2 Content' }, - { value: '2', label: 'Tab 3', icon: 'ti ti-bell', badge: '2', content: 'Tab 3 Content' } - ]; -} +export class TabsWithBadgeComponent {} export const WithBadge: StoryObj = { render: () => ({ @@ -30,27 +23,25 @@ export const WithBadge: StoryObj = { }), parameters: { docs: { - description: { story: 'Табы с бейджами.' }, + description: { story: 'Вкладки с бейджами — `badge`/`badgeSeverity` на `extra-tab-item`.' }, source: { language: 'ts', code: ` -import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; +import { ExtraTabsComponent, ExtraTabItemComponent } from '@cdek-it/angular-ui-kit'; @Component({ selector: 'app-tabs-with-badge', standalone: true, - imports: [ExtraTabsComponent], + imports: [ExtraTabsComponent, ExtraTabItemComponent], template: \` - + + Список входящих + Отправленные сообщения + Архивные записи + \`, }) -export class TabsWithBadgeComponent { - tabs: ExtraTabItem[] = [ - { value: '0', label: 'Tab 1', icon: 'ti ti-user', badge: '99+', content: 'Tab 1 Content' }, - { value: '1', label: 'Tab 2', icon: 'ti ti-settings', badge: '5', content: 'Tab 2 Content' }, - { value: '2', label: 'Tab 3', icon: 'ti ti-bell', badge: '2', content: 'Tab 3 Content' }, - ]; -} +export class TabsWithBadgeComponent {} ` } } diff --git a/src/stories/components/tabs/examples/tabs-with-disabled.component.ts b/src/stories/components/tabs/examples/tabs-with-disabled.component.ts index cab838aa..1c27ddc1 100644 --- a/src/stories/components/tabs/examples/tabs-with-disabled.component.ts +++ b/src/stories/components/tabs/examples/tabs-with-disabled.component.ts @@ -1,28 +1,21 @@ import { Component } from '@angular/core'; import { StoryObj } from '@storybook/angular'; -import { ExtraTabsComponent, ExtraTabItem } from '../../../../lib/components/tabs/tabs.component'; - -const template = ` -
- -
-`; -const styles = ''; +import { ExtraTabsComponent } from '../../../../lib/components/tabs/tabs.component'; +import { ExtraTabItemComponent } from '../../../../lib/components/tabs/tab-item.component'; @Component({ selector: 'app-tabs-with-disabled', standalone: true, - imports: [ExtraTabsComponent], - template, - styles + imports: [ExtraTabsComponent, ExtraTabItemComponent], + template: ` + + Активная вкладка + Обычная вкладка + Эта вкладка отключена + + ` }) -export class TabsWithDisabledComponent { - tabs: ExtraTabItem[] = [ - { value: '0', label: 'Active Tab', icon: 'ti ti-user', content: 'Active Tab Content' }, - { value: '1', label: 'Default Tab', icon: 'ti ti-settings', content: 'Default Tab Content' }, - { value: '2', label: 'Disabled Tab', icon: 'ti ti-bell', disabled: true, content: 'Disabled Tab Content' } - ]; -} +export class TabsWithDisabledComponent {} export const WithDisabled: StoryObj = { render: () => ({ @@ -30,27 +23,25 @@ export const WithDisabled: StoryObj = { }), parameters: { docs: { - description: { story: 'Табы с заблокированной вкладкой.' }, + description: { story: 'Вкладки с заблокированной вкладкой — `[disabled]="true"` на `extra-tab-item`.' }, source: { language: 'ts', code: ` -import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; +import { ExtraTabsComponent, ExtraTabItemComponent } from '@cdek-it/angular-ui-kit'; @Component({ selector: 'app-tabs-with-disabled', standalone: true, - imports: [ExtraTabsComponent], + imports: [ExtraTabsComponent, ExtraTabItemComponent], template: \` - + + Активная вкладка + Обычная вкладка + Эта вкладка отключена + \`, }) -export class TabsWithDisabledComponent { - tabs: ExtraTabItem[] = [ - { value: '0', label: 'Active Tab', icon: 'ti ti-user', content: 'Active Tab Content' }, - { value: '1', label: 'Default Tab', icon: 'ti ti-settings', content: 'Default Tab Content' }, - { value: '2', label: 'Disabled Tab', icon: 'ti ti-bell', disabled: true, content: 'Disabled Tab Content' }, - ]; -} +export class TabsWithDisabledComponent {} ` } } diff --git a/src/stories/components/tabs/tabs.stories.ts b/src/stories/components/tabs/tabs.stories.ts index 9d76203c..63bbae59 100644 --- a/src/stories/components/tabs/tabs.stories.ts +++ b/src/stories/components/tabs/tabs.stories.ts @@ -1,15 +1,17 @@ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { ExtraTabsComponent } from '../../../lib/components/tabs/tabs.component'; +import { ExtraTabItemComponent } from '../../../lib/components/tabs/tab-item.component'; +import { TabsDefaultComponent } from './examples/tabs-default.component'; import { TabsWithBadgeComponent, WithBadge } from './examples/tabs-with-badge.component'; import { TabsWithDisabledComponent, WithDisabled } from './examples/tabs-with-disabled.component'; -const meta: Meta = { +const meta: Meta = { title: 'Components/Menu/Tabs', - component: ExtraTabsComponent, + component: TabsDefaultComponent, tags: ['autodocs'], decorators: [ moduleMetadata({ - imports: [ExtraTabsComponent, TabsWithBadgeComponent, TabsWithDisabledComponent] + imports: [ExtraTabsComponent, ExtraTabItemComponent, TabsDefaultComponent, TabsWithBadgeComponent, TabsWithDisabledComponent] }) ], parameters: { @@ -17,28 +19,23 @@ const meta: Meta = { description: { component: `Организует контент по вкладкам с возможностью переключения между ними. +Реализовано по спецификации [tabs.md](https://github.com/cdek-it/angular-ui-kit/blob/main/docs/components-api/tabs.md). + \`\`\`typescript -import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; -\`\`\`` +import { ExtraTabsComponent, ExtraTabItemComponent } from '@cdek-it/angular-ui-kit'; +\`\`\` + +Вкладки задаются дочерними компонентами \`\`, содержимое панели — через их собственный content projection.` } }, designTokens: { prefix: '--p-tabs' } }, argTypes: { - value: { - control: 'text', - description: 'Значение активной вкладки', - table: { - category: 'Props', - defaultValue: { summary: '0' }, - type: { summary: 'string | number' } - } - }, scrollable: { control: 'boolean', description: 'Включает горизонтальную прокрутку списка вкладок', table: { - category: 'Props', + category: 'Свойства', defaultValue: { summary: 'false' }, type: { summary: 'boolean' } } @@ -47,58 +44,56 @@ import { ExtraTabsComponent, ExtraTabItem } from '@cdek-it/angular-ui-kit'; control: 'boolean', description: 'Ленивая инициализация панелей — содержимое рендерится только при первом открытии', table: { - category: 'Props', + category: 'Свойства', defaultValue: { summary: 'false' }, type: { summary: 'boolean' } } - }, - tabs: { - control: 'object', - description: 'Массив вкладок', - table: { - category: 'Props', - type: { summary: 'ExtraTabItem[]' } - } } + }, + args: { + scrollable: false, + lazy: false } }; export default meta; -type Story = StoryObj; - -// ── Default ─────────────────────────────────────────────────────────────────── +type Story = StoryObj; +// ── Default (интерактивная) ──────────────────────────────────────────────── export const Default: Story = { name: 'Default', - render: (args) => { - const parts: string[] = []; - - parts.push(`value="${args.value}"`); - parts.push(`[tabs]="tabs"`); - if (args.scrollable) parts.push(`[scrollable]="true"`); - if (args.lazy) parts.push(`[lazy]="true"`); - - const template = ``; - - return { props: args, template }; - }, - args: { - value: '0', - tabs: [ - { value: '0', label: 'Tab 1', icon: 'ti ti-user', content: 'Tab 1 Content' }, - { value: '1', label: 'Tab 2', icon: 'ti ti-settings', content: 'Tab 2 Content' }, - { value: '2', label: 'Tab 3', icon: 'ti ti-bell', content: 'Tab 3 Content' } - ], - scrollable: false, - lazy: false - }, + render: (args) => ({ + props: args, + template: `` + }), parameters: { docs: { description: { - story: 'Базовый пример компонента с иконками. Используйте Controls для интерактивного изменения пропсов.' + story: 'Базовый пример с тремя вкладками и иконками. Используйте Controls для `scrollable`/`lazy`.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraTabsComponent, ExtraTabItemComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + standalone: true, + imports: [ExtraTabsComponent, ExtraTabItemComponent], + template: \` + + Данные профиля + Параметры аккаунта + Настройки уведомлений + + \`, +}) +export class ExampleComponent {} + ` } } } }; +// ── Комбинаторные истории ────────────────────────────────────────────────── export { WithBadge, WithDisabled };