Skip to content
Merged
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
62 changes: 62 additions & 0 deletions src/lib/components/carousel/carousel.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { Carousel } from 'primeng/carousel';
import { PrimeTemplate } from 'primeng/api';
import type { CarouselResponsiveOptions, CarouselPageEvent } from 'primeng/types/carousel';

export type CarouselOrientation = 'horizontal' | 'vertical';

@Component({
selector: 'carousel',
standalone: true,
imports: [Carousel, NgTemplateOutlet, PrimeTemplate],
template: `
<p-carousel
[value]="value"
[numVisible]="numVisible"
[numScroll]="numScroll"
[circular]="circular"
[orientation]="orientation"
[autoplayInterval]="autoplayInterval"
[showIndicators]="showIndicators"
[showNavigators]="showNavigators"
[page]="page"
[responsiveOptions]="responsiveOptions"
[verticalViewPortHeight]="verticalViewPortHeight"
(onPage)="onPage.emit($event)"
>
@if (itemTemplate) {
<ng-template pTemplate="item" let-data>
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: data }"></ng-container>
</ng-template>
}
@if (headerTemplate) {
<ng-template pTemplate="header">
<ng-container [ngTemplateOutlet]="headerTemplate"></ng-container>
</ng-template>
}
@if (footerTemplate) {
<ng-template pTemplate="footer">
<ng-container [ngTemplateOutlet]="footerTemplate"></ng-container>
</ng-template>
}
</p-carousel>
`,
})
export class CarouselComponent {
@Input() value: any[] = [];
@Input() numVisible = 1;
@Input() numScroll = 1;
@Input() circular = false;
@Input() orientation: CarouselOrientation = 'horizontal';
@Input() autoplayInterval = 0;
@Input() showIndicators = true;
@Input() showNavigators = true;
@Input() page = 0;
@Input() responsiveOptions: CarouselResponsiveOptions[] | undefined;
@Input() verticalViewPortHeight = '300px';
@Input() itemTemplate: TemplateRef<any> | null = null;
@Input() headerTemplate: TemplateRef<any> | null = null;
@Input() footerTemplate: TemplateRef<any> | null = null;
@Output() onPage = new EventEmitter<CarouselPageEvent>();
}
5 changes: 5 additions & 0 deletions src/lib/providers/prime-preset/map-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { megamenuCss } from './tokens/components/megamenu';
import { selectCss } from './tokens/components/select';
import { messageCss } from './tokens/components/message';
import { inputotpCss } from './tokens/components/inputotp';
import { carouselCss } from './tokens/components/carousel';
import { galleriaCss } from './tokens/components/galleria';

