import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; export type ButtonType = 'button' | 'submit' | 'reset'; export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg'; export type IconPosition = 'left' | 'right'; export type ButtonVariant = | 'primary' | 'primary-full' | 'outline-primary' | 'secondary' | 'secondary-full' | 'success' | 'success-full' | 'danger' | 'danger-full' | 'warning' | 'warning-full' | 'info' | 'info-full' | 'light' | 'dark' | 'transparent' | 'custom'; export type ButtonAction = | 'add' | 'save' | 'submit' | 'update' | 'edit' | 'delete' | 'cancel' | 'close' | 'upload' | 'download' | 'import' | 'export' | 'view' | 'search' | 'refresh' | 'reset' | 'approve' | 'reject' | 'login' | 'next' | 'previous' | 'custom'; interface ButtonActionConfig { label: string; icon: string | null; variant: ButtonVariant; type: ButtonType; } const BUTTON_ACTION_CONFIG: Record = { add: { label: 'Add', icon: 'ri-add-line', variant: 'primary-full', type: 'button' }, save: { label: 'Save', icon: 'ri-save-line', variant: 'primary-full', type: 'submit' }, submit: { label: 'Submit', icon: 'ri-send-plane-line', variant: 'primary-full', type: 'submit' }, update: { label: 'Update', icon: 'ri-save-3-line', variant: 'primary-full', type: 'submit' }, edit: { label: 'Edit', icon: 'ri-edit-line', variant: 'primary-full', type: 'button' }, delete: { label: 'Delete', icon: 'ri-delete-bin-line', variant: 'danger-full', type: 'button' }, cancel: { label: 'Cancel', icon: 'ri-close-line', variant: 'light', type: 'button' }, close: { label: 'Close', icon: 'ri-close-line', variant: 'light', type: 'button' }, upload: { label: 'Upload', icon: 'ri-upload-cloud-2-line', variant: 'primary-full', type: 'button' }, download: { label: 'Download', icon: 'ri-download-cloud-2-line', variant: 'success-full', type: 'button' }, import: { label: 'Import', icon: 'ri-file-upload-line', variant: 'info-full', type: 'button' }, export: { label: 'Export', icon: 'ri-file-download-line', variant: 'success-full', type: 'button' }, view: { label: 'View', icon: 'ri-eye-line', variant: 'info-full', type: 'button' }, search: { label: 'Search', icon: 'ri-search-line', variant: 'primary-full', type: 'button' }, refresh: { label: 'Refresh', icon: 'ri-refresh-line', variant: 'light', type: 'button' }, reset: { label: 'Reset', icon: 'ri-restart-line', variant: 'light', type: 'reset' }, approve: { label: 'Approve', icon: 'ri-check-line', variant: 'success-full', type: 'button' }, reject: { label: 'Reject', icon: 'ri-close-circle-line', variant: 'danger-full', type: 'button' }, login: { label: 'Login', icon: 'ri-login-box-line', variant: 'primary-full', type: 'submit' }, next: { label: 'Next', icon: 'ri-arrow-right-line', variant: 'primary-full', type: 'button' }, previous: { label: 'Previous', icon: 'ri-arrow-left-line', variant: 'light', type: 'button' }, custom: { label: '', icon: null, variant: 'custom', type: 'button' } }; @Component({ selector: 'app-button', standalone: true, templateUrl: './button.html', styleUrl: './button.scss', host: { '[class.w-full]': 'fullWidth()' }, changeDetection: ChangeDetectionStrategy.OnPush }) export class Button { /* * Action provides sensible defaults. * Explicit inputs below override action configuration. */ readonly action = input(null); readonly label = input(null); readonly icon = input(null); readonly type = input(null); readonly variant = input(null); readonly size = input('md'); readonly iconPosition = input('left'); readonly showIcon = input(true); readonly iconOnly = input(false); readonly loading = input(false); readonly loadingLabel = input(null); readonly disabled = input(false); readonly fullWidth = input(false); readonly rounded = input(false); readonly className = input(''); readonly iconClass = input(''); readonly ariaLabel = input(null); readonly ariaExpanded = input(null); readonly ariaControls = input(null); readonly title = input(null); readonly buttonClicked = output(); private readonly actionConfig = computed(() => { const action = this.action(); return action ? BUTTON_ACTION_CONFIG[action] : BUTTON_ACTION_CONFIG.custom; }); readonly resolvedLabel = computed(() => { return this.label() ?? this.actionConfig().label; }); readonly resolvedIcon = computed(() => { // Explicit icon overrides the action icon. return this.icon() ?? this.actionConfig().icon; }); readonly resolvedType = computed(() => { return this.type() ?? this.actionConfig().type ?? 'button'; }); readonly resolvedVariant = computed(() => { const explicitVariant = this.variant(); if (explicitVariant) { return explicitVariant; } const action = this.action(); if (action) { return BUTTON_ACTION_CONFIG[action].variant; } return 'primary-full'; }); readonly isDisabled = computed(() => { return this.disabled() || this.loading(); }); readonly resolvedIconClass = computed(() => { return [ this.resolvedIcon(), 'align-middle leading-none', this.iconClass() ] .filter(Boolean) .join(' '); }); readonly buttonClass = computed(() => { const variants: Record = { primary: 'ti-btn-primary', 'primary-full': 'ti-btn-primary-full', 'outline-primary': 'ti-btn-outline-primary', secondary: 'ti-btn-secondary', 'secondary-full': 'ti-btn-secondary-full', success: 'ti-btn-success', 'success-full': 'ti-btn-success-full', danger: 'ti-btn-danger', 'danger-full': 'ti-btn-danger-full', warning: 'ti-btn-warning', 'warning-full': 'ti-btn-warning-full', info: 'ti-btn-info', 'info-full': 'ti-btn-info-full', light: 'ti-btn-light', dark: 'ti-btn-dark', transparent: 'bg-transparent', custom: '' }; const sizes: Record = { xs: '!px-2 !py-1 !text-[0.6875rem]', sm: '!px-3 !py-1.5 !text-[0.75rem]', md: '', lg: 'ti-btn-lg' }; return [ 'ti-btn', 'inline-flex items-center justify-center', 'cursor-pointer', variants[this.resolvedVariant()], sizes[this.size()], this.fullWidth() ? 'w-full' : '', this.iconOnly() ? '!p-0' : '', this.rounded() ? '!rounded-full' : '', this.isDisabled() ? 'cursor-not-allowed opacity-60' : 'cursor-pointer', this.className() ] .filter(Boolean) .join(' '); }); onClick(event: MouseEvent): void { if (this.isDisabled()) { event.preventDefault(); event.stopPropagation(); return; } this.buttonClicked.emit(event); } }