Skip to content
Closed
14 changes: 14 additions & 0 deletions src/lib/components/inputgroup/input-group-addon.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { InputGroupAddon } from 'primeng/inputgroupaddon';

@Component({
selector: 'input-group-addon',
standalone: true,
imports: [InputGroupAddon],
template: `
<p-inputgroup-addon>
<ng-content></ng-content>
</p-inputgroup-addon>
`,
})
export class InputGroupAddonComponent {}
24 changes: 24 additions & 0 deletions src/lib/components/inputgroup/input-group.component.ts
Original file line number Diff line number Diff line change
@@ -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: `
<p-inputgroup [ngClass]="sizeClass">
<ng-content></ng-content>
</p-inputgroup>
`,
})
export class InputGroupComponent {
@Input() size: InputGroupSize = 'base';

get sizeClass(): string {
if (this.size === 'xlarge') return 'p-inputgroup-xlg';
return '';
}
}
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');
}
}
}
}
1 change: 1 addition & 0 deletions src/prime-preset/map-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
94 changes: 94 additions & 0 deletions src/prime-preset/tokens/components/inputgroup.ts
Original file line number Diff line number Diff line change
@@ -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')};
}
`;
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,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 = `
<div class="bg-surface-ground p-4">
<input-group>
<input-group-addon><i class="ti ti-map-pin"></i></input-group-addon>
<input-text placeholder="Введите адрес..." [(ngModel)]="value" [fluid]="true"></input-text>
<input-group-addon><i class="ti ti-search"></i></input-group-addon>
</input-group>
</div>
`;
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: `<app-inputgroup-addon-both></app-inputgroup-addon-both>`,
}),
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: \`
<input-group>
<input-group-addon><i class="ti ti-map-pin"></i></input-group-addon>
<input-text placeholder="Введите адрес..." [(ngModel)]="value" [fluid]="true"></input-text>
<input-group-addon><i class="ti ti-search"></i></input-group-addon>
</input-group>
\`,
})
export class InputGroupAddonBothComponent {
value = '';
}
`,
},
},
},
};
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 { 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 = `
<div class="bg-surface-ground p-4">
<input-group>
<input-text placeholder="Сумма" [(ngModel)]="value" [fluid]="true"></input-text>
<input-group-addon>руб.</input-group-addon>
</input-group>
</div>
`;
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: `<app-inputgroup-addon-right></app-inputgroup-addon-right>`,
}),
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: \`
<input-group>
<input-text placeholder="Сумма" [(ngModel)]="value" [fluid]="true"></input-text>
<input-group-addon>руб.</input-group-addon>
</input-group>
\`,
})
export class InputGroupAddonRightComponent {
value = '';
}
`,
},
},
},
};
Loading
Loading