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
16 changes: 10 additions & 6 deletions docs/components-api/card.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

# ExtraCard

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

Карточка для отображения информации.

| Свойство | Описание | Типизация |
| ---------- | --------------------------- | ---------- |
| Свойство | Описание | Типизация |
| ---------- | ---------------------------- | --------- |
| `title` | заголовок карточки | `string` |
| `subtitle` | подзаголовок карточки | `string` |
| `overlay` | отображать тень (возвышение) | `boolean` |

# Слоты (content projection)

| Слот | Описание |
| --------- | --------------------------------- |
| Слот | Описание |
| --------- | ------------------------------------ |
| `header` | верхняя область (обычно изображение) |
| `content` | тело карточки |
| `footer` | нижний колонтитул |
| `content` | тело карточки |
| `footer` | нижний колонтитул |
29 changes: 29 additions & 0 deletions src/lib/components/card/card-template.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Directive, Input, TemplateRef } from '@angular/core';

export type ExtraCardTemplateName = 'header' | 'footer';

/**
* Именованные шаблоны слотов `header`/`footer` для ExtraCardComponent.
* Слот `content` не требует директивы — это содержимое карточки по умолчанию (`<ng-content>`).
*
* ```html
* <extra-card title="Заголовок">
* <ng-template extraCardTemplate="header">
* <img alt="" src="cover.jpg" />
* </ng-template>
* <p>Содержимое карточки</p>
* <ng-template extraCardTemplate="footer">
* <extra-button label="Действие" />
* </ng-template>
* </extra-card>
* ```
*/
@Directive({
selector: '[extraCardTemplate]'
})
export class ExtraCardTemplateDirective {
/** Имя слота: header | footer. */
@Input({ required: true }) extraCardTemplate!: ExtraCardTemplateName;

constructor(public template: TemplateRef<unknown>) {}
}
63 changes: 25 additions & 38 deletions src/lib/components/card/card.component.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import { AfterContentInit, ChangeDetectorRef, Component, ContentChildren, Input, QueryList } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { Card } from 'primeng/card';
import { PrimeTemplate, SharedModule } from 'primeng/api';
import { PrimeTemplate } from 'primeng/api';
import { ExtraCardTemplateDirective } from './card-template.directive';

