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: \` + + + + +
+ {{ item.label }} + {{ item['description'] }} +
+ + +
+
+
+ \`, +}) +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 };