UNPKG

@rxxuzi/gumi

Version:

Clean & minimal design system with delightful interactions

258 lines (217 loc) 7.66 kB
// components/progress.ts // Gumi.js v1.0.0 - Progress Bar Component import { ProgressOptions, GumiElement } from '../types'; import { $, addClass, removeClass } from '../core/dom'; import { clamp } from '../utils/helpers'; export class Progress { private container: HTMLElement; private bar: HTMLElement; private value: number = 0; private options: ProgressOptions; constructor(element: GumiElement, options: ProgressOptions) { const el = $(element); if (!el) throw new Error('Progress element not found'); // Check if element is container or bar if (el.classList.contains('progress')) { this.container = el; this.bar = el.querySelector('.progress-bar') as HTMLElement; if (!this.bar) { // Create bar if it doesn't exist this.bar = document.createElement('div'); this.bar.className = 'progress-bar'; this.container.appendChild(this.bar); } } else if (el.classList.contains('progress-bar')) { this.bar = el; this.container = el.parentElement as HTMLElement; } else { // Assume it's a bar element this.bar = el; this.container = el.parentElement as HTMLElement; } this.options = { animated: false, striped: false, ...options, value: options.value ?? 0 }; this.init(); } /** * Initialize progress bar */ private init(): void { // Set ARIA attributes this.bar.setAttribute('role', 'progressbar'); this.bar.setAttribute('aria-valuemin', '0'); this.bar.setAttribute('aria-valuemax', '100'); // Apply options if (this.options.striped) { addClass(this.container, 'progress-striped'); } if (this.options.animated) { addClass(this.container, 'progress-animated'); } // Set initial value this.setValue(this.options.value); } /** * Set progress value */ setValue(value: number): void { // Clamp value between 0 and 100 this.value = clamp(value, 0, 100); // Update bar width this.bar.style.width = `${this.value}%`; this.bar.setAttribute('aria-valuenow', String(this.value)); // Auto color based on percentage this.updateColor(); } /** * Get current value */ getValue(): number { return this.value; } /** * Update color based on value */ private updateColor(): void { let color: string; if (this.value < 25) { color = 'var(--gumi-error)'; } else if (this.value < 50) { color = 'var(--gumi-warning)'; } else if (this.value < 75) { color = 'var(--gumi-secondary)'; } else { color = 'var(--gumi-success)'; } this.bar.style.backgroundColor = color; } /** * Increment progress */ increment(amount: number = 1): void { this.setValue(this.value + amount); } /** * Decrement progress */ decrement(amount: number = 1): void { this.setValue(this.value - amount); } /** * Set to indeterminate state */ setIndeterminate(indeterminate: boolean = true): void { if (indeterminate) { this.bar.style.width = '100%'; addClass(this.container, 'progress-indeterminate'); this.bar.removeAttribute('aria-valuenow'); } else { removeClass(this.container, 'progress-indeterminate'); this.setValue(this.value); } } /** * Set striped style */ setStriped(striped: boolean = true): void { if (striped) { addClass(this.container, 'progress-striped'); } else { removeClass(this.container, 'progress-striped'); } } /** * Set animated style */ setAnimated(animated: boolean = true): void { if (animated) { addClass(this.container, 'progress-animated'); this.setStriped(true); // Animated requires striped } else { removeClass(this.container, 'progress-animated'); } } /** * Animate to value */ animateTo(targetValue: number, duration: number = 1000): Promise<void> { return new Promise((resolve) => { const startValue = this.value; const startTime = performance.now(); const animate = (currentTime: number) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Easing function (ease-in-out) const easing = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2; const currentValue = startValue + (targetValue - startValue) * easing; this.setValue(currentValue); if (progress < 1) { requestAnimationFrame(animate); } else { resolve(); } }; requestAnimationFrame(animate); }); } /** * Reset progress */ reset(): void { this.setValue(0); } /** * Complete progress */ complete(): void { this.setValue(100); } /** * Static helper to create progress bar */ static create(options: ProgressOptions & { container?: HTMLElement; className?: string; } = { value: 0 }): Progress { const container = options.container || document.body; const progressEl = document.createElement('div'); progressEl.className = 'progress ' + (options.className || ''); const barEl = document.createElement('div'); barEl.className = 'progress-bar'; progressEl.appendChild(barEl); container.appendChild(progressEl); return new Progress(progressEl, options); } /** * Static method to set progress on element */ static setProgress(selector: GumiElement, value: number): void { const el = $(selector); if (!el) return; // Check if it's a progress bar element const isBar = el.classList.contains('progress-bar'); const bar = isBar ? el : el.querySelector('.progress-bar') as HTMLElement; if (!bar) return; const clampedValue = clamp(value, 0, 100); bar.style.width = `${clampedValue}%`; bar.setAttribute('aria-valuenow', String(clampedValue)); // Auto color let color: string; if (clampedValue < 25) { color = 'var(--gumi-error)'; } else if (clampedValue < 50) { color = 'var(--gumi-warning)'; } else if (clampedValue < 75) { color = 'var(--gumi-secondary)'; } else { color = 'var(--gumi-success)'; } bar.style.backgroundColor = color; } }