Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/lib/components/panelmenu/panelmenu.component.ts
Original file line number Diff line number Diff line change
@@ -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: `
<p-panelmenu
[model]="model"
[multiple]="multiple"
[tabindex]="tabindex"
></p-panelmenu>
`,
})
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<HTMLElement>) {}

@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<HTMLElement>('.p-panelmenu-item-active')
.forEach(el => el.classList.remove('p-panelmenu-item-active'));

if (this.activeItemId) {
const active = root.querySelector<HTMLElement>(`#${CSS.escape(this.activeItemId)}`);
if (active) {
active.classList.add('p-panelmenu-item-active');
}
}
}
}
68 changes: 68 additions & 0 deletions src/prime-preset/tokens/components/panelmenu.ts
Original file line number Diff line number Diff line change
@@ -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')};
}
`;
Original file line number Diff line number Diff line change
@@ -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 = `
<div class="bg-surface-ground" style="width: 280px">
<panelmenu [model]="items"></panelmenu>
</div>
`;
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: `<app-panelmenu-basic></app-panelmenu-basic>`,
}),
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: \`
<panelmenu [model]="items"></panelmenu>
\`,
})
export class PanelMenuBasicComponent {
items: MenuItem[] = [
{
label: 'Отправления',
items: [
{ label: 'Новые' },
{ label: 'В пути' },
{ label: 'Доставленные' },
{ label: 'Возвраты', items: [{ label: 'Ожидают' }, { label: 'Завершённые' }] },
],
},
{ label: 'Маршруты' },
{
label: 'Склады',
items: [
{ label: 'Москва' },
{ label: 'Новосибирск' },
{ label: 'Екатеринбург' },
],
},
{ label: 'Настройки', disabled: true },
];
}
`,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -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 = `
<div class="bg-surface-ground" style="width: 300px">
<p-panelmenu [model]="items" [multiple]="true">
<ng-template #item let-item let-props="props" let-hasSubmenu="hasSubmenu">
<a [attr.href]="item.url" [attr.target]="item.target" v-bind="props?.action" class="p-panelmenu-item-link flex items-center gap-2 w-full">
<span *ngIf="item.icon" [class]="'p-panelmenu-item-icon ' + item.icon"></span>
<div class="panelmenu-item-label flex-1">
<span class="p-panelmenu-item-label">{{ item.label }}</span>
<small *ngIf="item['description']" class="panelmenu-item-caption">{{ item['description'] }}</small>
</div>
<p-badge *ngIf="item['badge']" [value]="item['badge']"></p-badge>
<span *ngIf="hasSubmenu" class="p-panelmenu-submenu-icon ti ti-chevron-right"></span>
</a>
</ng-template>
</p-panelmenu>
</div>
`;
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: `<app-panelmenu-custom></app-panelmenu-custom>`,
}),
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: \`
<p-panelmenu [model]="items" [multiple]="true">
<ng-template #item let-item let-props="props" let-hasSubmenu="hasSubmenu">
<a class="p-panelmenu-item-link flex items-center gap-2 w-full">
<span *ngIf="item.icon" [class]="'p-panelmenu-item-icon ' + item.icon"></span>
<div class="panelmenu-item-label flex-1">
<span class="p-panelmenu-item-label">{{ item.label }}</span>
<small *ngIf="item['description']" class="panelmenu-item-caption">{{ item['description'] }}</small>
</div>
<p-badge *ngIf="item['badge']" [value]="item['badge']"></p-badge>
<span *ngIf="hasSubmenu" class="p-panelmenu-submenu-icon ti ti-chevron-right"></span>
</a>
</ng-template>
</p-panelmenu>
\`,
})
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 },
];
}
`,
},
},
},
};
Loading
Loading