From 1497b2b9f6150183c8f415f62a95be17c7ebc796 Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Wed, 22 Apr 2026 11:21:34 +0700 Subject: [PATCH] =?UTF-8?q?carousel:=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F,=20=D1=81=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=81=D1=8B,=20=D0=BE=D0=B1=D1=91=D1=80=D1=82=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/carousel/carousel.component.ts | 62 +++++++ src/lib/providers/prime-preset/map-tokens.ts | 5 + .../tokens/components/carousel.ts | 34 ++++ .../components/carousel/carousel.stories.ts | 159 ++++++++++++++++++ .../examples/carousel-autoplay.component.ts | 86 ++++++++++ .../examples/carousel-vertical.component.ts | 88 ++++++++++ 6 files changed, 434 insertions(+) create mode 100644 src/lib/components/carousel/carousel.component.ts create mode 100644 src/lib/providers/prime-preset/tokens/components/carousel.ts create mode 100644 src/stories/components/carousel/carousel.stories.ts create mode 100644 src/stories/components/carousel/examples/carousel-autoplay.component.ts create mode 100644 src/stories/components/carousel/examples/carousel-vertical.component.ts diff --git a/src/lib/components/carousel/carousel.component.ts b/src/lib/components/carousel/carousel.component.ts new file mode 100644 index 00000000..46ad5fb3 --- /dev/null +++ b/src/lib/components/carousel/carousel.component.ts @@ -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: ` + + @if (itemTemplate) { + + + + } + @if (headerTemplate) { + + + + } + @if (footerTemplate) { + + + + } + + `, +}) +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 | null = null; + @Input() headerTemplate: TemplateRef | null = null; + @Input() footerTemplate: TemplateRef | null = null; + @Output() onPage = new EventEmitter(); +} diff --git a/src/lib/providers/prime-preset/map-tokens.ts b/src/lib/providers/prime-preset/map-tokens.ts index 7e452aca..ed585ea3 100644 --- a/src/lib/providers/prime-preset/map-tokens.ts +++ b/src/lib/providers/prime-preset/map-tokens.ts @@ -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 = { @@ -91,6 +92,10 @@ const presetTokens: Preset = { ...(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, diff --git a/src/lib/providers/prime-preset/tokens/components/carousel.ts b/src/lib/providers/prime-preset/tokens/components/carousel.ts new file mode 100644 index 00000000..298a3d13 --- /dev/null +++ b/src/lib/providers/prime-preset/tokens/components/carousel.ts @@ -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; + } +`; diff --git a/src/stories/components/carousel/carousel.stories.ts b/src/stories/components/carousel/carousel.stories.ts new file mode 100644 index 00000000..b5bf4629 --- /dev/null +++ b/src/stories/components/carousel/carousel.stories.ts @@ -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; + +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 = { + 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; + +// ── Default ─────────────────────────────────────────────────────────────────── + +export const Default: Story = { + name: 'Default', + render: (args) => ({ + props: { ...args, slides: SLIDES }, + template: ` + +
+ {{ item.title }} + {{ item.subtitle }} +
+
+ + `, + }), + parameters: { + docs: { + description: { + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.', + }, + }, + }, +}; + +// ── Vertical ────────────────────────────────────────────────────────────────── + +export const Vertical: Story = VerticalStory; + +// ── Autoplay ────────────────────────────────────────────────────────────────── + +export const Autoplay: Story = AutoplayStory; diff --git a/src/stories/components/carousel/examples/carousel-autoplay.component.ts b/src/stories/components/carousel/examples/carousel-autoplay.component.ts new file mode 100644 index 00000000..52a4fb73 --- /dev/null +++ b/src/stories/components/carousel/examples/carousel-autoplay.component.ts @@ -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 = ` + +
+ {{ item.title }} + {{ item.subtitle }} +
+
+ +`; +const styles = ''; + +@Component({ + selector: 'app-carousel-autoplay', + standalone: true, + imports: [CarouselComponent], + template, + styles, +}) +export class CarouselAutoplayComponent { + slides = SLIDES; +} + +export const Autoplay = { + render: () => ({ + template: ``, + }), + 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: \` + +
+ {{ item.title }} + {{ item.subtitle }} +
+
+ + \`, +}) +export class CarouselAutoplayComponent { + slides = SLIDES; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/carousel/examples/carousel-vertical.component.ts b/src/stories/components/carousel/examples/carousel-vertical.component.ts new file mode 100644 index 00000000..68f9572d --- /dev/null +++ b/src/stories/components/carousel/examples/carousel-vertical.component.ts @@ -0,0 +1,88 @@ +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 = ` +
+ +
+ {{ item.title }} + {{ item.subtitle }} +
+
+ +
+`; +const styles = ''; + +@Component({ + selector: 'app-carousel-vertical', + standalone: true, + imports: [CarouselComponent], + template, + styles, +}) +export class CarouselVerticalComponent { + slides = SLIDES; +} + +export const Vertical = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Карусель с вертикальной ориентацией.', + }, + 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-vertical', + standalone: true, + imports: [CarouselComponent], + template: \` +
+ +
+ {{ item.title }} + {{ item.subtitle }} +
+
+ +
+ \`, +}) +export class CarouselVerticalComponent { + slides = SLIDES; +} + `, + }, + }, + }, +};