From 4e63ff97bb51f7456a0ad98c2eb4f13f5ceaf1c7 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Wed, 15 Jul 2026 03:22:40 +0400 Subject: [PATCH] feat(design-system): add reusable Button primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 6 (pulled forward as prerequisite to Sprint 3). First Design System component: app-button, standalone/OnPush, variants (primary/secondary/ghost/danger), sizes (sm/md/lg), loading + disabled states, focus-visible ring, reduced-motion-aware spinner. Colors/radius/ shadow/spacing consumed via existing tenant-driven CSS vars (--primary-color, --bg-primary, --radius-md, --shadow-sm, --space-*) — no new hardcoded palette, fully compatible with bootstrap JSON theming and Project Editor live preview. Co-Authored-By: Claude Sonnet 5 --- .../shared/ui/button/button.component.html | 19 +++ .../shared/ui/button/button.component.scss | 119 ++++++++++++++++++ src/app/shared/ui/button/button.component.ts | 23 ++++ 3 files changed, 161 insertions(+) create mode 100644 src/app/shared/ui/button/button.component.html create mode 100644 src/app/shared/ui/button/button.component.scss create mode 100644 src/app/shared/ui/button/button.component.ts diff --git a/src/app/shared/ui/button/button.component.html b/src/app/shared/ui/button/button.component.html new file mode 100644 index 0000000..61aae18 --- /dev/null +++ b/src/app/shared/ui/button/button.component.html @@ -0,0 +1,19 @@ + diff --git a/src/app/shared/ui/button/button.component.scss b/src/app/shared/ui/button/button.component.scss new file mode 100644 index 0000000..d4f1c30 --- /dev/null +++ b/src/app/shared/ui/button/button.component.scss @@ -0,0 +1,119 @@ +:host { + display: inline-block; +} + +:host.app-button-host--full-width { + display: block; + width: 100%; +} + +.app-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-xs, 0.25rem); + width: 100%; + border: 1px solid transparent; + border-radius: var(--radius-md, 6px); + font-family: inherit; + font-weight: 600; + line-height: 1; + cursor: pointer; + transition: background-color var(--transition-fast, 120ms ease), + border-color var(--transition-fast, 120ms ease), + box-shadow var(--transition-fast, 120ms ease), + opacity var(--transition-fast, 120ms ease); + + &:focus-visible { + outline: 2px solid var(--primary-color, #2f6e5d); + outline-offset: 2px; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } +} + +.app-button--sm { + height: 32px; + padding: 0 var(--space-sm, 0.5rem); + font-size: 0.8125rem; +} + +.app-button--md { + height: 40px; + padding: 0 var(--space-md, 1rem); + font-size: 0.9375rem; +} + +.app-button--lg { + height: 48px; + padding: 0 var(--space-lg, 1.5rem); + font-size: 1rem; +} + +.app-button--primary { + background: var(--primary-color, #2f6e5d); + border-color: var(--primary-color, #2f6e5d); + color: var(--bg-primary, #fff); + + &:hover:not(:disabled) { + box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.08)); + } +} + +.app-button--secondary { + background: var(--bg-secondary, #f4f4f5); + border-color: var(--border-color, #e4e4e7); + color: var(--text-primary, #1f322d); + + &:hover:not(:disabled) { + border-color: var(--primary-color, #2f6e5d); + } +} + +.app-button--ghost { + background: transparent; + border-color: transparent; + color: var(--text-primary, #1f322d); + + &:hover:not(:disabled) { + background: var(--bg-secondary, #f4f4f5); + } +} + +.app-button--danger { + background: var(--error-color, #c0392b); + border-color: var(--error-color, #c0392b); + color: var(--bg-primary, #fff); + + &:hover:not(:disabled) { + box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.08)); + } +} + +.app-button__spinner { + width: 1em; + height: 1em; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: app-button-spin 0.6s linear infinite; +} + +@media (prefers-reduced-motion: reduce) { + .app-button { + transition: none; + } + + .app-button__spinner { + animation-duration: 1.2s; + } +} + +@keyframes app-button-spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/app/shared/ui/button/button.component.ts b/src/app/shared/ui/button/button.component.ts new file mode 100644 index 0000000..8189e22 --- /dev/null +++ b/src/app/shared/ui/button/button.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'; +export type ButtonSize = 'sm' | 'md' | 'lg'; + +@Component({ + selector: 'app-button', + standalone: true, + templateUrl: './button.component.html', + styleUrl: './button.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.app-button-host--full-width]': 'fullWidth()' + } +}) +export class ButtonComponent { + readonly variant = input('primary'); + readonly size = input('md'); + readonly type = input<'button' | 'submit' | 'reset'>('button'); + readonly disabled = input(false); + readonly loading = input(false); + readonly fullWidth = input(false); +}