diff --git a/src/lib/components/inputgroup/input-group-addon.component.ts b/src/lib/components/inputgroup/input-group-addon.component.ts
new file mode 100644
index 00000000..fba19611
--- /dev/null
+++ b/src/lib/components/inputgroup/input-group-addon.component.ts
@@ -0,0 +1,14 @@
+import { Component } from '@angular/core';
+import { InputGroupAddon } from 'primeng/inputgroupaddon';
+
+@Component({
+ selector: 'input-group-addon',
+ standalone: true,
+ imports: [InputGroupAddon],
+ template: `
+
+
+
+ `,
+})
+export class InputGroupAddonComponent {}
diff --git a/src/lib/components/inputgroup/input-group.component.ts b/src/lib/components/inputgroup/input-group.component.ts
new file mode 100644
index 00000000..273624fc
--- /dev/null
+++ b/src/lib/components/inputgroup/input-group.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input } from '@angular/core';
+import { NgClass } from '@angular/common';
+import { InputGroup } from 'primeng/inputgroup';
+
+export type InputGroupSize = 'small' | 'base' | 'large' | 'xlarge';
+
+@Component({
+ selector: 'input-group',
+ standalone: true,
+ imports: [InputGroup, NgClass],
+ template: `
+
+
+
+ `,
+})
+export class InputGroupComponent {
+ @Input() size: InputGroupSize = 'base';
+
+ get sizeClass(): string {
+ if (this.size === 'xlarge') return 'p-inputgroup-xlg';
+ return '';
+ }
+}
diff --git a/src/lib/components/panelmenu/panelmenu.component.ts b/src/lib/components/panelmenu/panelmenu.component.ts
new file mode 100644
index 00000000..efb3516b
--- /dev/null
+++ b/src/lib/components/panelmenu/panelmenu.component.ts
@@ -0,0 +1,58 @@
+import { AfterViewChecked, ChangeDetectionStrategy, Component, ElementRef, HostListener, Input } from '@angular/core';
+import { PanelMenu } from 'primeng/panelmenu';
+import { MenuItem } from 'primeng/api';
+
+@Component({
+ selector: 'panelmenu',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [PanelMenu],
+ template: `
+
+ `,
+})
+export class PanelMenuComponent implements AfterViewChecked {
+ @Input() model: MenuItem[] = [];
+ @Input() multiple = false;
+ @Input() tabindex: number | undefined = undefined;
+
+ private activeItemId: string | null = null;
+
+ constructor(private readonly el: ElementRef) {}
+
+ @HostListener('click', ['$event'])
+ onItemClick(event: MouseEvent): void {
+ const target = event.target as Element;
+
+ if (target.closest('.p-panelmenu-header')) return;
+
+ const item = target.closest('.p-panelmenu-item');
+ if (!item) return;
+
+ this.activeItemId = item.id || null;
+ this.applyActiveClass();
+ }
+
+ ngAfterViewChecked(): void {
+ if (this.activeItemId) {
+ this.applyActiveClass();
+ }
+ }
+
+ private applyActiveClass(): void {
+ const root = this.el.nativeElement;
+ root.querySelectorAll('.p-panelmenu-item-active')
+ .forEach(el => el.classList.remove('p-panelmenu-item-active'));
+
+ if (this.activeItemId) {
+ const active = root.querySelector(`#${CSS.escape(this.activeItemId)}`);
+ if (active) {
+ active.classList.add('p-panelmenu-item-active');
+ }
+ }
+ }
+}
diff --git a/src/prime-preset/map-tokens.ts b/src/prime-preset/map-tokens.ts
index 099454a8..4ab77a5e 100644
--- a/src/prime-preset/map-tokens.ts
+++ b/src/prime-preset/map-tokens.ts
@@ -14,6 +14,7 @@ import { passwordCss } from './tokens/components/password';
import { tagCss } from './tokens/components/tag';
import { textareaCss } from './tokens/components/textarea';
import { tooltipCss } from './tokens/components/tooltip';
+import { inputgroupCss } from './tokens/components/inputgroup'
import { megamenuCss } from './tokens/components/megamenu';
import { selectCss } from './tokens/components/select';
import { messageCss } from './tokens/components/message';
diff --git a/src/prime-preset/tokens/components/inputgroup.ts b/src/prime-preset/tokens/components/inputgroup.ts
new file mode 100644
index 00000000..eaf2c78a
--- /dev/null
+++ b/src/prime-preset/tokens/components/inputgroup.ts
@@ -0,0 +1,94 @@
+export const inputgroupCss = ({ dt }: { dt: (token: string) => string }): string => `
+
+/* ─── Корректировка flex-layout через Angular-обёртки ─── */
+
+/*
+ * display: contents делает враппер-элементы прозрачными в layout-дереве:
+ * p-inputgroupaddon и input.p-inputtext становятся прямыми flex-элементами
+ * p-inputgroup. Это позволяет:
+ * - align-items: stretch работать напрямую → правильная высота аддона
+ * - границам быть на одном уровне → нет удвоения top/bottom border
+ * - flex: 1 1 auto; width: 1% на input работать нативно через PrimeNG
+ * CSS-селекторы по-прежнему работают (DOM-дерево не меняется).
+ */
+.p-inputgroup > input-group-addon {
+ display: contents;
+}
+
+.p-inputgroup > input-text {
+ display: contents;
+}
+
+/* ─── Корректировка border-radius и границ ─── */
+
+/*
+ * p-inputgroupaddon является :first-child И :last-child своего прямого родителя
+ * input-group-addon, поэтому PrimeNG добавляет ему оба inline-бордера.
+ * Сбрасываем их и переназначаем по позиции аддона в группе.
+ */
+.p-inputgroup > input-group-addon > .p-inputgroupaddon {
+ border-radius: 0;
+ border-inline-start: none;
+ border-inline-end: none;
+}
+
+/* Первый элемент группы — аддон: левые углы + левая граница */
+.p-inputgroup > input-group-addon:first-child > .p-inputgroupaddon {
+ border-start-start-radius: ${dt('inputgroup.addon.borderRadius')};
+ border-end-start-radius: ${dt('inputgroup.addon.borderRadius')};
+ border-inline-start: ${dt('inputgroup.extend.borderWidth')} solid ${dt('inputgroup.addon.borderColor')};
+}
+
+/* Последний элемент группы — аддон: правые углы + правая граница */
+.p-inputgroup > input-group-addon:last-child > .p-inputgroupaddon {
+ border-start-end-radius: ${dt('inputgroup.addon.borderRadius')};
+ border-end-end-radius: ${dt('inputgroup.addon.borderRadius')};
+ border-inline-end: ${dt('inputgroup.extend.borderWidth')} solid ${dt('inputgroup.addon.borderColor')};
+}
+
+/* Аддон сразу после другого аддона: левая граница как разделитель */
+.p-inputgroup > input-group-addon + input-group-addon > .p-inputgroupaddon {
+ border-inline-start: ${dt('inputgroup.extend.borderWidth')} solid ${dt('inputgroup.addon.borderColor')};
+}
+
+/* Сброс border-radius у input внутри input-text-обёртки */
+.p-inputgroup > input-text .p-inputtext {
+ border-radius: 0;
+ margin: 0;
+}
+
+/* Первый элемент группы — input: левые углы */
+.p-inputgroup > input-text:first-child .p-inputtext {
+ border-start-start-radius: ${dt('inputgroup.addon.borderRadius')};
+ border-end-start-radius: ${dt('inputgroup.addon.borderRadius')};
+}
+
+/* Последний элемент группы — input: правые углы */
+.p-inputgroup > input-text:last-child .p-inputtext {
+ border-start-end-radius: ${dt('inputgroup.addon.borderRadius')};
+ border-end-end-radius: ${dt('inputgroup.addon.borderRadius')};
+}
+
+/* ─── Addon в disabled состоянии ─── */
+.p-inputgroup:has(input[disabled]) .p-inputgroupaddon,
+.p-inputgroup:has(.p-inputtext[disabled]) .p-inputgroupaddon,
+.p-inputgroup:has(.p-component[disabled]) .p-inputgroupaddon {
+ background: ${dt('inputtext.root.disabledBackground')};
+ color: ${dt('inputtext.root.disabledColor')};
+}
+
+/* ─── Иконка внутри addon ─── */
+.p-inputgroup .p-inputgroupaddon i {
+ font-size: ${dt('inputgroup.extend.iconSize')};
+}
+
+/* ─── Extra Large ─── */
+.p-inputgroup.p-inputgroup-xlg .p-inputgroupaddon {
+ font-size: ${dt('inputtext.extend.extXlg.fontSize')};
+ padding: ${dt('inputtext.extend.extXlg.paddingY')} ${dt('inputtext.extend.extXlg.paddingX')};
+}
+
+.p-inputgroup.p-inputgroup-xlg .p-inputgroupaddon i {
+ font-size: ${dt('inputtext.extend.extXlg.fontSize')};
+}
+`;
diff --git a/src/prime-preset/tokens/components/panelmenu.ts b/src/prime-preset/tokens/components/panelmenu.ts
new file mode 100644
index 00000000..0ef79947
--- /dev/null
+++ b/src/prime-preset/tokens/components/panelmenu.ts
@@ -0,0 +1,68 @@
+export const panelmenuCss = ({ dt }: { dt: (token: string) => string }): string => `
+ .p-panelmenu {
+ gap: ${dt('panelmenu.extend.extPanel.gap')};
+ }
+
+ .p-panelmenu-panel {
+ padding: ${dt('panelmenu.extend.extPanel.gap')};
+ }
+
+ .p-panelmenu-header-content,
+ .p-panelmenu-item-content {
+ font-size: ${dt('fonts.fontSize.300')};
+ }
+
+ .p-panelmenu-submenu-icon {
+ font-size: ${dt('panelmenu.extend.iconSize')};
+ }
+
+ /* ─── Active & Focused States ─── */
+
+ .p-panelmenu .p-panelmenu-item.p-panelmenu-item-active > .p-panelmenu-item-content,
+ .p-panelmenu .p-panelmenu-item.p-focus > .p-panelmenu-item-content,
+ .p-panelmenu .p-panelmenu-header.p-focus .p-panelmenu-header-content {
+ background: ${dt('panelmenu.extend.extItem.activeBackground')};
+ color: ${dt('panelmenu.extend.extItem.activeColor')};
+ }
+
+ .p-panelmenu .p-panelmenu-item.p-panelmenu-item-active > .p-panelmenu-item-content :is(.p-panelmenu-item-link, .p-panelmenu-item-label, .p-panelmenu-item-icon, .p-panelmenu-submenu-icon),
+ .p-panelmenu .p-panelmenu-item.p-focus > .p-panelmenu-item-content :is(.p-panelmenu-item-link, .p-panelmenu-item-label, .p-panelmenu-item-icon, .p-panelmenu-header-icon, .p-panelmenu-submenu-icon),
+ .p-panelmenu .p-panelmenu-header.p-focus .p-panelmenu-header-content :is(.p-panelmenu-header-link, .p-panelmenu-header-label, .p-panelmenu-submenu-icon, .p-panelmenu-item-icon, .p-panelmenu-header-icon) {
+ color: ${dt('panelmenu.extend.extItem.activeColor')};
+ }
+
+ /* ─── Hover on Active States ─── */
+
+ .p-panelmenu .p-panelmenu-item.p-panelmenu-item-active:not(.p-disabled) > .p-panelmenu-item-content:hover,
+ .p-panelmenu .p-panelmenu-item.p-focus:not(.p-disabled) > .p-panelmenu-item-content:hover,
+ .p-panelmenu .p-panelmenu-header.p-focus .p-panelmenu-header-content:hover {
+ background: ${dt('panelmenu.item.focusBackground')};
+ color: ${dt('panelmenu.item.focusColor')};
+ }
+
+ .p-panelmenu .p-panelmenu-item.p-panelmenu-item-active:not(.p-disabled) > .p-panelmenu-item-content:hover :is(.p-panelmenu-item-link, .p-panelmenu-item-label),
+ .p-panelmenu .p-panelmenu-item.p-focus:not(.p-disabled) > .p-panelmenu-item-content:hover :is(.p-panelmenu-item-link, .p-panelmenu-item-label),
+ .p-panelmenu .p-panelmenu-header.p-focus .p-panelmenu-header-content:hover :is(.p-panelmenu-header-link, .p-panelmenu-header-label) {
+ color: ${dt('panelmenu.item.focusColor')};
+ }
+
+ .p-panelmenu .p-panelmenu-item.p-panelmenu-item-active:not(.p-disabled) > .p-panelmenu-item-content:hover :is(.p-panelmenu-item-icon, .p-panelmenu-submenu-icon),
+ .p-panelmenu .p-panelmenu-item.p-focus:not(.p-disabled) > .p-panelmenu-item-content:hover :is(.p-panelmenu-item-icon, .p-panelmenu-submenu-icon),
+ .p-panelmenu .p-panelmenu-header.p-focus .p-panelmenu-header-content:hover :is(.p-panelmenu-submenu-icon, .p-panelmenu-item-icon) {
+ color: ${dt('panelmenu.item.icon.focusColor')};
+ }
+
+ /* ─── Captions ─── */
+
+ .p-panelmenu .panelmenu-item-label {
+ display: flex;
+ flex-direction: column;
+ gap: ${dt('panelmenu.extend.extItem.caption.gap')};
+ }
+
+ .p-panelmenu .panelmenu-item-caption {
+ font-size: ${dt('fonts.fontSize.200')};
+ line-height: ${dt('fonts.lineHeight.450')};
+ color: ${dt('panelmenu.extend.extItem.caption.color')};
+ }
+`;
diff --git a/src/stories/components/inputgroup/examples/inputgroup-addon-both.component.ts b/src/stories/components/inputgroup/examples/inputgroup-addon-both.component.ts
new file mode 100644
index 00000000..0fe1a713
--- /dev/null
+++ b/src/stories/components/inputgroup/examples/inputgroup-addon-both.component.ts
@@ -0,0 +1,63 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent } from '../../../../lib/components/inputgroup/input-group.component';
+import { InputGroupAddonComponent } from '../../../../lib/components/inputgroup/input-group-addon.component';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+const template = `
+
+
+
+
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-inputgroup-addon-both',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template,
+ styles,
+})
+export class InputGroupAddonBothComponent {
+ value = '';
+}
+
+export const AddonBoth: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Аддоны расположены с обеих сторон — например, иконка-префикс и кнопка поиска.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent, InputGroupAddonComponent, InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-inputgroup-addon-both',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template: \`
+
+
+
+
+
+ \`,
+})
+export class InputGroupAddonBothComponent {
+ value = '';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputgroup/examples/inputgroup-addon-right.component.ts b/src/stories/components/inputgroup/examples/inputgroup-addon-right.component.ts
new file mode 100644
index 00000000..1871478d
--- /dev/null
+++ b/src/stories/components/inputgroup/examples/inputgroup-addon-right.component.ts
@@ -0,0 +1,61 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent } from '../../../../lib/components/inputgroup/input-group.component';
+import { InputGroupAddonComponent } from '../../../../lib/components/inputgroup/input-group-addon.component';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+const template = `
+
+
+
+ руб.
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-inputgroup-addon-right',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template,
+ styles,
+})
+export class InputGroupAddonRightComponent {
+ value = '';
+}
+
+export const AddonRight: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Аддон расположен справа — используется для единиц измерения, валюты или суффиксов.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent, InputGroupAddonComponent, InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-inputgroup-addon-right',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template: \`
+
+
+ руб.
+
+ \`,
+})
+export class InputGroupAddonRightComponent {
+ value = '';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputgroup/examples/inputgroup-disabled.component.ts b/src/stories/components/inputgroup/examples/inputgroup-disabled.component.ts
new file mode 100644
index 00000000..a3a733ce
--- /dev/null
+++ b/src/stories/components/inputgroup/examples/inputgroup-disabled.component.ts
@@ -0,0 +1,61 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent } from '../../../../lib/components/inputgroup/input-group.component';
+import { InputGroupAddonComponent } from '../../../../lib/components/inputgroup/input-group-addon.component';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+const template = `
+
+
+
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-inputgroup-disabled',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template,
+ styles,
+})
+export class InputGroupDisabledComponent {
+ value = '';
+}
+
+export const Disabled: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Отключённое состояние — аддоны автоматически получают стили disabled вместе с полем ввода.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent, InputGroupAddonComponent, InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-inputgroup-disabled',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template: \`
+
+
+
+
+ \`,
+})
+export class InputGroupDisabledComponent {
+ value = '';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputgroup/examples/inputgroup-with-text.component.ts b/src/stories/components/inputgroup/examples/inputgroup-with-text.component.ts
new file mode 100644
index 00000000..3af11c41
--- /dev/null
+++ b/src/stories/components/inputgroup/examples/inputgroup-with-text.component.ts
@@ -0,0 +1,61 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent } from '../../../../lib/components/inputgroup/input-group.component';
+import { InputGroupAddonComponent } from '../../../../lib/components/inputgroup/input-group-addon.component';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+const template = `
+
+
+ @
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-inputgroup-with-text',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template,
+ styles,
+})
+export class InputGroupWithTextComponent {
+ value = '';
+}
+
+export const WithText: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'В качестве наполнения аддона можно использовать обычный текст — например, символ валюты или префикс.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent, InputGroupAddonComponent, InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-inputgroup-with-text',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template: \`
+
+ @
+
+
+ \`,
+})
+export class InputGroupWithTextComponent {
+ value = '';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputgroup/examples/inputgroup-xlarge.component.ts b/src/stories/components/inputgroup/examples/inputgroup-xlarge.component.ts
new file mode 100644
index 00000000..37643d5a
--- /dev/null
+++ b/src/stories/components/inputgroup/examples/inputgroup-xlarge.component.ts
@@ -0,0 +1,61 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent } from '../../../../lib/components/inputgroup/input-group.component';
+import { InputGroupAddonComponent } from '../../../../lib/components/inputgroup/input-group-addon.component';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+const template = `
+
+
+
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-inputgroup-xlarge',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template,
+ styles,
+})
+export class InputGroupXlargeComponent {
+ value = '';
+}
+
+export const XLarge: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Увеличенный размер группы ввода — для акцентных форм и поисковых строк.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent, InputGroupAddonComponent, InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-inputgroup-xlarge',
+ standalone: true,
+ imports: [InputGroupComponent, InputGroupAddonComponent, InputTextComponent, FormsModule],
+ template: \`
+
+
+
+
+ \`,
+})
+export class InputGroupXlargeComponent {
+ value = '';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputgroup/inputgroup.stories.ts b/src/stories/components/inputgroup/inputgroup.stories.ts
new file mode 100644
index 00000000..d833fc31
--- /dev/null
+++ b/src/stories/components/inputgroup/inputgroup.stories.ts
@@ -0,0 +1,92 @@
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
+import { FormsModule } from '@angular/forms';
+import { InputGroupComponent } from '../../../lib/components/inputgroup/input-group.component';
+import { InputGroupAddonComponent } from '../../../lib/components/inputgroup/input-group-addon.component';
+import { InputTextComponent } from '../../../lib/components/inputtext/inputtext.component';
+import { InputGroupWithTextComponent, WithText } from './examples/inputgroup-with-text.component';
+import { InputGroupDisabledComponent, Disabled } from './examples/inputgroup-disabled.component';
+import { InputGroupXlargeComponent, XLarge } from './examples/inputgroup-xlarge.component';
+import { InputGroupAddonRightComponent, AddonRight } from './examples/inputgroup-addon-right.component';
+import { InputGroupAddonBothComponent, AddonBoth } from './examples/inputgroup-addon-both.component';
+
+const meta: Meta = {
+ title: 'Components/Form/InputGroup',
+ component: InputGroupComponent,
+ tags: ['autodocs'],
+ decorators: [
+ moduleMetadata({
+ imports: [
+ InputGroupComponent,
+ InputGroupAddonComponent,
+ InputTextComponent,
+ FormsModule,
+ InputGroupWithTextComponent,
+ InputGroupDisabledComponent,
+ InputGroupXlargeComponent,
+ InputGroupAddonRightComponent,
+ InputGroupAddonBothComponent,
+ ],
+ }),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ component: `Группа полей ввода для объединения с аддонами (иконками или текстом).
+
+\`\`\`typescript
+import { InputGroupComponent, InputGroupAddonComponent } from '@cdek-it/angular-ui-kit';
+\`\`\``,
+ },
+ },
+ designTokens: { prefix: '--p-inputgroup' },
+ },
+ argTypes: {
+ size: {
+ control: 'select',
+ options: ['small', 'base', 'large', 'xlarge'],
+ description: 'Размер группы (влияет на паддинги и шрифты аддонов)',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: "'base'" },
+ type: { summary: "'small' | 'base' | 'large' | 'xlarge'" },
+ },
+ },
+ },
+ args: {
+ size: 'base',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ name: 'Default',
+ render: (args) => {
+ const groupParts: string[] = [];
+ if (args.size && args.size !== 'base') groupParts.push(`size="${args.size}"`);
+
+ const groupOpen = groupParts.length
+ ? ``
+ : ``;
+
+ const inputSize = args.size && args.size !== 'base' ? ` size="${args.size}"` : '';
+
+ const template = `
+${groupOpen}
+
+
+`;
+
+ return { props: { ...args, value: '' }, template };
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Базовый пример группы ввода с иконкой. Используйте Controls для изменения размера.',
+ },
+ },
+ },
+};
+
+export { WithText, AddonRight, AddonBoth, Disabled, XLarge };
diff --git a/src/stories/components/panelmenu/examples/panelmenu-basic.component.ts b/src/stories/components/panelmenu/examples/panelmenu-basic.component.ts
new file mode 100644
index 00000000..d19cafd4
--- /dev/null
+++ b/src/stories/components/panelmenu/examples/panelmenu-basic.component.ts
@@ -0,0 +1,93 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { MenuItem } from 'primeng/api';
+import { PanelMenuComponent } from '../../../../lib/components/panelmenu/panelmenu.component';
+
+const template = `
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-panelmenu-basic',
+ standalone: true,
+ imports: [PanelMenuComponent],
+ template,
+ styles,
+})
+export class PanelMenuBasicComponent {
+ items: MenuItem[] = [
+ {
+ label: 'Отправления',
+ items: [
+ { label: 'Новые' },
+ { label: 'В пути' },
+ { label: 'Доставленные' },
+ { label: 'Возвраты', items: [{ label: 'Ожидают' }, { label: 'Завершённые' }] },
+ ],
+ },
+ { label: 'Маршруты' },
+ {
+ label: 'Склады',
+ items: [
+ { label: 'Москва' },
+ { label: 'Новосибирск' },
+ { label: 'Екатеринбург' },
+ ],
+ },
+ { label: 'Настройки', disabled: true },
+ ];
+}
+
+export const Basic: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Базовое аккордеон-меню без иконок.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { MenuItem } from 'primeng/api';
+import { PanelMenuComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-panelmenu-basic',
+ standalone: true,
+ imports: [PanelMenuComponent],
+ template: \`
+
+ \`,
+})
+export class PanelMenuBasicComponent {
+ items: MenuItem[] = [
+ {
+ label: 'Отправления',
+ items: [
+ { label: 'Новые' },
+ { label: 'В пути' },
+ { label: 'Доставленные' },
+ { label: 'Возвраты', items: [{ label: 'Ожидают' }, { label: 'Завершённые' }] },
+ ],
+ },
+ { label: 'Маршруты' },
+ {
+ label: 'Склады',
+ items: [
+ { label: 'Москва' },
+ { label: 'Новосибирск' },
+ { label: 'Екатеринбург' },
+ ],
+ },
+ { label: 'Настройки', disabled: true },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/panelmenu/examples/panelmenu-custom.component.ts b/src/stories/components/panelmenu/examples/panelmenu-custom.component.ts
new file mode 100644
index 00000000..3a1ac663
--- /dev/null
+++ b/src/stories/components/panelmenu/examples/panelmenu-custom.component.ts
@@ -0,0 +1,134 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { MenuItem } from 'primeng/api';
+import { PanelMenu } from 'primeng/panelmenu';
+import { Badge } from 'primeng/badge';
+import { NgIf, NgClass } from '@angular/common';
+
+const template = `
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-panelmenu-custom',
+ standalone: true,
+ imports: [PanelMenu, Badge, NgIf, NgClass],
+ template,
+ styles,
+})
+export class PanelMenuCustomComponent {
+ items: MenuItem[] = [
+ {
+ label: 'Дашборд',
+ icon: 'ti ti-layout-dashboard',
+ description: 'Главная страница',
+ items: [
+ { label: 'Аналитика', icon: 'ti ti-chart-line', description: 'Аналитика данных' },
+ { label: 'Отчёты', icon: 'ti ti-file-analytics', description: 'Сводные отчёты' },
+ { label: 'Статистика', icon: 'ti ti-chart-bar', description: 'Показатели доставки' },
+ ],
+ },
+ {
+ label: 'Отправления',
+ icon: 'ti ti-package',
+ description: 'Управление заказами',
+ badge: 'New',
+ },
+ {
+ label: 'Склады',
+ icon: 'ti ti-building-warehouse',
+ description: 'Складское хранение',
+ items: [
+ { label: 'Документы', icon: 'ti ti-file-text', description: 'Накладные и акты' },
+ { label: 'Фото', icon: 'ti ti-photo', description: 'Фотофиксация грузов' },
+ ],
+ },
+ {
+ label: 'Настройки',
+ icon: 'ti ti-settings',
+ description: 'Параметры системы',
+ disabled: true,
+ },
+ ];
+}
+
+export const Custom: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Кастомный шаблон пункта меню с описанием и бейджем.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { MenuItem } from 'primeng/api';
+import { PanelMenu } from 'primeng/panelmenu';
+import { Badge } from 'primeng/badge';
+import { NgIf } from '@angular/common';
+
+@Component({
+ selector: 'app-panelmenu-custom',
+ standalone: true,
+ imports: [PanelMenu, Badge, NgIf],
+ template: \`
+
+
+
+
+
+ \`,
+})
+export class PanelMenuCustomComponent {
+ items: MenuItem[] = [
+ {
+ label: 'Дашборд',
+ icon: 'ti ti-layout-dashboard',
+ description: 'Главная страница',
+ items: [
+ { label: 'Аналитика', icon: 'ti ti-chart-line', description: 'Аналитика данных' },
+ { label: 'Отчёты', icon: 'ti ti-file-analytics', description: 'Сводные отчёты' },
+ ],
+ },
+ { label: 'Отправления', icon: 'ti ti-package', description: 'Управление заказами', badge: 'New' },
+ {
+ label: 'Склады',
+ icon: 'ti ti-building-warehouse',
+ description: 'Складское хранение',
+ items: [
+ { label: 'Документы', icon: 'ti ti-file-text', description: 'Накладные и акты' },
+ { label: 'Фото', icon: 'ti ti-photo', description: 'Фотофиксация грузов' },
+ ],
+ },
+ { label: 'Настройки', icon: 'ti ti-settings', description: 'Параметры системы', disabled: true },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/panelmenu/examples/panelmenu-multiple.component.ts b/src/stories/components/panelmenu/examples/panelmenu-multiple.component.ts
new file mode 100644
index 00000000..606aaa41
--- /dev/null
+++ b/src/stories/components/panelmenu/examples/panelmenu-multiple.component.ts
@@ -0,0 +1,97 @@
+import { Component } from '@angular/core';
+import { StoryObj } from '@storybook/angular';
+import { MenuItem } from 'primeng/api';
+import { PanelMenuComponent } from '../../../../lib/components/panelmenu/panelmenu.component';
+
+const template = `
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-panelmenu-multiple',
+ standalone: true,
+ imports: [PanelMenuComponent],
+ template,
+ styles,
+})
+export class PanelMenuMultipleComponent {
+ items: MenuItem[] = [
+ {
+ label: 'Отправления',
+ icon: 'ti ti-package',
+ items: [
+ { label: 'Новые', icon: 'ti ti-circle-plus' },
+ { label: 'В пути', icon: 'ti ti-truck' },
+ { label: 'Доставленные', icon: 'ti ti-circle-check' },
+ {
+ label: 'Возвраты',
+ icon: 'ti ti-arrow-back',
+ items: [{ label: 'Ожидают' }, { label: 'Завершённые' }],
+ },
+ ],
+ },
+ { label: 'Маршруты', icon: 'ti ti-route' },
+ {
+ label: 'Склады',
+ icon: 'ti ti-building-warehouse',
+ items: [
+ { label: 'Москва' },
+ { label: 'Новосибирск' },
+ { label: 'Екатеринбург' },
+ ],
+ },
+ { label: 'Настройки', icon: 'ti ti-settings', disabled: true },
+ ];
+}
+
+export const Multiple: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Несколько панелей могут быть раскрыты одновременно.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { MenuItem } from 'primeng/api';
+import { PanelMenuComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-panelmenu-multiple',
+ standalone: true,
+ imports: [PanelMenuComponent],
+ template: \`
+
+ \`,
+})
+export class PanelMenuMultipleComponent {
+ items: MenuItem[] = [
+ {
+ label: 'Отправления',
+ icon: 'ti ti-package',
+ items: [
+ { label: 'Новые', icon: 'ti ti-circle-plus' },
+ { label: 'В пути', icon: 'ti ti-truck' },
+ { label: 'Доставленные', icon: 'ti ti-circle-check' },
+ { label: 'Возвраты', icon: 'ti ti-arrow-back', items: [{ label: 'Ожидают' }, { label: 'Завершённые' }] },
+ ],
+ },
+ { label: 'Маршруты', icon: 'ti ti-route' },
+ {
+ label: 'Склады',
+ icon: 'ti ti-building-warehouse',
+ items: [{ label: 'Москва' }, { label: 'Новосибирск' }, { label: 'Екатеринбург' }],
+ },
+ { label: 'Настройки', icon: 'ti ti-settings', disabled: true },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/panelmenu/panelmenu.stories.ts b/src/stories/components/panelmenu/panelmenu.stories.ts
new file mode 100644
index 00000000..8a600d6d
--- /dev/null
+++ b/src/stories/components/panelmenu/panelmenu.stories.ts
@@ -0,0 +1,108 @@
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
+import { PanelMenuComponent } from '../../../lib/components/panelmenu/panelmenu.component';
+import { PanelMenuBasicComponent, Basic } from './examples/panelmenu-basic.component';
+import { PanelMenuMultipleComponent, Multiple } from './examples/panelmenu-multiple.component';
+import { PanelMenuCustomComponent, Custom } from './examples/panelmenu-custom.component';
+
+const meta: Meta = {
+ title: 'Components/Menu/PanelMenu',
+ component: PanelMenuComponent,
+ tags: ['autodocs'],
+ decorators: [
+ moduleMetadata({
+ imports: [
+ PanelMenuComponent,
+ PanelMenuBasicComponent,
+ PanelMenuMultipleComponent,
+ PanelMenuCustomComponent,
+ ],
+ }),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ component: `Аккордеон-меню с поддержкой вложенных подменю и раскрытием нескольких панелей.
+
+\`\`\`typescript
+import { PanelMenuComponent } from '@cdek-it/angular-ui-kit';
+\`\`\``,
+ },
+ },
+ designTokens: { prefix: '--p-panelmenu' },
+ },
+ argTypes: {
+ model: {
+ table: { disable: true },
+ },
+ multiple: {
+ control: 'boolean',
+ description: 'Разрешает одновременное раскрытие нескольких панелей',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ tabindex: {
+ control: 'number',
+ description: 'Порядок фокуса при навигации клавиатурой',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'undefined' },
+ type: { summary: 'number' },
+ },
+ },
+ },
+ args: {
+ multiple: false,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ── Default ───────────────────────────────────────────────────────────────────
+
+export const Default: Story = {
+ name: 'Default',
+ render: (args) => {
+ const parts: string[] = [`[model]="model"`];
+ if (args.multiple) parts.push(`[multiple]="true"`);
+ if (args.tabindex !== undefined) parts.push(`[tabindex]="${args.tabindex}"`);
+
+ const template = ``;
+
+ return {
+ props: {
+ ...args,
+ model: [
+ {
+ label: 'Отправления',
+ items: [
+ { label: 'Новые' },
+ { label: 'В пути' },
+ { label: 'Доставленные' },
+ ],
+ },
+ { label: 'Маршруты' },
+ {
+ label: 'Склады',
+ items: [{ label: 'Москва' }, { label: 'Новосибирск' }],
+ },
+ { label: 'Настройки', disabled: true },
+ ],
+ },
+ template,
+ };
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.',
+ },
+ },
+ },
+};
+
+// ── Re-exports from example components ────────────────────────────────────
+export { Basic, Multiple, Custom };