diff --git a/docs/components-api/button.md b/docs/components-api/button.md index 0ce680ff..a056d2a8 100644 --- a/docs/components-api/button.md +++ b/docs/components-api/button.md @@ -4,15 +4,22 @@ > ✅ **Реализован**: `ExtraButtonComponent` (`@cdek-it/angular-ui-kit`) соответствует спецификации. -| Свойство | Описание | Типизация | -| --------------- | --------------------------------------------- | -------------------------------------------------- | -| `label` | текст на кнопке | `string` | -| `icon` | класс иконки tabler icon на кнопке | `string` | -| `iconPosition` | позиция иконки относительно текста | `left \| right` | -| `variant` | вариант отображения кнопки | `primary \| secondary \| tertiary \| text \| link` | -| `severity` | семантическое состояние кнопки | `base \| danger \| warning \| success \| info` | -| `size` | размер кнопки | `small \| base \| large \| xlarge` | -| `rounded` | скруглённая форма кнопки | `boolean` | -| `fluid` | растягивает кнопку на всю ширину контейнера | `boolean` | -| `disabled` | отключённое состояние кнопки | `boolean` | -| `loading` | состояние загрузки: спиннер, кнопка неактивна | `boolean` | +| Свойство | Описание | Типизация | +| --------------- | ------------------------------------------------ | ----------------------------------------------------------------------- | +| `label` | текст на кнопке | `string` | +| `icon` | класс иконки tabler icon на кнопке | `string` | +| `iconPosition` | позиция иконки относительно текста | `left \| right` | +| `variant` | вариант отображения кнопки | `primary \| secondary \| tertiary \| outlined \| text \| link` | +| `severity` | семантическое состояние кнопки | `base \| danger \| warning \| success \| info` | +| `size` | размер кнопки | `small \| base \| large \| xlarge` | +| `rounded` | скруглённая форма кнопки | `boolean` | +| `fluid` | растягивает кнопку на всю ширину контейнера | `boolean` | +| `disabled` | отключённое состояние кнопки | `boolean` | +| `loading` | состояние загрузки: спиннер, кнопка неактивна | `boolean` | +| `iconOnly` | режим «только иконка» без текста | `boolean` | +| `showBadge` | показывать бейдж поверх кнопки | `boolean` | +| `badge` | текст бейджа (виден только при `showBadge=true`) | `string` | +| `badgeSeverity` | цветовая схема бейджа | `success \| info \| warning \| danger \| secondary \| contrast \| null` | +| `ariaLabel` | метка для программ экранного доступа | `string \| undefined` | +| `autofocus` | автофокус при монтировании компонента | `boolean` | +| `tabindex` | порядок перехода по Tab | `number \| undefined` | diff --git a/src/lib/components/button/button.component.ts b/src/lib/components/button/button.component.ts index ecf14393..42940120 100644 --- a/src/lib/components/button/button.component.ts +++ b/src/lib/components/button/button.component.ts @@ -1,10 +1,14 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { Button, ButtonSeverity as PrimeButtonSeverity } from 'primeng/button'; -export type ExtraButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'text' | 'link'; +export type ExtraButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outlined' | 'text' | 'link'; export type ExtraButtonSeverity = 'base' | 'danger' | 'warning' | 'success' | 'info'; export type ExtraButtonSize = 'small' | 'base' | 'large' | 'xlarge'; export type ExtraButtonIconPosition = 'left' | 'right'; +export type ExtraButtonBadgeSeverity = 'success' | 'info' | 'warning' | 'danger' | 'secondary' | 'contrast' | null; +type PrimeBadgeSeverity = Extract; +type ExtraButtonSeverityValue = PrimeButtonSeverity; +type ExtraButtonBadgeSeverityValue = PrimeBadgeSeverity; @Component({ selector: 'extra-button', @@ -13,18 +17,24 @@ export type ExtraButtonIconPosition = 'left' | 'right'; imports: [Button], template: ` @@ -32,15 +42,22 @@ export type ExtraButtonIconPosition = 'left' | 'right'; }) export class ExtraButtonComponent { @Input() label = ''; - @Input() icon = ''; - @Input() iconPosition: ExtraButtonIconPosition = 'left'; @Input() variant: ExtraButtonVariant = 'primary'; @Input() severity: ExtraButtonSeverity = 'base'; @Input() size: ExtraButtonSize = 'base'; @Input() rounded = false; - @Input() fluid = false; + @Input() iconPosition: ExtraButtonIconPosition = 'left'; + @Input() iconOnly = false; + @Input() icon = ''; @Input() disabled = false; @Input() loading = false; + @Input() badge = ''; + @Input() badgeSeverity: ExtraButtonBadgeSeverity = null; + @Input() showBadge = false; + @Input() fluid = false; + @Input() ariaLabel: string | undefined = undefined; + @Input() autofocus = false; + @Input() tabindex: number | undefined = undefined; @Output() focus = new EventEmitter(); @Output() blur = new EventEmitter(); @@ -55,12 +72,17 @@ export class ExtraButtonComponent { return this.size === 'xlarge' ? 'p-button-xlg' : ''; } - get primeSeverity(): PrimeButtonSeverity { + get primeSeverity(): ExtraButtonSeverityValue | null { if (this.severity === 'base') { if (this.variant === 'secondary') return 'secondary'; - if (this.variant === 'tertiary') return 'contrast'; + if (this.variant === 'tertiary' || this.variant === 'outlined') return 'contrast'; return null; } return this.severity === 'warning' ? 'warn' : this.severity; } + + get primeBadgeSeverity(): ExtraButtonBadgeSeverityValue { + if (this.badgeSeverity === 'warning') return 'warn'; + return this.badgeSeverity; + } } diff --git a/src/lib/components/button/button.figma.md b/src/lib/components/button/button.figma.md index 7d81e513..a8cb314b 100644 --- a/src/lib/components/button/button.figma.md +++ b/src/lib/components/button/button.figma.md @@ -23,12 +23,12 @@ updated: '2026-06-22' | Свойство | Тип | По умолчанию | Описание | |----------|-----|--------------|---------| -| `label` | `string` | `'Button'` | Текст кнопки | -| `variant` | `'primary' \| 'secondary' \| 'outlined' \| 'text' \| 'link'` | `'primary'` | Вариант отображения — соответствует Figma-свойству `variant` | -| `severity` | `'success' \| 'warning' \| 'danger' \| 'info' \| null` | `null` | Семантический акцент — соответствует Figma-компоненту `` и аналогам | +| `label` | `string` | `''` | Текст кнопки | +| `variant` | `'primary' \| 'secondary' \| 'tertiary' \| 'outlined' \| 'text' \| 'link'` | `'primary'` | Вариант отображения — `primary`/`secondary`/`tertiary`/`text`/`link` соответствуют Figma-свойству `variant` на `` (nodeId `160:5223`), `outlined` — Figma-свойству `variant=outlined` на ``/``/``/`` (nodeId `162:5457` и аналоги) | +| `severity` | `'base' \| 'danger' \| 'warning' \| 'success' \| 'info'` | `'base'` | Семантический акцент — соответствует Figma-компоненту `` и аналогам | | `size` | `'small' \| 'base' \| 'large' \| 'xlarge'` | `'base'` | Размер кнопки — соответствует Figma-свойству `size` | | `icon` | `string` | `''` | CSS-класс иконки (например `'pi pi-check'`); доступные иконки — [icons.md](../../figma-code-connect/icons.md) | -| `iconPos` | `'prefix' \| 'postfix' \| null` | `null` | Позиция иконки — соответствует Figma-свойству `icon-position` | +| `iconPosition` | `'left' \| 'right'` | `'left'` | Позиция иконки — соответствует Figma-свойству `icon-position` | | `iconOnly` | `boolean` | `false` | Режим «только иконка» без текста — соответствует Figma-свойству `icon-only` | | `disabled` | `boolean` | `false` | Отключённое состояние — соответствует Figma-состоянию `state=disabled` | | `loading` | `boolean` | `false` | Состояние загрузки со спиннером — соответствует Figma-состоянию `state=loading` | @@ -37,11 +37,43 @@ updated: '2026-06-22' | `showBadge` | `boolean` | `false` | Показывать бейдж поверх кнопки | | `badge` | `string` | `''` | Текст бейджа (отображается только при `showBadge=true`) | | `badgeSeverity` | `'success' \| 'info' \| 'warning' \| 'danger' \| 'secondary' \| 'contrast' \| null` | `null` | Цветовая схема бейджа; цвета соответствуют токенам [tokens.md](../../figma-code-connect/tokens.md) | -| `text` | `boolean` | `false` | Текстовый вариант без фона и рамки (алиас для `variant='text'`) | | `ariaLabel` | `string \| undefined` | `undefined` | Метка для программ экранного доступа | | `autofocus` | `boolean` | `false` | Автофокус при монтировании компонента | | `tabindex` | `number \| undefined` | `undefined` | Порядок перехода по Tab | +Отдельного `text` нет — текстовый стиль задаётся только через `variant="text"`; дублирующий булев +проп был в черновике ветки, убран как нарушающий правило «одно имя — один смысл» +(`docs/naming-rules.md`). + +### `tertiary` vs `outlined` — два разных Figma-компонента, два разных кода-значения + +В Figma `` и ``/``/``/`` — +РАЗНЫЕ компонент-сеты с разными наборами значений `variant`: у Base — `primary | secondary | +tertiary | text | link`, у severity-семейства — `basic | outlined | text` (подтверждено +скриншотами `` nodeId `160:5223` и `` nodeId `162:5457`). Визуально это +два разных вида: + +- `` variant=tertiary — сплошная тёмная заливка без рамки (`severity=contrast` у + PrimeNG, `outlined=false`). +- ``/аналоги variant=outlined — кнопка с цветной рамкой и прозрачным фоном + (`outlined=true`). + +Раньше оба вида смешивались в один код-уровневый `variant="tertiary"`, который неявно превращался +в контурный при `severity !== 'base'`. Это было неочевидно и не давало собрать контурную кнопку +нейтрального цвета. Теперь `outlined` — самостоятельное значение `variant` +(`[outlined]="variant === 'outlined'"`, без зависимости от `severity`): `tertiary` всегда даёт +тёмную заливку, `outlined` всегда даёт рамку, цвет рамки/текста в обоих случаях задаётся отдельно +через `severity`. + +> ⚠️ **`variant="outlined"` + `severity="base"` не имеет референса в Figma и визуально не +> работает.** В Figma `outlined` существует только у ``/``/ +> ``/`` — у `` такого состояния нет. На уровне кода +> комбинация технически доступна (оба пропа независимы), но при `severity="base"` она маппится на +> PrimeNG `severity="contrast"`, чей цветовой токен — белый текст, рассчитанный на тёмную заливку +> (`#2b2e33`); при `outlined=true` заливка снимается, и текст остаётся белым на прозрачном фоне — +> нечитаемо на светлой странице. Использовать `outlined` только вместе с `severity !== 'base'` +> (`danger`/`warning`/`success`/`info`). + ## Variants ### Primary / Base (основная кнопка) @@ -54,18 +86,29 @@ Figma: ``, variant=primary, size=base, state=default — nodeId `16 ### Secondary / Base (вторичная кнопка) -Figma: ``, variant=tertiary (в коде: `variant="secondary"`), size=base, state=default +Figma: ``, variant=secondary, size=base, state=default ```html ``` -### Outlined / Large (контурная кнопка, большой размер) +### Tertiary / Large (тёмная заливка, большой размер) + +Figma: ``, variant=tertiary, size=large, state=default — сплошная тёмная заливка, +не зависит от `severity` (см. раздел «`tertiary` vs `outlined`» выше) + +```html + +``` + +### Outlined (контурная кнопка) -Figma: ``, variant=outlined, size=large, state=default +Figma: ``/``/``/``, variant=outlined — +nodeId `162:5457` и аналоги. Только с `severity` отличной от `base` — см. предупреждение выше. ```html - + + ``` ### Text / Base (текстовая кнопка) @@ -84,7 +127,7 @@ Figma: ``, variant=link, size=base, state=default ``` -### С иконкой слева (prefix) +### С иконкой слева (left) Figma: ``, icon-position=prefix, icon-only=false @@ -92,7 +135,7 @@ Figma: ``, icon-position=prefix, icon-only=false ``` @@ -164,7 +207,7 @@ Figma: ``, rounded=true ## Do / Don't **Do:** -- Используйте `variant="text"` для текстовых кнопок; `[text]="true"` оставлен только как алиас для совместимости с PrimeNG. +- Используйте `variant="text"` для текстовых кнопок — отдельного булева `text` нет, это единственный способ - Используйте `variant="primary"` для единственного главного действия на экране - Для деструктивных действий задавайте `severity="danger"` и добавляйте иконку через [icons.md](../../figma-code-connect/icons.md) - При кнопке только с иконкой всегда задавайте `ariaLabel` для доступности diff --git a/src/lib/providers/prime-preset/tokens/components/button.ts b/src/lib/providers/prime-preset/tokens/components/button.ts index 8fa91484..f8c32a44 100644 --- a/src/lib/providers/prime-preset/tokens/components/button.ts +++ b/src/lib/providers/prime-preset/tokens/components/button.ts @@ -57,13 +57,13 @@ export const buttonCss = ({ dt }: { dt: (token: string) => string }): string => } .p-button.p-button-outlined:is(.p-disabled, :disabled, .p-button-loading) { color: ${dt('button.extend.disabledColor')}; - background: transparent; - border-color: transparent; + background: ${dt('button.extend.disabledBackground')}; + border-color: ${dt('button.extend.disabledBackground')}; } .p-button.p-button-text:is(.p-disabled, :disabled, .p-button-loading) { color: ${dt('button.extend.disabledColor')}; - background: transparent; - border-color: transparent; + background: ${dt('button.extend.disabledBackground')}; + border-color: ${dt('button.extend.disabledBackground')}; } .p-button.p-button-link:is(.p-disabled, :disabled, .p-button-loading) { color: ${dt('button.extend.disabledColor')}; @@ -71,6 +71,16 @@ export const buttonCss = ({ dt }: { dt: (token: string) => string }): string => border-color: transparent; } + /* ─── Нейтральное кольцо фокуса для text/link без severity ─── + У text/link без severity-класса нет собственного focusRing, поэтому + они наследуют primary (зелёное кольцо). В Figma (Button.Base, + variant=text/link) кольцо должно быть нейтральным, как у secondary/ + tertiary (contrast): zinc.300 1px + black 10% 2px. */ + .p-button-text:not(.p-button-danger):not(.p-button-warn):not(.p-button-info):not(.p-button-help):not(.p-button-success):focus-visible, + .p-button-link:not(.p-button-danger):not(.p-button-warn):not(.p-button-info):not(.p-button-help):not(.p-button-success):focus-visible { + box-shadow: 0 0 0 ${dt('button.extend.extFocusRing.width')} ${dt('button.extend.extFocusRing.borderColor')}, 0 0 0 ${dt('button.extend.extFocusRing.ringWidth')} ${dt('button.extend.extFocusRing.neutralColor')}; + } + /* ─── Link кнопки ─── */ .p-button-link.p-button:is(.p-button, .p-button-xlg) { padding: ${dt('button.extend.extLink.paddingY')} ${dt('button.extend.extLink.paddingX')}; diff --git a/src/lib/providers/prime-preset/tokens/tokens.json b/src/lib/providers/prime-preset/tokens/tokens.json index 4b6d069e..cb9a6225 100644 --- a/src/lib/providers/prime-preset/tokens/tokens.json +++ b/src/lib/providers/prime-preset/tokens/tokens.json @@ -1658,6 +1658,12 @@ "sm": "{dimension.size.450}", "md": "{dimension.size.600}", "lg": "{dimension.size.700}" + }, + "extFocusRing": { + "borderColor": "{colors.solid.zinc.300}", + "neutralColor": "{colors.alpha.black.100}", + "width": "{dimension.borderWidth.100}", + "ringWidth": "{dimension.borderWidth.200}" } }, "colorScheme": { @@ -1674,8 +1680,8 @@ "hoverColor": "{color.fg.default}", "activeColor": "{color.fg.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.200}" + "color": "{colors.alpha.green.200}", + "shadow": "0 0 0 {dimension.borderWidth.200} {colors.alpha.green.200}" } }, "secondary": { @@ -1689,8 +1695,8 @@ "hoverColor": "{color.fg.on.fill.hover}", "activeColor": "{color.fg.on.fill.active}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.200}" + "color": "{colors.alpha.black.100}", + "shadow": "0 0 0 {dimension.borderWidth.100} {colors.solid.zinc.300}, 0 0 0 {dimension.borderWidth.200} {colors.alpha.black.100}" } }, "contrast": { @@ -1704,8 +1710,8 @@ "hoverColor": "{color.fg.default}", "activeColor": "{color.fg.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.200}" + "color": "{colors.alpha.black.100}", + "shadow": "0 0 0 {dimension.borderWidth.100} {colors.solid.zinc.300}, 0 0 0 {dimension.borderWidth.200} {colors.alpha.black.100}" } }, "info": { @@ -1719,8 +1725,8 @@ "hoverColor": "{color.fg.on.status.info.hover}", "activeColor": "{color.fg.on.status.info.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.none}" + "color": "{colors.alpha.blue.200}", + "shadow": "0 0 0 {dimension.borderWidth.200} {colors.alpha.blue.200}" } }, "success": { @@ -1734,8 +1740,8 @@ "hoverColor": "{color.fg.on.status.success.hover}", "activeColor": "{color.fg.on.status.success.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.none}" + "color": "{colors.alpha.green.200}", + "shadow": "0 0 0 {dimension.borderWidth.200} {colors.alpha.green.200}" } }, "warn": { @@ -1749,8 +1755,8 @@ "hoverColor": "{color.fg.on.status.warning.hover}", "activeColor": "{color.fg.on.status.warning.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.none}" + "color": "{colors.alpha.yellow.200}", + "shadow": "0 0 0 {dimension.borderWidth.200} {colors.alpha.yellow.200}" } }, "help": { @@ -1779,8 +1785,8 @@ "hoverColor": "{color.fg.on.status.danger.hover}", "activeColor": "{color.fg.on.status.danger.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.none}" + "color": "{colors.alpha.red.200}", + "shadow": "0 0 0 {dimension.borderWidth.200} {colors.alpha.red.200}" } } }, @@ -1964,8 +1970,8 @@ "borderColor": "transparent", "color": "{color.fg.inverse.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.200}" + "color": "{colors.alpha.black.100}", + "shadow": "0 0 0 {dimension.borderWidth.100} {colors.solid.zinc.300}, 0 0 0 {dimension.borderWidth.200} {colors.alpha.black.100}" }, "hoverBackground": "{color.bg.neutral.strong.active}", "hoverBorderColor": "transparent", @@ -2024,8 +2030,8 @@ "borderColor": "transparent", "color": "{color.fg.inverse.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.200}" + "color": "{colors.alpha.green.200}", + "shadow": "0 0 0 {dimension.borderWidth.200} {colors.alpha.green.200}" }, "hoverBackground": "{color.bg.status.success.strong.hover}", "hoverBorderColor": "transparent", @@ -2039,8 +2045,8 @@ "borderColor": "transparent", "color": "{color.fg.default}", "focusRing": { - "color": "{color.border.focus}", - "shadow": "{effects.elevation.200}" + "color": "{colors.alpha.black.100}", + "shadow": "0 0 0 {dimension.borderWidth.100} {colors.solid.zinc.300}, 0 0 0 {dimension.borderWidth.200} {colors.alpha.black.100}" }, "hoverBackground": "{color.bg.surface.canvas.hover}", "hoverBorderColor": "transparent", diff --git a/src/stories/components/button/button.stories.ts b/src/stories/components/button/button.stories.ts index 3ec246e4..9ad90a3e 100644 --- a/src/stories/components/button/button.stories.ts +++ b/src/stories/components/button/button.stories.ts @@ -5,6 +5,7 @@ import { ButtonVariantsComponent, Variants } from './examples/button-variants.co import { ButtonIconsComponent, Icons } from './examples/button-icons.component'; import { ButtonStatesComponent, States } from './examples/button-states.component'; import { ButtonRoundedComponent, Rounded } from './examples/button-rounded.component'; +import { Badge, ButtonBadgeComponent } from './examples/button-badge.component'; import { ButtonFluidComponent, Fluid } from './examples/button-fluid.component'; import { ButtonEventsComponent, Events } from './examples/button-events.component'; @@ -22,11 +23,12 @@ const meta: Meta = { moduleMetadata({ imports: [ ExtraButtonComponent, - ButtonVariantsComponent, ButtonSizesComponent, + ButtonVariantsComponent, ButtonIconsComponent, ButtonStatesComponent, ButtonRoundedComponent, + ButtonBadgeComponent, ButtonFluidComponent, ButtonEventsComponent ] @@ -35,35 +37,63 @@ const meta: Meta = { parameters: { docs: { description: { - component: `Кнопка — интерактивный элемент интерфейса для инициализации действий, отправки форм и навигации. + component: `Интерактивный элемент интерфейса. Используется для инициации действий, отправки форм и навигации. -Реализована по спецификации [button.md](https://github.com/cdek-it/angular-ui-kit/blob/main/docs/components-api/button.md). +Помимо перечисленных свойств, на кнопку можно вешать нативные DOM-события (\`click\`, \`focus\`, \`blur\`, \`keydown\` и т.п.) напрямую; \`focus\` и \`blur\` также объявлены как типизированные события компонента. \`\`\`typescript import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; -\`\`\` - -Помимо перечисленных свойств, на кнопку можно вешать нативные DOM-события (\`click\`, \`focus\`, \`blur\`, \`keydown\` и т.п.) напрямую; \`focus\` и \`blur\` также объявлены как типизированные события компонента.` +\`\`\`` } } }, argTypes: { - // ── Свойства (docs/components-api/button.md) ─────────────── + // ── Props ──────────────────────────────────────────────── label: { control: 'text', - description: 'Текст на кнопке', + description: 'Текст кнопки', table: { - category: 'Свойства', + category: 'Props', defaultValue: { summary: "''" }, type: { summary: 'string' } } }, + severity: { + control: 'select', + options: ['base', 'danger', 'warning', 'success', 'info'], + description: 'Семантический вариант кнопки', + table: { + category: 'Props', + defaultValue: { summary: 'base' }, + type: { summary: "'base' | 'danger' | 'warning' | 'success' | 'info'" } + } + }, + variant: { + control: 'select', + options: ['primary', 'secondary', 'tertiary', 'outlined', 'text', 'link'], + description: 'Вариант отображения кнопки', + table: { + category: 'Props', + defaultValue: { summary: 'primary' }, + type: { summary: "'primary' | 'secondary' | 'tertiary' | 'outlined' | 'text' | 'link'" } + } + }, + size: { + control: 'select', + options: ['small', 'base', 'large', 'xlarge'], + description: 'Размер кнопки', + table: { + category: 'Props', + defaultValue: { summary: 'base' }, + type: { summary: "'small' | 'base' | 'large' | 'xlarge'" } + } + }, icon: { control: 'text', - description: 'Класс иконки tabler icon на кнопке', + description: 'CSS-класс иконки (например: ti ti-check)', table: { - category: 'Свойства', - defaultValue: { summary: "''" }, + category: 'Props', + defaultValue: { summary: '' }, type: { summary: 'string' } } }, @@ -72,84 +102,119 @@ import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; options: ['left', 'right'], description: 'Позиция иконки относительно текста', table: { - category: 'Свойства', + category: 'Props', defaultValue: { summary: 'left' }, type: { summary: "'left' | 'right'" } } }, - variant: { - control: 'select', - options: ['primary', 'secondary', 'tertiary', 'text', 'link'], - description: 'Вариант отображения кнопки', + iconOnly: { + control: 'boolean', + description: 'Только иконка, без текста', table: { - category: 'Свойства', - defaultValue: { summary: 'primary' }, - type: { summary: "'primary' | 'secondary' | 'tertiary' | 'text' | 'link'" } + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' } } }, - severity: { - control: 'select', - options: ['base', 'danger', 'warning', 'success', 'info'], - description: 'Семантическое состояние кнопки', + rounded: { + control: 'boolean', + description: 'Скруглённая форма кнопки', table: { - category: 'Свойства', - defaultValue: { summary: 'base' }, - type: { summary: "'base' | 'danger' | 'warning' | 'success' | 'info'" } + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' } } }, - size: { - control: 'select', - options: ['small', 'base', 'large', 'xlarge'], - description: 'Размер кнопки', + disabled: { + control: 'boolean', + description: 'Отключённое состояние', table: { - category: 'Свойства', - defaultValue: { summary: 'base' }, - type: { summary: "'small' | 'base' | 'large' | 'xlarge'" } + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' } } }, - rounded: { + loading: { control: 'boolean', - description: 'Скруглённая форма кнопки', + description: 'Состояние загрузки с индикатором', table: { - category: 'Свойства', + category: 'Props', defaultValue: { summary: 'false' }, type: { summary: 'boolean' } } }, fluid: { control: 'boolean', - description: 'Растягивает кнопку на всю ширину контейнера', + description: 'Растягивать ли кнопку на всю ширину контейнера', table: { - category: 'Свойства', + category: 'Props', defaultValue: { summary: 'false' }, type: { summary: 'boolean' } } }, - disabled: { + ariaLabel: { + control: 'text', + description: 'Метка для экранных дикторов', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'string' } + } + }, + autofocus: { control: 'boolean', - description: 'Отключённое состояние кнопки', + description: 'Автофокус при загрузке', table: { - category: 'Свойства', + category: 'Props', defaultValue: { summary: 'false' }, type: { summary: 'boolean' } } }, - loading: { + tabindex: { + control: 'number', + description: 'Порядок фокуса', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'number' } + } + }, + // ── Badge ──────────────────────────────────────────────── + badge: { + control: 'text', + description: 'Значение бейджа', + table: { + category: 'Badge', + defaultValue: { summary: '' }, + type: { summary: 'string' } + } + }, + badgeSeverity: { + control: 'select', + options: [null, 'success', 'info', 'warning', 'danger', 'secondary', 'contrast'], + description: 'Цветовая схема бейджа', + table: { + category: 'Badge', + defaultValue: { summary: 'null' }, + type: { summary: "'success' | 'info' | 'warning' | 'danger' | 'secondary' | 'contrast' | null" } + } + }, + showBadge: { control: 'boolean', - description: 'Состояние загрузки: спиннер, кнопка неактивна', + description: 'Показывать ли бейдж', table: { - category: 'Свойства', + category: 'Badge', defaultValue: { summary: 'false' }, type: { summary: 'boolean' } } }, - // ── События ───────────────────────────────────────────────── + // ── Events ─────────────────────────────────────────────── focus: { control: false, description: 'Кнопка получила фокус', action: 'focus', table: { - category: 'События', + category: 'Events', type: { summary: 'EventEmitter' } } }, @@ -158,87 +223,74 @@ import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; description: 'Кнопка потеряла фокус', action: 'blur', table: { - category: 'События', + category: 'Events', type: { summary: 'EventEmitter' } } }, onClick: { control: false, - description: 'Нативный клик по кнопке', - action: 'click', - table: { - category: 'События', - type: { summary: 'DOM-событие click (нативное)' } - } - }, - onFocus: { - control: false, - description: 'Нативный фокус (прослушивается напрямую на extra-button)', - table: { - category: 'События', - disable: true - } - }, - onBlur: { - control: false, - description: 'Нативная потеря фокуса (прослушивается напрямую на extra-button)', + description: 'Событие клика по кнопке', table: { - category: 'События', - disable: true + category: 'Events', + type: { summary: 'EventEmitter' } } } }, args: { - label: 'Button', - icon: '', iconPosition: 'left', - variant: 'primary', severity: 'base', - size: 'base', - rounded: false, + showBadge: false, + badge: '', + badgeSeverity: null, fluid: false, - disabled: false, - loading: false + autofocus: false } }; export default meta; type Story = StoryObj; -// ── Primary (интерактивная) ────────────────────────────────────────────────── +// ── Default ────────────────────────────────────────────────────────────────── export const Default: Story = { - name: 'Primary', + name: 'Default', render: (args) => { const parts: string[] = []; if (args.label != null && args.label !== '') parts.push(`label="${args.label}"`); - if (args.icon != null && args.icon !== '') parts.push(`icon="${args.icon}"`); - if (args.iconPosition != null && args.iconPosition !== 'left') parts.push(`iconPosition="${args.iconPosition}"`); - if (args.variant != null && args.variant !== 'primary') parts.push(`variant="${args.variant}"`); if (args.severity != null && args.severity !== 'base') parts.push(`severity="${args.severity}"`); - if (args.size != null && args.size !== 'base') parts.push(`size="${args.size}"`); + if (args.variant != null) parts.push(`variant="${args.variant}"`); + if (args.size != null) parts.push(`size="${args.size}"`); + if (args.icon != null && (args.icon as string) !== '') parts.push(`icon="${args.icon}"`); + if (args.iconPosition != null && args.iconPosition !== 'left') parts.push(`iconPosition="${args.iconPosition}"`); + if (args.iconOnly) parts.push(`[iconOnly]="true"`); if (args.rounded) parts.push(`[rounded]="true"`); if (args.fluid) parts.push(`[fluid]="true"`); if (args.disabled) parts.push(`[disabled]="true"`); if (args.loading) parts.push(`[loading]="true"`); + if (args.showBadge) parts.push(`[showBadge]="true"`); + if (args.badge) parts.push(`badge="${args.badge}"`); + if (args.badgeSeverity != null) parts.push(`badgeSeverity="${args.badgeSeverity}"`); + if (args.ariaLabel) parts.push(`ariaLabel="${args.ariaLabel}"`); + if (args.autofocus) parts.push(`[autofocus]="true"`); + if (args.tabindex != null) parts.push(`[tabindex]="${args.tabindex}"`); const template = parts.length - ? `` - : ``; + ? `` + : ``; return { props: args, template }; }, + args: { + label: 'Button' + }, parameters: { docs: { description: { - story: - 'Интерактивная кнопка со всеми свойствами спецификации (по умолчанию — primary). Используйте Controls для изменения пропсов; click/focus/blur логируются в панель Actions.' + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.' } } } }; -// ── Комбинаторные истории ──────────────────────────────────────────────────── - -export { Variants, Sizes, Icons, States, Rounded, Fluid, Events }; +export { Sizes, Variants, Icons, States, Rounded, Badge, Fluid, Events }; diff --git a/src/stories/components/button/examples/button-badge.component.ts b/src/stories/components/button/examples/button-badge.component.ts new file mode 100644 index 00000000..8c8bdb40 --- /dev/null +++ b/src/stories/components/button/examples/button-badge.component.ts @@ -0,0 +1,70 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; + +const template = ` +
+
+ + + + +
+
+`; + +@Component({ + selector: 'app-button-badge', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ExtraButtonComponent], + template +}) +export class ButtonBadgeComponent {} + +export const Badge: StoryObj = { + render: () => ({ + template: `` + }), + parameters: { + docs: { + description: { + story: 'Бейдж поверх кнопки (`showBadge` + `badge` + `badgeSeverity`) — например, для счётчиков уведомлений.' + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-button-badge', + standalone: true, + imports: [ExtraButtonComponent], + template: \` + + + \`, +}) +export class ButtonBadgeComponent {} + ` + } + } + } +}; diff --git a/src/stories/components/button/examples/button-events.component.ts b/src/stories/components/button/examples/button-events.component.ts index aa481587..1a646b37 100644 --- a/src/stories/components/button/examples/button-events.component.ts +++ b/src/stories/components/button/examples/button-events.component.ts @@ -3,7 +3,7 @@ import { StoryObj } from '@storybook/angular'; import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; const template = ` -
+
+
diff --git a/src/stories/components/button/examples/button-icons.component.ts b/src/stories/components/button/examples/button-icons.component.ts index 04338de3..ae462c76 100644 --- a/src/stories/components/button/examples/button-icons.component.ts +++ b/src/stories/components/button/examples/button-icons.component.ts @@ -3,7 +3,7 @@ import { StoryObj } from '@storybook/angular'; import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; const template = ` -
+
@@ -20,6 +20,7 @@ const template = ` +
@@ -33,6 +34,7 @@ const template = ` +
@@ -83,6 +85,7 @@ import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + @@ -96,6 +99,7 @@ import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + \`, diff --git a/src/stories/components/button/examples/button-rounded.component.ts b/src/stories/components/button/examples/button-rounded.component.ts index b3691efb..10fb4131 100644 --- a/src/stories/components/button/examples/button-rounded.component.ts +++ b/src/stories/components/button/examples/button-rounded.component.ts @@ -3,7 +3,7 @@ import { StoryObj } from '@storybook/angular'; import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; const template = ` -
+
@@ -17,6 +17,12 @@ const template = `
+
+ + + + +
`; @@ -38,7 +44,7 @@ export const Rounded: StoryObj = { docs: { description: { story: - 'Скруглённая форма (rounded): варианты отображения при severity="base", семантические состояния (severity) и иконочные кнопки. Пересечения severity с вариантами не показываются — severity, кроме "base", перекрывает их.' + 'Скруглённая форма (rounded): варианты отображения при severity="base", семантические состояния (severity, variant="primary") и variant="outlined" × severity (контурная скруглённая кнопка).' }, source: { language: 'ts', @@ -64,6 +70,13 @@ import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit';
+ +
+ + + + +
\`, }) diff --git a/src/stories/components/button/examples/button-sizes.component.ts b/src/stories/components/button/examples/button-sizes.component.ts index 20c8677a..5f8db78b 100644 --- a/src/stories/components/button/examples/button-sizes.component.ts +++ b/src/stories/components/button/examples/button-sizes.component.ts @@ -3,36 +3,25 @@ import { StoryObj } from '@storybook/angular'; import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; const template = ` -
-
-
- - - - -
-
- - - - -
-
- - - - -
+
+
+ + + +
`; +const styles = ''; + @Component({ selector: 'app-button-sizes', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ExtraButtonComponent], - template + template, + styles }) export class ButtonSizesComponent {} @@ -43,38 +32,22 @@ export const Sizes: StoryObj = { parameters: { docs: { description: { - story: 'Размеры кнопки: small, base, large, xlarge — обычные, с иконкой и скруглённые.' + story: 'Кнопки разных размеров' }, source: { language: 'ts', code: ` import { Component } from '@angular/core'; -import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; @Component({ selector: 'app-button-sizes', standalone: true, - imports: [ExtraButtonComponent], - template: \` -
- - - - -
-
- - - - -
-
- - - - -
- \`, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ExtraButtonComponent + ], + template: ${template}, + styles: ${styles} }) export class ButtonSizesComponent {} ` diff --git a/src/stories/components/button/examples/button-states.component.ts b/src/stories/components/button/examples/button-states.component.ts index 3eb4fd36..bcefc06f 100644 --- a/src/stories/components/button/examples/button-states.component.ts +++ b/src/stories/components/button/examples/button-states.component.ts @@ -3,7 +3,7 @@ import { StoryObj } from '@storybook/angular'; import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; const template = ` -
+
diff --git a/src/stories/components/button/examples/button-variants.component.ts b/src/stories/components/button/examples/button-variants.component.ts index ccf644e4..93022451 100644 --- a/src/stories/components/button/examples/button-variants.component.ts +++ b/src/stories/components/button/examples/button-variants.component.ts @@ -3,7 +3,7 @@ import { StoryObj } from '@storybook/angular'; import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; const template = ` -
+
@@ -18,6 +18,12 @@ const template = `
+
+ + + + +
`; @@ -39,7 +45,7 @@ export const Variants: StoryObj = { docs: { description: { story: - 'Варианты отображения (variant) при severity="base" и семантические состояния (severity). Пересечения не показываются: severity, кроме "base", перекрывает варианты primary/secondary/tertiary, и кнопка выглядит как обычная severity-кнопка.' + 'Варианты отображения (variant) при severity="base", семантические состояния (severity) при variant="primary" (сплошная заливка) и variant="outlined" × severity (контурная кнопка с цветной рамкой). "outlined" используется только вместе с severity (danger, warning, success, info): рамка и текст окрашиваются по severity.' }, source: { language: 'ts', @@ -66,6 +72,13 @@ import { ExtraButtonComponent } from '@cdek-it/angular-ui-kit';
+ +
+ + + + +
\`, })