Skip to content
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ src/assets/components/themes

.claude/*

.playwright-mcp/*
.playwright-mcp/*
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Project Rules

Основные правила и запреты — в `.claude/skills/generate-component/references/red-lines.md`.
107 changes: 107 additions & 0 deletions src/lib/components/inputmask/inputmask.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ChangeDetectionStrategy, Component, DestroyRef, inject, Injector, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { ControlValueAccessor, FormControl, NgControl, ReactiveFormsModule } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { InputMask } from 'primeng/inputmask';

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


@Component({
selector: 'input-mask',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [InputMask, ReactiveFormsModule],
host: {
style: 'display: block',
'[class.input-mask-xlg]': 'size === "xlarge"',
},
template: `
<p-inputmask
[size]="primeSize"
[mask]="mask"
[slotChar]="slotChar"
[autoClear]="autoClear"
[showClear]="showClear"
[unmask]="unmask"
[readonly]="readonly"
[placeholder]="placeholder"
[fluid]="fluid"

[characterPattern]="characterPattern"
[keepBuffer]="keepBuffer"
[invalid]="invalid"
[autocomplete]="autocomplete"
[formControl]="control"
(onComplete)="onComplete.emit($event)"
(onFocus)="onFocusEvent.emit($event)"
(onBlur)="onBlurEvent.emit($event)"
(onInput)="onInputEvent.emit($event)"
(onKeydown)="onKeydownEvent.emit($event)"
(onClear)="onClearEvent.emit($event)"
></p-inputmask>
`,
})
export class InputMaskComponent implements ControlValueAccessor, OnInit {
private readonly _injector = inject(Injector);
private readonly destroyRef = inject(DestroyRef);
private _ngControl: NgControl | null = null;

readonly control = new FormControl<string | null>(null);

@Input() mask = '';
@Input() slotChar = '_';
@Input() autoClear = true;
@Input() showClear = false;
@Input() unmask = false;
@Input() placeholder = '';
@Input() size: InputMaskSize = 'base';
@Input() readonly = false;
@Input() fluid = false;
@Input() characterPattern = '[A-Za-z]';
@Input() keepBuffer = false;
@Input() autocomplete = '';

@Output() onComplete = new EventEmitter<void>();
@Output() onFocusEvent = new EventEmitter<Event>();
@Output() onBlurEvent = new EventEmitter<Event>();
@Output() onInputEvent = new EventEmitter<Event>();
@Output() onKeydownEvent = new EventEmitter<Event>();
@Output() onClearEvent = new EventEmitter<void>();

private _onChange: (value: string | null) => 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));
}

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;
}

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

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

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

setDisabledState(isDisabled: boolean): void {
isDisabled ? this.control.disable({ emitEvent: false }) : this.control.enable({ emitEvent: false });
}
}
5 changes: 5 additions & 0 deletions src/prime-preset/map-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { avatarCss } from './tokens/components/avatar';
import { buttonCss } from './tokens/components/button';
import { cardCss } from './tokens/components/card';
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 { tagCss } from './tokens/components/tag';
import { timelineCss } from './tokens/components/timeline';
import { tooltipCss } from './tokens/components/tooltip';
import { megamenuCss } from './tokens/components/megamenu';
import { selectCss } from './tokens/components/select';
import { messageCss } from './tokens/components/message';

const presetTokens: Preset<AuraBaseDesignTokens> = {
primitive: tokens.primitive as unknown as AuraBaseDesignTokens['primitive'],
Expand Down Expand Up @@ -48,6 +50,9 @@ const presetTokens: Preset<AuraBaseDesignTokens> = {
...(tokens.components.inputtext as unknown as ComponentsDesignTokens['inputtext']),
css: inputtextCss,
},
inputmask: {
css: inputmaskCss,
},
tag: {
...(tokens.components.tag as unknown as ComponentsDesignTokens['tag']),
css: tagCss,
Expand Down
8 changes: 8 additions & 0 deletions src/prime-preset/tokens/components/inputmask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const inputmaskCss = ({ dt }: { dt: (token: string) => string }): string => `

/* ─── Sizes ─── */
input-mask.input-mask-xlg .p-inputtext {
font-size: ${dt('inputtext.extend.extXlg.fontSize')};
padding: ${dt('inputtext.extend.extXlg.paddingY')} ${dt('inputtext.extend.extXlg.paddingX')};
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { StoryObj } from '@storybook/angular';
import { InputMaskComponent } from '../../../../lib/components/inputmask/inputmask.component';

export const Disabled: StoryObj = {
name: 'Disabled',
render: (args) => {
const control = new FormControl({ value: '12-34-56', disabled: true });
return {
props: { ...args, control },
template: `<input-mask mask="99-99-99" placeholder="99-99-99" [formControl]="control"></input-mask>`,
};
},
decorators: [
(story: any) => ({
...story(),
moduleMetadata: {
imports: [InputMaskComponent, ReactiveFormsModule],
},
}),
],
parameters: {
controls: { disable: true },
docs: {
description: {
story: 'Отключённое состояние — управляется через `FormControl`.',
},
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputMaskComponent } from '@cdek-it/angular-ui-kit';

@Component({
standalone: true,
imports: [InputMaskComponent, ReactiveFormsModule],
template: \`<input-mask mask="99-99-99" [formControl]="control"></input-mask>\`,
})
export class DisabledExample {
control = new FormControl({ value: '12-34-56', disabled: true });
}
`,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { StoryObj } from '@storybook/angular';
import { InputMask } from 'primeng/inputmask';
import { FloatLabel } from 'primeng/floatlabel';

export const template = `
<div class="pt-6 w-64">
<p-floatlabel variant="in">
<p-inputmask id="fl-mask" mask="99-99-99" [formControl]="control"></p-inputmask>
<label for="fl-mask">Дата</label>
</p-floatlabel>
</div>
`;
const styles = '';

@Component({
selector: 'app-inputmask-float-label',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [InputMask, FloatLabel, ReactiveFormsModule],
template,
styles,
})
export class InputMaskFloatLabelComponent {
readonly control = new FormControl('');
}

export const FloatLabelStory: StoryObj = {
name: 'FloatLabel',
render: () => ({
template: `<app-inputmask-float-label></app-inputmask-float-label>`,
}),
parameters: {
controls: { disable: true },
docs: {
description: {
story:
'Интеграция с `p-floatlabel` — плавающая метка внутри поля. Кликните на поле чтобы увидеть анимацию. Требует нативный `<p-inputmask>` как прямой дочерний элемент `p-floatlabel`.',
},
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { InputMask } from 'primeng/inputmask';
import { FloatLabel } from 'primeng/floatlabel';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

@Component({
selector: 'app-inputmask-float-label',
standalone: true,
imports: [InputMask, FloatLabel, ReactiveFormsModule],
template: \`
<p-floatlabel variant="in">
<p-inputmask id="fl-mask" mask="99-99-99" [formControl]="control"></p-inputmask>
<label for="fl-mask">Дата</label>
</p-floatlabel>
\`,
})
export class InputMaskFloatLabelComponent {
readonly control = new FormControl('');
}
`,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { StoryObj } from '@storybook/angular';
import { InputMaskComponent } from '../../../../lib/components/inputmask/inputmask.component';

export const Invalid: StoryObj = {
name: 'Invalid',
render: (args) => {
const control = new FormControl('', Validators.required);
return {
props: { ...args, control },
template: `<input-mask mask="99-99-99" placeholder="Обязательное поле" [formControl]="control"></input-mask>`,
};
},
decorators: [
(story: any) => ({
...story(),
moduleMetadata: {
imports: [InputMaskComponent, ReactiveFormsModule],
},
}),
],
parameters: {
controls: { disable: true },
docs: {
description: {
story: 'Невалидное состояние — определяется через валидаторы `FormControl`.',
},
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
import { InputMaskComponent } from '@cdek-it/angular-ui-kit';

@Component({
standalone: true,
imports: [InputMaskComponent, ReactiveFormsModule],
template: \`<input-mask mask="99-99-99" [formControl]="control" placeholder="Обязательное поле"></input-mask>\`,
})
export class InvalidExample {
control = new FormControl('', Validators.required);
}
`,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { StoryObj } from '@storybook/angular';
import { InputMaskComponent } from '../../../../lib/components/inputmask/inputmask.component';

export const Readonly: StoryObj = {
name: 'Readonly',
render: (args) => {
const control = new FormControl('12-34-56');
return {
props: { ...args, control },
template: `<input-mask mask="99-99-99" placeholder="99-99-99" [readonly]="true" [formControl]="control"></input-mask>`,
};
},
decorators: [
(story: any) => ({
...story(),
moduleMetadata: {
imports: [InputMaskComponent, ReactiveFormsModule],
},
}),
],
parameters: {
controls: { disable: true },
docs: {
description: {
story: 'Режим только для чтения — поле отображает значение, но недоступно для редактирования.',
},
source: {
language: 'ts',
code: `
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { InputMaskComponent } from '@cdek-it/angular-ui-kit';

@Component({
standalone: true,
imports: [InputMaskComponent, ReactiveFormsModule],
template: \`<input-mask mask="99-99-99" [readonly]="true" [formControl]="control"></input-mask>\`,
})
export class ReadonlyExample {
control = new FormControl('12-34-56');
}
`,
},
},
},
};
Loading
Loading