diff --git a/src/lib/components/select-button/select-button.component.ts b/src/lib/components/select-button/select-button.component.ts
new file mode 100644
index 00000000..ab8009d9
--- /dev/null
+++ b/src/lib/components/select-button/select-button.component.ts
@@ -0,0 +1,91 @@
+import { Component, EventEmitter, Input, Optional, Output, Self } from '@angular/core';
+import { NgClass } from '@angular/common';
+import { ControlValueAccessor, FormsModule, NgControl } from '@angular/forms';
+import { SelectButton } from 'primeng/selectbutton';
+import { SharedModule } from 'primeng/api';
+
+export interface SelectButtonItem {
+ label: string;
+ value: string;
+ icon?: string;
+ disabled?: boolean;
+}
+
+@Component({
+ selector: 'select-button',
+ standalone: true,
+ imports: [SelectButton, SharedModule, FormsModule, NgClass],
+ template: `
+
+
+ @if (item['icon']) {
+
+ }
+ {{ item[optionLabel] }}
+
+
+ `,
+})
+export class SelectButtonComponent implements ControlValueAccessor {
+ @Input() options: any[] = [];
+ @Input() optionLabel = 'label';
+ @Input() optionValue = 'value';
+ @Input() optionDisabled = 'disabled';
+ @Input() size: 'default' | 'sm' | 'lg' | 'xlg' = 'default';
+ @Input() multiple = false;
+ @Input() allowEmpty = true;
+
+ @Output() valueChange = new EventEmitter();
+
+ value: string | string[] = '';
+
+ private _disabled = false;
+ private onChange = (_: string | string[]) => {};
+ private onTouched = () => {};
+
+ constructor(@Optional() @Self() private ngControl: NgControl) {
+ if (ngControl) ngControl.valueAccessor = this;
+ }
+
+ get isDisabled(): boolean {
+ return this._disabled;
+ }
+
+ get sizeClass(): string {
+ return this.size === 'default' ? '' : `p-selectbutton-${this.size}`;
+ }
+
+ writeValue(value: string | string[]): void {
+ this.value = value ?? '';
+ }
+
+ registerOnChange(fn: (value: string | string[]) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this._disabled = isDisabled;
+ }
+
+ onValueChange(newValue: string | string[]): void {
+ this.value = newValue;
+ this.onChange(newValue);
+ this.onTouched();
+ this.valueChange.emit(newValue);
+ }
+}
diff --git a/src/prime-preset/map-tokens.ts b/src/prime-preset/map-tokens.ts
index d0a44965..5d945f1e 100644
--- a/src/prime-preset/map-tokens.ts
+++ b/src/prime-preset/map-tokens.ts
@@ -13,6 +13,7 @@ import { tagCss } from './tokens/components/tag';
import { timelineCss } from './tokens/components/timeline';
import { tooltipCss } from './tokens/components/tooltip';
import { megamenuCss } from './tokens/components/megamenu';
+import { selectbuttonCss } from './tokens/components/selectbutton';
const presetTokens: Preset = {
primitive: tokens.primitive as unknown as AuraBaseDesignTokens['primitive'],
@@ -59,6 +60,10 @@ const presetTokens: Preset = {
...(tokens.components.megamenu as unknown as ComponentsDesignTokens['megamenu']),
css: megamenuCss,
},
+ selectbutton: {
+ ...(tokens.components.selectbutton as unknown as ComponentsDesignTokens['selectbutton']),
+ css: selectbuttonCss,
+ },
} as ComponentsDesignTokens,
};
diff --git a/src/prime-preset/tokens/components/selectbutton.ts b/src/prime-preset/tokens/components/selectbutton.ts
new file mode 100644
index 00000000..33880591
--- /dev/null
+++ b/src/prime-preset/tokens/components/selectbutton.ts
@@ -0,0 +1,90 @@
+export const selectbuttonCss = ({ dt }: { dt: (token: string) => string }): string => `
+.p-selectbutton.p-component {
+ background: ${dt('selectbutton.colorScheme.light.extend.background')};
+ padding: ${dt('selectbutton.extend.paddingY')} ${dt('selectbutton.extend.paddingX')};
+ gap: ${dt('selectbutton.extend.gap')};
+ font-family: ${dt('fonts.fontFamily.heading')};
+ font-weight: ${dt('fonts.fontWeight.demibold')};
+}
+
+.p-selectbutton.p-component .p-togglebutton.p-component {
+ font-family: ${dt('fonts.fontFamily.heading')};
+ font-weight: ${dt('fonts.fontWeight.demibold')};
+ line-height: ${dt('fonts.lineHeight.500')};
+ height: ${dt('controls.iconOnly.700')};
+ border-radius: ${dt('selectbutton.extend.ext.borderRadius')};
+}
+
+.p-selectbutton.p-component .p-togglebutton .p-togglebutton-label,
+.p-selectbutton.p-component .p-togglebutton .p-togglebutton-content > span {
+ line-height: ${dt('fonts.lineHeight.400')};
+}
+
+.p-selectbutton.p-component .p-togglebutton.p-togglebutton-checked.p-component,
+.p-selectbutton.p-component .p-togglebutton.p-togglebutton-checked.p-component:hover {
+ background: ${dt('selectbutton.extend.checkedBackground')};
+ border-radius: ${dt('selectbutton.extend.ext.borderRadius')};
+ border-color: ${dt('selectbutton.extend.checkedBorderColor')};
+ color: ${dt('selectbutton.extend.checkedColor')};
+}
+
+.p-selectbutton.p-component .p-togglebutton.p-component:not(:disabled):not(.p-togglebutton-checked):hover {
+ border-radius: ${dt('selectbutton.extend.ext.borderRadius')};
+ border-color: ${dt('togglebutton.extend.hoverBorderColor')};
+}
+
+/* Size: sm */
+.p-selectbutton.p-selectbutton-sm.p-component .p-togglebutton.p-component {
+ line-height: ${dt('fonts.lineHeight.300')};
+ height: ${dt('controls.iconOnly.600')};
+}
+
+.p-selectbutton.p-selectbutton-sm.p-component .p-togglebutton .p-togglebutton-label,
+.p-selectbutton.p-selectbutton-sm.p-component .p-togglebutton .p-togglebutton-content > span {
+ line-height: ${dt('fonts.lineHeight.250')};
+}
+
+.p-selectbutton.p-selectbutton-sm.p-component .p-togglebutton .p-togglebutton-icon,
+.p-selectbutton.p-selectbutton-sm.p-component .p-togglebutton i {
+ font-size: ${dt('selectbutton.extend.iconSize.sm')};
+}
+
+/* Size: md (base) */
+.p-selectbutton.p-component:not(.p-selectbutton-sm):not(.p-selectbutton-lg):not(.p-selectbutton-xlg) .p-togglebutton .p-togglebutton-icon,
+.p-selectbutton.p-component:not(.p-selectbutton-sm):not(.p-selectbutton-lg):not(.p-selectbutton-xlg) .p-togglebutton i {
+ font-size: ${dt('selectbutton.extend.iconSize.md')};
+}
+
+/* Size: lg */
+.p-selectbutton.p-selectbutton-lg.p-component .p-togglebutton.p-component {
+ line-height: ${dt('fonts.lineHeight.550')};
+ height: ${dt('controls.iconOnly.850')};
+}
+
+.p-selectbutton.p-selectbutton-lg.p-component .p-togglebutton .p-togglebutton-label,
+.p-selectbutton.p-selectbutton-lg.p-component .p-togglebutton .p-togglebutton-content > span {
+ line-height: ${dt('fonts.lineHeight.550')};
+}
+
+.p-selectbutton.p-selectbutton-lg.p-component .p-togglebutton .p-togglebutton-icon,
+.p-selectbutton.p-selectbutton-lg.p-component .p-togglebutton i {
+ font-size: ${dt('selectbutton.extend.iconSize.lg')};
+}
+
+/* Size: xlg */
+.p-selectbutton.p-selectbutton-xlg.p-component .p-togglebutton.p-component {
+ font-size: ${dt('fonts.fontSize.600')};
+ line-height: ${dt('fonts.lineHeight.550')};
+ height: ${dt('controls.iconOnly.900')};
+}
+
+.p-selectbutton.p-selectbutton-xlg.p-component .p-togglebutton .p-togglebutton-label,
+.p-selectbutton.p-selectbutton-xlg.p-component .p-togglebutton .p-togglebutton-content > span {
+ line-height: ${dt('fonts.lineHeight.700')};
+}
+
+.p-selectbutton.p-selectbutton-xlg.p-component .p-togglebutton .p-togglebutton-icon,
+.p-selectbutton.p-selectbutton-xlg.p-component .p-togglebutton i {
+ font-size: ${dt('selectbutton.extend.iconSize.xlg')};
+}
+`;
diff --git a/src/stories/components/select-button/examples/select-button-disabled.component.ts b/src/stories/components/select-button/examples/select-button-disabled.component.ts
new file mode 100644
index 00000000..1e31d5c5
--- /dev/null
+++ b/src/stories/components/select-button/examples/select-button-disabled.component.ts
@@ -0,0 +1,65 @@
+import { Component } from '@angular/core';
+import { ReactiveFormsModule, FormControl } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { SelectButtonComponent, SelectButtonItem } from '../../../../lib/components/select-button/select-button.component';
+
+const template = `
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-select-button-disabled',
+ standalone: true,
+ imports: [SelectButtonComponent, ReactiveFormsModule],
+ template,
+ styles,
+})
+export class SelectButtonDisabledComponent {
+ control = new FormControl({ value: '1', disabled: true });
+ options: SelectButtonItem[] = [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ];
+}
+
+export const Disabled: StoryObj = {
+ name: 'Disabled',
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: { story: 'Весь компонент отключён через `FormControl`.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { ReactiveFormsModule, FormControl } from '@angular/forms';
+import { SelectButtonComponent, SelectButtonItem } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-select-button-disabled',
+ standalone: true,
+ imports: [SelectButtonComponent, ReactiveFormsModule],
+ template: \`
+
+ \`,
+})
+export class SelectButtonDisabledComponent {
+ control = new FormControl({ value: '1', disabled: true });
+ options: SelectButtonItem[] = [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/select-button/examples/select-button-icons.component.ts b/src/stories/components/select-button/examples/select-button-icons.component.ts
new file mode 100644
index 00000000..d3d94974
--- /dev/null
+++ b/src/stories/components/select-button/examples/select-button-icons.component.ts
@@ -0,0 +1,65 @@
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { SelectButtonComponent, SelectButtonItem } from '../../../../lib/components/select-button/select-button.component';
+
+const template = `
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-select-button-icons',
+ standalone: true,
+ imports: [SelectButtonComponent, FormsModule],
+ template,
+ styles,
+})
+export class SelectButtonIconsComponent {
+ value = 'left';
+ options: SelectButtonItem[] = [
+ { label: 'Left', value: 'left', icon: 'ti ti-align-left' },
+ { label: 'Center', value: 'center', icon: 'ti ti-align-center' },
+ { label: 'Right', value: 'right', icon: 'ti ti-align-right' },
+ ];
+}
+
+export const WithIcons: StoryObj = {
+ name: 'With Icons',
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: { story: 'Опции с иконками — иконка отображается автоматически если задано поле `icon`.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { SelectButtonComponent, SelectButtonItem } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-select-button-icons',
+ standalone: true,
+ imports: [SelectButtonComponent, FormsModule],
+ template: \`
+
+ \`,
+})
+export class SelectButtonIconsComponent {
+ value = 'left';
+ options: SelectButtonItem[] = [
+ { label: 'Left', value: 'left', icon: 'ti ti-align-left' },
+ { label: 'Center', value: 'center', icon: 'ti ti-align-center' },
+ { label: 'Right', value: 'right', icon: 'ti ti-align-right' },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/select-button/examples/select-button-selected.component.ts b/src/stories/components/select-button/examples/select-button-selected.component.ts
new file mode 100644
index 00000000..22ff962a
--- /dev/null
+++ b/src/stories/components/select-button/examples/select-button-selected.component.ts
@@ -0,0 +1,65 @@
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { SelectButtonComponent, SelectButtonItem } from '../../../../lib/components/select-button/select-button.component';
+
+const template = `
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-select-button-selected',
+ standalone: true,
+ imports: [SelectButtonComponent, FormsModule],
+ template,
+ styles,
+})
+export class SelectButtonSelectedComponent {
+ value = '2';
+ options: SelectButtonItem[] = [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ];
+}
+
+export const Selected: StoryObj = {
+ name: 'Selected',
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: { story: 'Второй вариант выбран по умолчанию.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { SelectButtonComponent, SelectButtonItem } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-select-button-selected',
+ standalone: true,
+ imports: [SelectButtonComponent, FormsModule],
+ template: \`
+
+ \`,
+})
+export class SelectButtonSelectedComponent {
+ value = '2';
+ options: SelectButtonItem[] = [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/select-button/examples/select-button-semi-disabled.component.ts b/src/stories/components/select-button/examples/select-button-semi-disabled.component.ts
new file mode 100644
index 00000000..4d1f68dc
--- /dev/null
+++ b/src/stories/components/select-button/examples/select-button-semi-disabled.component.ts
@@ -0,0 +1,65 @@
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { StoryObj } from '@storybook/angular';
+import { SelectButtonComponent, SelectButtonItem } from '../../../../lib/components/select-button/select-button.component';
+
+const template = `
+
+
+
+`;
+const styles = '';
+
+@Component({
+ selector: 'app-select-button-semi-disabled',
+ standalone: true,
+ imports: [SelectButtonComponent, FormsModule],
+ template,
+ styles,
+})
+export class SelectButtonSemiDisabledComponent {
+ value = '1';
+ options: SelectButtonItem[] = [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2', disabled: true },
+ { label: 'Option 3', value: '3' },
+ ];
+}
+
+export const SemiDisabled: StoryObj = {
+ name: 'Semi-disabled',
+ render: () => ({
+ template: ``,
+ }),
+ parameters: {
+ controls: { disable: true },
+ docs: {
+ description: { story: 'Отдельная опция отключена через поле `disabled` в объекте опции.' },
+ source: {
+ language: 'ts',
+ code: `
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { SelectButtonComponent, SelectButtonItem } from '@cdek-it/angular-ui-kit';
+
+@Component({
+ selector: 'app-select-button-semi-disabled',
+ standalone: true,
+ imports: [SelectButtonComponent, FormsModule],
+ template: \`
+
+ \`,
+})
+export class SelectButtonSemiDisabledComponent {
+ value = '1';
+ options: SelectButtonItem[] = [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2', disabled: true },
+ { label: 'Option 3', value: '3' },
+ ];
+}
+ `,
+ },
+ },
+ },
+};
diff --git a/src/stories/components/select-button/select-button.stories.ts b/src/stories/components/select-button/select-button.stories.ts
new file mode 100644
index 00000000..39c89c03
--- /dev/null
+++ b/src/stories/components/select-button/select-button.stories.ts
@@ -0,0 +1,139 @@
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
+import { SelectButtonComponent } from '../../../lib/components/select-button/select-button.component';
+import { SelectButtonSelectedComponent, Selected as SelectedStory } from './examples/select-button-selected.component';
+import { SelectButtonDisabledComponent, Disabled as DisabledStory } from './examples/select-button-disabled.component';
+import { SelectButtonSemiDisabledComponent, SemiDisabled as SemiDisabledStory } from './examples/select-button-semi-disabled.component';
+import { SelectButtonIconsComponent, WithIcons as WithIconsStory } from './examples/select-button-icons.component';
+
+type SelectButtonArgs = SelectButtonComponent;
+
+const meta: Meta = {
+ title: 'Components/Form/SelectButton',
+ component: SelectButtonComponent,
+ tags: ['autodocs'],
+ decorators: [
+ moduleMetadata({
+ imports: [
+ SelectButtonComponent,
+ SelectButtonSelectedComponent,
+ SelectButtonDisabledComponent,
+ SelectButtonSemiDisabledComponent,
+ SelectButtonIconsComponent,
+ ],
+ }),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ component: `Группа кнопок-переключателей с поддержкой одиночного и множественного выбора.
+
+\`\`\`typescript
+import { SelectButtonComponent, SelectButtonItem } from '@cdek-it/angular-ui-kit';
+\`\`\``,
+ },
+ },
+ designTokens: { prefix: '--p-selectbutton' },
+ },
+ argTypes: {
+ value: {
+ control: 'text',
+ description: 'Текущее выбранное значение',
+ table: {
+ category: 'Props',
+ type: { summary: 'string | string[]' },
+ },
+ },
+ size: {
+ control: 'radio',
+ options: ['default', 'sm', 'lg', 'xlg'],
+ description: 'Размер компонента',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'default' },
+ type: { summary: "'default' | 'sm' | 'lg' | 'xlg'" },
+ },
+ },
+ multiple: {
+ control: 'boolean',
+ description: 'Разрешает выбор нескольких опций',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'false' },
+ type: { summary: 'boolean' },
+ },
+ },
+ allowEmpty: {
+ control: 'boolean',
+ description: 'Разрешает снятие выбора',
+ table: {
+ category: 'Props',
+ defaultValue: { summary: 'true' },
+ type: { summary: 'boolean' },
+ },
+ },
+ options: {
+ control: 'object',
+ description: 'Массив опций',
+ table: {
+ category: 'Props',
+ type: { summary: 'SelectButtonItem[]' },
+ },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ── Default ───────────────────────────────────────────────────────────────────
+
+export const Default: Story = {
+ name: 'Default',
+ render: (args) => {
+ const parts: string[] = [];
+
+ parts.push(`[(value)]="value"`);
+ parts.push(`[options]="options"`);
+ if (args.size && args.size !== 'default') parts.push(`size="${args.size}"`);
+ if (args.multiple) parts.push(`[multiple]="true"`);
+ if (!args.allowEmpty) parts.push(`[allowEmpty]="false"`);
+
+ const template = ``;
+
+ return { props: args, template };
+ },
+ args: {
+ value: '1',
+ options: [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ],
+ size: 'default',
+ multiple: false,
+ allowEmpty: true,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Базовый SelectButton. Используйте Controls для интерактивного изменения пропсов.',
+ },
+ },
+ },
+};
+
+// ── Selected ──────────────────────────────────────────────────────────────────
+
+export const Selected: Story = SelectedStory;
+
+// ── Disabled ──────────────────────────────────────────────────────────────────
+
+export const Disabled: Story = DisabledStory;
+
+// ── Semi-disabled ─────────────────────────────────────────────────────────────
+
+export const SemiDisabled: Story = SemiDisabledStory;
+
+// ── With Icons ────────────────────────────────────────────────────────────────
+
+export const WithIcons: Story = WithIconsStory;