From de70553e9fe71a581d42098d32bc204a783ea9bb Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Thu, 16 Apr 2026 22:30:41 +0700 Subject: [PATCH 1/8] =?UTF-8?q?inputnumber:=20=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F,=20=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=81=D1=8B,=20=D0=BE=D0=B1=D1=91=D1=80=D1=82?= =?UTF-8?q?=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-04-16-inputnumber.md | 808 ++++++++++++++++++ .../specs/2026-04-16-inputnumber-design.md | 163 ++++ .../inputnumber/inputnumber.component.ts | 108 +++ src/prime-preset/map-tokens.ts | 5 + .../tokens/components/inputnumber.ts | 23 + .../examples/inputnumber-buttons.component.ts | 37 + .../inputnumber-currency.component.ts | 42 + .../inputnumber-disabled.component.ts | 38 + .../inputnumber-float-label.component.ts | 82 ++ .../inputnumber/inputnumber.stories.ts | 252 ++++++ 10 files changed, 1558 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-16-inputnumber.md create mode 100644 docs/superpowers/specs/2026-04-16-inputnumber-design.md create mode 100644 src/lib/components/inputnumber/inputnumber.component.ts create mode 100644 src/prime-preset/tokens/components/inputnumber.ts create mode 100644 src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts create mode 100644 src/stories/components/inputnumber/examples/inputnumber-currency.component.ts create mode 100644 src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts create mode 100644 src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts create mode 100644 src/stories/components/inputnumber/inputnumber.stories.ts diff --git a/docs/superpowers/plans/2026-04-16-inputnumber.md b/docs/superpowers/plans/2026-04-16-inputnumber.md new file mode 100644 index 00000000..ab3591e0 --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-inputnumber.md @@ -0,0 +1,808 @@ +# InputNumber Component — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Создать Angular wrapper-компонент InputNumber с CSS-переопределениями и Storybook-сториями. + +**Architecture:** Standalone CVA-компонент `InputNumberComponent`, оборачивающий PrimeNG `p-inputnumber`. CSS-оверрайды в `src/prime-preset/tokens/components/inputnumber.ts`, подключаются через `map-tokens.ts`. Четыре стории: Default (динамический template из args), FloatLabel (нативный `p-inputnumber` внутри `p-floatlabel`), Currency, MinMax. + +**Tech Stack:** Angular 20, PrimeNG 20, Storybook 8, Tailwind, `dt()` токены, Tabler Icons. + +--- + +## File Map + +| Действие | Путь | +|---|---| +| Создать | `src/lib/components/inputnumber/inputnumber.component.ts` | +| Создать | `src/prime-preset/tokens/components/inputnumber.ts` | +| Изменить | `src/prime-preset/map-tokens.ts` | +| Создать | `src/stories/components/inputnumber/inputnumber.stories.ts` | +| Создать | `src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts` | +| Создать | `src/stories/components/inputnumber/examples/inputnumber-currency.component.ts` | +| Создать | `src/stories/components/inputnumber/examples/inputnumber-minmax.component.ts` | + +--- + +### Task 1: CSS-переопределения InputNumber + +**Files:** +- Create: `src/prime-preset/tokens/components/inputnumber.ts` +- Modify: `src/prime-preset/map-tokens.ts` + +- [ ] **Step 1: Создать файл CSS-токенов** + +Создать `src/prime-preset/tokens/components/inputnumber.ts`: + +```typescript +export const inputnumberCss = ({ dt }: { dt: (token: string) => string }): string => ` + +/* ─── Кнопки +/− ─── */ +.p-inputnumber-button { + border-width: ${dt('inputnumber.extend.borderWidth')}; +} + +.p-inputnumber-horizontal .p-inputnumber-button { + min-height: ${dt('inputnumber.extend.extButton.height')}; +} + +/* ─── Disabled состояние кнопок ─── */ +.p-inputnumber-horizontal:has(.p-inputnumber-input:disabled) .p-inputnumber-button { + background: ${dt('inputtext.root.disabledBackground')}; + color: ${dt('inputtext.root.disabledColor')}; +} + +/* ─── Extra Large ─── */ +.p-inputnumber.p-inputnumber-xlg .p-inputnumber-input { + font-size: ${dt('inputtext.extend.extXlg.fontSize')}; + padding: ${dt('inputtext.extend.extXlg.paddingY')} ${dt('inputtext.extend.extXlg.paddingX')}; +} +`; +``` + +- [ ] **Step 2: Зарегистрировать CSS в map-tokens.ts** + +Открыть `src/prime-preset/map-tokens.ts`. Добавить импорт после строки с `inputtextCss`: + +```typescript +import { inputnumberCss } from './tokens/components/inputnumber'; +``` + +Добавить запись в объект `components` после блока `inputtext`: + +```typescript +inputnumber: { + ...(tokens.components.inputnumber as unknown as ComponentsDesignTokens['inputnumber']), + css: inputnumberCss, +}, +``` + +- [ ] **Step 3: Проверить компиляцию** + +```bash +cd /Users/d.khaliulin/Downloads/angular-ui-kit-feature-styles-debug +npx tsc --noEmit +``` + +Ожидается: нет ошибок. + +- [ ] **Step 4: Коммит** + +```bash +git add src/prime-preset/tokens/components/inputnumber.ts src/prime-preset/map-tokens.ts +git commit -m "feat(inputnumber): добавить CSS-переопределения токенов" +``` + +--- + +### Task 2: InputNumberComponent + +**Files:** +- Create: `src/lib/components/inputnumber/inputnumber.component.ts` + +- [ ] **Step 1: Создать компонент** + +Создать `src/lib/components/inputnumber/inputnumber.component.ts`: + +```typescript +import { Component, Input, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'; +import { NgClass } from '@angular/common'; +import { InputNumber } from 'primeng/inputnumber'; + +export type InputNumberSize = 'small' | 'base' | 'large' | 'xlarge'; +export type InputNumberButtonLayout = 'horizontal' | 'vertical' | 'stacked'; +export type InputNumberMode = 'decimal' | 'currency'; + +@Component({ + selector: 'input-number', + standalone: true, + imports: [InputNumber, NgClass, FormsModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => InputNumberComponent), + multi: true, + }, + ], + template: ` + + `, +}) +export class InputNumberComponent implements ControlValueAccessor { + @Input() size: InputNumberSize = 'base'; + @Input() placeholder = ''; + @Input() disabled = false; + @Input() readonly = false; + @Input() invalid = false; + @Input() showButtons = true; + @Input() buttonLayout: InputNumberButtonLayout = 'horizontal'; + @Input() mode: InputNumberMode = 'decimal'; + @Input() currency = 'RUB'; + @Input() locale = 'ru-RU'; + @Input() prefix: string | undefined = undefined; + @Input() suffix: string | undefined = undefined; + @Input() min: number | undefined = undefined; + @Input() max: number | undefined = undefined; + @Input() step = 1; + @Input() minFractionDigits = 0; + @Input() maxFractionDigits = 20; + @Input() fluid = false; + + modelValue: number | null = null; + + private _onChange: (value: number | null) => void = () => {}; + onTouched: () => 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-inputnumber-xlg': this.size === 'xlarge' }; + } + + onInputChange(event: { value: number | null | undefined }): void { + const value = event.value ?? null; + this.modelValue = value; + this._onChange(value); + } + + writeValue(value: number | null): void { + this.modelValue = value ?? null; + } + + registerOnChange(fn: (value: number | null) => void): void { + this._onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } +} +``` + +- [ ] **Step 2: Проверить компиляцию** + +```bash +npx tsc --noEmit +``` + +Ожидается: нет ошибок. + +- [ ] **Step 3: Коммит** + +```bash +git add src/lib/components/inputnumber/inputnumber.component.ts +git commit -m "feat(inputnumber): добавить компонент InputNumberComponent" +``` + +--- + +### Task 3: FloatLabel story + +**Files:** +- Create: `src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts` + +- [ ] **Step 1: Создать файл** + +Создать `src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts`: + +```typescript +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { StoryObj } from '@storybook/angular'; +import { InputNumber } from 'primeng/inputnumber'; +import { FloatLabel } from 'primeng/floatlabel'; + +const template = ` +
+ + + + +
+`; +const styles = ''; + +@Component({ + selector: 'app-inputnumber-float-label', + standalone: true, + imports: [InputNumber, FloatLabel, FormsModule], + template, + styles, +}) +export class InputNumberFloatLabelComponent { + value: number | null = null; +} + +export const FloatLabelStory: StoryObj = { + name: 'FloatLabel', + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { + story: + 'Интеграция с `p-floatlabel` — плавающая метка внутри поля. Требует нативный `p-inputnumber` как прямой дочерний элемент `p-floatlabel`.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { InputNumber } from 'primeng/inputnumber'; +import { FloatLabel } from 'primeng/floatlabel'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-inputnumber-float-label', + standalone: true, + imports: [InputNumber, FloatLabel, FormsModule], + template: \` + + + + + \`, +}) +export class InputNumberFloatLabelComponent { + value: number | null = null; +} + `, + }, + }, + }, +}; +``` + +- [ ] **Step 2: Проверить компиляцию** + +```bash +npx tsc --noEmit +``` + +Ожидается: нет ошибок. + +--- + +### Task 4: Currency story + +**Files:** +- Create: `src/stories/components/inputnumber/examples/inputnumber-currency.component.ts` + +- [ ] **Step 1: Создать файл** + +Создать `src/stories/components/inputnumber/examples/inputnumber-currency.component.ts`: + +```typescript +import { StoryObj } from '@storybook/angular'; +import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; + +type Story = StoryObj; + +export const Currency: Story = { + name: 'Currency', + render: (args) => ({ + props: { ...args, value: null }, + template: ` + + `, + }), + args: { + mode: 'currency', + currency: 'RUB', + locale: 'ru-RU', + minFractionDigits: 2, + maxFractionDigits: 2, + }, + parameters: { + docs: { + description: { + story: 'Режим валюты — форматирует значение с символом валюты по заданной локали.', + }, + source: { + language: 'ts', + code: ` +import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; +import { FormsModule } from '@angular/forms'; + +// template: +// + `, + }, + }, + }, +}; +``` + +--- + +### Task 5: MinMax story + +**Files:** +- Create: `src/stories/components/inputnumber/examples/inputnumber-minmax.component.ts` + +- [ ] **Step 1: Создать файл** + +Создать `src/stories/components/inputnumber/examples/inputnumber-minmax.component.ts`: + +```typescript +import { StoryObj } from '@storybook/angular'; +import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; + +type Story = StoryObj; + +export const MinMax: Story = { + name: 'Min / Max / Step', + render: (args) => ({ + props: { ...args, value: null }, + template: ` + + `, + }), + args: { + min: 0, + max: 100, + step: 1, + placeholder: '0–100', + }, + parameters: { + docs: { + description: { + story: 'Ограничения min/max и шаг изменения через кнопки и клавиатуру.', + }, + source: { + language: 'ts', + code: ` +import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; +import { FormsModule } from '@angular/forms'; + +// template: +// + `, + }, + }, + }, +}; +``` + +--- + +### Task 6: Main stories file + +**Files:** +- Create: `src/stories/components/inputnumber/inputnumber.stories.ts` + +- [ ] **Step 1: Создать файл** + +Создать `src/stories/components/inputnumber/inputnumber.stories.ts`: + +```typescript +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { FormsModule } from '@angular/forms'; +import { InputNumberComponent } from '../../../lib/components/inputnumber/inputnumber.component'; +import { InputNumberFloatLabelComponent, FloatLabelStory } from './examples/inputnumber-float-label.component'; +import { Currency } from './examples/inputnumber-currency.component'; +import { MinMax } from './examples/inputnumber-minmax.component'; + +type InputNumberArgs = InputNumberComponent; + +const meta: Meta = { + title: 'Components/Form/InputNumber', + component: InputNumberComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [ + InputNumberComponent, + FormsModule, + InputNumberFloatLabelComponent, + ], + }), + ], + parameters: { + designTokens: { prefix: '--p-inputnumber' }, + docs: { + description: { + component: `Числовое поле ввода с поддержкой форматирования и кнопок +/−. + +\`\`\`typescript +import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; +\`\`\``, + }, + }, + }, + argTypes: { + // ── Props ──────────────────────────────────────────────── + size: { + control: 'select', + options: ['small', 'base', 'large', 'xlarge'], + description: 'Размер поля', + table: { + category: 'Props', + defaultValue: { summary: "'base'" }, + type: { summary: "'small' | 'base' | 'large' | 'xlarge'" }, + }, + }, + placeholder: { + control: 'text', + description: 'Подсказка при пустом поле', + table: { + category: 'Props', + defaultValue: { summary: "''" }, + type: { summary: 'string' }, + }, + }, + disabled: { + control: 'boolean', + description: 'Отключает взаимодействие', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + readonly: { + control: 'boolean', + description: 'Только для чтения', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + invalid: { + control: 'boolean', + description: 'Невалидное состояние', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + showButtons: { + control: 'boolean', + description: 'Показывать кнопки +/−', + table: { + category: 'Props', + defaultValue: { summary: 'true' }, + type: { summary: 'boolean' }, + }, + }, + buttonLayout: { + control: 'select', + options: ['horizontal', 'vertical', 'stacked'], + description: 'Расположение кнопок', + table: { + category: 'Props', + defaultValue: { summary: "'horizontal'" }, + type: { summary: "'horizontal' | 'vertical' | 'stacked'" }, + }, + }, + mode: { + control: 'select', + options: ['decimal', 'currency'], + description: 'Режим отображения значения', + table: { + category: 'Props', + defaultValue: { summary: "'decimal'" }, + type: { summary: "'decimal' | 'currency'" }, + }, + }, + currency: { + control: 'text', + description: 'Код валюты ISO 4217, используется при mode="currency"', + table: { + category: 'Props', + defaultValue: { summary: "'RUB'" }, + type: { summary: 'string' }, + }, + }, + locale: { + control: 'text', + description: 'Локаль для форматирования числа', + table: { + category: 'Props', + defaultValue: { summary: "'ru-RU'" }, + type: { summary: 'string' }, + }, + }, + prefix: { + control: 'text', + description: 'Префикс перед значением', + table: { + category: 'Props', + type: { summary: 'string' }, + }, + }, + suffix: { + control: 'text', + description: 'Суффикс после значения (например, "%")', + table: { + category: 'Props', + type: { summary: 'string' }, + }, + }, + min: { + control: 'number', + description: 'Минимально допустимое значение', + table: { + category: 'Props', + type: { summary: 'number' }, + }, + }, + max: { + control: 'number', + description: 'Максимально допустимое значение', + table: { + category: 'Props', + type: { summary: 'number' }, + }, + }, + step: { + control: 'number', + description: 'Шаг изменения значения', + table: { + category: 'Props', + defaultValue: { summary: '1' }, + type: { summary: 'number' }, + }, + }, + minFractionDigits: { + control: { type: 'number', min: 0, max: 20 }, + description: 'Минимальное количество знаков после запятой', + table: { + category: 'Props', + defaultValue: { summary: '0' }, + type: { summary: 'number' }, + }, + }, + maxFractionDigits: { + control: { type: 'number', min: 0, max: 20 }, + description: 'Максимальное количество знаков после запятой', + table: { + category: 'Props', + defaultValue: { summary: '20' }, + type: { summary: 'number' }, + }, + }, + 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 } }, + }, + args: { + size: 'base', + placeholder: '0', + disabled: false, + readonly: false, + invalid: false, + showButtons: true, + buttonLayout: 'horizontal', + mode: 'decimal', + currency: 'RUB', + locale: 'ru-RU', + step: 1, + minFractionDigits: 0, + maxFractionDigits: 20, + fluid: false, + }, +}; + +export default meta; +type Story = StoryObj; + +// ── Default ────────────────────────────────────────────────────────────────── +export const Default: Story = { + name: 'Default', + render: (args) => { + const parts: string[] = []; + + if (args.size && args.size !== 'base') parts.push(`size="${args.size}"`); + if (args.placeholder) parts.push(`placeholder="${args.placeholder}"`); + if (args.disabled) parts.push(`[disabled]="true"`); + if (args.readonly) parts.push(`[readonly]="true"`); + if (args.invalid) parts.push(`[invalid]="true"`); + if (!args.showButtons) parts.push(`[showButtons]="false"`); + if (args.buttonLayout && args.buttonLayout !== 'horizontal') parts.push(`buttonLayout="${args.buttonLayout}"`); + if (args.mode && args.mode !== 'decimal') parts.push(`mode="${args.mode}"`); + if (args.mode === 'currency' && args.currency) parts.push(`currency="${args.currency}"`); + if (args.locale && args.locale !== 'ru-RU') parts.push(`locale="${args.locale}"`); + if (args.prefix) parts.push(`prefix="${args.prefix}"`); + if (args.suffix) parts.push(`suffix="${args.suffix}"`); + if (args.min !== undefined) parts.push(`[min]="${args.min}"`); + if (args.max !== undefined) parts.push(`[max]="${args.max}"`); + if (args.step && args.step !== 1) parts.push(`[step]="${args.step}"`); + if (args.minFractionDigits) parts.push(`[minFractionDigits]="${args.minFractionDigits}"`); + if (args.maxFractionDigits && args.maxFractionDigits !== 20) parts.push(`[maxFractionDigits]="${args.maxFractionDigits}"`); + if (args.fluid) parts.push(`[fluid]="true"`); + parts.push(`[(ngModel)]="value"`); + + const template = ``; + + return { props: { ...args, value: null }, template }; + }, + parameters: { + docs: { + description: { + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.', + }, + }, + }, +}; + +// ── Re-exports from example components ──────────────────────────────────── +export { FloatLabelStory as FloatLabel, Currency, MinMax }; +``` + +- [ ] **Step 2: Проверить компиляцию** + +```bash +npx tsc --noEmit +``` + +Ожидается: нет ошибок. + +- [ ] **Step 3: Коммит** + +```bash +git add src/stories/components/inputnumber/ +git commit -m "feat(inputnumber): добавить Storybook-стории" +``` + +--- + +### Task 7: Финальная проверка + +- [ ] **Step 1: Полная проверка TypeScript** + +```bash +npx tsc --noEmit +``` + +Ожидается: нет ошибок. + +- [ ] **Step 2: Запустить Storybook и проверить визуально** + +```bash +npm run storybook +``` + +Открыть `http://localhost:6006` и проверить: +- `Components/Form/InputNumber` → Default: Controls меняют пропсы, code-snippet обновляется +- FloatLabel: метка анимируется при фокусе/вводе +- Currency: значение форматируется с символом ₽ +- Min/Max: кнопки не выходят за диапазон 0–100 + +- [ ] **Step 3: Финальный коммит** + +```bash +git add -A +git commit -m "feat(inputnumber): компонент InputNumber готов" +``` + +--- + +## Self-Review + +**Spec coverage:** +- ✅ Компонент с CVA — Task 2 +- ✅ Все 18 пропсов — Task 2 (InputNumberComponent) +- ✅ Size mapping (primeSize + p-inputnumber-xlg) — Task 2 +- ✅ Tabler Icons через `incrementButtonIcon` / `decrementButtonIcon` — Task 2 +- ✅ CSS overrides (border, height, disabled, xlarge) — Task 1 +- ✅ map-tokens регистрация — Task 1 +- ✅ Default story с динамическим template — Task 6 +- ✅ FloatLabel story (нативный p-inputnumber) — Task 3 +- ✅ Currency story — Task 4 +- ✅ MinMax story — Task 5 +- ✅ modelValue / primeSize / sizeClass скрыты в argTypes — Task 6 + +**Placeholder scan:** нет TBD/TODO. + +**Type consistency:** `InputNumberSize`, `InputNumberButtonLayout`, `InputNumberMode` определены в Task 2 и используются в `argTypes` Task 6 через строковые литералы — согласованы. diff --git a/docs/superpowers/specs/2026-04-16-inputnumber-design.md b/docs/superpowers/specs/2026-04-16-inputnumber-design.md new file mode 100644 index 00000000..a0331966 --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-inputnumber-design.md @@ -0,0 +1,163 @@ +# InputNumber Component — Design Spec + +**Date:** 2026-04-16 +**Branch:** `form.inputnumber` (to be created) +**Reference:** [Vue InputNumber](https://github.com/cdek-it/vue-ui-kit/tree/form.InputNumber/src/plugins/prime/stories/Form/InputNumber) + +--- + +## Overview + +Angular wrapper component for PrimeNG `InputNumber`, following the same patterns as `InputTextComponent`. Provides a styled numeric input with optional increment/decrement buttons, currency formatting, and min/max/step constraints. Integrates with Angular Forms via `ControlValueAccessor`. + +--- + +## File Structure + +``` +src/lib/components/inputnumber/ + inputnumber.component.ts + +src/prime-preset/tokens/components/ + inputnumber.ts ← new CSS override file + +src/prime-preset/ + map-tokens.ts ← add inputnumber CSS + +src/stories/components/inputnumber/ + inputnumber.stories.ts + examples/ + inputnumber-float-label.component.ts + inputnumber-currency.component.ts + inputnumber-minmax.component.ts +``` + +--- + +## Component API (`InputNumberComponent`) + +**Selector:** `input-number` +**Standalone:** yes +**CVA value type:** `number | null` + +### Inputs + +| Prop | Type | Default | Description | +|---|---|---|---| +| `size` | `'small' \| 'base' \| 'large' \| 'xlarge'` | `'base'` | Размер поля | +| `placeholder` | `string` | `''` | Подсказка при пустом поле | +| `disabled` | `boolean` | `false` | Отключает взаимодействие | +| `readonly` | `boolean` | `false` | Только для чтения | +| `invalid` | `boolean` | `false` | Невалидное состояние | +| `showButtons` | `boolean` | `true` | Показывать кнопки +/− | +| `buttonLayout` | `'horizontal' \| 'vertical' \| 'stacked'` | `'horizontal'` | Расположение кнопок | +| `mode` | `'decimal' \| 'currency'` | `'decimal'` | Режим отображения | +| `currency` | `string` | `'RUB'` | Код валюты (ISO 4217) при `mode="currency"` | +| `locale` | `string` | `'ru-RU'` | Локаль форматирования | +| `prefix` | `string \| undefined` | `undefined` | Префикс перед значением | +| `suffix` | `string \| undefined` | `undefined` | Суффикс после значения | +| `min` | `number \| undefined` | `undefined` | Минимальное значение | +| `max` | `number \| undefined` | `undefined` | Максимальное значение | +| `step` | `number` | `1` | Шаг изменения | +| `minFractionDigits` | `number` | `0` | Мин. знаков после запятой | +| `maxFractionDigits` | `number` | `20` | Макс. знаков после запятой | +| `fluid` | `boolean` | `false` | Растягивает на всю ширину | + +### Size mapping + +| `size` | `pSize` (PrimeNG) | CSS class | +|---|---|---| +| `'small'` | `'small'` | — | +| `'base'` | `undefined` | — | +| `'large'` | `'large'` | — | +| `'xlarge'` | `'large'` | `p-inputnumber-xlg` (on host) | + +The `p-inputnumber-xlg` class is applied via `[ngClass]` on the `p-inputnumber` element so CSS cascade can target `.p-inputnumber-xlg .p-inputnumber-input`. + +### Icons + +Increment button: `` via `#incrementicon` ng-template. +Decrement button: `` via `#decrementicon` ng-template. + +### CVA + +- `writeValue(v: number | null)` — stores to `modelValue` +- `registerOnChange` / `registerOnTouched` — standard +- `setDisabledState` — sets `disabled` +- `onValueChange(v: number | null)` — called on PrimeNG `(onInput)` event, calls `_onChange` + +--- + +## CSS Overrides (`src/prime-preset/tokens/components/inputnumber.ts`) + +```typescript +export const inputnumberCss = ({ dt }) => ` + .p-inputnumber-button { + border-width: ${dt('inputnumber.extend.borderWidth')}; + } + + .p-inputnumber-horizontal .p-inputnumber-button { + min-height: ${dt('inputnumber.extend.extButton.height')}; + } + + .p-inputnumber-horizontal:has(.p-inputnumber-input:disabled) .p-inputnumber-button { + background: ${dt('inputtext.root.disabledBackground')}; + color: ${dt('inputtext.root.disabledColor')}; + } + + .p-inputnumber.p-inputnumber-xlg .p-inputnumber-input { + font-size: ${dt('inputtext.extend.extXlg.fontSize')}; + padding: ${dt('inputtext.extend.extXlg.paddingY')} ${dt('inputtext.extend.extXlg.paddingX')}; + } +`; +``` + +--- + +## map-tokens.ts + +Add import and entry: + +```typescript +import { inputnumberCss } from './tokens/components/inputnumber'; + +// in components: +inputnumber: { + ...(tokens.components.inputnumber as unknown as ComponentsDesignTokens['inputnumber']), + css: inputnumberCss, +}, +``` + +--- + +## Stories + +### `inputnumber.stories.ts` + +- `meta`: `title: 'Components/Form/InputNumber'`, `component: InputNumberComponent`, `tags: ['autodocs']` +- `argTypes`: all props from API table above +- `args`: defaults from API table +- `Default` story: dynamic template built from args (same pattern as InputText Default) +- Re-exports: `FloatLabel`, `Currency`, `MinMax` + +### `examples/inputnumber-float-label.component.ts` + +Uses native `p-inputnumber` (not the wrapper) as direct child of `p-floatlabel variant="in"`, because PrimeNG FloatLabel CSS relies on sibling selectors that don't work through wrapper components. Shows `showButtons`, `buttonLayout="horizontal"`, Tabler icon templates. `controls: { disable: true }`. + +### `examples/inputnumber-currency.component.ts` + +Pure `StoryObj` (no `@Component`), `render: (args) => ({ props: { ...args, value: null }, template })`. Args preset: `mode: 'currency'`, `currency: 'RUB'`, `locale: 'ru-RU'`. All other props bound through Controls. + +### `examples/inputnumber-minmax.component.ts` + +Pure `StoryObj`. Args preset: `min: 0`, `max: 100`, `step: 1`. Shows constraint behaviour. + +--- + +## Constraints + +- No `styles: [...]` in Angular `@Component` decorator — use `const styles = ''` (webpack base64 path bug) +- Storybook story layout: Tailwind classes only, no inline `style="..."` +- Float label: always use native `p-inputnumber` directly — never the wrapper component — inside `p-floatlabel` +- Default story must build template dynamically from args so the code snippet updates with Controls +- `source.code` in float-label example should not include the outer `
` wrapper diff --git a/src/lib/components/inputnumber/inputnumber.component.ts b/src/lib/components/inputnumber/inputnumber.component.ts new file mode 100644 index 00000000..d9a4c435 --- /dev/null +++ b/src/lib/components/inputnumber/inputnumber.component.ts @@ -0,0 +1,108 @@ +import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { InputNumber } from 'primeng/inputnumber'; +import { SharedModule } from 'primeng/api'; + +export type InputNumberButtonLayout = 'stacked' | 'horizontal' | 'vertical'; + +@Component({ + selector: 'input-number', + standalone: true, + imports: [InputNumber, SharedModule, FormsModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => InputNumberComponent), + multi: true, + }, + ], + template: ` + + @if (!incrementButtonIcon) { + + + + } + @if (!decrementButtonIcon) { + + + + } + + `, +}) +export class InputNumberComponent implements ControlValueAccessor { + @Input() showButtons = false; + @Input() buttonLayout: InputNumberButtonLayout = 'stacked'; + @Input() mode = 'decimal'; + @Input() currency: string | undefined; + @Input() locale: string | undefined; + @Input() placeholder = ''; + @Input() disabled = false; + @Input() invalid = false; + @Input() readonly = false; + @Input() fluid = false; + @Input() min: number | undefined; + @Input() max: number | undefined; + @Input() step = 1; + @Input() prefix: string | undefined; + @Input() suffix: string | undefined; + @Input() minFractionDigits: number | undefined; + @Input() maxFractionDigits: number | undefined; + @Input() useGrouping = true; + @Input() incrementButtonIcon: string | undefined; + @Input() decrementButtonIcon: string | undefined; + + @Output() onInput = new EventEmitter<{ value: number | null }>(); + + modelValue: number | null = null; + + private _onChange: (value: number | null) => void = () => {}; + onTouched: () => void = () => {}; + + onModelChange(value: number | null): void { + this.modelValue = value; + this._onChange(value); + this.onInput.emit({ value }); + } + + writeValue(value: number | null): void { + this.modelValue = value ?? null; + } + + registerOnChange(fn: (value: number | null) => 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 09d9449d..02eeeb27 100644 --- a/src/prime-preset/map-tokens.ts +++ b/src/prime-preset/map-tokens.ts @@ -9,6 +9,7 @@ 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 { inputnumberCss } from './tokens/components/inputnumber'; import { tooltipCss } from './tokens/components/tooltip'; const presetTokens: Preset = { @@ -32,6 +33,10 @@ const presetTokens: Preset = { ...(tokens.components.progressspinner as unknown as ComponentsDesignTokens['progressspinner']), css: progressspinnerCss, }, + inputnumber: { + ...(tokens.components.inputnumber as unknown as ComponentsDesignTokens['inputnumber']), + css: inputnumberCss, + }, inputtext: { ...(tokens.components.inputtext as unknown as ComponentsDesignTokens['inputtext']), css: inputtextCss, diff --git a/src/prime-preset/tokens/components/inputnumber.ts b/src/prime-preset/tokens/components/inputnumber.ts new file mode 100644 index 00000000..f28e4fb7 --- /dev/null +++ b/src/prime-preset/tokens/components/inputnumber.ts @@ -0,0 +1,23 @@ +export const inputnumberCss = ({ dt }: { dt: (token: string) => string }): string => ` + +/* ─── Кнопки увеличения/уменьшения ─── */ +.p-inputnumber-button { + border-width: ${dt('inputnumber.extend.borderWidth')}; +} + +.p-inputnumber-horizontal .p-inputnumber-button { + min-height: ${dt('inputnumber.extend.extButton.height')}; +} + +/* ─── Disabled состояние ─── */ +.p-inputnumber-horizontal:has(.p-inputnumber-input:disabled) .p-inputnumber-button { + background: ${dt('inputtext.root.disabledBackground')}; + color: ${dt('inputtext.root.disabledColor')}; +} + +/* ─── Extra Large ─── */ +.p-inputnumber-input[data-p~="xlarge"] { + font-size: ${dt('inputtext.extend.extXlg.fontSize')}; + padding: ${dt('inputtext.extend.extXlg.paddingY')} ${dt('inputtext.extend.extXlg.paddingX')}; +} +`; diff --git a/src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts b/src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts new file mode 100644 index 00000000..b366e5f8 --- /dev/null +++ b/src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts @@ -0,0 +1,37 @@ +import { StoryObj } from '@storybook/angular'; +import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; + +type Story = StoryObj; + +export const Buttons: Story = { + name: 'Buttons', + render: (args) => ({ + props: { ...args, value: null }, + template: ` + + `, + }), + args: {}, + parameters: { + controls: { disable: true }, + docs: { + description: { + story: 'Числовое поле с кнопками увеличения/уменьшения в горизонтальной раскладке. Кастомные SVG-иконки +/− используются по умолчанию.', + }, + source: { + language: 'ts', + code: ` +import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; +import { FormsModule } from '@angular/forms'; + +// template: +// + `, + }, + }, + }, +}; diff --git a/src/stories/components/inputnumber/examples/inputnumber-currency.component.ts b/src/stories/components/inputnumber/examples/inputnumber-currency.component.ts new file mode 100644 index 00000000..186e3a9b --- /dev/null +++ b/src/stories/components/inputnumber/examples/inputnumber-currency.component.ts @@ -0,0 +1,42 @@ +import { StoryObj } from '@storybook/angular'; +import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; + +type Story = StoryObj; + +export const Currency: Story = { + name: 'Currency', + render: (args) => ({ + props: { ...args, value: null }, + template: ` + + `, + }), + args: { + mode: 'currency', + currency: 'RUB', + locale: 'ru-RU', + }, + parameters: { + controls: { disable: true }, + docs: { + description: { + story: 'Форматирование значения как валюты (рубли). Используются `mode="currency"`, `currency="RUB"` и `locale="ru-RU"`.', + }, + source: { + language: 'ts', + code: ` +import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; +import { FormsModule } from '@angular/forms'; + +// template: +// + `, + }, + }, + }, +}; diff --git a/src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts b/src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts new file mode 100644 index 00000000..19d14edb --- /dev/null +++ b/src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts @@ -0,0 +1,38 @@ +import { StoryObj } from '@storybook/angular'; +import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; + +type Story = StoryObj; + +export const Disabled: Story = { + name: 'Disabled', + render: (args) => ({ + props: { ...args, value: 42 }, + template: ` + + `, + }), + args: {}, + parameters: { + controls: { disable: true }, + docs: { + description: { + story: 'Отключённое состояние — поле и кнопки недоступны для взаимодействия.', + }, + source: { + language: 'ts', + code: ` +import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; +import { FormsModule } from '@angular/forms'; + +// template: +// + `, + }, + }, + }, +}; diff --git a/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts b/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts new file mode 100644 index 00000000..d85ef06b --- /dev/null +++ b/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { StoryObj } from '@storybook/angular'; +import { InputNumber } from 'primeng/inputnumber'; +import { FloatLabel } from 'primeng/floatlabel'; +import { SharedModule } from 'primeng/api'; + +const template = ` +
+ + + + + + + + + + + +
+`; +const styles = ''; + +@Component({ + selector: 'app-inputnumber-float-label', + standalone: true, + imports: [InputNumber, FloatLabel, FormsModule, SharedModule], + template, + styles, +}) +export class InputNumberFloatLabelComponent { + value: number | null = null; +} + +export const FloatLabelStory: StoryObj = { + name: 'FloatLabel', + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { + story: + 'Интеграция с `p-floatlabel` — плавающая метка внутри поля. Требует нативный `p-inputNumber` как прямой дочерний элемент `p-floatlabel`.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { InputNumber } from 'primeng/inputnumber'; +import { FloatLabel } from 'primeng/floatlabel'; +import { SharedModule } from 'primeng/api'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-inputnumber-float-label', + standalone: true, + imports: [InputNumber, FloatLabel, FormsModule, SharedModule], + template: \` + + + + + + + + + + + + \`, +}) +export class InputNumberFloatLabelComponent { + value: number | null = null; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/inputnumber/inputnumber.stories.ts b/src/stories/components/inputnumber/inputnumber.stories.ts new file mode 100644 index 00000000..54c02273 --- /dev/null +++ b/src/stories/components/inputnumber/inputnumber.stories.ts @@ -0,0 +1,252 @@ +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { FormsModule } from '@angular/forms'; +import { InputNumberComponent } from '../../../lib/components/inputnumber/inputnumber.component'; +import { InputNumberFloatLabelComponent, FloatLabelStory } from './examples/inputnumber-float-label.component'; +import { Currency } from './examples/inputnumber-currency.component'; +import { Buttons } from './examples/inputnumber-buttons.component'; +import { Disabled } from './examples/inputnumber-disabled.component'; + +type InputNumberArgs = InputNumberComponent; + +const meta: Meta = { + title: 'Components/Form/InputNumber', + component: InputNumberComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [ + InputNumberComponent, + FormsModule, + InputNumberFloatLabelComponent, + ], + }), + ], + parameters: { + designTokens: { prefix: '--p-inputnumber' }, + docs: { + description: { + component: `Числовое поле ввода с поддержкой форматирования, валюты и кнопок увеличения/уменьшения. + +\`\`\`typescript +import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; +\`\`\``, + }, + }, + }, + argTypes: { + // ── Props ──────────────────────────────────────────────── + placeholder: { + control: 'text', + description: 'Подсказка при пустом поле', + table: { + category: 'Props', + defaultValue: { summary: "''" }, + type: { summary: 'string' }, + }, + }, + showButtons: { + control: 'boolean', + description: 'Отображает кнопки увеличения/уменьшения', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + buttonLayout: { + control: 'select', + options: ['stacked', 'horizontal', 'vertical'], + description: 'Расположение кнопок', + table: { + category: 'Props', + defaultValue: { summary: "'stacked'" }, + type: { summary: "'stacked' | 'horizontal' | 'vertical'" }, + }, + }, + mode: { + control: 'select', + options: ['decimal', 'currency'], + description: 'Режим форматирования', + table: { + category: 'Props', + defaultValue: { summary: "'decimal'" }, + type: { summary: "'decimal' | 'currency'" }, + }, + }, + currency: { + control: 'text', + description: 'ISO 4217 код валюты (при `mode="currency"`)', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'string' }, + }, + }, + locale: { + control: 'text', + description: 'Локаль для форматирования', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'string' }, + }, + }, + disabled: { + control: 'boolean', + description: 'Отключает взаимодействие', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + invalid: { + control: 'boolean', + description: 'Невалидное состояние', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + readonly: { + 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' }, + }, + }, + min: { + control: 'number', + description: 'Минимальное значение', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'number' }, + }, + }, + max: { + control: 'number', + description: 'Максимальное значение', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'number' }, + }, + }, + step: { + control: 'number', + description: 'Шаг изменения значения', + table: { + category: 'Props', + defaultValue: { summary: '1' }, + type: { summary: 'number' }, + }, + }, + useGrouping: { + control: 'boolean', + description: 'Использовать разделитель групп разрядов', + table: { + category: 'Props', + defaultValue: { summary: 'true' }, + type: { summary: 'boolean' }, + }, + }, + prefix: { + control: 'text', + description: 'Текст перед значением', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'string' }, + }, + }, + suffix: { + control: 'text', + description: 'Текст после значения', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'string' }, + }, + }, + // Hidden computed props + modelValue: { table: { disable: true } }, + + // ── Events ─────────────────────────────────────────────── + onInput: { + control: false, + description: 'Событие при изменении значения', + table: { + category: 'Events', + type: { summary: 'EventEmitter<{ value: number | null }>' }, + }, + }, + }, + args: { + placeholder: 'Введите число...', + showButtons: false, + buttonLayout: 'stacked', + mode: 'decimal', + disabled: false, + invalid: false, + readonly: false, + fluid: false, + step: 1, + useGrouping: true, + }, +}; + +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.showButtons) parts.push(`[showButtons]="true"`); + if (args.buttonLayout && args.buttonLayout !== 'stacked') parts.push(`buttonLayout="${args.buttonLayout}"`); + if (args.mode && args.mode !== 'decimal') parts.push(`mode="${args.mode}"`); + if (args.currency) parts.push(`currency="${args.currency}"`); + if (args.locale) parts.push(`locale="${args.locale}"`); + if (args.disabled) parts.push(`[disabled]="true"`); + if (args.invalid) parts.push(`[invalid]="true"`); + if (args.readonly) parts.push(`[readonly]="true"`); + if (args.fluid) parts.push(`[fluid]="true"`); + if (args.min != null) parts.push(`[min]="${args.min}"`); + if (args.max != null) parts.push(`[max]="${args.max}"`); + if (args.step && args.step !== 1) parts.push(`[step]="${args.step}"`); + if (args.prefix) parts.push(`prefix="${args.prefix}"`); + if (args.suffix) parts.push(`suffix="${args.suffix}"`); + if (!args.useGrouping) parts.push(`[useGrouping]="false"`); + parts.push(`[(ngModel)]="value"`); + + const template = ``; + + return { props: { ...args, value: null }, template }; + }, + parameters: { + docs: { + description: { + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.', + }, + }, + }, +}; + +// ── Re-exports from example components ──────────────────────────────────── +export { Currency, Buttons, Disabled, FloatLabelStory as FloatLabel }; From 3cdc5eaf1dd877cbae4032cdd0bf25396cb2a8be Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Fri, 17 Apr 2026 13:05:30 +0700 Subject: [PATCH 2/8] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D1=80=D0=B0=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inputnumber/inputnumber.component.ts | 17 ++++++++++++++++- .../tokens/components/inputnumber.ts | 7 ++++++- src/prime-preset/tokens/tokens.json | 2 +- .../inputnumber/inputnumber.stories.ts | 14 ++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/lib/components/inputnumber/inputnumber.component.ts b/src/lib/components/inputnumber/inputnumber.component.ts index d9a4c435..4ca5bf39 100644 --- a/src/lib/components/inputnumber/inputnumber.component.ts +++ b/src/lib/components/inputnumber/inputnumber.component.ts @@ -1,14 +1,16 @@ import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core'; import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NgClass } from '@angular/common'; import { InputNumber } from 'primeng/inputnumber'; import { SharedModule } from 'primeng/api'; +export type InputNumberSize = 'small' | 'base' | 'large' | 'xlarge'; export type InputNumberButtonLayout = 'stacked' | 'horizontal' | 'vertical'; @Component({ selector: 'input-number', standalone: true, - imports: [InputNumber, SharedModule, FormsModule], + imports: [InputNumber, SharedModule, FormsModule, NgClass], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -18,6 +20,8 @@ export type InputNumberButtonLayout = 'stacked' | 'horizontal' | 'vertical'; ], template: ` void = () => {}; onTouched: () => 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-inputnumber-xlg': this.size === 'xlarge' }; + } + onModelChange(value: number | null): void { this.modelValue = value; this._onChange(value); diff --git a/src/prime-preset/tokens/components/inputnumber.ts b/src/prime-preset/tokens/components/inputnumber.ts index f28e4fb7..4be65a84 100644 --- a/src/prime-preset/tokens/components/inputnumber.ts +++ b/src/prime-preset/tokens/components/inputnumber.ts @@ -15,8 +15,13 @@ export const inputnumberCss = ({ dt }: { dt: (token: string) => string }): strin color: ${dt('inputtext.root.disabledColor')}; } +/* ─── FloatLabel: кнопки на полную высоту поля ─── */ +.p-floatlabel:has(.p-inputnumber-horizontal) .p-inputnumber-button { + align-self: stretch; +} + /* ─── Extra Large ─── */ -.p-inputnumber-input[data-p~="xlarge"] { +.p-inputnumber.p-inputnumber-xlg .p-inputnumber-input { font-size: ${dt('inputtext.extend.extXlg.fontSize')}; padding: ${dt('inputtext.extend.extXlg.paddingY')} ${dt('inputtext.extend.extXlg.paddingX')}; } diff --git a/src/prime-preset/tokens/tokens.json b/src/prime-preset/tokens/tokens.json index 6fd82189..d8078a4a 100644 --- a/src/prime-preset/tokens/tokens.json +++ b/src/prime-preset/tokens/tokens.json @@ -3091,7 +3091,7 @@ "transitionDuration": "{form.transitionDuration}" }, "button": { - "width": "{form.width.300}", + "width": "{form.size.600}", "borderRadius": "{form.borderRadius.200}", "verticalPadding": "{form.padding.300}" } diff --git a/src/stories/components/inputnumber/inputnumber.stories.ts b/src/stories/components/inputnumber/inputnumber.stories.ts index 54c02273..7c37fc85 100644 --- a/src/stories/components/inputnumber/inputnumber.stories.ts +++ b/src/stories/components/inputnumber/inputnumber.stories.ts @@ -35,6 +35,16 @@ import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; }, argTypes: { // ── Props ──────────────────────────────────────────────── + size: { + control: 'select', + options: ['small', 'base', 'large', 'xlarge'], + description: 'Размер компонента', + table: { + category: 'Props', + defaultValue: { summary: "'base'" }, + type: { summary: "'small' | 'base' | 'large' | 'xlarge'" }, + }, + }, placeholder: { control: 'text', description: 'Подсказка при пустом поле', @@ -183,6 +193,8 @@ import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; }, // Hidden computed props modelValue: { table: { disable: true } }, + primeSize: { table: { disable: true } }, + sizeClass: { table: { disable: true } }, // ── Events ─────────────────────────────────────────────── onInput: { @@ -195,6 +207,7 @@ import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; }, }, args: { + size: 'base', placeholder: 'Введите число...', showButtons: false, buttonLayout: 'stacked', @@ -217,6 +230,7 @@ export const Default: Story = { render: (args) => { const parts: string[] = []; + if (args.size && args.size !== 'base') parts.push(`size="${args.size}"`); if (args.placeholder) parts.push(`placeholder="${args.placeholder}"`); if (args.showButtons) parts.push(`[showButtons]="true"`); if (args.buttonLayout && args.buttonLayout !== 'stacked') parts.push(`buttonLayout="${args.buttonLayout}"`); From f1f83f9d14813163ceaa16d15d9e2b8840e55250 Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Fri, 17 Apr 2026 15:05:28 +0700 Subject: [PATCH 3/8] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=86=20=D0=B4=D0=BB=D1=8F=20=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=81=20Buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/inputnumber/inputnumber.component.ts | 10 +++++----- src/prime-preset/tokens/components/inputnumber.ts | 5 +++++ .../components/inputnumber/inputnumber.stories.ts | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/components/inputnumber/inputnumber.component.ts b/src/lib/components/inputnumber/inputnumber.component.ts index 4ca5bf39..4dd3e4e5 100644 --- a/src/lib/components/inputnumber/inputnumber.component.ts +++ b/src/lib/components/inputnumber/inputnumber.component.ts @@ -21,7 +21,7 @@ export type InputNumberButtonLayout = 'stacked' | 'horizontal' | 'vertical'; template: ` void = () => {}; onTouched: () => void = () => {}; - get primeSize(): 'small' | 'large' | undefined { - if (this.size === 'small') return 'small'; - if (this.size === 'large' || this.size === 'xlarge') return 'large'; - return undefined; + get inputSizeClass(): string { + if (this.size === 'small') return 'p-inputtext-sm'; + if (this.size === 'large' || this.size === 'xlarge') return 'p-inputtext-lg'; + return ''; } get sizeClass(): Record { diff --git a/src/prime-preset/tokens/components/inputnumber.ts b/src/prime-preset/tokens/components/inputnumber.ts index 4be65a84..dc4f5f57 100644 --- a/src/prime-preset/tokens/components/inputnumber.ts +++ b/src/prime-preset/tokens/components/inputnumber.ts @@ -7,6 +7,11 @@ export const inputnumberCss = ({ dt }: { dt: (token: string) => string }): strin .p-inputnumber-horizontal .p-inputnumber-button { min-height: ${dt('inputnumber.extend.extButton.height')}; + border: ${dt('inputnumber.extend.borderWidth')} solid ${dt('inputnumber.button.borderColor')}; +} + +.p-inputnumber-horizontal .p-inputnumber-decrement-button { + border-right: none; } /* ─── Disabled состояние ─── */ diff --git a/src/stories/components/inputnumber/inputnumber.stories.ts b/src/stories/components/inputnumber/inputnumber.stories.ts index 7c37fc85..9a34f283 100644 --- a/src/stories/components/inputnumber/inputnumber.stories.ts +++ b/src/stories/components/inputnumber/inputnumber.stories.ts @@ -193,7 +193,7 @@ import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; }, // Hidden computed props modelValue: { table: { disable: true } }, - primeSize: { table: { disable: true } }, + inputSizeClass: { table: { disable: true } }, sizeClass: { table: { disable: true } }, // ── Events ─────────────────────────────────────────────── From 48ba27a5a10ca53e6b9d233eb22a98697df571fc Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Tue, 21 Apr 2026 23:36:48 +0700 Subject: [PATCH 4/8] =?UTF-8?q?inputnumber:=20box-shadow=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D1=84=D0=BE=D0=BA=D1=83=D1=81=D0=B5,=20invalid=20bord?= =?UTF-8?q?er=20=D0=BF=D1=80=D0=B8=20focus,=20NgControl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inputnumber/inputnumber.component.ts | 21 ++++++++++++++----- .../tokens/components/inputnumber.ts | 11 ++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/lib/components/inputnumber/inputnumber.component.ts b/src/lib/components/inputnumber/inputnumber.component.ts index 4dd3e4e5..2a1dafe0 100644 --- a/src/lib/components/inputnumber/inputnumber.component.ts +++ b/src/lib/components/inputnumber/inputnumber.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core'; -import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Component, Input, Output, EventEmitter, forwardRef, inject, Injector, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms'; import { NgClass } from '@angular/common'; import { InputNumber } from 'primeng/inputnumber'; import { SharedModule } from 'primeng/api'; @@ -59,7 +59,14 @@ export type InputNumberButtonLayout = 'stacked' | 'horizontal' | 'vertical'; `, }) -export class InputNumberComponent implements ControlValueAccessor { +export class InputNumberComponent 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() size: InputNumberSize = 'base'; @Input() showButtons = false; @Input() buttonLayout: InputNumberButtonLayout = 'stacked'; @@ -67,8 +74,6 @@ export class InputNumberComponent implements ControlValueAccessor { @Input() currency: string | undefined; @Input() locale: string | undefined; @Input() placeholder = ''; - @Input() disabled = false; - @Input() invalid = false; @Input() readonly = false; @Input() fluid = false; @Input() min: number | undefined; @@ -82,6 +87,12 @@ export class InputNumberComponent implements ControlValueAccessor { @Input() incrementButtonIcon: string | undefined; @Input() decrementButtonIcon: string | undefined; + disabled = false; + + get invalid(): boolean { + return this._ngControl?.invalid ?? false; + } + @Output() onInput = new EventEmitter<{ value: number | null }>(); modelValue: number | null = null; diff --git a/src/prime-preset/tokens/components/inputnumber.ts b/src/prime-preset/tokens/components/inputnumber.ts index dc4f5f57..ee036c31 100644 --- a/src/prime-preset/tokens/components/inputnumber.ts +++ b/src/prime-preset/tokens/components/inputnumber.ts @@ -14,6 +14,17 @@ export const inputnumberCss = ({ dt }: { dt: (token: string) => string }): strin border-right: none; } +/* ─── Focus ─── */ +.p-inputnumber .p-inputnumber-input:enabled:focus { + box-shadow: 0 0 0 ${dt('inputtext.focusRing.width')} ${dt('inputtext.focusRing.color')}; +} + +/* ─── Invalid + Focus ─── */ +.p-inputnumber.p-invalid .p-inputnumber-input:focus { + border-color: ${dt('inputtext.root.invalidBorderColor')}; + box-shadow: 0 0 0 1px ${dt('inputtext.root.invalidBorderColor')}; +} + /* ─── Disabled состояние ─── */ .p-inputnumber-horizontal:has(.p-inputnumber-input:disabled) .p-inputnumber-button { background: ${dt('inputtext.root.disabledBackground')}; From 4b749f6137818a798591e4cc625b64525c5b5160 Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Tue, 21 Apr 2026 23:36:53 +0700 Subject: [PATCH 5/8] =?UTF-8?q?inputnumber=20stories=20Buttons/Disabled/Fl?= =?UTF-8?q?oatLabel:=20formControl,=20source.code=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../examples/inputnumber-buttons.component.ts | 46 ++++++++++++------ .../inputnumber-disabled.component.ts | 47 +++++++++++++------ .../inputnumber-float-label.component.ts | 22 ++++----- 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts b/src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts index b366e5f8..9f774228 100644 --- a/src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts +++ b/src/stories/components/inputnumber/examples/inputnumber-buttons.component.ts @@ -1,3 +1,4 @@ +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { StoryObj } from '@storybook/angular'; import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; @@ -5,17 +6,27 @@ type Story = StoryObj; export const Buttons: Story = { name: 'Buttons', - render: (args) => ({ - props: { ...args, value: null }, - template: ` - - `, - }), - args: {}, + render: () => { + const control = new FormControl(null); + return { + props: { control }, + template: ` + + `, + }; + }, + decorators: [ + (story: any) => ({ + ...story(), + moduleMetadata: { + imports: [InputNumberComponent, ReactiveFormsModule], + }, + }), + ], parameters: { controls: { disable: true }, docs: { @@ -25,11 +36,18 @@ export const Buttons: Story = { source: { language: 'ts', code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; -import { FormsModule } from '@angular/forms'; -// template: -// +@Component({ + standalone: true, + imports: [InputNumberComponent, ReactiveFormsModule], + template: \`\`, +}) +export class ButtonsExample { + control = new FormControl(null); +} `, }, }, diff --git a/src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts b/src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts index 19d14edb..a2d3f495 100644 --- a/src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts +++ b/src/stories/components/inputnumber/examples/inputnumber-disabled.component.ts @@ -1,3 +1,4 @@ +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { StoryObj } from '@storybook/angular'; import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; @@ -5,18 +6,27 @@ type Story = StoryObj; export const Disabled: Story = { name: 'Disabled', - render: (args) => ({ - props: { ...args, value: 42 }, - template: ` - - `, - }), - args: {}, + render: () => { + const control = new FormControl({ value: 42, disabled: true }); + return { + props: { control }, + template: ` + + `, + }; + }, + decorators: [ + (story: any) => ({ + ...story(), + moduleMetadata: { + imports: [InputNumberComponent, ReactiveFormsModule], + }, + }), + ], parameters: { controls: { disable: true }, docs: { @@ -26,11 +36,18 @@ export const Disabled: Story = { source: { language: 'ts', code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; -import { FormsModule } from '@angular/forms'; -// template: -// +@Component({ + standalone: true, + imports: [InputNumberComponent, ReactiveFormsModule], + template: \`\`, +}) +export class DisabledExample { + control = new FormControl({ value: 42, disabled: true }); +} `, }, }, diff --git a/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts b/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts index d85ef06b..532093fb 100644 --- a/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts +++ b/src/stories/components/inputnumber/examples/inputnumber-float-label.component.ts @@ -1,5 +1,5 @@ -import { Component } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { StoryObj } from '@storybook/angular'; import { InputNumber } from 'primeng/inputnumber'; import { FloatLabel } from 'primeng/floatlabel'; @@ -8,7 +8,7 @@ import { SharedModule } from 'primeng/api'; const template = `
- + @@ -25,12 +25,13 @@ const styles = ''; @Component({ selector: 'app-inputnumber-float-label', standalone: true, - imports: [InputNumber, FloatLabel, FormsModule, SharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [InputNumber, FloatLabel, ReactiveFormsModule, SharedModule], template, styles, }) export class InputNumberFloatLabelComponent { - value: number | null = null; + control = new FormControl(null); } export const FloatLabelStory: StoryObj = { @@ -49,18 +50,17 @@ export const FloatLabelStory: StoryObj = { language: 'ts', code: ` import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { InputNumber } from 'primeng/inputnumber'; import { FloatLabel } from 'primeng/floatlabel'; import { SharedModule } from 'primeng/api'; -import { FormsModule } from '@angular/forms'; @Component({ - selector: 'app-inputnumber-float-label', standalone: true, - imports: [InputNumber, FloatLabel, FormsModule, SharedModule], + imports: [InputNumber, FloatLabel, ReactiveFormsModule, SharedModule], template: \` - + @@ -72,8 +72,8 @@ import { FormsModule } from '@angular/forms'; \`, }) -export class InputNumberFloatLabelComponent { - value: number | null = null; +export class InputNumberFloatLabelExample { + control = new FormControl(null); } `, }, From bdbe29aee9669dd2c8cb4dbb270f94befff9582e Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Tue, 21 Apr 2026 23:36:59 +0700 Subject: [PATCH 6/8] =?UTF-8?q?inputnumber=20story=20Currency:=20mode=3Dcu?= =?UTF-8?q?rrency=20=E2=86=92=20suffix=3D'=20=E2=82=BD'=20(=D0=B1=D0=B0?= =?UTF-8?q?=D0=B3=20PrimeNG=20=D1=81=20=D0=BA=D0=B0=D1=80=D0=B5=D1=82?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inputnumber-currency.component.ts | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/stories/components/inputnumber/examples/inputnumber-currency.component.ts b/src/stories/components/inputnumber/examples/inputnumber-currency.component.ts index 186e3a9b..6bb38c49 100644 --- a/src/stories/components/inputnumber/examples/inputnumber-currency.component.ts +++ b/src/stories/components/inputnumber/examples/inputnumber-currency.component.ts @@ -1,3 +1,4 @@ +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { StoryObj } from '@storybook/angular'; import { InputNumberComponent } from '../../../../lib/components/inputnumber/inputnumber.component'; @@ -5,36 +6,48 @@ type Story = StoryObj; export const Currency: Story = { name: 'Currency', - render: (args) => ({ - props: { ...args, value: null }, - template: ` - - `, - }), - args: { - mode: 'currency', - currency: 'RUB', - locale: 'ru-RU', + render: () => { + const control = new FormControl(null); + return { + props: { control }, + template: ` + + `, + }; }, + decorators: [ + (story: any) => ({ + ...story(), + moduleMetadata: { + imports: [InputNumberComponent, ReactiveFormsModule], + }, + }), + ], parameters: { controls: { disable: true }, docs: { description: { - story: 'Форматирование значения как валюты (рубли). Используются `mode="currency"`, `currency="RUB"` и `locale="ru-RU"`.', + story: 'Форматирование значения как валюты через `suffix`. Режим `mode="currency"` не используется из-за известного бага PrimeNG с кареткой.', }, source: { language: 'ts', code: ` +import { Component } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; -import { FormsModule } from '@angular/forms'; -// template: -// +@Component({ + standalone: true, + imports: [InputNumberComponent, ReactiveFormsModule], + template: \`\`, +}) +export class CurrencyExample { + control = new FormControl(null); +} `, }, }, From 397206cedd8bc671b8c35495fed292265b15afbb Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Tue, 21 Apr 2026 23:37:05 +0700 Subject: [PATCH 7/8] =?UTF-8?q?inputnumber=20stories:=20minFractionDigits/?= =?UTF-8?q?maxFractionDigits=20=D0=B2=20argTypes,=20Default=20=E2=86=92=20?= =?UTF-8?q?formControl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inputnumber/inputnumber.stories.ts | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/stories/components/inputnumber/inputnumber.stories.ts b/src/stories/components/inputnumber/inputnumber.stories.ts index 9a34f283..1f2d9d20 100644 --- a/src/stories/components/inputnumber/inputnumber.stories.ts +++ b/src/stories/components/inputnumber/inputnumber.stories.ts @@ -1,12 +1,12 @@ import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; -import { FormsModule } from '@angular/forms'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { InputNumberComponent } from '../../../lib/components/inputnumber/inputnumber.component'; import { InputNumberFloatLabelComponent, FloatLabelStory } from './examples/inputnumber-float-label.component'; import { Currency } from './examples/inputnumber-currency.component'; import { Buttons } from './examples/inputnumber-buttons.component'; import { Disabled } from './examples/inputnumber-disabled.component'; -type InputNumberArgs = InputNumberComponent; +type InputNumberArgs = InputNumberComponent & { disabled: boolean; invalid: boolean }; const meta: Meta = { title: 'Components/Form/InputNumber', @@ -16,7 +16,7 @@ const meta: Meta = { moduleMetadata({ imports: [ InputNumberComponent, - FormsModule, + ReactiveFormsModule, InputNumberFloatLabelComponent, ], }), @@ -103,7 +103,7 @@ import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; }, disabled: { control: 'boolean', - description: 'Отключает взаимодействие', + description: 'Отключает взаимодействие — управляется через FormControl', table: { category: 'Props', defaultValue: { summary: 'false' }, @@ -112,7 +112,7 @@ import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; }, invalid: { control: 'boolean', - description: 'Невалидное состояние', + description: 'Невалидное состояние — управляется через FormControl', table: { category: 'Props', defaultValue: { summary: 'false' }, @@ -191,6 +191,24 @@ import { InputNumberComponent } from '@cdek-it/angular-ui-kit'; type: { summary: 'string' }, }, }, + minFractionDigits: { + control: 'number', + description: 'Минимальное количество знаков после запятой', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'number' }, + }, + }, + maxFractionDigits: { + control: 'number', + description: 'Максимальное количество знаков после запятой', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'number' }, + }, + }, // Hidden computed props modelValue: { table: { disable: true } }, inputSizeClass: { table: { disable: true } }, @@ -237,8 +255,6 @@ export const Default: Story = { if (args.mode && args.mode !== 'decimal') parts.push(`mode="${args.mode}"`); if (args.currency) parts.push(`currency="${args.currency}"`); if (args.locale) parts.push(`locale="${args.locale}"`); - if (args.disabled) parts.push(`[disabled]="true"`); - if (args.invalid) parts.push(`[invalid]="true"`); if (args.readonly) parts.push(`[readonly]="true"`); if (args.fluid) parts.push(`[fluid]="true"`); if (args.min != null) parts.push(`[min]="${args.min}"`); @@ -246,12 +262,18 @@ export const Default: Story = { if (args.step && args.step !== 1) parts.push(`[step]="${args.step}"`); if (args.prefix) parts.push(`prefix="${args.prefix}"`); if (args.suffix) parts.push(`suffix="${args.suffix}"`); + if (args.minFractionDigits != null) parts.push(`[minFractionDigits]="${args.minFractionDigits}"`); + if (args.maxFractionDigits != null) parts.push(`[maxFractionDigits]="${args.maxFractionDigits}"`); if (!args.useGrouping) parts.push(`[useGrouping]="false"`); - parts.push(`[(ngModel)]="value"`); - const template = ``; + const validators = []; + if (args.invalid) validators.push(Validators.required); + + const control = new FormControl({ value: null, disabled: args.disabled }, validators); + + const template = ``; - return { props: { ...args, value: null }, template }; + return { props: { ...args, control }, template }; }, parameters: { docs: { From d59162d20d16628b376155b680f3a8ac3807a36a Mon Sep 17 00:00:00 2001 From: Danil Khaliulin Date: Tue, 21 Apr 2026 23:38:26 +0700 Subject: [PATCH 8/8] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D1=91?= =?UTF-8?q?=D0=BD=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 808f9bdc..ce8e3c03 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,5 @@ src/assets/components/themes /storybook-static /debug-storybook.log /documentation.json + +.claude/* \ No newline at end of file