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
751 changes: 0 additions & 751 deletions .claude/CLAUDE-v1.6.md

This file was deleted.

58 changes: 58 additions & 0 deletions src/lib/components/tieredmenu/tieredmenu.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 { TieredMenu } from 'primeng/tieredmenu';
import { MenuItem } from 'primeng/api';

@Component({
selector: 'tieredmenu',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TieredMenu],
template: `
<p-tieredmenu
[model]="model"
[autoDisplay]="autoDisplay"
[tabindex]="tabindex"
></p-tieredmenu>
`,
})
export class TieredMenuComponent implements AfterViewChecked {
@Input() model: MenuItem[] = [];
@Input() autoDisplay = true;
@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;
const item = target.closest('.p-tieredmenu-item');
if (!item) return;

const hasSubmenu = item.querySelector(':scope > .p-tieredmenu-submenu, :scope > [class*="content-container"]');
if (hasSubmenu) 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-tieredmenu-item-checked')
.forEach(el => el.classList.remove('p-tieredmenu-item-checked'));

if (this.activeItemId) {
const active = root.querySelector<HTMLElement>(`#${CSS.escape(this.activeItemId)}`);
if (active) {
active.classList.add('p-tieredmenu-item-checked');
}
}
}
}
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 { tieredmenuCss } from './tokens/components/tieredmenu';
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,
},
tieredmenu: {
...(tokens.components.tieredmenu as unknown as ComponentsDesignTokens['tieredmenu']),
css: tieredmenuCss,
},
tooltip: {
...(tokens.components.tooltip as unknown as ComponentsDesignTokens['tooltip']),
css: tooltipCss,
Expand Down
52 changes: 52 additions & 0 deletions src/prime-preset/tokens/components/tieredmenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export const tieredmenuCss = ({ dt }: { dt: (token: string) => string }): string => `
.p-tieredmenu {
width: min-content;
}

.p-tieredmenu-item-content {
font-size: ${dt('fonts.fontSize.300')};
}

.p-tieredmenu-submenu-icon {
font-size: ${dt('tieredmenu.extend.iconSize')};
}

/* ─── Selected (checked) item ─── */

.p-tieredmenu .p-tieredmenu-item.p-tieredmenu-item-checked > .p-tieredmenu-item-content {
background: ${dt('tieredmenu.item.activeBackground')};
color: ${dt('tieredmenu.item.activeColor')};
}

.p-tieredmenu .p-tieredmenu-item.p-tieredmenu-item-checked > .p-tieredmenu-item-content :is(.p-tieredmenu-item-link, .p-tieredmenu-item-label, .p-tieredmenu-item-icon, .p-tieredmenu-submenu-icon) {
color: ${dt('tieredmenu.item.activeColor')};
}

/* ─── Hover on selected ─── */

.p-tieredmenu .p-tieredmenu-item.p-tieredmenu-item-checked:not(.p-disabled) > .p-tieredmenu-item-content:hover {
background: ${dt('tieredmenu.item.focusBackground')};
color: ${dt('tieredmenu.item.focusColor')};
}

.p-tieredmenu .p-tieredmenu-item.p-tieredmenu-item-checked:not(.p-disabled) > .p-tieredmenu-item-content:hover :is(.p-tieredmenu-item-link, .p-tieredmenu-item-label) {
color: ${dt('tieredmenu.item.focusColor')};
}

.p-tieredmenu .p-tieredmenu-item.p-tieredmenu-item-checked:not(.p-disabled) > .p-tieredmenu-item-content:hover :is(.p-tieredmenu-item-icon, .p-tieredmenu-submenu-icon) {
color: ${dt('tieredmenu.item.icon.focusColor')};
}

/* ─── Captions ─── */

.p-tieredmenu .p-tieredmenu-item-caption {
display: flex;
flex-direction: column;
gap: ${dt('tieredmenu.extend.extItem.caption.gap')};
}

.p-tieredmenu .p-tieredmenu-item-caption-text {
font-size: ${dt('fonts.fontSize.200')};
color: ${dt('tieredmenu.extend.extItem.caption.color')};
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Component } from '@angular/core';
import { StoryObj } from '@storybook/angular';
import { MenuItem } from 'primeng/api';
import { TieredMenuComponent } from '../../../../lib/components/tieredmenu/tieredmenu.component';
import { basicItems } from '../tieredmenu.data';

const template = `
<div class="bg-surface-ground" style="min-height: 280px">
<tieredmenu [model]="items"></tieredmenu>
</div>
`;
const styles = '';

@Component({
selector: 'app-tieredmenu-basic',
standalone: true,
imports: [TieredMenuComponent],
template,
styles,
})
export class TieredMenuBasicComponent {
items: MenuItem[] = basicItems;
}

export const Basic: StoryObj = {
render: () => ({
template: `<app-tieredmenu-basic></app-tieredmenu-basic>`,
}),
parameters: {
docs: {
description: { story: 'Базовое иерархическое меню с вложенными подменю.' },
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { MenuItem } from 'primeng/api';
import { TieredMenuComponent } from '@cdek-it/angular-ui-kit';

@Component({
selector: 'app-tieredmenu-basic',
standalone: true,
imports: [TieredMenuComponent],
template: \`
<tieredmenu [model]="items"></tieredmenu>
\`,
})
export class TieredMenuBasicComponent {
items: MenuItem[] = [
{
label: 'Отправления',
icon: 'ti ti-package',
items: [
{ label: 'Новые' },
{ label: 'В обработке' },
{ label: 'Доставленные' },
{ label: 'Возвраты', items: [{ label: 'Ожидают' }, { label: 'Завершённые' }] },
],
},
{ label: 'Маршруты' },
{
label: 'Склады',
items: [{ label: 'Москва' }, { label: 'Новосибирск' }, { label: 'Екатеринбург' }],
},
{ label: 'Настройки', icon: 'ti ti-settings', disabled: true },
];
}
`,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Component } from '@angular/core';
import { StoryObj } from '@storybook/angular';
import { MenuItem } from 'primeng/api';
import { TieredMenu } from 'primeng/tieredmenu';
import { Badge } from 'primeng/badge';
import { NgIf } from '@angular/common';

