Skip to content
Open
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');
}
}
}
}
5 changes: 5 additions & 0 deletions src/prime-preset/map-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AuraBaseDesignTokens } from '@primeuix/themes/aura/base';
import tokens from './tokens/tokens.json';
import { avatarCss } from './tokens/components/avatar';
import { buttonCss } from './tokens/components/button';
import { panelmenuCss } from './tokens/components/panelmenu';
import { tooltipCss } from './tokens/components/tooltip';

const presetTokens: Preset<AuraBaseDesignTokens> = {
Expand All @@ -20,6 +21,10 @@ const presetTokens: Preset<AuraBaseDesignTokens> = {
...(tokens.components.button as unknown as ComponentsDesignTokens['button']),
css: buttonCss,
},
panelmenu: {
...(tokens.components.panelmenu as unknown as ComponentsDesignTokens['panelmenu']),
css: panelmenuCss,
},
tooltip: {
...(tokens.components.tooltip as unknown as ComponentsDesignTokens['tooltip']),
css: tooltipCss,
Expand Down
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