From c52854f90222baa76feb1202cf939923e2f88d44 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 2 Apr 2026 19:03:32 +0000 Subject: [PATCH] feat(dees-simple-appdash): add global message banners with actions and dismissal support --- changelog.md | 8 + ts_web/00_commitinfo_data.ts | 2 +- .../dees-simple-appdash.demo.ts | 40 ++- .../dees-simple-appdash.ts | 308 +++++++++++++++++- 4 files changed, 353 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index d838cf3..2f02fa1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-02 - 3.50.0 - feat(dees-simple-appdash) +add global message banners with actions and dismissal support + +- introduces typed global message APIs and public methods to add, remove, and clear banners +- renders info, success, warning, and error banners with optional icons and action buttons +- adjusts app content and terminal positioning to account for banner height dynamically +- updates the demo to showcase dismissible and actionable global messages + ## 2026-04-02 - 3.49.2 - fix(dees-input-list) refine dees-input-list spacing and simplify the add item action button diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 25721f5..8e86f03 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.49.2', + version: '3.50.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.demo.ts b/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.demo.ts index 5486ced..51c7393 100644 --- a/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.demo.ts +++ b/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.demo.ts @@ -1,5 +1,5 @@ import { html, DeesElement, customElement, css, cssManager } from '@design.estate/dees-element'; -import type { IView } from './dees-simple-appdash.js'; +import type { IView, IGlobalMessage } from './dees-simple-appdash.js'; import '../../00group-form/dees-form/dees-form.js'; import '../../00group-input/dees-input-text/dees-input-text.js'; import '../../00group-input/dees-input-checkbox/dees-input-checkbox.js'; @@ -263,6 +263,44 @@ export const demoFunc = () => html` alert('Updating...'), + }, + { + name: 'Release Notes', + action: () => alert('Opening release notes...'), + }, + ], + }, + { + id: 'maintenance', + type: 'warning', + message: 'Scheduled maintenance window: April 5, 2026 02:00–06:00 UTC. Some services may be temporarily unavailable.', + dismissible: true, + }, + { + id: 'critical', + type: 'error', + message: 'Your SSL certificate expires in 3 days. Renew now to avoid service disruption.', + dismissible: false, + actions: [ + { + name: 'Renew Certificate', + iconName: 'lucide:shieldCheck', + action: () => alert('Renewing certificate...'), + }, + ], + }, + ] as IGlobalMessage[]} .viewTabs=${[ { name: 'Dashboard', diff --git a/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.ts b/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.ts index 74449ad..5c341b5 100644 --- a/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.ts +++ b/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.ts @@ -29,6 +29,23 @@ export interface IView { element: DeesElement['constructor']['prototype']; } +export type TGlobalMessageType = 'info' | 'success' | 'warning' | 'error'; + +export interface IGlobalMessageAction { + name: string; + iconName?: string; + action: () => void | Promise; +} + +export interface IGlobalMessage { + id: string; + type: TGlobalMessageType; + message: string; + dismissible?: boolean; + icon?: string; + actions?: IGlobalMessageAction[]; +} + @customElement('dees-simple-appdash') export class DeesSimpleAppDash extends DeesElement { // STATIC @@ -45,9 +62,15 @@ export class DeesSimpleAppDash extends DeesElement { @property({ type: String }) accessor terminalSetupCommand: string = `echo "Terminal ready"`; + @property({ type: Array }) + accessor globalMessages: IGlobalMessage[] = []; + @state() accessor selectedView!: IView; + @state() + accessor _activeMessages: IGlobalMessage[] = []; + public static styles = [ themeDefaultStyles, @@ -327,6 +350,170 @@ export class DeesSimpleAppDash extends DeesElement { .control.status-terminal dees-icon { color: hsl(45 90% 55%); } + + /* Global Message Banners */ + .messageBannerArea { + position: absolute; + top: 0; + left: 240px; + right: 0; + z-index: 3; + display: flex; + flex-direction: column; + transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1); + } + + .messageBanner { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + font-size: 13px; + font-family: 'Geist Sans', sans-serif; + font-weight: 500; + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 13%)')}; + animation: bannerSlideDown 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + .messageBanner.dismissing { + animation: bannerSlideUp 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + @keyframes bannerSlideDown { + from { + opacity: 0; + transform: translateY(-100%); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes bannerSlideUp { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-100%); + } + } + + .messageBanner dees-icon { + font-size: 16px; + flex-shrink: 0; + } + + .messageBanner-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .messageBanner-actions { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + } + + .messageBanner-action { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 4px; + cursor: default; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.01em; + transition: all 0.15s ease; + white-space: nowrap; + background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.08)', 'hsl(0 0% 100% / 0.1)')}; + color: inherit; + } + + .messageBanner-action:hover { + background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.15)', 'hsl(0 0% 100% / 0.18)')}; + } + + .messageBanner-action:active { + transform: scale(0.97); + } + + .messageBanner-action dees-icon { + font-size: 13px; + } + + .messageBanner-dismiss { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; + cursor: default; + opacity: 0.5; + transition: all 0.15s ease; + } + + .messageBanner-dismiss:hover { + opacity: 1; + background: hsl(0 0% 0% / 0.1); + } + + /* Message type: info */ + .messageBanner-info { + background: ${cssManager.bdTheme('hsl(210 100% 97%)', 'hsl(210 50% 10%)')}; + color: ${cssManager.bdTheme('hsl(210 70% 30%)', 'hsl(210 70% 80%)')}; + border-left: 3px solid #0084ff; + } + .messageBanner-info dees-icon { + color: #0084ff; + } + + /* Message type: success */ + .messageBanner-success { + background: ${cssManager.bdTheme('hsl(142 70% 97%)', 'hsl(142 30% 10%)')}; + color: ${cssManager.bdTheme('hsl(142 50% 25%)', 'hsl(142 50% 80%)')}; + border-left: 3px solid #22c55e; + } + .messageBanner-success dees-icon { + color: #22c55e; + } + + /* Message type: warning */ + .messageBanner-warning { + background: ${cssManager.bdTheme('hsl(38 90% 97%)', 'hsl(38 40% 10%)')}; + color: ${cssManager.bdTheme('hsl(38 60% 25%)', 'hsl(38 60% 80%)')}; + border-left: 3px solid #f59e0b; + } + .messageBanner-warning dees-icon { + color: #f59e0b; + } + + /* Message type: error */ + .messageBanner-error { + background: ${cssManager.bdTheme('hsl(0 70% 97%)', 'hsl(0 40% 10%)')}; + color: ${cssManager.bdTheme('hsl(0 60% 30%)', 'hsl(0 60% 80%)')}; + border-left: 3px solid #ef4444; + } + .messageBanner-error dees-icon { + color: #ef4444; + } + + .messageBanner-dismiss:hover { + background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.08)', 'hsl(0 0% 100% / 0.1)')}; + } + + .appcontent { + top: var(--banner-area-height, 0px); + height: calc(100% - 24px - var(--banner-area-height, 0px)); + } `, ]; @@ -369,6 +556,34 @@ export class DeesSimpleAppDash extends DeesElement { + ${this._activeMessages.length > 0 ? html` +
+ ${this._activeMessages.map(msg => html` +
+ + ${msg.message} + ${msg.actions?.length ? html` +
+ ${msg.actions.map(a => html` +
a.action()}> + ${a.iconName ? html`` : ''} + ${a.name} +
+ `)} +
+ ` : ''} + ${msg.dismissible !== false ? html` +
this.removeMessage(msg.id)}> + +
+ ` : ''} +
+ `)} +
+ ` : ''}
@@ -394,6 +609,91 @@ export class DeesSimpleAppDash extends DeesElement { await this.loadView(viewToLoad); } } + + public willUpdate(changedProperties: Map) { + if (changedProperties.has('globalMessages')) { + // Sync globalMessages property into _activeMessages + // Keep any messages added via addMessage() that aren't in globalMessages + const propertyIds = new Set(this.globalMessages.map(m => m.id)); + const existingIds = new Set(this._activeMessages.map(m => m.id)); + + // Add new messages from property that aren't already active + const newMessages = this.globalMessages.filter(m => !existingIds.has(m.id)); + + // Keep messages added via API (those not in globalMessages are kept as-is) + // Remove messages that were in the previous globalMessages but are no longer + const previousGlobalMessages = (changedProperties.get('globalMessages') as IGlobalMessage[]) || []; + const previousIds = new Set(previousGlobalMessages.map(m => m.id)); + const removedIds = new Set([...previousIds].filter(id => !propertyIds.has(id))); + + this._activeMessages = [ + ...this._activeMessages.filter(m => !removedIds.has(m.id)), + ...newMessages, + ]; + } + } + + public updated(changedProperties: Map) { + super.updated(changedProperties); + this.updateBannerOffset(); + } + + private updateBannerOffset() { + requestAnimationFrame(() => { + const bannerArea = this.shadowRoot?.querySelector('.messageBannerArea') as HTMLElement; + const maincontainer = this.shadowRoot?.querySelector('.maincontainer') as HTMLElement; + const height = bannerArea ? bannerArea.offsetHeight : 0; + maincontainer?.style.setProperty('--banner-area-height', `${height}px`); + + // Keep terminal in sync with banner height + if (this.currentTerminal) { + this.currentTerminal.style.top = `${height}px`; + } + }); + } + + private getMessageIcon(msg: IGlobalMessage): string { + if (msg.icon) return msg.icon; + const defaults: Record = { + info: 'lucide:info', + success: 'lucide:circleCheck', + warning: 'lucide:triangleAlert', + error: 'lucide:circleX', + }; + return defaults[msg.type]; + } + + public addMessage(message: Omit & { id?: string }): string { + const id = message.id || `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const fullMessage: IGlobalMessage = { + dismissible: true, + ...message, + id, + }; + this._activeMessages = [...this._activeMessages, fullMessage]; + return id; + } + + public removeMessage(id: string): void { + const bannerEl = this.shadowRoot?.querySelector(`[data-message-id="${id}"]`) as HTMLElement; + if (bannerEl) { + bannerEl.classList.add('dismissing'); + bannerEl.addEventListener('animationend', () => { + this._activeMessages = this._activeMessages.filter(m => m.id !== id); + this.dispatchEvent(new CustomEvent('message-dismiss', { + detail: { id }, + bubbles: true, + composed: true, + })); + }, { once: true }); + } else { + this._activeMessages = this._activeMessages.filter(m => m.id !== id); + } + } + + public clearMessages(): void { + this._activeMessages = []; + } public currentTerminal: DeesWorkspaceTerminal | null = null; public async launchTerminal() { @@ -410,9 +710,11 @@ export class DeesSimpleAppDash extends DeesElement { terminal.setupCommand = this.terminalSetupCommand; this.currentTerminal = terminal; maincontainer.appendChild(terminal); + const bannerArea = this.shadowRoot?.querySelector('.messageBannerArea') as HTMLElement; + const bannerHeight = bannerArea ? bannerArea.offsetHeight : 0; terminal.style.position = 'absolute'; terminal.style.zIndex = '10'; - terminal.style.top = '0px'; + terminal.style.top = `${bannerHeight}px`; terminal.style.left = '240px'; terminal.style.right = '0px'; terminal.style.bottom = '24px'; @@ -420,8 +722,8 @@ export class DeesSimpleAppDash extends DeesElement { terminal.style.transform = 'translateY(8px) scale(0.99)'; terminal.style.transition = 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)'; terminal.style.boxShadow = '0 25px 50px -12px rgb(0 0 0 / 0.5), 0 0 0 1px rgb(255 255 255 / 0.05)'; - terminal.style.maxWidth = `calc(${maincontainer.clientWidth}px -240px)`; - terminal.style.maxHeight = `calc(${maincontainer.clientHeight}px - 24px)`; + terminal.style.maxWidth = `calc(${maincontainer.clientWidth}px - 240px)`; + terminal.style.maxHeight = `calc(${maincontainer.clientHeight}px - 24px - ${bannerHeight}px)`; // Add close button to terminal terminal.addEventListener('close', () => this.closeTerminal());