import { customElement, DeesElement, type TemplateResult, html, css, state, cssManager, } from '@design.estate/dees-element'; import { themeDefaultStyles } from '../00theme.js'; import '../dees-icon/dees-icon.js'; import type { IActionBarOptions, IActionBarResult, IActionBarQueueItem, IActionBarAction, } from './actionbar.interfaces.js'; declare global { interface HTMLElementTagNameMap { 'dees-actionbar': DeesActionbar; } } @customElement('dees-actionbar') export class DeesActionbar extends DeesElement { // STATIC public static demo = () => { const getActionbar = (e: Event) => { const button = e.currentTarget as HTMLElement; const container = button.closest('.demo-container'); return container?.querySelector('dees-actionbar') as DeesActionbar | null; }; const showActionBar = async (e: Event) => { const actionbar = getActionbar(e); if (!actionbar) return; const result = await actionbar.show({ message: 'File changed externally. Reload?', type: 'warning', icon: 'lucide:alertTriangle', actions: [ { id: 'reload', label: 'Reload', primary: true }, { id: 'ignore', label: 'Ignore' }, ], timeout: { duration: 5000, defaultActionId: 'reload' }, dismissible: true, }); console.log('Action bar result:', result); }; const showErrorBar = async (e: Event) => { const actionbar = getActionbar(e); if (!actionbar) return; const result = await actionbar.show({ message: 'Process failed with exit code 1', type: 'error', icon: 'lucide:xCircle', actions: [ { id: 'retry', label: 'Retry', primary: true }, { id: 'dismiss', label: 'Dismiss' }, ], timeout: { duration: 10000, defaultActionId: 'dismiss' }, }); console.log('Error bar result:', result); }; const showQuestionBar = async (e: Event) => { const actionbar = getActionbar(e); if (!actionbar) return; const result = await actionbar.show({ message: 'Save changes before closing?', type: 'question', icon: 'lucide:helpCircle', actions: [ { id: 'save', label: 'Save', primary: true }, { id: 'discard', label: 'Discard' }, { id: 'cancel', label: 'Cancel' }, ], }); console.log('Question bar result:', result); }; return html`
Warning Error Question
`; }; // Queue of pending action bars private queue: IActionBarQueueItem[] = []; // Current active bar state @state() accessor currentBar: IActionBarOptions | null = null; @state() accessor timeRemaining: number = 0; @state() accessor progressPercent: number = 100; @state() accessor isVisible: boolean = false; // Timeout handling private timeoutInterval: ReturnType | null = null; private currentResolve: ((result: IActionBarResult) => void) | null = null; public static styles = [ themeDefaultStyles, cssManager.defaultStyles, css` :host { display: block; overflow: hidden; height: 0; transition: height 0.2s ease-out; } :host(.visible) { height: auto; } .actionbar-item { display: flex; flex-direction: column; background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 12%)')}; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 20%)')}; overflow: hidden; } .progress-bar { height: 3px; background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 18%)')}; overflow: hidden; } .progress-bar-fill { height: 100%; background: ${cssManager.bdTheme('hsl(210 100% 50%)', 'hsl(210 100% 60%)')}; transition: width 0.1s linear; } .progress-bar-fill.warning { background: ${cssManager.bdTheme('hsl(38 92% 50%)', 'hsl(38 92% 55%)')}; } .progress-bar-fill.error { background: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 55%)')}; } .progress-bar-fill.question { background: ${cssManager.bdTheme('hsl(270 70% 50%)', 'hsl(270 70% 60%)')}; } .content { display: flex; align-items: center; padding: 8px 12px; gap: 12px; min-height: 32px; } .message-section { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; } .message-icon { flex-shrink: 0; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')}; } .message-icon.info { color: ${cssManager.bdTheme('hsl(210 100% 45%)', 'hsl(210 100% 60%)')}; } .message-icon.warning { color: ${cssManager.bdTheme('hsl(38 92% 45%)', 'hsl(38 92% 55%)')}; } .message-icon.error { color: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 55%)')}; } .message-icon.question { color: ${cssManager.bdTheme('hsl(270 70% 50%)', 'hsl(270 70% 60%)')}; } .message-text { font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .actions-section { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } .action-button { display: flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 500; cursor: pointer; border: 1px solid transparent; transition: all 0.15s ease; white-space: nowrap; } .action-button.secondary { background: transparent; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')}; border-color: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(0 0% 30%)')}; } .action-button.secondary:hover { background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 18%)')}; } .action-button.primary { background: ${cssManager.bdTheme('hsl(210 100% 50%)', 'hsl(210 100% 55%)')}; color: white; } .action-button.primary:hover { background: ${cssManager.bdTheme('hsl(210 100% 45%)', 'hsl(210 100% 50%)')}; } .action-button.primary.warning { background: ${cssManager.bdTheme('hsl(38 92% 45%)', 'hsl(38 92% 50%)')}; } .action-button.primary.warning:hover { background: ${cssManager.bdTheme('hsl(38 92% 40%)', 'hsl(38 92% 45%)')}; } .action-button.primary.error { background: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 55%)')}; } .action-button.primary.error:hover { background: ${cssManager.bdTheme('hsl(0 70% 45%)', 'hsl(0 70% 50%)')}; } .action-button.primary.question { background: ${cssManager.bdTheme('hsl(270 70% 50%)', 'hsl(270 70% 55%)')}; } .action-button.primary.question:hover { background: ${cssManager.bdTheme('hsl(270 70% 45%)', 'hsl(270 70% 50%)')}; } .countdown { font-size: 11px; opacity: 0.8; margin-left: 2px; } .dismiss-button { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 4px; cursor: pointer; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; transition: all 0.15s ease; } .dismiss-button:hover { background: ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 22%)')}; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')}; } `, ]; public render(): TemplateResult { if (!this.currentBar) { return html``; } const bar = this.currentBar; const type = bar.type || 'info'; const hasTimeout = bar.timeout && this.timeRemaining > 0; return html`
${hasTimeout ? html`
` : ''}
${bar.icon ? html` ` : ''} ${bar.message}
${bar.actions.map(action => this.renderActionButton(action, bar, hasTimeout))} ${bar.dismissible ? html`
this.handleDismiss()} title="Dismiss" >
` : ''}
`; } private renderActionButton( action: IActionBarAction, bar: IActionBarOptions, hasTimeout: boolean | undefined ): TemplateResult { const isPrimary = action.primary; const type = bar.type || 'info'; const isDefaultAction = bar.timeout?.defaultActionId === action.id; const showCountdown = hasTimeout && isDefaultAction; const seconds = Math.ceil(this.timeRemaining / 1000); return html` `; } // ========== Public API ========== /** * Show an action bar with the given options. * Returns a promise that resolves when an action is taken. */ public async show(options: IActionBarOptions): Promise { return new Promise((resolve) => { // Add to queue this.queue.push({ options, resolve }); // If no current bar, process queue if (!this.currentBar) { this.processQueue(); } }); } /** * Dismiss the current action bar without triggering any action. */ public dismiss(): void { this.handleDismiss(); } /** * Clear all pending action bars in the queue. */ public clearQueue(): void { // Resolve all queued items with dismiss for (const item of this.queue) { item.resolve({ actionId: 'dismissed', timedOut: false }); } this.queue = []; } // ========== Private Methods ========== private processQueue(): void { if (this.queue.length === 0) { this.currentBar = null; this.currentResolve = null; this.isVisible = false; this.classList.remove('visible'); return; } const item = this.queue.shift()!; this.currentBar = item.options; this.currentResolve = item.resolve; this.isVisible = true; this.classList.add('visible'); // Setup timeout if configured if (item.options.timeout) { this.startTimeout(item.options.timeout.duration, item.options.timeout.defaultActionId); } } private startTimeout(duration: number, defaultActionId: string): void { this.timeRemaining = duration; this.progressPercent = 100; const startTime = Date.now(); const updateInterval = 50; // Update every 50ms for smooth animation this.timeoutInterval = setInterval(() => { const elapsed = Date.now() - startTime; this.timeRemaining = Math.max(0, duration - elapsed); this.progressPercent = (this.timeRemaining / duration) * 100; if (this.timeRemaining <= 0) { this.clearTimeoutInterval(); this.handleAction(defaultActionId, true); } }, updateInterval); } private clearTimeoutInterval(): void { if (this.timeoutInterval) { clearInterval(this.timeoutInterval); this.timeoutInterval = null; } } private handleAction(actionId: string, timedOut: boolean): void { this.clearTimeoutInterval(); if (this.currentResolve) { this.currentResolve({ actionId, timedOut }); } // Process next item in queue this.processQueue(); } private handleDismiss(): void { this.handleAction('dismissed', false); } public async disconnectedCallback(): Promise { await super.disconnectedCallback(); this.clearTimeoutInterval(); } }