const presetTokens: Preset<AuraBaseDesignTokens> = {
Expand Down Expand Up @@ -91,6 +92,10 @@ const presetTokens: Preset<AuraBaseDesignTokens> = {
...(tokens.components.password as unknown as ComponentsDesignTokens['password']),
css: passwordCss
},
carousel: {
...(tokens.components.carousel as unknown as ComponentsDesignTokens['carousel']),
css: carouselCss,
},
galleria: {
...(tokens.components.galleria as unknown as ComponentsDesignTokens['galleria']),
css: galleriaCss,
Expand Down
34 changes: 34 additions & 0 deletions src/lib/providers/prime-preset/tokens/components/carousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const carouselCss = ({ dt }: { dt: (token: string) => string }): string => `
.p-carousel .p-carousel-prev-button.p-button-secondary,
.p-carousel .p-carousel-next-button.p-button-secondary {
background: ${dt('surface.200')};
color: ${dt('text.color')};
border-color: transparent;
}

.p-carousel .p-carousel-prev-button.p-button-secondary:not(:disabled):hover,
.p-carousel .p-carousel-next-button.p-button-secondary:not(:disabled):hover {
background: ${dt('surface.300')};
color: ${dt('text.color')};
}

.p-carousel .p-carousel-prev-button.p-button-secondary:not(:disabled):active,
.p-carousel .p-carousel-next-button.p-button-secondary:not(:disabled):active {
background: ${dt('surface.400')};
color: ${dt('text.color')};
}

.p-carousel .p-button-icon-only.p-button-rounded {
border-radius: ${dt('button.roundedBorderRadius')};
}

.p-carousel .p-carousel-item {
padding-inline: calc(${dt('carousel.content.gap')} / 2);
}

/* Убираем visibility:hidden для неактивных слайдов.
Отсечение за пределами viewport обеспечивается через overflow:hidden на контейнере карточки. */
.p-carousel .p-items-hidden .p-carousel-item {
visibility: visible;
}
`;
159 changes: 159 additions & 0 deletions src/stories/components/carousel/carousel.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
import { CarouselComponent } from '../../../lib/components/carousel/carousel.component';
import { CarouselVerticalComponent, Vertical as VerticalStory } from './examples/carousel-vertical.component';
import { CarouselAutoplayComponent, Autoplay as AutoplayStory } from './examples/carousel-autoplay.component';

type CarouselArgs = Pick<CarouselComponent, 'numVisible' | 'numScroll' | 'circular' | 'orientation' | 'autoplayInterval' | 'showIndicators' | 'showNavigators'>;

const SLIDES = Array.from({ length: 8 }, (_, i) => ({
title: `Lorem Ipsum ${i + 1}`,
subtitle: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus, saepe.',
}));

const meta: Meta<CarouselArgs> = {
title: 'Components/Media/Carousel',
component: CarouselComponent,
tags: ['autodocs'],
decorators: [
moduleMetadata({
imports: [
CarouselComponent,
CarouselVerticalComponent,
CarouselAutoplayComponent,
],
}),
],
parameters: {
docs: {
description: {
component: `Компонент для отображения контента в виде слайдов с возможностью горизонтальной и вертикальной прокрутки, настройки количества видимых элементов и автовоспроизведения.

\`\`\`typescript
import { CarouselComponent } from '@cdek-it/angular-ui-kit';
\`\`\``,
},
},
designTokens: { prefix: '--p-carousel' },
},
argTypes: {
numVisible: {
control: { type: 'number', min: 1 },
description: 'Количество видимых слайдов',
table: {
category: 'Props',
defaultValue: { summary: '1' },
type: { summary: 'number' },
},
},
numScroll: {
control: { type: 'number', min: 1 },
description: 'Количество слайдов для прокрутки за один шаг',
table: {
category: 'Props',
defaultValue: { summary: '1' },
type: { summary: 'number' },
},
},
circular: {
control: 'boolean',
description: 'Бесконечная прокрутка',
table: {
category: 'Props',
defaultValue: { summary: 'false' },
type: { summary: 'boolean' },
},
},
orientation: {
control: { type: 'select' },
options: ['horizontal', 'vertical'],
description: 'Ориентация карусели',
table: {
category: 'Props',
defaultValue: { summary: 'horizontal' },
type: { summary: "'horizontal' | 'vertical'" },
},
},
autoplayInterval: {
control: { type: 'number', min: 0 },
description: 'Интервал автопрокрутки в миллисекундах (0 — отключено)',
table: {
category: 'Props',
defaultValue: { summary: '0' },
type: { summary: 'number' },
},
},
showIndicators: {
control: 'boolean',
description: 'Показывать индикаторы (точки)',
table: {
category: 'Props',
defaultValue: { summary: 'true' },
type: { summary: 'boolean' },
},
},
showNavigators: {
control: 'boolean',
description: 'Показывать кнопки навигации',
table: {
category: 'Props',
defaultValue: { summary: 'true' },
type: { summary: 'boolean' },
},
},
},
args: {
numVisible: 5,
numScroll: 1,
circular: false,
orientation: 'horizontal',
autoplayInterval: 0,
showIndicators: true,
showNavigators: true,
},
};

export default meta;
type Story = StoryObj<CarouselArgs>;

// ── Default ───────────────────────────────────────────────────────────────────

export const Default: Story = {
name: 'Default',
render: (args) => ({
props: { ...args, slides: SLIDES },
template: `
<ng-template #itemTpl let-item>
<div class="flex flex-col gap-3 px-3 py-3 border rounded min-w-0 overflow-hidden">
<span class="font-bold truncate">{{ item.title }}</span>
<span>{{ item.subtitle }}</span>
</div>
</ng-template>
<carousel
[value]="slides"
[numVisible]="numVisible"
[numScroll]="numScroll"
[circular]="circular"
[orientation]="orientation"
[autoplayInterval]="autoplayInterval"
[showIndicators]="showIndicators"
[showNavigators]="showNavigators"
[itemTemplate]="itemTpl"
></carousel>
`,
}),
parameters: {
docs: {
description: {
story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.',
},
},
},
};

// ── Vertical ──────────────────────────────────────────────────────────────────

export const Vertical: Story = VerticalStory;

// ── Autoplay ──────────────────────────────────────────────────────────────────

export const Autoplay: Story = AutoplayStory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Component } from '@angular/core';
import { CarouselComponent } from '../../../../lib/components/carousel/carousel.component';

const SLIDES = Array.from({ length: 8 }, (_, i) => ({
title: `Lorem Ipsum ${i + 1}`,
subtitle: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus, saepe.',
}));

const template = `
<ng-template #itemTpl let-item>
<div class="flex flex-col gap-3 px-3 py-3 border rounded min-w-0 overflow-hidden">
<span class="font-bold truncate">{{ item.title }}</span>
<span>{{ item.subtitle }}</span>
</div>
</ng-template>
<carousel
[value]="slides"
[numVisible]="3"
[numScroll]="1"
[circular]="true"
[autoplayInterval]="3000"
[itemTemplate]="itemTpl"
></carousel>
`;
const styles = '';

@Component({
selector: 'app-carousel-autoplay',
standalone: true,
imports: [CarouselComponent],
template,
styles,
})
export class CarouselAutoplayComponent {
slides = SLIDES;
}

export const Autoplay = {
render: () => ({
template: `<app-carousel-autoplay></app-carousel-autoplay>`,
}),
parameters: {
docs: {
description: {
story: 'Карусель с автоматической прокруткой слайдов каждые 3 секунды (`autoplayInterval`, `circular`).',
},
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { CarouselComponent } from '@cdek-it/angular-ui-kit';

const SLIDES = Array.from({ length: 8 }, (_, i) => ({
title: \`Lorem Ipsum \${i + 1}\`,
subtitle: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
}));

@Component({
selector: 'app-carousel-autoplay',
standalone: true,
imports: [CarouselComponent],
template: \`
<ng-template #itemTpl let-item>
<div class="flex flex-col gap-3 px-3 py-3 border rounded min-w-0 overflow-hidden">
<span class="font-bold truncate">{{ item.title }}</span>
<span>{{ item.subtitle }}</span>
</div>
</ng-template>
<carousel
[value]="slides"
[numVisible]="3"
[numScroll]="1"
[circular]="true"
[autoplayInterval]="3000"
[itemTemplate]="itemTpl"
></carousel>
\`,
})
export class CarouselAutoplayComponent {
slides = SLIDES;
}
`,
},
},
},
};
Loading
Loading