diff --git a/src/lib/components/radiobutton/radiobutton.component.ts b/src/lib/components/radiobutton/radiobutton.component.ts
new file mode 100644
index 00000000..e7d36073
--- /dev/null
+++ b/src/lib/components/radiobutton/radiobutton.component.ts
@@ -0,0 +1,92 @@
+import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
+import { RadioButton, RadioButtonClickEvent } from 'primeng/radiobutton';
+
+export type RadiobuttonVariant = 'outlined' | 'filled';
+export type RadiobuttonSize = 'small' | 'base' | 'large';
+
+@Component({
+ selector: 'radiobutton',
+ standalone: true,
+ imports: [RadioButton, FormsModule],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => RadiobuttonComponent),
+ multi: true,
+ },
+ ],
+ template: `
+
+ `,
+})
+export class RadiobuttonComponent implements ControlValueAccessor {
+ @Input() value: any = null;
+ @Input() name: string | undefined = undefined;
+ @Input() disabled = false;
+ @Input() invalid = false;
+ @Input() variant: RadiobuttonVariant = 'outlined';
+ @Input() size: RadiobuttonSize = 'base';
+ @Input() inputId: string | undefined = undefined;
+ @Input() tabindex: number | undefined = undefined;
+ @Input() ariaLabel: string | undefined = undefined;
+ @Input() ariaLabelledBy: string | undefined = undefined;
+ @Input() autofocus = false;
+
+ @Output() onClick = new EventEmitter();
+ @Output() onFocus = new EventEmitter();
+ @Output() onBlur = new EventEmitter();
+
+ modelValue: any = null;
+
+ private _onChange: (value: any) => void = () => {};
+ private _onTouched: () => void = () => {};
+
+ get primeSize(): 'small' | 'large' | undefined {
+ if (this.size === 'small') return 'small';
+ if (this.size === 'large') return 'large';
+ return undefined;
+ }
+
+ get primeVariant(): 'filled' | undefined {
+ return this.variant === 'filled' ? 'filled' : undefined;
+ }
+
+ onClickHandler(event: RadioButtonClickEvent): void {
+ this._onChange(event.value);
+ this._onTouched();
+ this.onClick.emit(event);
+ }
+
+ writeValue(value: any): void {
+ this.modelValue = value;
+ }
+
+ registerOnChange(fn: (value: any) => 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 39627587..f7ec1c72 100644
--- a/src/prime-preset/map-tokens.ts
+++ b/src/prime-preset/map-tokens.ts
@@ -4,6 +4,7 @@ import type { AuraBaseDesignTokens } from '@primeuix/themes/aura/base';
import tokens from './tokens/tokens.json';
import { buttonCss } from './tokens/components/button';
+import { radiobuttonCss } from './tokens/components/radiobutton';
const presetTokens: Preset = {
primitive: tokens.primitive as unknown as AuraBaseDesignTokens['primitive'],
@@ -14,6 +15,10 @@ const presetTokens: Preset = {
...(tokens.components.button as unknown as ComponentsDesignTokens['button']),
css: buttonCss,
},
+ radiobutton: {
+ ...(tokens.components.radiobutton as unknown as ComponentsDesignTokens['radiobutton']),
+ css: radiobuttonCss,
+ },
} as ComponentsDesignTokens,
};
diff --git a/src/prime-preset/tokens/components/radiobutton.ts b/src/prime-preset/tokens/components/radiobutton.ts
new file mode 100644
index 00000000..975dce6f
--- /dev/null
+++ b/src/prime-preset/tokens/components/radiobutton.ts
@@ -0,0 +1,16 @@
+export const radiobuttonCss = ({ dt }: { dt: (token: string) => string }): string => `
+/* Focus ring с зеленым цветом для валидных состояний */
+.p-radiobutton:not(.p-disabled):not(.p-invalid):has(.p-radiobutton-input:focus-visible) .p-radiobutton-box,
+.p-radiobutton-checked:not(.p-disabled):not(.p-invalid):has(.p-radiobutton-input:focus-visible) .p-radiobutton-box {
+ outline: none;
+ box-shadow: 0 0 0 ${dt('radiobutton.focusRing.width')} ${dt('focusRing.extend.success')};
+}
+
+/* Focus ring с красным цветом для состояний с ошибкой */
+.p-radiobutton.p-invalid .p-radiobutton-box,
+.p-radiobutton.p-invalid:not(.p-disabled):has(.p-radiobutton-input:focus-visible) .p-radiobutton-box,
+.p-radiobutton-checked.p-invalid .p-radiobutton-box,
+.p-radiobutton-checked.p-invalid:not(.p-disabled):has(.p-radiobutton-input:focus-visible) .p-radiobutton-box {
+ box-shadow: 0 0 0 ${dt('radiobutton.focusRing.width')} ${dt('focusRing.extend.invalid')};
+}
+`;
diff --git a/src/prime-preset/tokens/tokens.json b/src/prime-preset/tokens/tokens.json
index f9ea56a8..4b767ce7 100644
--- a/src/prime-preset/tokens/tokens.json
+++ b/src/prime-preset/tokens/tokens.json
@@ -4071,7 +4071,7 @@
"height": "{form.size.350}"
},
"icon": {
- "size": "{form.icon.200}",
+ "size": "0.7rem",
"checkedColor": "{text.extend.colorInverted}",
"checkedHoverColor": "{text.extend.colorInverted}",
"disabledColor": "{text.mutedColor}",
diff --git a/src/stories/components/radiobutton/examples/radiobutton-disabled.component.ts b/src/stories/components/radiobutton/examples/radiobutton-disabled.component.ts
new file mode 100644
index 00000000..497053dd
--- /dev/null
+++ b/src/stories/components/radiobutton/examples/radiobutton-disabled.component.ts
@@ -0,0 +1,59 @@
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { RadiobuttonComponent } from '../../../../lib/components/radiobutton/radiobutton.component';
+
+const template = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+@Component({
+ selector: 'app-radiobutton-disabled',
+ standalone: true,
+ imports: [RadiobuttonComponent, FormsModule],
+ template,
+})
+export class RadiobuttonDisabledComponent {
+ selected = '2';
+}
+
+export const Disabled: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Заблокированное состояние радиокнопки.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { RadiobuttonComponent } from '@cdek-it/angular-ui-kit';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'app-radiobutton-disabled',
+ standalone: true,
+ imports: [RadiobuttonComponent, FormsModule],
+ template: \`
+
+
+ \`,
+})
+export class RadiobuttonDisabledComponent {
+ selected = '2';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/radiobutton/examples/radiobutton-group.component.ts b/src/stories/components/radiobutton/examples/radiobutton-group.component.ts
new file mode 100644
index 00000000..8bd36b0e
--- /dev/null
+++ b/src/stories/components/radiobutton/examples/radiobutton-group.component.ts
@@ -0,0 +1,67 @@
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { RadiobuttonComponent } from '../../../../lib/components/radiobutton/radiobutton.component';
+
+const template = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+@Component({
+ selector: 'app-radiobutton-group',
+ standalone: true,
+ imports: [RadiobuttonComponent, FormsModule],
+ template,
+})
+export class RadiobuttonGroupComponent {
+ selected = '1';
+}
+
+export const Group: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Группа радиокнопок для выбора одного варианта из нескольких.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { RadiobuttonComponent } from '@cdek-it/angular-ui-kit';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'app-radiobutton-group',
+ standalone: true,
+ imports: [RadiobuttonComponent, FormsModule],
+ template: \`
+
+
+
+
+
+
+ \`,
+})
+export class RadiobuttonGroupComponent {
+ selected = '1';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/radiobutton/examples/radiobutton-invalid.component.ts b/src/stories/components/radiobutton/examples/radiobutton-invalid.component.ts
new file mode 100644
index 00000000..430a3b93
--- /dev/null
+++ b/src/stories/components/radiobutton/examples/radiobutton-invalid.component.ts
@@ -0,0 +1,59 @@
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { RadiobuttonComponent } from '../../../../lib/components/radiobutton/radiobutton.component';
+
+const template = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+@Component({
+ selector: 'app-radiobutton-invalid',
+ standalone: true,
+ imports: [RadiobuttonComponent, FormsModule],
+ template,
+})
+export class RadiobuttonInvalidComponent {
+ selected = '2';
+}
+
+export const Invalid: StoryObj = {
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ description: { story: 'Невалидное состояние радиокнопки.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { RadiobuttonComponent } from '@cdek-it/angular-ui-kit';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'app-radiobutton-invalid',
+ standalone: true,
+ imports: [RadiobuttonComponent, FormsModule],
+ template: \`
+
+
+ \`,
+})
+export class RadiobuttonInvalidComponent {
+ selected = '2';
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/radiobutton/radiobutton.stories.ts b/src/stories/components/radiobutton/radiobutton.stories.ts
new file mode 100644
index 00000000..760cd599
--- /dev/null
+++ b/src/stories/components/radiobutton/radiobutton.stories.ts
@@ -0,0 +1,122 @@
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
+import { FormsModule } from '@angular/forms';
+import { RadiobuttonComponent } from '../../../lib/components/radiobutton/radiobutton.component';
+import { RadiobuttonGroupComponent, Group } from './examples/radiobutton-group.component';
+import { RadiobuttonInvalidComponent, Invalid } from './examples/radiobutton-invalid.component';
+import { RadiobuttonDisabledComponent, Disabled } from './examples/radiobutton-disabled.component';
+
+type RadiobuttonArgs = RadiobuttonComponent;
+
+const meta: Meta = {
+ title: 'Components/Form/RadioButton',
+ component: RadiobuttonComponent,
+ tags: ['autodocs'],
+ decorators: [
+ moduleMetadata({
+ imports: [
+ RadiobuttonComponent,
+ FormsModule,
+ RadiobuttonGroupComponent,
+ RadiobuttonInvalidComponent,
+ RadiobuttonDisabledComponent,
+ ]
+ })
+ ],
+ parameters: {
+ designTokens: { prefix: '--p-radiobutton' },
+ docs: {
+ description: {
+ component: `Компонент для выбора одного варианта из группы.`,
+ },
+ },
+ },
+ argTypes: {
+ // ── Props ────────────────────────────────────────────────
+ 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' },
+ },
+ },
+ variant: { table: { disable: true } },
+ // Hidden props
+ value: { table: { disable: true } },
+ name: { table: { disable: true } },
+ size: { table: { disable: true } },
+ inputId: { table: { disable: true } },
+ tabindex: { table: { disable: true } },
+ ariaLabel: { table: { disable: true } },
+ ariaLabelledBy: { table: { disable: true } },
+ autofocus: { table: { disable: true } },
+
+ // ── Events ───────────────────────────────────────────────
+ onClick: {
+ control: false,
+ description: 'Событие выбора радиокнопки',
+ table: {
+ category: 'Events',
+ type: { summary: 'EventEmitter' },
+ },
+ },
+ onFocus: {
+ control: false,
+ description: 'Событие фокуса',
+ table: {
+ category: 'Events',
+ type: { summary: 'EventEmitter' },
+ },
+ },
+ onBlur: {
+ control: false,
+ description: 'Событие потери фокуса',
+ table: {
+ category: 'Events',
+ type: { summary: 'EventEmitter' },
+ },
+ },
+ },
+ args: {
+ disabled: false,
+ invalid: false,
+ variant: 'outlined',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ── Default ──────────────────────────────────────────────────────────────────
+export const Default: Story = {
+ name: 'Default',
+ render: (args) => {
+ const parts: string[] = [`value="option1"`, `name="demo"`, `[(ngModel)]="selected"`];
+ if (args.disabled) parts.push(`[disabled]="true"`);
+ if (args.invalid) parts.push(`[invalid]="true"`);
+ if (args.variant && args.variant !== 'outlined') parts.push(`variant="${args.variant}"`);
+
+ const template = ``;
+ return { props: { ...args, selected: 'option1' }, template };
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.',
+ },
+ },
+ },
+};
+
+// ── Re-exports from example components ────────────────────────────────────
+export { Group, Invalid, Disabled };