diff --git a/src/lib/components/menu/menu.component.ts b/src/lib/components/menu/menu.component.ts
new file mode 100644
index 0000000..a692f69
--- /dev/null
+++ b/src/lib/components/menu/menu.component.ts
@@ -0,0 +1,59 @@
+import { Component, Input, TemplateRef, ViewChild } from '@angular/core';
+import { NgTemplateOutlet } from '@angular/common';
+import { Menu } from 'primeng/menu';
+import { MenuItem, PrimeTemplate } from 'primeng/api';
+
+export interface MenuModel extends MenuItem {
+ caption?: string;
+}
+
+@Component({
+ selector: 'menu',
+ host: { style: 'display: contents' },
+ standalone: true,
+ imports: [Menu, PrimeTemplate, NgTemplateOutlet],
+ template: `
+
+
+ @if (itemTemplate) {
+
+
+ } @else {
+
+ }
+
+
+ `,
+})
+export class MenuComponent {
+ @ViewChild('menuRef') menuRef!: Menu;
+
+ @Input() model: MenuModel[] = [];
+ @Input() popup = false;
+ @Input() itemTemplate: TemplateRef | null = null;
+
+ toggle(event: Event): void {
+ this.menuRef.toggle(event);
+ }
+}
diff --git a/src/prime-preset/tokens/components/menu.ts b/src/prime-preset/tokens/components/menu.ts
new file mode 100644
index 0000000..2ef5fcf
--- /dev/null
+++ b/src/prime-preset/tokens/components/menu.ts
@@ -0,0 +1,67 @@
+export const menuCss = ({ dt }: { dt: (token: string) => string }): string => `
+ .p-menu.p-component {
+ padding: ${dt('menu.extend.paddingY')} ${dt('menu.extend.paddingX')};
+ }
+
+ .p-menu .p-menu-item-content .p-menu-item-link .p-menu-item-label {
+ font-family: ${dt('fonts.fontFamily.base')};
+ font-size: ${dt('fonts.fontSize.300')};
+ font-weight: ${dt('fonts.fontWeight.regular')};
+ line-height: ${dt('fonts.lineHeight.400')};
+ }
+
+ .p-menu .p-menu-item-content .menu-item-label {
+ display: flex;
+ flex-direction: column;
+ gap: ${dt('menu.extend.extItem.caption.gap')};
+ }
+
+ .p-menu .p-menu-item-content .menu-item-caption {
+ font-family: ${dt('fonts.fontFamily.base')};
+ font-size: ${dt('fonts.fontSize.200')};
+ font-weight: ${dt('fonts.fontWeight.regular')};
+ color: ${dt('menu.colorScheme.light.extend.extItem.caption.color')};
+ }
+
+ .p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover,
+ .p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover .p-menu-item-link,
+ .p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover .p-menu-item-label,
+ .p-menu .p-menu-item:not(.p-disabled) .p-menu-item-content:hover .p-menu-item-icon {
+ background: ${dt('menu.colorScheme.light.item.focusBackground')};
+ color: ${dt('menu.colorScheme.light.item.focusColor')};
+ }
+
+ .p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content,
+ .p-menu .p-menu-item.p-focus > .p-menu-item-content {
+ background: ${dt('menu.extend.extItem.activeBackground')};
+ color: ${dt('menu.extend.extItem.activeColor')};
+ }
+
+ .p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content .p-menu-item-link,
+ .p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content .p-menu-item-label,
+ .p-menu .p-menu-item.p-focus > .p-menu-item-content .p-menu-item-link,
+ .p-menu .p-menu-item.p-focus > .p-menu-item-content .p-menu-item-label {
+ color: ${dt('menu.extend.extItem.activeColor')};
+ }
+
+ .p-menu .p-menu-item.p-menuitem-checked > .p-menu-item-content .p-menu-item-icon,
+ .p-menu .p-menu-item.p-focus > .p-menu-item-content .p-menu-item-icon {
+ color: ${dt('menu.colorScheme.light.extend.extItem.icon.activeColor')};
+ }
+
+ .p-menu .p-menu-item.p-menuitem-checked:not(.p-disabled) > .p-menu-item-content:hover {
+ background: ${dt('menu.colorScheme.light.item.focusBackground')};
+ color: ${dt('menu.colorScheme.light.item.focusColor')};
+ }
+
+ .p-menu .p-menu-item.p-menuitem-checked:not(.p-disabled) > .p-menu-item-content:hover .p-menu-item-icon {
+ color: ${dt('menu.colorScheme.light.item.focusColor')};
+ }
+
+ .p-menu .p-menu-submenu-label {
+ text-transform: uppercase;
+ font-size: ${dt('fonts.fontSize.200')};
+ font-family: ${dt('fonts.fontFamily.heading')};
+ line-height: ${dt('fonts.lineHeight.400')};
+ }
+`;
diff --git a/src/stories/components/menu/examples/menu-basic.component.ts b/src/stories/components/menu/examples/menu-basic.component.ts
new file mode 100644
index 0000000..e18fe08
--- /dev/null
+++ b/src/stories/components/menu/examples/menu-basic.component.ts
@@ -0,0 +1,23 @@
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '../../../../lib/components/menu/menu.component';
+
+const template = `
+
+
+
+`;
+
+@Component({
+ selector: 'app-menu-basic',
+ standalone: true,
+ imports: [MenuComponent],
+ template,
+})
+export class MenuBasicComponent {
+ items: MenuModel[] = [
+ { label: 'Новый заказ' },
+ { label: 'Поиск отправления' },
+ { separator: true },
+ { label: 'Экспорт' },
+ ];
+}
diff --git a/src/stories/components/menu/examples/menu-custom.component.ts b/src/stories/components/menu/examples/menu-custom.component.ts
new file mode 100644
index 0000000..3d04f2b
--- /dev/null
+++ b/src/stories/components/menu/examples/menu-custom.component.ts
@@ -0,0 +1,35 @@
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '../../../../lib/components/menu/menu.component';
+
+const template = `
+
+
+
+`;
+
+@Component({
+ selector: 'app-menu-custom',
+ standalone: true,
+ imports: [MenuComponent],
+ template,
+})
+export class MenuCustomComponent {
+ items: MenuModel[] = [
+ {
+ label: 'Создать отправление',
+ caption: 'Оформление нового заказа',
+ icon: 'ti ti-file-plus',
+ },
+ {
+ label: 'Найти посылку',
+ caption: 'Поиск по трек-номеру',
+ icon: 'ti ti-map-pin',
+ },
+ { separator: true },
+ {
+ label: 'Экспорт данных',
+ caption: 'Выгрузка в CSV или Excel',
+ icon: 'ti ti-download',
+ },
+ ];
+}
diff --git a/src/stories/components/menu/examples/menu-grouped.component.ts b/src/stories/components/menu/examples/menu-grouped.component.ts
new file mode 100644
index 0000000..d1050f3
--- /dev/null
+++ b/src/stories/components/menu/examples/menu-grouped.component.ts
@@ -0,0 +1,35 @@
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '../../../../lib/components/menu/menu.component';
+
+const template = `
+
+
+
+`;
+
+@Component({
+ selector: 'app-menu-grouped',
+ standalone: true,
+ imports: [MenuComponent],
+ template,
+})
+export class MenuGroupedComponent {
+ items: MenuModel[] = [
+ {
+ label: 'Заказы',
+ items: [
+ { label: 'Новый заказ', icon: 'ti ti-plus' },
+ { label: 'Список заказов', icon: 'ti ti-list' },
+ { label: 'Архив', icon: 'ti ti-archive' },
+ ],
+ },
+ {
+ label: 'Отправления',
+ items: [
+ { label: 'Создать накладную', icon: 'ti ti-file-invoice' },
+ { label: 'Отследить посылку', icon: 'ti ti-map-pin' },
+ { label: 'Отменить отправление', icon: 'ti ti-ban' },
+ ],
+ },
+ ];
+}
diff --git a/src/stories/components/menu/examples/menu-popup.component.ts b/src/stories/components/menu/examples/menu-popup.component.ts
new file mode 100644
index 0000000..2b18c79
--- /dev/null
+++ b/src/stories/components/menu/examples/menu-popup.component.ts
@@ -0,0 +1,31 @@
+import { Component, ViewChild } from '@angular/core';
+import { Button } from 'primeng/button';
+import { MenuComponent, MenuModel } from '../../../../lib/components/menu/menu.component';
+
+const template = `
+
+`;
+
+@Component({
+ selector: 'app-menu-popup',
+ standalone: true,
+ imports: [MenuComponent, Button],
+ template,
+})
+export class MenuPopupComponent {
+ @ViewChild('menuRef') menuRef!: MenuComponent;
+
+ items: MenuModel[] = [
+ { label: 'Создать отправление', icon: 'ti ti-file-plus' },
+ { label: 'Найти по трек-номеру', icon: 'ti ti-search' },
+ { separator: true },
+ { label: 'Экспорт данных', icon: 'ti ti-download' },
+ ];
+
+ toggle(event: Event): void {
+ this.menuRef.toggle(event);
+ }
+}
diff --git a/src/stories/components/menu/examples/menu-with-icons.component.ts b/src/stories/components/menu/examples/menu-with-icons.component.ts
new file mode 100644
index 0000000..dbffd8d
--- /dev/null
+++ b/src/stories/components/menu/examples/menu-with-icons.component.ts
@@ -0,0 +1,25 @@
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '../../../../lib/components/menu/menu.component';
+
+const template = `
+
+
+
+`;
+
+@Component({
+ selector: 'app-menu-with-icons',
+ standalone: true,
+ imports: [MenuComponent],
+ template,
+})
+export class MenuWithIconsComponent {
+ items: MenuModel[] = [
+ { label: 'Создать отправление', icon: 'ti ti-file-plus' },
+ { label: 'Открыть список заказов', icon: 'ti ti-folder-open' },
+ { label: 'Сохранить черновик', icon: 'ti ti-device-floppy' },
+ { separator: true },
+ { label: 'Распечатать накладную', icon: 'ti ti-printer' },
+ { label: 'Экспорт данных', icon: 'ti ti-download' },
+ ];
+}
diff --git a/src/stories/components/menu/menu.stories.ts b/src/stories/components/menu/menu.stories.ts
new file mode 100644
index 0000000..b226d21
--- /dev/null
+++ b/src/stories/components/menu/menu.stories.ts
@@ -0,0 +1,276 @@
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
+import { MenuComponent } from '../../../lib/components/menu/menu.component';
+import { MenuPopupComponent } from './examples/menu-popup.component';
+import { MenuBasicComponent } from './examples/menu-basic.component';
+import { MenuWithIconsComponent } from './examples/menu-with-icons.component';
+import { MenuGroupedComponent } from './examples/menu-grouped.component';
+import { MenuCustomComponent } from './examples/menu-custom.component';
+
+const meta: Meta = {
+ title: 'Components/Menu/Menu',
+ component: MenuComponent,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ description: {
+ component: `Компонент навигационного меню. Поддерживает режим popup (по нажатию на триггер) и inline-отображение, группировку пунктов и пункты с описанием (caption).
+
+\`\`\`typescript
+import { MenuComponent } from '@cdek-it/angular-ui-kit';
+\`\`\``,
+ },
+ },
+ designTokens: { prefix: '--p-menu' },
+ },
+ argTypes: {
+ model: {
+ control: false,
+ description: 'Массив пунктов меню.',
+ table: {
+ category: 'Props',
+ type: { summary: 'MenuModel[]' },
+ },
+ },
+ popup: {
+ control: 'boolean',
+ description: 'Режим popup — меню отображается при вызове метода toggle().',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ── Popup ─────────────────────────────────────────────────────────────────────
+
+export const Default: Story = {
+ name: 'Popup',
+ decorators: [moduleMetadata({ imports: [MenuPopupComponent] })],
+ render: () => ({ template: `` }),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Меню вызывается по нажатию на кнопку. Используйте метод toggle() для показа/скрытия.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component, ViewChild } from '@angular/core';
+import { Button } from 'primeng/button';
+import { MenuComponent, MenuModel } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-menu-popup',
+ standalone: true,
+ imports: [MenuComponent, Button],
+ template: \`
+
+
+ \`,
+})
+export class MenuPopupComponent {
+ @ViewChild('menuRef') menuRef!: MenuComponent;
+
+ items: MenuModel[] = [
+ { label: 'Создать отправление', icon: 'ti ti-file-plus' },
+ { label: 'Найти по трек-номеру', icon: 'ti ti-search' },
+ { separator: true },
+ { label: 'Экспорт данных', icon: 'ti ti-download' },
+ ];
+
+ toggle(event: Event): void {
+ this.menuRef.toggle(event);
+ }
+}
+ `,
+ },
+ },
+ },
+};
+
+// ── Basic ─────────────────────────────────────────────────────────────────────
+
+export const Basic: Story = {
+ name: 'Basic',
+ decorators: [moduleMetadata({ imports: [MenuBasicComponent] })],
+ render: () => ({ template: `` }),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Базовый вариант inline-меню без иконок.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-menu-basic',
+ standalone: true,
+ imports: [MenuComponent],
+ template: \`
+
+ \`,
+})
+export class MenuBasicComponent {
+ items: MenuModel[] = [
+ { label: 'Новый заказ' },
+ { label: 'Поиск отправления' },
+ { separator: true },
+ { label: 'Экспорт' },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
+
+// ── WithIcons ─────────────────────────────────────────────────────────────────
+
+export const WithIcons: Story = {
+ name: 'WithIcons',
+ decorators: [moduleMetadata({ imports: [MenuWithIconsComponent] })],
+ render: () => ({ template: `` }),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Пункты меню с иконками.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-menu-with-icons',
+ standalone: true,
+ imports: [MenuComponent],
+ template: \`
+
+ \`,
+})
+export class MenuWithIconsComponent {
+ items: MenuModel[] = [
+ { label: 'Создать отправление', icon: 'ti ti-file-plus' },
+ { label: 'Открыть список заказов', icon: 'ti ti-folder-open' },
+ { label: 'Сохранить черновик', icon: 'ti ti-device-floppy' },
+ { separator: true },
+ { label: 'Распечатать накладную', icon: 'ti ti-printer' },
+ { label: 'Экспорт данных', icon: 'ti ti-download' },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
+
+// ── Grouped ───────────────────────────────────────────────────────────────────
+
+export const Grouped: Story = {
+ name: 'Grouped',
+ decorators: [moduleMetadata({ imports: [MenuGroupedComponent] })],
+ render: () => ({ template: `` }),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Группировка пунктов меню через label у родительского элемента.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-menu-grouped',
+ standalone: true,
+ imports: [MenuComponent],
+ template: \`
+
+ \`,
+})
+export class MenuGroupedComponent {
+ items: MenuModel[] = [
+ {
+ label: 'Заказы',
+ items: [
+ { label: 'Новый заказ', icon: 'ti ti-plus' },
+ { label: 'Список заказов', icon: 'ti ti-list' },
+ { label: 'Архив', icon: 'ti ti-archive' },
+ ],
+ },
+ {
+ label: 'Отправления',
+ items: [
+ { label: 'Создать накладную', icon: 'ti ti-file-invoice' },
+ { label: 'Отследить посылку', icon: 'ti ti-map-pin' },
+ { label: 'Отменить отправление', icon: 'ti ti-ban' },
+ ],
+ },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
+
+// ── Custom ────────────────────────────────────────────────────────────────────
+
+export const Custom: Story = {
+ name: 'Custom',
+ decorators: [moduleMetadata({ imports: [MenuCustomComponent] })],
+ render: () => ({ template: `` }),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Пункты меню с иконкой и описанием (caption). Поле caption передаётся через MenuModel.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { MenuComponent, MenuModel } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-menu-custom',
+ standalone: true,
+ imports: [MenuComponent],
+ template: \`
+
+ \`,
+})
+export class MenuCustomComponent {
+ items: MenuModel[] = [
+ {
+ label: 'Создать отправление',
+ caption: 'Оформление нового заказа',
+ icon: 'ti ti-file-plus',
+ },
+ {
+ label: 'Найти посылку',
+ caption: 'Поиск по трек-номеру',
+ icon: 'ti ti-map-pin',
+ },
+ { separator: true },
+ {
+ label: 'Экспорт данных',
+ caption: 'Выгрузка в CSV или Excel',
+ icon: 'ti ti-download',
+ },
+ ];
+}
+ `,
+ },
+ },
+ },
+};