import { demoFunc } from './dees-simple-appdash.demo.js'; import { customElement, html, DeesElement, property, type TemplateResult, cssManager, css, unsafeCSS, type CSSResult, state, domtools, } from '@design.estate/dees-element'; import '../../00group-utility/dees-icon/dees-icon.js'; import type { DeesWorkspaceTerminal } from '../../00group-workspace/dees-workspace-terminal/dees-workspace-terminal.js'; import { themeDefaultStyles } from '../../00theme.js'; declare global { interface HTMLElementTagNameMap { 'dees-simple-appdash': DeesSimpleAppDash; } } export interface IView { name: string; iconName?: string; 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 public static demo = demoFunc; public static demoGroups = ['Simple']; // INSTANCE @property() accessor name: string = 'Application Dashboard'; @property({ type: Array }) accessor viewTabs: IView[] = []; @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, cssManager.defaultStyles, css` /* TODO: Migrate hardcoded values to --dees-* CSS variables */ :host { color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; user-select: none; display: block; overflow: hidden; position: relative; height: 100%; width: 100%; } .maincontainer { position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px; overflow: hidden; } .appbar { position: absolute; top: 0px; left: 0px; height: calc(100% - 24px); width: 240px; background: ${cssManager.bdTheme('hsl(0 0% 99%)', 'hsl(0 0% 7%)')}; border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 13%)')}; font-size: 13px; font-family: 'Geist Sans', sans-serif; z-index: 2; display: grid; grid-template-rows: auto 1fr min-content; overflow: hidden; } .sidebar-header { padding: 20px 16px; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 13%)')}; display: flex; align-items: center; gap: 12px; } .header-icon-wrapper { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 10px; background: ${cssManager.bdTheme( 'linear-gradient(135deg, hsl(215 20% 95%) 0%, hsl(215 20% 90%) 100%)', 'linear-gradient(135deg, hsl(215 20% 18%) 0%, hsl(215 20% 14%) 100%)' )}; box-shadow: ${cssManager.bdTheme( '0 1px 2px rgb(0 0 0 / 0.05), inset 0 1px 0 rgb(255 255 255 / 0.5)', '0 1px 2px rgb(0 0 0 / 0.2), inset 0 1px 0 rgb(255 255 255 / 0.05)' )}; } .header-icon-wrapper dees-icon { font-size: 18px; color: ${cssManager.bdTheme('hsl(215 20% 40%)', 'hsl(215 20% 70%)')}; } .appName { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; letter-spacing: -0.02em; } .viewTabs-container { overflow-y: auto; padding: 12px 8px; scrollbar-width: thin; scrollbar-color: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')} transparent; } .viewTabs-container::-webkit-scrollbar { width: 6px; } .viewTabs-container::-webkit-scrollbar-track { background: transparent; } .viewTabs-container::-webkit-scrollbar-thumb { background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')}; border-radius: 3px; } .viewTabs-container::-webkit-scrollbar-thumb:hover { background: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(0 0% 30%)')}; } .section-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; padding: 8px 12px 8px; margin-bottom: 4px; } .viewTabs { display: flex; flex-direction: column; gap: 2px; } .viewTab { display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: default; transition: all 0.15s ease; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')}; user-select: none; position: relative; border-radius: 8px; } .viewTab:hover { background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.04)', 'hsl(0 0% 100% / 0.05)')}; color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; } .viewTab:active { background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 100% / 0.07)')}; transform: scale(0.99); } .viewTab.selected { background: ${cssManager.bdTheme('hsl(215 25% 95%)', 'hsl(215 20% 15%)')}; color: ${cssManager.bdTheme('hsl(215 25% 30%)', 'hsl(215 25% 85%)')}; font-weight: 500; } .viewTab.selected::before { content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px; border-radius: 0 2px 2px 0; background: ${cssManager.bdTheme('hsl(215 70% 50%)', 'hsl(215 70% 60%)')}; } .viewTab dees-icon { font-size: 16px; opacity: 0.55; transition: all 0.15s ease; } .viewTab:hover dees-icon { opacity: 0.75; } .viewTab.selected dees-icon { opacity: 0.9; color: ${cssManager.bdTheme('hsl(215 70% 45%)', 'hsl(215 70% 65%)')}; } .viewTab span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .appActions { padding: 12px 8px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 13%)')}; } .action { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; cursor: default; transition: all 0.15s ease; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')}; } .action:hover { background: ${cssManager.bdTheme('hsl(0 84% 60% / 0.08)', 'hsl(0 84% 60% / 0.12)')}; color: ${cssManager.bdTheme('hsl(0 84% 45%)', 'hsl(0 84% 65%)')}; } .action dees-icon { font-size: 16px; opacity: 0.6; transition: all 0.15s ease; } .action:hover dees-icon { opacity: 0.9; color: ${cssManager.bdTheme('hsl(0 84% 45%)', 'hsl(0 84% 65%)')}; } .appcontent { z-index: 1; position: absolute; top: 0px; right: 0px; height: calc(100% - 24px); bottom: 24px; width: calc(100% - 240px); overflow: auto; background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 5%)')}; overscroll-behavior: contain; } .controlbar { color: #fff; position: absolute; bottom: 0px; left: 0px; width: 100%; height: 24px; background: ${cssManager.bdTheme('hsl(220 13% 18%)', 'hsl(220 13% 12%)')}; z-index: 2; display: flex; justify-content: flex-end; align-items: center; flex-direction: row; font-size: 11px; font-weight: 500; letter-spacing: 0.01em; } .control { display: flex; align-items: center; gap: 6px; padding: 0 12px; height: 100%; white-space: nowrap; cursor: default; color: hsl(0 0% 70%); transition: all 0.15s ease; border-left: 1px solid hsl(0 0% 100% / 0.08); } .control:first-child { border-left: none; } .control:hover { background: hsl(0 0% 100% / 0.06); color: hsl(0 0% 95%); } .control dees-icon { font-size: 13px; } .control.status-connected dees-icon { color: hsl(142 70% 50%); } .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)); } `, ]; public render(): TemplateResult { return html`
${this.viewTabs.map( (view) => html`
this.loadView(view)} > ${view.iconName ? html` ` : html` `} ${view.name}
` )}
{ this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true })); }}> Sign out
${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)}>
` : ''}
`)}
` : ''}
Connected
Terminal
`; } public async firstUpdated(_changedProperties: Map): Promise { const domtools = await this.domtoolsPromise; super.firstUpdated(_changedProperties); if (this.viewTabs && this.viewTabs.length > 0) { const viewToLoad = this.selectedView || this.viewTabs[0]; 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`); }); } 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() { const domtools = await this.domtoolsPromise; if (this.currentTerminal) { // If terminal already exists, remove it await this.closeTerminal(); return; } const maincontainer = this.shadowRoot!.querySelector('.maincontainer')! as HTMLElement; const { DeesWorkspaceTerminal } = await import('../../00group-workspace/dees-workspace-terminal/dees-workspace-terminal.js'); const terminal = new DeesWorkspaceTerminal(); terminal.setupCommand = this.terminalSetupCommand; this.currentTerminal = terminal; maincontainer.appendChild(terminal); terminal.style.position = 'absolute'; terminal.style.zIndex = '10'; terminal.style.top = 'var(--banner-area-height, 0px)'; terminal.style.left = '240px'; terminal.style.right = '0px'; terminal.style.height = 'auto'; terminal.style.bottom = '24px'; terminal.style.opacity = '0'; 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)'; // Add close button to terminal terminal.addEventListener('close', () => this.closeTerminal()); await domtools.convenience.smartdelay.delayFor(0); terminal.style.opacity = '1'; terminal.style.transform = 'translateY(0) scale(1)'; return terminal; } private async closeTerminal() { const domtools = await this.domtoolsPromise; if (this.currentTerminal) { this.currentTerminal.style.opacity = '0'; this.currentTerminal.style.transform = 'translateY(8px) scale(0.99)'; await domtools.convenience.smartdelay.delayFor(250); this.currentTerminal.remove(); this.currentTerminal = null; } } private currentView!: DeesElement; public async loadView(viewArg: IView) { const appcontent = this.shadowRoot!.querySelector('.appcontent')!; const view = new viewArg.element(); if (this.currentView) { this.currentView.remove(); } appcontent.appendChild(view); this.currentView = view; this.selectedView = viewArg; // Emit view-select event this.dispatchEvent(new CustomEvent('view-select', { detail: { view: viewArg }, bubbles: true, composed: true })); } }