Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
92e91fc
inputotp: стилизация, сторисы, обёртки
Apr 17, 2026
763bb6d
фикс размера xlarge
Apr 17, 2026
2766472
password: стилизация, сторисы, обёртик
Apr 20, 2026
6256c6d
добавление размеров для компонента
Apr 20, 2026
25a7cfe
Merge branch 'form.inputtext' into form.password
khaliulin Apr 27, 2026
7591ddb
password: цвет meter-text на text.mutedColor, focus box-shadow, inval…
Apr 27, 2026
2707e74
password: floatLabel и label props с p-floatlabel обёрткой
Apr 27, 2026
ec63673
password: стори FloatLabel через обёртку, argTypes floatLabel и label
Apr 27, 2026
21de785
подключение файла стилей для компонентом password
Apr 27, 2026
e1ea9fe
Merge branch 'form.inputtext' into form.inputotp
khaliulin Apr 28, 2026
fcf206a
фикс превью сторисов; доработка template
Apr 28, 2026
a571d3c
фикс применения пропса floatLabel
Apr 28, 2026
f81dcbb
formControl
Apr 30, 2026
c804cb6
setDisabledState(true) вызывается при disabled FormControl
May 4, 2026
458bb99
Merge pull request #58 from cdek-it/form.InputMask
ichiesov May 4, 2026
83afd47
Merge branch 'form.inputtext' into form.inputotp
khaliulin May 4, 2026
7f09939
ngModel заменён на formControl; Code snippets — оформлены как полный …
May 4, 2026
2925824
~fix(input-otp): add missing import.
ichiesov May 4, 2026
c3c0931
argTypes: readonly, tabindex, autofocus отображаются на странице Docs
May 4, 2026
65ddc76
Merge pull request #53 from cdek-it/form.inputotp
ichiesov May 4, 2026
bf7242d
Merge branch 'form.inputtext' into form.password
khaliulin May 4, 2026
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
108 changes: 108 additions & 0 deletions src/lib/components/inputotp/inputotp.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Component, DestroyRef, forwardRef, inject, Injector, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgClass } from '@angular/common';
import { InputOtp, InputOtpChangeEvent } from 'primeng/inputotp';

export type InputOtpSize = 'small' | 'base' | 'large' | 'xlarge';

@Component({
selector: 'input-otp',
standalone: true,
imports: [InputOtp, ReactiveFormsModule, NgClass],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputOtpComponent),
multi: true,
},
],
template: `
<p-inputotp
[length]="length"
[mask]="mask"
[integerOnly]="integerOnly"
[disabled]="disabled"
[readonly]="readonly"
[invalid]="invalid"
[size]="primeSize"
[ngClass]="sizeClass"
[tabindex]="tabindex"
[autofocus]="autofocus"
[formControl]="control"
(onChange)="onChangeHandler($event)"
(onFocus)="onFocus.emit($event)"
(onBlur)="onBlur.emit($event)"
></p-inputotp>
`,
})
export class InputOtpComponent implements ControlValueAccessor, OnInit {
private readonly _injector = inject(Injector);
private readonly destroyRef = inject(DestroyRef);
private _ngControl: NgControl | null = null;

readonly control = new FormControl<any>(null);

@Input() length = 4;
@Input() mask = false;
@Input() integerOnly = false;
@Input() readonly = false;
@Input() size: InputOtpSize = 'base';
@Input() tabindex: number | null = null;
@Input() autofocus = false;

disabled = false;

@Output() onChange = new EventEmitter<InputOtpChangeEvent>();
@Output() onFocus = new EventEmitter<Event>();
@Output() onBlur = new EventEmitter<Event>();

private _onChange: (value: any) => void = () => {};
private _onTouched: () => void = () => {};

ngOnInit(): void {
this._ngControl = this._injector.get(NgControl, null, { self: true, optional: true });

this.control.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(v => {
this._onChange(v);
this._onTouched();
});
}

get invalid(): boolean {
return this._ngControl?.invalid ?? false;
}

get primeSize(): 'small' | 'large' | undefined {
if (this.size === 'small') return 'small';
if (this.size === 'large' || this.size === 'xlarge') return 'large';
return undefined;
}

get sizeClass(): Record<string, boolean> {
return { 'p-inputotp-xlg': this.size === 'xlarge' };
}

onChangeHandler(event: InputOtpChangeEvent): void {
this.onChange.emit(event);
}

writeValue(value: any): void {
this.control.setValue(value ?? null, { emitEvent: false });
}

registerOnChange(fn: (value: any) => void): void {
this._onChange = fn;
}

registerOnTouched(fn: () => void): void {
this._onTouched = fn;
}

setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
isDisabled ? this.control.disable({ emitEvent: false }) : this.control.enable({ emitEvent: false });
}
}
136 changes: 136 additions & 0 deletions src/lib/components/password/password.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output, TemplateRef, forwardRef } from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgTemplateOutlet } from '@angular/common';
import { Password } from 'primeng/password';
import { PrimeTemplate } from 'primeng/api';
import { FloatLabel } from 'primeng/floatlabel';

