From f5e3b8ce0f17a5dc58cef3939f073a8a096143a4 Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Thu, 23 Apr 2026 11:46:57 +0700 Subject: [PATCH 1/2] =?UTF-8?q?timeline:=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 --- .gitignore | 4 +- .../prime/stories/Timeline/Timeline.mdx | 50 ++++++ .../stories/Timeline/Timeline.stories.js | 156 ++++++++++++++++++ .../stories/Timeline/Timeline.template.js | 123 ++++++++++++++ .../prime/theme3.0/components/css/timeline.ts | 43 +++++ .../PBlockTimeline/PBlockTimeline.vue | 57 +++++++ src/primeBlocks/index.ts | 3 +- 7 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 src/plugins/prime/stories/Timeline/Timeline.mdx create mode 100644 src/plugins/prime/stories/Timeline/Timeline.stories.js create mode 100644 src/plugins/prime/stories/Timeline/Timeline.template.js create mode 100644 src/plugins/prime/theme3.0/components/css/timeline.ts create mode 100644 src/primeBlocks/PBlockTimeline/PBlockTimeline.vue diff --git a/.gitignore b/.gitignore index 20eea3e4..45f18911 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ coverage storybook-static -.claude/* \ No newline at end of file +.claude/* + +tsconfig.app.tsbuildinfo \ No newline at end of file diff --git a/src/plugins/prime/stories/Timeline/Timeline.mdx b/src/plugins/prime/stories/Timeline/Timeline.mdx new file mode 100644 index 00000000..9e52758b --- /dev/null +++ b/src/plugins/prime/stories/Timeline/Timeline.mdx @@ -0,0 +1,50 @@ +import { Meta, Canvas, Title, Description, Controls } from '@storybook/addon-docs/blocks'; +import * as TimelineStories from './Timeline.stories'; + + + + +<Description /> + +## Импорт + +```js +import Timeline from 'primevue/timeline'; +``` + +## Варианты использования + +### Horizontal + +Горизонтальная ориентация с событиями в ряд. + +<Canvas of={TimelineStories.Horizontal} /> + +#### Список аргументов компонента. +<Controls of={TimelineStories.Horizontal} /> + +### Vertical + +Вертикальная ориентация — события сверху вниз. + +<Canvas of={TimelineStories.Vertical} /> + +### Vertical Alternate + +Чередующееся расположение контента (слева-справа). + +<Canvas of={TimelineStories.VerticalAlternate} /> + +### Markers + +Кастомные маркеры с цветами и иконками. + +Доступны два варианта: +- **Без иконки** — цветная обводка через CSS-класс `timeline-marker-dot` и inline-стиль `borderColor` +- **С иконкой** — иконка Tabler через CSS-класс `timeline-marker-icon` и inline-стили `color`, `borderColor` + +Классы `timeline-marker-dot` и `timeline-marker-icon` — кастомные классы из дизайн-системы CDEK. +Они стилизуют маркер на основе токенов `timeline.eventMarker.*` (размер, фон, скругление, толщина обводки). +Применяются внутри слота `#marker` компонента Timeline. + +<Canvas of={TimelineStories.Markers} /> diff --git a/src/plugins/prime/stories/Timeline/Timeline.stories.js b/src/plugins/prime/stories/Timeline/Timeline.stories.js new file mode 100644 index 00000000..292da323 --- /dev/null +++ b/src/plugins/prime/stories/Timeline/Timeline.stories.js @@ -0,0 +1,156 @@ +import Timeline from 'primevue/timeline'; +import { + HorizontalTemplate, + VerticalTemplate, + AlternateTemplate, + MarkersTemplate, +} from './Timeline.template'; + +const meta = { + title: 'Prime/Timeline', + component: Timeline, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Компонент для визуализации последовательности событий в хронологическом порядке. Поддерживает горизонтальную и вертикальную ориентацию, кастомные маркеры.', + }, + }, + designToken: { disable: false }, + designTokens: { prefix: '--p-timeline' }, + }, + argTypes: { + value: { + control: 'object', + description: 'Массив событий для отображения', + table: { + category: 'Props', + type: { summary: 'array' }, + }, + }, + layout: { + control: 'select', + options: ['vertical', 'horizontal'], + description: 'Ориентация таймлайна', + table: { + category: 'Props', + defaultValue: { summary: "'vertical'" }, + type: { summary: "'vertical' | 'horizontal'" }, + }, + }, + align: { + control: 'select', + options: ['left', 'right', 'top', 'bottom', 'alternate'], + description: 'Положение контента относительно маркера', + table: { + category: 'Props', + defaultValue: { summary: "'left'" }, + type: { summary: "'left' | 'right' | 'top' | 'bottom' | 'alternate'" }, + }, + }, + }, +}; + +export default meta; + +// ── Horizontal ────────────────────────────────────────────────────────────── + +export const Horizontal = { + render: HorizontalTemplate, + args: {}, + parameters: { + docs: { + description: { + story: 'Горизонтальная ориентация Timeline с событиями в ряд.', + }, + source: { + code: `<Timeline :value="events" layout="horizontal"> + <template #content="slotProps"> + <div class="body-bold-base">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> +</Timeline>`, + }, + }, + }, +}; + +// ── Vertical ──────────────────────────────────────────────────────────────── + +export const Vertical = { + render: VerticalTemplate, + args: {}, + parameters: { + docs: { + description: { + story: 'Вертикальная ориентация Timeline — события сверху вниз.', + }, + source: { + code: `<Timeline :value="events" layout="vertical"> + <template #content="slotProps"> + <div class="body-bold-lg">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> +</Timeline>`, + }, + }, + }, +}; + +// ── Vertical Alternate ────────────────────────────────────────────────────── + +export const VerticalAlternate = { + render: AlternateTemplate, + args: {}, + parameters: { + docs: { + description: { + story: 'Вертикальная ориентация с чередующимся расположением контента.', + }, + source: { + code: `<Timeline :value="events" layout="vertical" align="alternate"> + <template #content="slotProps"> + <div class="body-bold-lg">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> +</Timeline>`, + }, + }, + }, +}; + +// ── Markers ───────────────────────────────────────────────────────────────── + +export const Markers = { + render: MarkersTemplate, + args: {}, + parameters: { + docs: { + description: { + story: + 'Кастомные маркеры с цветами и иконками. Доступны варианты: с иконкой (через слот #marker) и без (цветная обводка).', + }, + source: { + code: `<PBlockTimeline :value="events" layout="vertical"> + <template #marker="slotProps"> + <i + v-if="slotProps.item.icon" + :class="[slotProps.item.icon, 'timeline-marker-icon']" + :style="{ color: slotProps.item.iconColor, borderColor: slotProps.item.borderColor }" + ></i> + <div + v-else + class="timeline-marker-dot" + :style="{ borderColor: slotProps.item.borderColor }" + ></div> + </template> + <template #content="slotProps"> + <div class="body-bold-lg">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> +</PBlockTimeline>`, + }, + }, + }, +}; diff --git a/src/plugins/prime/stories/Timeline/Timeline.template.js b/src/plugins/prime/stories/Timeline/Timeline.template.js new file mode 100644 index 00000000..62f69c40 --- /dev/null +++ b/src/plugins/prime/stories/Timeline/Timeline.template.js @@ -0,0 +1,123 @@ +import Timeline from 'primevue/timeline'; +import PBlockTimeline from '@/primeBlocks/PBlockTimeline/PBlockTimeline.vue'; + +const defaultEvents = [ + { status: 'Заказ создан', date: '15 апр 2026, 10:00' }, + { status: 'Принят на склад', date: '16 апр 2026, 14:30' }, + { status: 'В пути', date: '17 апр 2026, 09:15' }, + { status: 'Доставлен', date: '18 апр 2026, 11:45' }, +]; + +const markerEvents = [ + { status: 'Успех', date: 'caption', borderColor: 'var(--p-green-500)' }, + { + status: 'Успех с иконкой', + date: 'caption', + icon: 'ti ti-circle-check', + iconColor: 'var(--p-green-500)', + borderColor: 'var(--p-green-500)', + }, + { + status: 'Предупреждение', + date: 'caption', + borderColor: 'var(--p-orange-500)', + }, + { + status: 'Предупреждение с иконкой', + date: 'caption', + icon: 'ti ti-alert-circle', + iconColor: 'var(--p-orange-500)', + borderColor: 'var(--p-orange-500)', + }, + { status: 'Ошибка', date: 'caption', borderColor: 'var(--p-red-500)' }, + { + status: 'Ошибка с иконкой', + date: 'caption', + icon: 'ti ti-circle-x', + iconColor: 'var(--p-red-500)', + borderColor: 'var(--p-red-500)', + }, + { status: 'Информация', date: 'caption', borderColor: 'var(--p-blue-500)' }, + { + status: 'Информация с иконкой', + date: 'caption', + icon: 'ti ti-info-circle', + iconColor: 'var(--p-blue-500)', + borderColor: 'var(--p-blue-500)', + }, +]; + +export const HorizontalTemplate = (args) => ({ + components: { Timeline }, + setup() { + return { args, events: defaultEvents }; + }, + template: ` + <Timeline :value="events" layout="horizontal" v-bind="args"> + <template #content="slotProps"> + <div class="body-bold-base">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> + </Timeline> + `, +}); + +export const VerticalTemplate = (args) => ({ + components: { Timeline }, + setup() { + return { args, events: defaultEvents }; + }, + template: ` + <Timeline :value="events" layout="vertical" v-bind="args"> + <template #content="slotProps"> + <div class="body-bold-lg">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> + </Timeline> + `, +}); + +export const AlternateTemplate = (args) => ({ + components: { Timeline }, + setup() { + return { args, events: defaultEvents }; + }, + template: ` + <Timeline :value="events" layout="vertical" align="alternate" v-bind="args"> + <template #content="slotProps"> + <div class="body-bold-lg">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> + </Timeline> + `, +}); + +export const MarkersTemplate = (args) => ({ + components: { PBlockTimeline }, + setup() { + return { args, events: markerEvents }; + }, + template: ` + <PBlockTimeline :value="events" layout="vertical" v-bind="args"> + <template #marker="slotProps"> + <i + v-if="slotProps.item.icon" + :class="[slotProps.item.icon, 'timeline-marker-icon']" + :style="{ + color: slotProps.item.iconColor, + borderColor: slotProps.item.borderColor, + }" + ></i> + <div + v-else + class="timeline-marker-dot" + :style="{ borderColor: slotProps.item.borderColor }" + ></div> + </template> + <template #content="slotProps"> + <div class="body-bold-lg">{{ slotProps.item.status }}</div> + <div class="caption-secondary">{{ slotProps.item.date }}</div> + </template> + </PBlockTimeline> + `, +}); diff --git a/src/plugins/prime/theme3.0/components/css/timeline.ts b/src/plugins/prime/theme3.0/components/css/timeline.ts new file mode 100644 index 00000000..6158e626 --- /dev/null +++ b/src/plugins/prime/theme3.0/components/css/timeline.ts @@ -0,0 +1,43 @@ +const css = ({ dt }: { dt: (token: string) => string }) => ` + +/* --- Base --- */ + +.p-timeline .p-timeline-event-content.p-timeline-event-content { + display: flex; + flex-direction: column; + gap: ${dt('timeline.extend.extEvent.gap')}; +} + +/* --- Orientation paddings --- */ + +.p-timeline-horizontal .p-timeline-event-opposite, +.p-timeline-horizontal .p-timeline-event-content { + padding: ${dt('feedback.padding.100')} 0; +} + +.p-timeline-vertical .p-timeline-event-opposite, +.p-timeline-vertical .p-timeline-event-content { + padding: 0 ${dt('feedback.padding.100')}; +} + +/* --- Horizontal --- */ + +.p-timeline.p-timeline-horizontal { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + align-items: start; +} + +/* Connector для последнего элемента в горизонтальной ориентации */ +.p-timeline.p-timeline-horizontal .p-timeline-event:last-child .p-timeline-event-separator::after { + content: ''; + display: block; + flex: 1; + background-color: ${dt('timeline.eventConnector.color')}; + height: ${dt('timeline.eventConnector.size')}; +} + +`; + +export default css; diff --git a/src/primeBlocks/PBlockTimeline/PBlockTimeline.vue b/src/primeBlocks/PBlockTimeline/PBlockTimeline.vue new file mode 100644 index 00000000..93b68597 --- /dev/null +++ b/src/primeBlocks/PBlockTimeline/PBlockTimeline.vue @@ -0,0 +1,57 @@ +<script setup lang="ts"> +import Timeline from 'primevue/timeline'; +import type { TimelineProps } from 'primevue/timeline'; + +interface IPBlockTimeline extends /* @vue-ignore */ TimelineProps {} + +defineProps<IPBlockTimeline>(); +</script> + +<template> + <Timeline v-bind="$props"> + <template #marker="slotProps"> + <slot name="marker" v-bind="slotProps"> + <div class="timeline-marker-dot"></div> + </slot> + </template> + <template #content="slotProps"> + <slot name="content" v-bind="slotProps" /> + </template> + <template #opposite="slotProps"> + <slot name="opposite" v-bind="slotProps" /> + </template> + <template #connector="slotProps"> + <slot name="connector" v-bind="slotProps" /> + </template> + </Timeline> +</template> + +<style scoped> +/* Кастомный маркер — без иконки */ +:deep(.timeline-marker-dot) { + display: flex; + width: var(--p-timeline-event-marker-size); + height: var(--p-timeline-event-marker-size); + background: var(--p-timeline-event-marker-background); + border-radius: var(--p-timeline-event-marker-border-radius); + border-width: var(--p-timeline-event-marker-border-width); + border-style: solid; + border-color: var(--p-timeline-event-marker-border-color); + box-sizing: border-box; +} + +/* Кастомный маркер — с иконкой */ +:deep(.timeline-marker-icon) { + display: flex; + align-items: center; + justify-content: center; + width: var(--p-timeline-event-marker-size); + height: var(--p-timeline-event-marker-size); + background: var(--p-timeline-event-marker-background); + border-radius: var(--p-timeline-event-marker-border-radius); + border-width: var(--p-timeline-event-marker-border-width); + border-style: solid; + border-color: transparent; + box-sizing: border-box; +} +</style> diff --git a/src/primeBlocks/index.ts b/src/primeBlocks/index.ts index d36572ae..4f5235aa 100644 --- a/src/primeBlocks/index.ts +++ b/src/primeBlocks/index.ts @@ -1,4 +1,5 @@ import PBlockPassword from './PBlockExample/PBlockPassword.vue'; +import PBlockTimeline from './PBlockTimeline/PBlockTimeline.vue'; import PBlockToggleButton from './PBlockToggleButton/PBlockToggleButton.vue'; -export { PBlockPassword, PBlockToggleButton }; +export { PBlockPassword, PBlockTimeline, PBlockToggleButton }; From 19a206f526d01ce556650761256da25690bd09a8 Mon Sep 17 00:00:00 2001 From: Danil Khaliulin <danielooneilie@gmail.com> Date: Thu, 23 Apr 2026 12:32:48 +0700 Subject: [PATCH 2/2] =?UTF-8?q?=D0=BA=D0=B0=D1=81=D1=82=D0=BE=D0=BC=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BC=D0=B0=D1=80=D0=BA=D0=B5=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prime/stories/Timeline/Timeline.mdx | 9 ++++- .../stories/Timeline/Timeline.stories.js | 4 +- .../stories/Timeline/Timeline.template.js | 37 ++++++++----------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/plugins/prime/stories/Timeline/Timeline.mdx b/src/plugins/prime/stories/Timeline/Timeline.mdx index 9e52758b..864b2ee4 100644 --- a/src/plugins/prime/stories/Timeline/Timeline.mdx +++ b/src/plugins/prime/stories/Timeline/Timeline.mdx @@ -39,12 +39,17 @@ import Timeline from 'primevue/timeline'; Кастомные маркеры с цветами и иконками. +<Canvas of={TimelineStories.Markers} /> + +<details> +<summary>Подробнее о кастомных маркерах</summary> + Доступны два варианта: - **Без иконки** — цветная обводка через CSS-класс `timeline-marker-dot` и inline-стиль `borderColor` -- **С иконкой** — иконка Tabler через CSS-класс `timeline-marker-icon` и inline-стили `color`, `borderColor` +- **С иконкой** — иконка Tabler через CSS-класс `timeline-marker-icon` и inline-стиль `color` Классы `timeline-marker-dot` и `timeline-marker-icon` — кастомные классы из дизайн-системы CDEK. Они стилизуют маркер на основе токенов `timeline.eventMarker.*` (размер, фон, скругление, толщина обводки). Применяются внутри слота `#marker` компонента Timeline. -<Canvas of={TimelineStories.Markers} /> +</details> diff --git a/src/plugins/prime/stories/Timeline/Timeline.stories.js b/src/plugins/prime/stories/Timeline/Timeline.stories.js index 292da323..c563cc39 100644 --- a/src/plugins/prime/stories/Timeline/Timeline.stories.js +++ b/src/plugins/prime/stories/Timeline/Timeline.stories.js @@ -137,12 +137,12 @@ export const Markers = { <i v-if="slotProps.item.icon" :class="[slotProps.item.icon, 'timeline-marker-icon']" - :style="{ color: slotProps.item.iconColor, borderColor: slotProps.item.borderColor }" + :style="{ color: slotProps.item.color }" ></i> <div v-else class="timeline-marker-dot" - :style="{ borderColor: slotProps.item.borderColor }" + :style="{ borderColor: slotProps.item.color }" ></div> </template> <template #content="slotProps"> diff --git a/src/plugins/prime/stories/Timeline/Timeline.template.js b/src/plugins/prime/stories/Timeline/Timeline.template.js index 62f69c40..8fb27bbc 100644 --- a/src/plugins/prime/stories/Timeline/Timeline.template.js +++ b/src/plugins/prime/stories/Timeline/Timeline.template.js @@ -9,42 +9,38 @@ const defaultEvents = [ ]; const markerEvents = [ - { status: 'Успех', date: 'caption', borderColor: 'var(--p-green-500)' }, { - status: 'Успех с иконкой', + status: 'Успех', date: 'caption', icon: 'ti ti-circle-check', - iconColor: 'var(--p-green-500)', - borderColor: 'var(--p-green-500)', + color: 'var(--p-green-500)', }, + { status: 'Успех (точка)', date: 'caption', color: 'var(--p-green-500)' }, { status: 'Предупреждение', date: 'caption', - borderColor: 'var(--p-orange-500)', + icon: 'ti ti-alert-circle', + color: 'var(--p-orange-500)', }, { - status: 'Предупреждение с иконкой', + status: 'Предупреждение (точка)', date: 'caption', - icon: 'ti ti-alert-circle', - iconColor: 'var(--p-orange-500)', - borderColor: 'var(--p-orange-500)', + color: 'var(--p-orange-500)', }, - { status: 'Ошибка', date: 'caption', borderColor: 'var(--p-red-500)' }, { - status: 'Ошибка с иконкой', + status: 'Ошибка', date: 'caption', icon: 'ti ti-circle-x', - iconColor: 'var(--p-red-500)', - borderColor: 'var(--p-red-500)', + color: 'var(--p-red-500)', }, - { status: 'Информация', date: 'caption', borderColor: 'var(--p-blue-500)' }, + { status: 'Ошибка (точка)', date: 'caption', color: 'var(--p-red-500)' }, { - status: 'Информация с иконкой', + status: 'Информация', date: 'caption', icon: 'ti ti-info-circle', - iconColor: 'var(--p-blue-500)', - borderColor: 'var(--p-blue-500)', + color: 'var(--p-blue-500)', }, + { status: 'Информация (точка)', date: 'caption', color: 'var(--p-blue-500)' }, ]; export const HorizontalTemplate = (args) => ({ @@ -103,15 +99,12 @@ export const MarkersTemplate = (args) => ({ <i v-if="slotProps.item.icon" :class="[slotProps.item.icon, 'timeline-marker-icon']" - :style="{ - color: slotProps.item.iconColor, - borderColor: slotProps.item.borderColor, - }" + :style="{ color: slotProps.item.color }" ></i> <div v-else class="timeline-marker-dot" - :style="{ borderColor: slotProps.item.borderColor }" + :style="{ borderColor: slotProps.item.color }" ></div> </template> <template #content="slotProps">