@Component({
selector: 'extra-card',
host: { style: 'display: block' },
host: {
style: 'display: block',
// title/subtitle — входы компонента, но как статические атрибуты Angular оставляет их
// в DOM: нативный `title` даёт браузерный тултип на всю карточку, `subtitle` — просто
// несуществующий атрибут. Снимаем оба с хоста.
'[attr.title]': 'null',
'[attr.subtitle]': 'null'
},
standalone: true,
imports: [Card, SharedModule, NgTemplateOutlet],
imports: [Card, PrimeTemplate, NgTemplateOutlet],
template: `
<p-card [styleClass]="overlay ? 'shadow-md' : ''">
<!--
Заголовок и подзаголовок отдаём штатными входами p-card: PrimeNG сам кладёт их
в .p-card-title / .p-card-subtitle. Своя разметка в pTemplate="title" попадала
внутрь .p-card-title и дублировала этот класс вложенным элементом.
-->
<p-card [header]="title" [subheader]="subtitle" [styleClass]="overlay ? 'shadow-md' : ''">
@if (headerTpl) {
<ng-template pTemplate="header">
<ng-container [ngTemplateOutlet]="headerTpl.template"></ng-container>
</ng-template>
}
@if (title || subtitle) {
<ng-template pTemplate="title">
<div class="p-card-caption">
@if (title) {
<div class="p-card-title m-0" data-pc-section="title">{{ title }}</div>
}
@if (subtitle) {
<div class="p-card-subtitle m-0" data-pc-section="subtitle">{{ subtitle }}</div>
}
</div>
</ng-template>
}
@if (contentTpl) {
<ng-template pTemplate="content">
<ng-container [ngTemplateOutlet]="contentTpl.template"></ng-container>
</ng-template>
}
<ng-template pTemplate="content">
<ng-content></ng-content>
</ng-template>
@if (footerTpl) {
<ng-template pTemplate="footer">
<ng-container [ngTemplateOutlet]="footerTpl.template"></ng-container>
Expand All @@ -45,28 +44,16 @@ export class ExtraCardComponent implements AfterContentInit {
@Input() subtitle = '';
@Input() overlay = false;

@ContentChildren(PrimeTemplate) templates!: QueryList<PrimeTemplate>;
@ContentChildren(ExtraCardTemplateDirective) templates!: QueryList<ExtraCardTemplateDirective>;

headerTpl?: PrimeTemplate;
contentTpl?: PrimeTemplate;
footerTpl?: PrimeTemplate;
headerTpl?: ExtraCardTemplateDirective;
footerTpl?: ExtraCardTemplateDirective;

constructor(private cdr: ChangeDetectorRef) {}

ngAfterContentInit(): void {
this.templates.forEach((tpl) => {
switch (tpl.getType()) {
case 'header':
this.headerTpl = tpl;
break;
case 'content':
this.contentTpl = tpl;
break;
case 'footer':
this.footerTpl = tpl;
break;
}
});
this.headerTpl = this.templates.find((t) => t.extraCardTemplate === 'header');
this.footerTpl = this.templates.find((t) => t.extraCardTemplate === 'footer');
this.cdr.detectChanges();
}
}
51 changes: 21 additions & 30 deletions src/lib/components/card/card.figma.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ figma:
componentKey: '55dc631832ae2e7da5810b95b0537584bf146f60'
name: '<Card>'
status: stable
updated: '2026-06-22'
updated: '2026-09-29'
---

## Overview

`ExtraCardComponent` — контейнер-поверхность для группировки связанного контента и действий вокруг одной темы или объекта; визуально отделяет блок от фона как единое целое. Оборачивает PrimeNG `p-card` и добавляет типизированные пропсы заголовка/подзаголовка и режим тени.

Компонент соответствует Figma-компоненту `<Card>` (nodeId `1213:4134`, библиотека «UI Kit (DS) v2.0»). Содержимое заголовка, тела и футера проецируется через слоты PrimeNG `pTemplate`.
Компонент соответствует Figma-компоненту `<Card>` (nodeId `1213:4134`, библиотека «UI Kit (DS) v2.1»). Слоты `header`/`footer` — собственная директива `extraCardTemplate` (не протекает PrimeNG `pTemplate` в публичное API); `content` — обычный `<ng-content>`.

## Props mapping

| Свойство | Тип | По умолчанию | Описание |
|----------|-----|--------------|---------|
| `title` | `string` | `''` | Заголовок карточки — соответствует Figma-свойству `text-title` |
| `subtitle` | `string` | `''` | Подзаголовок карточки — соответствует Figma-свойству `text-caption` |
| `overlay` | `boolean` | `false` | Тень вокруг карточки (`shadow-md`) — соответствует Figma-варианту `overlay=true/false` |
| Свойство | Тип | По умолчанию | Описание |
| ---------- | --------- | ------------ | -------------------------------------------------------------------------------------- |
| `title` | `string` | `''` | Заголовок карточки — соответствует Figma-свойству `text-title` |
| `subtitle` | `string` | `''` | Подзаголовок карточки — соответствует Figma-свойству `text-caption` |
| `overlay` | `boolean` | `false` | Тень вокруг карточки (`shadow-md`) — соответствует Figma-варианту `overlay=true/false` |

## Variants

Expand All @@ -35,9 +35,7 @@ Figma: `<Card>`, overlay=false — nodeId `1213:4135`

```html
<extra-card title="Заголовок" subtitle="Подзаголовок">
<ng-template pTemplate="content">
<p class="text-sm">Контент карточки.</p>
</ng-template>
<p class="text-sm">Контент карточки.</p>
</extra-card>
```

Expand All @@ -47,9 +45,7 @@ Figma: `<Card>`, overlay=true — nodeId `1156:4676`

```html
<extra-card title="Заголовок" subtitle="Подзаголовок" [overlay]="true">
<ng-template pTemplate="content">
<p class="text-sm">Контент карточки с тенью.</p>
</ng-template>
<p class="text-sm">Контент карточки с тенью.</p>
</extra-card>
```

Expand All @@ -59,12 +55,10 @@ Figma: `<Card>`, show-header=true

```html
<extra-card title="Заголовок" subtitle="Подзаголовок">
<ng-template pTemplate="header">
<ng-template extraCardTemplate="header">
<img alt="Заголовок" src="assets/cover.jpg" class="w-full" />
</ng-template>
<ng-template pTemplate="content">
<p class="text-sm">Контент карточки с изображением в шапке.</p>
</ng-template>
<p class="text-sm">Контент карточки с изображением в шапке.</p>
</extra-card>
```

Expand All @@ -74,29 +68,24 @@ Figma: `<Card>`, show-footer=true

```html
<extra-card title="Заголовок">
<ng-template pTemplate="content">
<p class="text-sm">Контент карточки.</p>
</ng-template>
<ng-template pTemplate="footer">
<p class="text-sm">Контент карточки.</p>
<ng-template extraCardTemplate="footer">
<extra-button label="Действие" size="small" [fluid]="true"></extra-button>
</ng-template>
</extra-card>
```

```ts
import { ExtraCardComponent, ExtraButtonComponent } from '@cdek-it/angular-ui-kit';
import { SharedModule } from 'primeng/api';
import { ExtraCardComponent, ExtraCardTemplateDirective, ExtraButtonComponent } from '@cdek-it/angular-ui-kit';
```

## Slots

Слоты реализованы через директиву PrimeNG `pTemplate` (`<ng-template pTemplate="...">`). Импортируйте `SharedModule` из `'primeng/api'` в массив `imports` компонента.

| Слот | Синтаксис | Figma-свойство | Описание |
|------|-----------|----------------|---------|
| Шапка (header) | `<ng-template pTemplate="header">` | `show-header` | Изображение или произвольный блок над заголовком |
| Тело (content) | `<ng-template pTemplate="content">` | `change-layout-body` | Основное содержимое карточки |
| Футер (footer) | `<ng-template pTemplate="footer">` | `change-layout-footer` / `show-footer` | Кнопки действий и прочий контент нижней части |
| Слот | Синтаксис | Figma-свойство | Описание |
| -------------- | ------------------------------------------ | -------------------------------------- | ------------------------------------------------ |
| Шапка (header) | `<ng-template extraCardTemplate="header">` | `show-header` | Изображение или произвольный блок над заголовком |
| Тело (content) | обычный `<ng-content>`, без обёртки | `change-layout-body` | Основное содержимое карточки |
| Футер (footer) | `<ng-template extraCardTemplate="footer">` | `change-layout-footer` / `show-footer` | Кнопки действий и прочий контент нижней части |

Заголовок и подзаголовок задаются не слотами, а пропсами `title` и `subtitle` (Figma: `text-title`, `text-caption`).

Expand All @@ -110,12 +99,14 @@ import { SharedModule } from 'primeng/api';
## Do / Don't

**Do:**

- Используйте карточку для группировки контента одной темы (header / media / body / footer).
- Задавайте `[overlay]="true"`, когда карточку нужно визуально приподнять над фоном (grid/list превью).
- Передавайте заголовок и подзаголовок через `title` и `subtitle`, а не вёрсткой внутри `content`.
- Размещайте кнопки действий в слоте `footer`, используя [Button](../button/button.figma.md).

**Don't:**

- Не используйте Card вместо `Dialog` для модальных сценариев — Card не прерывает workflow.
- Не применяйте Card как зону-секцию без выделенной поверхности (для этого подходит Panel).
- Не размещайте в Card критичные уведомления или табличные данные.
Expand Down
4 changes: 1 addition & 3 deletions src/lib/components/card/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export * from './card.component';



export * from './card-template.directive';
11 changes: 6 additions & 5 deletions src/lib/providers/prime-preset/tokens/components/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ export const cardCss = ({ dt }: { dt: (token: string) => string }): string => `
box-shadow: ${dt('card.overlay.shadow')};
}

/* ─── Caption: обёртка заголовка и подзаголовка ─── */
.p-card-caption {
display: flex;
flex-direction: column;
gap: ${dt('card.caption.gap')};
/* ─── Caption: заголовок и подзаголовок стоят ближе друг к другу, чем остальные блоки ───
В Angular-версии p-card нет обёртки .p-card-caption: .p-card-title и .p-card-subtitle —
прямые дети .p-card-body с общим gap. Поэтому пару title+subtitle стягиваем до
card.caption.gap отрицательным отступом на разницу с card.body.gap. */
.p-card-title + .p-card-subtitle {
margin-top: calc(${dt('card.caption.gap')} - ${dt('card.body.gap')});
}

/* ─── Типографика подзаголовка ─── */
Expand Down
Loading
Loading