diff --git a/src/lib/components/inputtext/inputtext.component.ts b/src/lib/components/inputtext/inputtext.component.ts
new file mode 100644
index 00000000..3904b924
--- /dev/null
+++ b/src/lib/components/inputtext/inputtext.component.ts
@@ -0,0 +1,130 @@
+import { Component, Input, Output, EventEmitter, forwardRef, inject, Injector, OnInit } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
+import { NgClass } from '@angular/common';
+import { InputText } from 'primeng/inputtext';
+import { IconField } from 'primeng/iconfield';
+import { InputIcon } from 'primeng/inputicon';
+
+export type InputTextSize = 'small' | 'base' | 'large' | 'xlarge';
+
+
+@Component({
+ selector: 'input-text',
+ standalone: true,
+ imports: [InputText, IconField, InputIcon, NgClass],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => InputTextComponent),
+ multi: true,
+ },
+ ],
+ template: `
+ @if (showClear) {
+
+
+
+
+ } @else {
+
+ }
+ `,
+})
+export class InputTextComponent implements ControlValueAccessor, OnInit {
+ private readonly _injector = inject(Injector);
+ private _ngControl: NgControl | null = null;
+
+ ngOnInit(): void {
+ this._ngControl = this._injector.get(NgControl, null, { self: true, optional: true });
+ }
+
+ @Input() placeholder = '';
+ @Input() size: InputTextSize = 'base';
+ @Input() readonly = false;
+ @Input() showClear = false;
+ @Input() fluid = false;
+
+ disabled = false;
+
+ get invalid(): boolean {
+ return this._ngControl?.invalid ?? false;
+ }
+
+ @Output() onClear = new EventEmitter();
+
+ modelValue = '';
+
+ private _onChange: (value: string) => void = () => {};
+
+ 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 {
+ return { 'p-inputtext-xlg': this.size === 'xlarge' };
+ }
+
+ onInput(event: Event): void {
+ const value = (event.target as HTMLInputElement).value;
+ this.modelValue = value;
+ this._onChange(value);
+ }
+
+ onTouched: () => void = () => {};
+
+ clearValue(): void {
+ this.modelValue = '';
+ this._onChange('');
+ this.onClear.emit();
+ }
+
+ writeValue(value: string): void {
+ this.modelValue = value ?? '';
+ }
+
+ registerOnChange(fn: (value: string) => void): void {
+ this._onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+}
diff --git a/src/prime-preset/map-tokens.ts b/src/prime-preset/map-tokens.ts
index 0194af83..09d9449d 100644
--- a/src/prime-preset/map-tokens.ts
+++ b/src/prime-preset/map-tokens.ts
@@ -6,6 +6,7 @@ import tokens from './tokens/tokens.json';
import { avatarCss } from './tokens/components/avatar';
import { buttonCss } from './tokens/components/button';
import { checkboxCss } from './tokens/components/checkbox';
+import { inputtextCss } from './tokens/components/inputtext';
import { progressspinnerCss } from './tokens/components/progressspinner';
import { tagCss } from './tokens/components/tag';
import { tooltipCss } from './tokens/components/tooltip';
@@ -31,6 +32,10 @@ const presetTokens: Preset = {
...(tokens.components.progressspinner as unknown as ComponentsDesignTokens['progressspinner']),
css: progressspinnerCss,
},
+ inputtext: {
+ ...(tokens.components.inputtext as unknown as ComponentsDesignTokens['inputtext']),
+ css: inputtextCss,
+ },
tag: {
...(tokens.components.tag as unknown as ComponentsDesignTokens['tag']),
css: tagCss,
diff --git a/src/prime-preset/tokens/components/inputtext.ts b/src/prime-preset/tokens/components/inputtext.ts
new file mode 100644
index 00000000..027ae6cc
--- /dev/null
+++ b/src/prime-preset/tokens/components/inputtext.ts
@@ -0,0 +1,49 @@
+export const inputtextCss = ({ dt }: { dt: (token: string) => string }): string => `
+
+/* ─── Базовые стили ─── */
+.p-inputtext {
+ border-width: ${dt('inputtext.extend.borderWidth')};
+ line-height: ${dt('fonts.lineHeight.250')};
+}
+
+/* ─── Disabled ─── */
+.p-inputtext:disabled {
+ background: ${dt('inputtext.root.disabledBackground')};
+ color: ${dt('inputtext.root.disabledColor')};
+}
+
+/* ─── Readonly ─── */
+.p-inputtext:enabled:read-only {
+ background: ${dt('inputtext.extend.readonlyBackground')};
+ color: ${dt('inputtext.root.color')};
+}
+
+/* ─── Focus ─── */
+.p-inputtext:enabled:focus {
+ box-shadow: 0 0 0 ${dt('inputtext.focusRing.width')} ${dt('inputtext.focusRing.color')};
+}
+
+/* ─── Invalid + Focus ─── */
+.p-inputtext.p-invalid:focus {
+ border-color: ${dt('inputtext.root.invalidBorderColor')};
+ box-shadow: 0 0 0 ${dt('inputtext.focusRing.width')} ${dt('focusRing.extend.invalid')};
+}
+
+/* ─── Extra Large ─── */
+.p-inputtext.p-inputtext-xlg {
+ font-size: ${dt('inputtext.extend.extXlg.fontSize')};
+ padding: ${dt('inputtext.extend.extXlg.paddingY')} ${dt('inputtext.extend.extXlg.paddingX')};
+}
+
+/* ─── IconField ─── */
+.p-iconfield[data-pc-name="iconfield"] {
+ width: fit-content;
+}
+
+.p-iconfield .p-inputicon {
+ font-size: ${dt('inputtext.extend.iconSize')};
+ width: ${dt('inputtext.extend.iconSize')};
+ height: ${dt('inputtext.extend.iconSize')};
+ cursor: pointer;
+}
+`;
diff --git a/src/prime-preset/tokens/tokens.json b/src/prime-preset/tokens/tokens.json
index 78e7c3d9..a28b400a 100644
--- a/src/prime-preset/tokens/tokens.json
+++ b/src/prime-preset/tokens/tokens.json
@@ -2909,7 +2909,7 @@
"top": "{form.padding.400}"
}
},
- "inside": {
+ "in": {
"input": {
"paddingTop": "{form.padding.700}",
"paddingBottom": "{form.padding.300}"
@@ -3120,9 +3120,9 @@
"iconSize": "{form.icon.300}",
"borderWidth": "{form.borderWidth}",
"extXlg": {
- "fontSize": "{form.fontSize}",
- "paddingX": "{form.paddingX}",
- "paddingY": "{form.paddingY}"
+ "fontSize": "{form.xlg.fontSize}",
+ "paddingX": "{form.padding.300}",
+ "paddingY": "{form.padding.600}"
}
},
"root": {
@@ -3140,19 +3140,19 @@
"placeholderColor": "{form.placeholderColor}",
"invalidPlaceholderColor": "{form.invalidPlaceholderColor}",
"shadow": "0",
- "paddingX": "{form.paddingX}",
- "paddingY": "{form.paddingY}",
+ "paddingX": "{form.padding.300}",
+ "paddingY": "{form.padding.300}",
"borderRadius": "{form.borderRadius.200}",
"transitionDuration": "{form.transitionDuration}",
"sm": {
- "fontSize": "{form.fontSize}",
- "paddingX": "{form.paddingX}",
- "paddingY": "{form.paddingY}"
+ "fontSize": "{fonts.fontSize.300}",
+ "paddingX": "{form.padding.300}",
+ "paddingY": "{form.padding.200}"
},
"lg": {
- "fontSize": "{form.fontSize}",
- "paddingX": "{form.paddingX}",
- "paddingY": "{form.paddingY}"
+ "fontSize": "{fonts.fontSize.300}",
+ "paddingX": "{form.padding.300}",
+ "paddingY": "{form.padding.400}"
},
"focusRing": {
"width": "{form.focusRing.width}",
diff --git a/src/stories/components/inputtext/examples/inputtext-clear.component.ts b/src/stories/components/inputtext/examples/inputtext-clear.component.ts
new file mode 100644
index 00000000..d0c13671
--- /dev/null
+++ b/src/stories/components/inputtext/examples/inputtext-clear.component.ts
@@ -0,0 +1,40 @@
+import { StoryObj } from '@storybook/angular';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+type Story = StoryObj;
+
+export const ClearButton: Story = {
+ name: 'ClearButton',
+ render: (args) => ({
+ props: { ...args },
+ template: `
+
+ `,
+ }),
+ args: {
+ showClear: true,
+ placeholder: 'Введите текст...',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Поле с кнопкой очистки через `showClear`. Иконка × появляется при вводе первого символа.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+// template:
+//
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputtext/examples/inputtext-disabled.component.ts b/src/stories/components/inputtext/examples/inputtext-disabled.component.ts
new file mode 100644
index 00000000..4bf1948d
--- /dev/null
+++ b/src/stories/components/inputtext/examples/inputtext-disabled.component.ts
@@ -0,0 +1,45 @@
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+export const Disabled: StoryObj = {
+ name: 'Disabled',
+ render: (args) => {
+ const control = new FormControl({ value: '', disabled: true });
+ return {
+ props: { ...args, control },
+ template: ``,
+ };
+ },
+ decorators: [
+ (story: any) => ({
+ ...story(),
+ moduleMetadata: {
+ imports: [InputTextComponent, 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 { InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ standalone: true,
+ imports: [InputTextComponent, ReactiveFormsModule],
+ template: \`\`,
+})
+export class DisabledExample {
+ control = new FormControl({ value: '', disabled: true });
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputtext/examples/inputtext-float-label-invalid.component.ts b/src/stories/components/inputtext/examples/inputtext-float-label-invalid.component.ts
new file mode 100644
index 00000000..35e09cc1
--- /dev/null
+++ b/src/stories/components/inputtext/examples/inputtext-float-label-invalid.component.ts
@@ -0,0 +1,75 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { NgIf } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { InputText } from 'primeng/inputtext';
+import { FloatLabel } from 'primeng/floatlabel';
+
+@Component({
+ selector: 'app-inputtext-float-label-invalid',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [InputText, FloatLabel, FormsModule, NgIf],
+ template: `
+
+`,
+})
+export class InputTextFloatLabelInvalidComponent {
+ value = '';
+ @Input() required = false;
+}
+
+export const FloatLabelInvalid: StoryObj = {
+ name: 'FloatLabel + Invalid',
+ render: (args) => ({
+ template: ``,
+ props: { required: args['required'] },
+ }),
+ args: { required: true },
+ argTypes: {
+ required: {
+ control: 'boolean',
+ description: 'Показывает маркер обязательного поля `*` рядом с меткой',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'FloatLabel с невалидным состоянием — демонстрирует стилизацию ошибки в комбинации с плавающей меткой.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { InputText } from 'primeng/inputtext';
+import { FloatLabel } from 'primeng/floatlabel';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ standalone: true,
+ imports: [InputText, FloatLabel, FormsModule],
+ template: \`
+
+
+
+
+ \`,
+})
+export class FloatLabelInvalidExample {
+ value = '';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputtext/examples/inputtext-float-label.component.ts b/src/stories/components/inputtext/examples/inputtext-float-label.component.ts
new file mode 100644
index 00000000..2993498b
--- /dev/null
+++ b/src/stories/components/inputtext/examples/inputtext-float-label.component.ts
@@ -0,0 +1,75 @@
+import { Component, Input } from '@angular/core';
+import { NgIf } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { InputText } from 'primeng/inputtext';
+import { FloatLabel } from 'primeng/floatlabel';
+
+@Component({
+ selector: 'app-inputtext-float-label',
+ standalone: true,
+ imports: [InputText, FloatLabel, FormsModule, NgIf],
+ template: `
+
+`,
+})
+export class InputTextFloatLabelComponent {
+ value = '';
+ @Input() required = false;
+}
+
+export const FloatLabelStory: StoryObj = {
+ name: 'FloatLabel',
+ render: (args) => ({
+ template: ``,
+ props: { required: args['required'] },
+ }),
+ args: { required: true },
+ argTypes: {
+ required: {
+ control: 'boolean',
+ description: 'Показывает маркер обязательного поля `*` рядом с меткой',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Интеграция с `p-floatlabel` — плавающая метка внутри поля. Кликните на поле чтобы увидеть анимацию. Требует нативный `` как прямой дочерний элемент `p-floatlabel`.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { InputText } from 'primeng/inputtext';
+import { FloatLabel } from 'primeng/floatlabel';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ standalone: true,
+ imports: [InputText, FloatLabel, FormsModule],
+ template: \`
+
+
+
+
+ \`,
+})
+export class FloatLabelExample {
+ value = '';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputtext/examples/inputtext-invalid.component.ts b/src/stories/components/inputtext/examples/inputtext-invalid.component.ts
new file mode 100644
index 00000000..ee49015b
--- /dev/null
+++ b/src/stories/components/inputtext/examples/inputtext-invalid.component.ts
@@ -0,0 +1,45 @@
+import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+export const Invalid: StoryObj = {
+ name: 'Invalid',
+ render: (args) => {
+ const control = new FormControl('', Validators.required);
+ return {
+ props: { ...args, control },
+ template: ``,
+ };
+ },
+ decorators: [
+ (story: any) => ({
+ ...story(),
+ moduleMetadata: {
+ imports: [InputTextComponent, ReactiveFormsModule],
+ },
+ }),
+ ],
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: { story: 'Невалидное состояние — управляется через FormControl + Validators.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
+import { InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ standalone: true,
+ imports: [InputTextComponent, ReactiveFormsModule],
+ template: \`\`,
+})
+export class InvalidExample {
+ control = new FormControl('', Validators.required);
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputtext/examples/inputtext-readonly.component.ts b/src/stories/components/inputtext/examples/inputtext-readonly.component.ts
new file mode 100644
index 00000000..35ce1dee
--- /dev/null
+++ b/src/stories/components/inputtext/examples/inputtext-readonly.component.ts
@@ -0,0 +1,40 @@
+import { StoryObj } from '@storybook/angular';
+import { InputTextComponent } from '../../../../lib/components/inputtext/inputtext.component';
+
+type Story = StoryObj;
+
+export const Readonly: Story = {
+ name: 'Readonly',
+ render: (args) => ({
+ props: { ...args },
+ template: `
+
+ `,
+ }),
+ args: {
+ readonly: true,
+ placeholder: 'Введите текст...',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Режим только для чтения — поле отображает значение, но недоступно для редактирования.',
+ },
+ source: {
+ language: 'ts',
+ code: `
+import { InputTextComponent } from '@cdek-it/angular-ui-kit';
+
+// template:
+//
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/inputtext/inputtext.stories.ts b/src/stories/components/inputtext/inputtext.stories.ts
new file mode 100644
index 00000000..57545c18
--- /dev/null
+++ b/src/stories/components/inputtext/inputtext.stories.ts
@@ -0,0 +1,165 @@
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
+import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
+import { InputTextComponent } from '../../../lib/components/inputtext/inputtext.component';
+import { ClearButton } from './examples/inputtext-clear.component';
+import { InputTextFloatLabelComponent, FloatLabelStory } from './examples/inputtext-float-label.component';
+import { InputTextFloatLabelInvalidComponent, FloatLabelInvalid } from './examples/inputtext-float-label-invalid.component';
+import { Disabled } from './examples/inputtext-disabled.component';
+import { Readonly } from './examples/inputtext-readonly.component';
+import { Invalid } from './examples/inputtext-invalid.component';
+
+type InputTextArgs = InputTextComponent & { disabled: boolean; invalid: boolean };
+
+const meta: Meta = {
+ title: 'Components/Form/InputText',
+ component: InputTextComponent,
+ tags: ['autodocs'],
+ decorators: [
+ moduleMetadata({
+ imports: [
+ InputTextComponent,
+ ReactiveFormsModule,
+ InputTextFloatLabelComponent,
+ InputTextFloatLabelInvalidComponent,
+ ],
+ }),
+ ],
+ parameters: {
+ designTokens: { prefix: '--p-inputtext' },
+ docs: {
+ description: {
+ component: `Текстовое поле для ввода данных.
+
+\`\`\`typescript
+import { InputTextModule } from 'primeng/inputtext';
+\`\`\``,
+ },
+ },
+ },
+ argTypes: {
+ // ── Props ────────────────────────────────────────────────
+ placeholder: {
+ control: 'text',
+ description: 'Подсказка при пустом поле',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: "''" },
+ type: { summary: 'string' },
+ },
+ },
+ size: {
+ control: 'select',
+ options: ['small', 'base', 'large', 'xlarge'],
+ description: 'Размер поля',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: "'base'" },
+ type: { summary: "'small' | 'base' | 'large' | 'xlarge'" },
+ },
+ },
+ disabled: {
+ control: 'boolean',
+ description: 'Отключает взаимодействие — управляется через FormControl',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ invalid: {
+ control: 'boolean',
+ description: 'Невалидное состояние — управляется через FormControl',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ readonly: {
+ control: 'boolean',
+ description: 'Только для чтения',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ showClear: {
+ control: 'boolean',
+ description: 'Показывает иконку очистки при наличии значения',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ fluid: {
+ control: 'boolean',
+ description: 'Растягивает поле на всю ширину контейнера',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ // Hidden computed props
+ modelValue: { table: { disable: true } },
+ primeSize: { table: { disable: true } },
+ sizeClass: { table: { disable: true } },
+
+ // ── Events ───────────────────────────────────────────────
+ onClear: {
+ control: false,
+ description: 'Событие очистки поля (при showClear)',
+ table: {
+ category: 'Events',
+ type: { summary: 'EventEmitter' },
+ },
+ },
+ },
+ args: {
+ placeholder: 'Введите текст...',
+ size: 'base',
+ disabled: false,
+ invalid: false,
+ readonly: false,
+ showClear: false,
+ fluid: false,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ── Default ──────────────────────────────────────────────────────────────────
+export const Default: Story = {
+ name: 'Default',
+ render: (args) => {
+ const parts: string[] = [];
+
+ if (args.placeholder) parts.push(`placeholder="${args.placeholder}"`);
+ if (args.size && args.size !== 'base') parts.push(`size="${args.size}"`);
+ if (args.readonly) parts.push(`[readonly]="true"`);
+ if (args.showClear) parts.push(`[showClear]="true"`);
+ if (args.fluid) parts.push(`[fluid]="true"`);
+
+ const validators = [];
+ if (args.invalid) validators.push(Validators.required);
+
+ const control = new FormControl({ value: '', disabled: args.disabled }, validators);
+
+ const template = ``;
+
+ return { props: { ...args, control }, template };
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.',
+ },
+ },
+ },
+};
+
+// ── Re-exports from example components ────────────────────────────────────
+export { ClearButton, FloatLabelStory as FloatLabel, FloatLabelInvalid, Disabled, Readonly, Invalid };