UNPKG

neumorphic-peripheral

Version:

A lightweight, framework-agnostic JavaScript/TypeScript library for beautiful neumorphic styling

277 lines (276 loc) 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ButtonComponent = void 0; exports.button = button; const base_1 = require("./base"); const utils_1 = require("../utils"); class ButtonComponent extends base_1.BaseComponent { constructor(element, config = {}) { if (!(element instanceof HTMLButtonElement)) { throw new Error('Button component requires an HTMLButtonElement'); } super(element, config); this._buttonConfig = { variant: 'primary', size: 'md', loading: false, ...config }; } get buttonElement() { return this._element; } init() { this.applyBaseStyles(); this.applyButtonStyles(); this.setupInteractions(); } bindEvents() { super.bindEvents(); // Click handler this.addEventListener(this._element, 'click', (e) => { if (this._config.disabled || this._buttonConfig.loading) { e.preventDefault(); e.stopPropagation(); return; } if (this._buttonConfig.onClick) { this._buttonConfig.onClick(e); } this.emit('click', { event: e }); }); // Keyboard handler this.addEventListener(this._element, 'keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.buttonElement.click(); } }); } applyButtonStyles() { (0, utils_1.addClassName)(this._element, 'button'); (0, utils_1.addClassName)(this._element, `button-${this._buttonConfig.variant}`); (0, utils_1.addClassName)(this._element, `button-${this._buttonConfig.size}`); // Core button styles this._element.style.cursor = 'pointer'; this._element.style.userSelect = 'none'; this._element.style.display = 'inline-flex'; this._element.style.alignItems = 'center'; this._element.style.justifyContent = 'center'; this._element.style.gap = '8px'; this._element.style.fontWeight = '500'; this._element.style.textDecoration = 'none'; this._element.style.whiteSpace = 'nowrap'; this._element.style.position = 'relative'; this._element.style.overflow = 'hidden'; // Apply variant-specific styles this.applyVariantStyles(); // Apply size-specific styles this.applySizeStyles(); // Handle loading state if (this._buttonConfig.loading) { this.setLoading(true); } } applyVariantStyles() { const variant = this._buttonConfig.variant; switch (variant) { case 'primary': this._element.style.backgroundColor = this._theme.colors.accent; this._element.style.color = '#ffffff'; this._element.style.boxShadow = this.createShadowStyle('raised'); break; case 'secondary': this._element.style.backgroundColor = this._theme.colors.surface; this._element.style.color = this._theme.colors.text; this._element.style.boxShadow = this.createShadowStyle('raised'); break; case 'ghost': this._element.style.backgroundColor = 'transparent'; this._element.style.color = this._theme.colors.text; this._element.style.boxShadow = 'none'; break; } } applySizeStyles() { const size = this._buttonConfig.size; this._element.style.padding = (0, utils_1.getSizeValue)(size, 'padding'); this._element.style.fontSize = (0, utils_1.getSizeValue)(size, 'fontSize'); this._element.style.minHeight = (0, utils_1.getSizeValue)(size, 'height'); } setupInteractions() { const variant = this._buttonConfig.variant; // Mouse interactions this.addEventListener(this._element, 'mouseenter', () => { if (this._config.disabled || this._buttonConfig.loading) return; switch (variant) { case 'primary': this._element.style.boxShadow = this.createHoverShadowStyle('raised'); this._element.style.transform = 'translateY(-1px)'; break; case 'secondary': this._element.style.boxShadow = this.createHoverShadowStyle('raised'); this._element.style.transform = 'translateY(-1px)'; break; case 'ghost': this._element.style.backgroundColor = this._theme.colors.surface; this._element.style.boxShadow = this.createShadowStyle('flat'); break; } }); this.addEventListener(this._element, 'mouseleave', () => { if (this._config.disabled || this._buttonConfig.loading) return; switch (variant) { case 'primary': case 'secondary': this._element.style.boxShadow = this.createShadowStyle('raised'); this._element.style.transform = 'translateY(0)'; break; case 'ghost': this._element.style.backgroundColor = 'transparent'; this._element.style.boxShadow = 'none'; break; } }); // Active state this.addEventListener(this._element, 'mousedown', () => { if (this._config.disabled || this._buttonConfig.loading) return; switch (variant) { case 'primary': case 'secondary': this._element.style.boxShadow = this.createActiveShadowStyle('raised'); this._element.style.transform = 'translateY(1px)'; break; case 'ghost': this._element.style.boxShadow = this.createShadowStyle('inset'); break; } }); this.addEventListener(this._element, 'mouseup', () => { if (this._config.disabled || this._buttonConfig.loading) return; // Return to hover state this.buttonElement.dispatchEvent(new Event('mouseenter')); }); // Focus styles this.addEventListener(this._element, 'focus', () => { this._element.style.outline = `2px solid ${this._theme.colors.accent}40`; this._element.style.outlineOffset = '2px'; }); this.addEventListener(this._element, 'blur', () => { this._element.style.outline = 'none'; }); } createLoadingSpinner() { const spinner = (0, utils_1.createElement)('span', { class: 'np-loading-spinner' }); spinner.style.display = 'inline-block'; spinner.style.width = '16px'; spinner.style.height = '16px'; spinner.style.border = '2px solid transparent'; spinner.style.borderTop = '2px solid currentColor'; spinner.style.borderRadius = '50%'; spinner.style.animation = 'np-spin 1s linear infinite'; // Add spinner keyframes if not already added this.ensureSpinnerKeyframes(); return spinner; } ensureSpinnerKeyframes() { const styleId = 'np-spinner-keyframes'; if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` @keyframes np-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } } // Public API methods setLoading(loading) { this._buttonConfig.loading = loading; if (loading) { // Store original content this._originalContent = this._element.innerHTML; // Disable button this.buttonElement.disabled = true; this._element.style.cursor = 'not-allowed'; this._element.style.opacity = '0.7'; // Add loading spinner this._loadingSpinner = this.createLoadingSpinner(); this._element.innerHTML = ''; this._element.appendChild(this._loadingSpinner); // Add loading text if desired const loadingText = (0, utils_1.createElement)('span'); loadingText.textContent = 'Loading...'; this._element.appendChild(loadingText); (0, utils_1.addClassName)(this._element, 'loading'); } else { // Restore original content if (this._originalContent) { this._element.innerHTML = this._originalContent; } // Re-enable button this.buttonElement.disabled = this._config.disabled || false; this._element.style.cursor = this._config.disabled ? 'not-allowed' : 'pointer'; this._element.style.opacity = this._config.disabled ? '0.5' : '1'; // Remove loading spinner this._loadingSpinner = undefined; this._element.classList.remove('np-loading'); } this.emit('loading-change', { loading }); } setVariant(variant) { this.update({ variant }); } setSize(size) { this.update({ size }); } click() { this.buttonElement.click(); } onUpdate(newConfig) { const oldConfig = { ...this._buttonConfig }; this._buttonConfig = { ...this._buttonConfig, ...newConfig }; // Update variant if changed if (newConfig.variant && newConfig.variant !== oldConfig.variant) { this._element.classList.remove(`np-button-${oldConfig.variant}`); (0, utils_1.addClassName)(this._element, `button-${newConfig.variant}`); this.applyVariantStyles(); this.setupInteractions(); } // Update size if changed if (newConfig.size && newConfig.size !== oldConfig.size) { this._element.classList.remove(`np-button-${oldConfig.size}`); (0, utils_1.addClassName)(this._element, `button-${newConfig.size}`); this.applySizeStyles(); } // Update loading state if changed if (newConfig.loading !== oldConfig.loading) { this.setLoading(newConfig.loading || false); } // Update click handler if changed if (newConfig.onClick !== oldConfig.onClick) { // Event handler will be updated automatically through the config } } onDestroy() { // Restore original content if in loading state if (this._buttonConfig.loading && this._originalContent) { this._element.innerHTML = this._originalContent; } } } exports.ButtonComponent = ButtonComponent; // Factory function for easy usage function button(element, config = {}) { return new ButtonComponent(element, config); }