const template = `
<div class="bg-surface-ground" style="min-height: 280px">
<p-tieredmenu [model]="items">
<ng-template #item let-item let-hasSubmenu="hasSubmenu">
<a class="p-tieredmenu-item-link flex items-center gap-2 w-full">
<span *ngIf="item.icon" [class]="'p-tieredmenu-item-icon ' + item.icon"></span>
<div class="p-tieredmenu-item-caption flex-1">
<span class="p-tieredmenu-item-label">{{ item.label }}</span>
<small *ngIf="item['description']" class="p-tieredmenu-item-caption-text">{{ item['description'] }}</small>
</div>
<p-badge *ngIf="item['badge']" [value]="item['badge']"></p-badge>
<span *ngIf="hasSubmenu" class="p-tieredmenu-submenu-icon ti ti-chevron-right"></span>
</a>
</ng-template>
</p-tieredmenu>
</div>
`;
const styles = '';

@Component({
selector: 'app-tieredmenu-custom',
standalone: true,
imports: [TieredMenu, Badge, NgIf],
template,
styles,
})
export class TieredMenuCustomComponent {
items: MenuItem[] = [
{
label: 'Дашборд',
icon: 'ti ti-home',
description: 'Перейти на главную',
},
{
label: 'Отправления',
icon: 'ti ti-package',
description: 'Управление заказами',
badge: 'New',
items: [
{ label: 'Активные', icon: 'ti ti-circle-check', description: 'Текущие заказы' },
{ label: 'Архив', icon: 'ti ti-archive', description: 'Завершённые заказы' },
],
},
{
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-tieredmenu-custom></app-tieredmenu-custom>`,
}),
parameters: {
docs: {
description: { story: 'Кастомный шаблон пункта меню с описанием и бейджем.' },
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { MenuItem } from 'primeng/api';
import { TieredMenu } from 'primeng/tieredmenu';
import { Badge } from 'primeng/badge';
import { NgIf } from '@angular/common';

@Component({
selector: 'app-tieredmenu-custom',
standalone: true,
imports: [TieredMenu, Badge, NgIf],
template: \`
<p-tieredmenu [model]="items">
<ng-template #item let-item let-hasSubmenu="hasSubmenu">
<a class="p-tieredmenu-item-link flex items-center gap-2 w-full">
<span *ngIf="item.icon" [class]="'p-tieredmenu-item-icon ' + item.icon"></span>
<div class="p-tieredmenu-item-caption flex-1">
<span class="p-tieredmenu-item-label">{{ item.label }}</span>
<small *ngIf="item['description']" class="p-tieredmenu-item-caption-text">{{ item['description'] }}</small>
</div>
<p-badge *ngIf="item['badge']" [value]="item['badge']"></p-badge>
<span *ngIf="hasSubmenu" class="p-tieredmenu-submenu-icon ti ti-chevron-right"></span>
</a>
</ng-template>
</p-tieredmenu>
\`,
})
export class TieredMenuCustomComponent {
items: MenuItem[] = [
{ label: 'Дашборд', icon: 'ti ti-home', description: 'Перейти на главную' },
{ label: 'Отправления', icon: 'ti ti-package', description: 'Управление заказами', badge: 'New',
items: [
{ label: 'Активные', icon: 'ti ti-circle-check', description: 'Текущие заказы' },
{ label: 'Архив', icon: 'ti ti-archive', description: 'Завершённые заказы' },
],
},
{ label: 'Настройки', icon: 'ti ti-settings', description: 'Параметры системы', disabled: true },
];
}
`,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Component } from '@angular/core';
import { StoryObj } from '@storybook/angular';
import { MenuItem } from 'primeng/api';
import { TieredMenuComponent } from '../../../../lib/components/tieredmenu/tieredmenu.component';

const template = `
<div class="bg-surface-ground" style="min-height: 280px">
<tieredmenu [model]="items"></tieredmenu>
</div>
`;
const styles = '';

@Component({
selector: 'app-tieredmenu-selected',
standalone: true,
imports: [TieredMenuComponent],
template,
styles,
})
export class TieredMenuSelectedComponent {
items: MenuItem[] = [
{ label: 'Отправления', icon: 'ti ti-package' },
{ label: 'Маршруты', icon: 'ti ti-route' },
{ label: 'Склады', icon: 'ti ti-building-warehouse' },
];
}

export const WithSelected: StoryObj = {
render: () => ({
template: `<app-tieredmenu-selected></app-tieredmenu-selected>`,
}),
parameters: {
docs: {
description: { story: 'При клике на пункт меню он визуально выделяется как активный.' },
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { MenuItem } from 'primeng/api';
import { TieredMenuComponent } from '@cdek-it/angular-ui-kit';

@Component({
selector: 'app-tieredmenu-selected',
standalone: true,
imports: [TieredMenuComponent],
template: \`
<tieredmenu [model]="items"></tieredmenu>
\`,
})
export class TieredMenuSelectedComponent {
items: MenuItem[] = [
{ label: 'Отправления', icon: 'ti ti-package' },
{ label: 'Маршруты', icon: 'ti ti-route' },
{ label: 'Склады', icon: 'ti ti-building-warehouse' },
];
}
`,
},
},
},
};
Loading
Loading