export type PasswordSize = 'small' | 'base' | 'large' | 'xlarge';

@Component({
selector: 'password',
host: { style: 'display: block' },
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [Password, FormsModule, FloatLabel, NgTemplateOutlet, PrimeTemplate],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => PasswordComponent),
multi: true,
},
],
template: `
@if (floatLabel) {
<p-floatlabel variant="in">
<ng-container *ngTemplateOutlet="passwordTpl"></ng-container>
<label [attr.for]="inputId">{{ label }}</label>
</p-floatlabel>
} @else {
<ng-container *ngTemplateOutlet="passwordTpl"></ng-container>
}

<ng-template #passwordTpl>
<p-password
[ngModel]="modelValue"
(ngModelChange)="handleChange($event)"
[feedback]="feedback"
[toggleMask]="toggleMask"
[inputStyleClass]="computedInputStyleClass"
[disabled]="disabled"
[placeholder]="placeholder"
[promptLabel]="promptLabel"
[weakLabel]="weakLabel"
[mediumLabel]="mediumLabel"
[strongLabel]="strongLabel"
[variant]="variant"
[fluid]="fluid"
[invalid]="invalid"
[inputId]="inputId"
[ariaLabel]="ariaLabel"
[ariaLabelledBy]="ariaLabelledBy"
[appendTo]="appendTo"
[autofocus]="autofocus"
(onFocus)="onFocus.emit($event)"
(onBlur)="onBlur.emit($event)"
>
@if (headerTemplate) {
<ng-template pTemplate="header">
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</ng-template>
}
@if (footerTemplate) {
<ng-template pTemplate="footer">
<ng-container *ngTemplateOutlet="footerTemplate"></ng-container>
</ng-template>
}
</p-password>
</ng-template>
`,
})
export class PasswordComponent implements ControlValueAccessor {
@ContentChild('header') headerTemplate: TemplateRef<any> | null = null;
@ContentChild('footer') footerTemplate: TemplateRef<any> | null = null;

@Input() feedback = true;
@Input() toggleMask = false;
@Input() disabled = false;
@Input() placeholder: string | undefined = undefined;
@Input() size: PasswordSize = 'base';
@Input() variant: 'filled' | 'outlined' = 'outlined';
@Input() fluid = false;
@Input() invalid = false;
@Input() floatLabel = false;
@Input() label = '';
@Input() promptLabel = 'Введите пароль';
@Input() weakLabel = 'Слабый';
@Input() mediumLabel = 'Средний';
@Input() strongLabel = 'Надёжный';
@Input() inputId: string | undefined = undefined;
@Input() inputStyleClass: string | undefined = undefined;
@Input() ariaLabel: string | undefined = undefined;
@Input() ariaLabelledBy: string | undefined = undefined;
@Input() appendTo: any = 'body';
@Input() autofocus = false;

@Output() onFocus = new EventEmitter<Event>();
@Output() onBlur = new EventEmitter<Event>();

get sizeClass(): string {
if (this.size === 'small') return 'p-inputtext-sm';
if (this.size === 'large') return 'p-inputtext-lg';
if (this.size === 'xlarge') return 'p-inputtext-lg p-inputtext-xlg';
return '';
}

get computedInputStyleClass(): string {
return [this.sizeClass, this.inputStyleClass].filter(Boolean).join(' ');
}

modelValue: string | null = null;

private _onChange: (value: string | null) => void = () => {};
private _onTouched: () => void = () => {};

handleChange(value: string | null): void {
this.modelValue = value;
this._onChange(value);
this._onTouched();
}

writeValue(value: string | null): void {
this.modelValue = value;
}

registerOnChange(fn: (value: string | null) => void): void {
this._onChange = fn;
}

registerOnTouched(fn: () => void): void {
this._onTouched = fn;
}

setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
14 changes: 10 additions & 4 deletions src/prime-preset/map-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { checkboxCss } from './tokens/components/checkbox';
import { inputmaskCss } from './tokens/components/inputmask';
import { inputtextCss } from './tokens/components/inputtext';
import { progressspinnerCss } from './tokens/components/progressspinner';
import { passwordCss } from './tokens/components/password';
import { tagCss } from './tokens/components/tag';
import { timelineCss } from './tokens/components/timeline';
import { textareaCss } from './tokens/components/textarea';
import { tooltipCss } from './tokens/components/tooltip';
import { megamenuCss } from './tokens/components/megamenu';
import { selectCss } from './tokens/components/select';
import { messageCss } from './tokens/components/message';
import { inputotpCss } from './tokens/components/inputotp';

const presetTokens: Preset<AuraBaseDesignTokens> = {
primitive: tokens.primitive as unknown as AuraBaseDesignTokens['primitive'],
Expand Down Expand Up @@ -46,6 +48,10 @@ const presetTokens: Preset<AuraBaseDesignTokens> = {
...(tokens.components.progressspinner as unknown as ComponentsDesignTokens['progressspinner']),
css: progressspinnerCss,
},
inputotp: {
...(tokens.components.inputotp as unknown as ComponentsDesignTokens['inputotp']),
css: inputotpCss,
},
inputtext: {
...(tokens.components.inputtext as unknown as ComponentsDesignTokens['inputtext']),
css: inputtextCss,
Expand All @@ -57,9 +63,9 @@ const presetTokens: Preset<AuraBaseDesignTokens> = {
...(tokens.components.tag as unknown as ComponentsDesignTokens['tag']),
css: tagCss,
},
timeline: {
...(tokens.components.timeline as unknown as ComponentsDesignTokens['timeline']),
css: timelineCss,
textarea: {
...(tokens.components.textarea as unknown as ComponentsDesignTokens['textarea']),
css: textareaCss,
},
tooltip: {
...(tokens.components.tooltip as unknown as ComponentsDesignTokens['tooltip']),
Expand Down
51 changes: 51 additions & 0 deletions src/prime-preset/tokens/components/inputotp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export const inputotpCss = ({ dt }: { dt: (token: string) => string }): string => `
/* Стили границы */
.p-inputotp.p-component .p-inputtext {
border-width: ${dt('inputotp.extend.borderWidth')};
padding-inline: 0;
}

/* ─── Disabled ─── */
.p-inputotp.p-component .p-inputtext:disabled {
background: ${dt('inputtext.root.disabledBackground')};
color: ${dt('inputtext.root.disabledColor')};
}

/* ─── Readonly ─── */
.p-inputotp.p-component .p-inputtext:enabled:read-only {
background: ${dt('inputtext.extend.readonlyBackground')};
color: ${dt('inputtext.root.color')};
}

/* ─── Focus ─── */
.p-inputotp.p-component .p-inputtext:enabled:focus {
box-shadow: 0 0 0 ${dt('inputtext.focusRing.width')} ${dt('inputtext.focusRing.color')};
}

/* ─── Invalid + Focus ─── */
.p-inputotp.p-component .p-inputtext.p-invalid:focus {
border-color: ${dt('inputtext.root.invalidBorderColor')};
box-shadow: 0 0 0 ${dt('inputtext.focusRing.width')} ${dt('focusRing.extend.invalid')};
}

/* ─── Small ─── */
.p-inputotp.p-component .p-inputtext.p-inputtext-sm {
padding-block: ${dt('inputtext.root.sm.paddingY')};
}

/* ─── Base ─── */
.p-inputotp.p-component .p-inputtext:not(.p-inputtext-sm):not(.p-inputtext-lg):not(.p-inputtext-xlg) {
padding-block: ${dt('inputtext.root.paddingY')};
}

/* ─── Large ─── */
.p-inputotp.p-component .p-inputtext.p-inputtext-lg {
padding-block: ${dt('inputtext.root.lg.paddingY')};
}

/* ─── Extra Large ─── */
.p-inputotp.p-component.p-inputotp-xlg .p-inputtext {
font-size: ${dt('inputtext.extend.extXlg.fontSize')};
padding-block: ${dt('inputtext.extend.extXlg.paddingY')};
}
`;
Loading
Loading