/** * zen-banner.js — Zen dashboard banner Web Component * * Usage: * * * * * * * Attributes: * variant "full" (default) | "compact" * cluster-status "nominal" (default) | "degraded" | "offline" * load 0–100 integer, shown as a percentage (default: hidden) * tagline custom tagline text (default: "Orchestrator Overview" / "Orchestrator") * subtitle text after "ZEN" in the wordmark (default: "COMPUTE") */ class ZenBanner extends HTMLElement { static get observedAttributes() { return ['variant', 'cluster-status', 'load', 'tagline', 'subtitle', 'logo-src']; } attributeChangedCallback() { if (this.shadowRoot) this._render(); } connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._render(); } // ───────────────────────────────────────────── // Derived values // ───────────────────────────────────────────── get _variant() { return this.getAttribute('variant') || 'full'; } get _status() { return (this.getAttribute('cluster-status') || 'nominal').toLowerCase(); } get _load() { return this.getAttribute('load'); } // null → hidden get _tagline() { return this.getAttribute('tagline'); } // null → default get _subtitle() { return this.getAttribute('subtitle'); } // null → "COMPUTE" get _logoSrc() { return this.getAttribute('logo-src'); } // null → inline SVG get _statusColor() { return { nominal: '#7ecfb8', degraded: '#d4a84b', offline: '#c0504d' }[this._status] ?? '#7ecfb8'; } get _statusLabel() { return { nominal: 'NOMINAL', degraded: 'DEGRADED', offline: 'OFFLINE' }[this._status] ?? 'NOMINAL'; } get _loadColor() { const v = parseInt(this._load, 10); if (isNaN(v)) return '#7ecfb8'; if (v >= 85) return '#c0504d'; if (v >= 60) return '#d4a84b'; return '#7ecfb8'; } // ───────────────────────────────────────────── // Render // ───────────────────────────────────────────── _render() { const compact = this._variant === 'compact'; this.shadowRoot.innerHTML = ` ${this._html(compact)} `; } // ───────────────────────────────────────────── // CSS // ───────────────────────────────────────────── _css(compact) { const height = compact ? '60px' : '100px'; const padding = compact ? '0 24px' : '0 32px'; const gap = compact ? '16px' : '24px'; const markSize = compact ? '34px' : '52px'; const divH = compact ? '32px' : '48px'; const nameSize = compact ? '15px' : '22px'; const tagSize = compact ? '9px' : '11px'; const sc = this._statusColor; const lc = this._loadColor; return ` @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@300;400&family=Space+Mono:wght@400;700&display=swap'); *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :host { display: block; font-family: 'Space Mono', monospace; } .banner { width: 100%; height: ${height}; background: var(--theme_g3, #0b0d10); border: 1px solid var(--theme_g2, #1e2330); border-radius: 6px; display: flex; align-items: center; padding: ${padding}; gap: ${gap}; position: relative; overflow: hidden; text-decoration: none; color: inherit; cursor: pointer; } /* scan-line texture */ .banner::before { content: ''; position: absolute; inset: 0; background: repeating-linear-gradient( 0deg, transparent, transparent 3px, rgba(255,255,255,0.012) 3px, rgba(255,255,255,0.012) 4px ); pointer-events: none; } /* ambient glow */ .banner::after { content: ''; position: absolute; right: -60px; top: 50%; transform: translateY(-50%); width: 280px; height: 280px; background: radial-gradient(circle, rgba(130,200,180,0.06) 0%, transparent 70%); pointer-events: none; } .logo-mark { flex-shrink: 0; width: ${markSize}; height: ${markSize}; } .logo-mark svg, .logo-mark img { width: 100%; height: 100%; object-fit: contain; } .divider { width: 1px; height: ${divH}; background: linear-gradient(to bottom, transparent, var(--theme_g2, #2a3040), transparent); flex-shrink: 0; } .text-block { display: flex; flex-direction: column; gap: 4px; } .wordmark { font-weight: 700; font-size: ${nameSize}; letter-spacing: 0.12em; color: var(--theme_bright, #e8e4dc); text-transform: uppercase; line-height: 1; } .wordmark span { color: #7ecfb8; } .tagline { font-family: 'Noto Serif JP', serif; font-weight: 300; font-size: ${tagSize}; letter-spacing: 0.3em; color: var(--theme_faint, #4a5a68); text-transform: uppercase; } .spacer { flex: 1; } /* ── right-side decorative circuit ── */ .circuit { flex-shrink: 0; opacity: 0.22; } /* ── status cluster ── */ .status-cluster { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; } .status-row { display: flex; align-items: center; gap: 8px; } .status-lbl { font-size: 9px; letter-spacing: 0.18em; color: var(--theme_faint, #3a4555); text-transform: uppercase; } .pill { display: flex; align-items: center; gap: 5px; border-radius: 20px; padding: 2px 10px; font-size: 10px; letter-spacing: 0.1em; } .pill.cluster { color: ${sc}; background: color-mix(in srgb, ${sc} 8%, transparent); border: 1px solid color-mix(in srgb, ${sc} 28%, transparent); } .pill.load-pill { color: ${lc}; background: color-mix(in srgb, ${lc} 8%, transparent); border: 1px solid color-mix(in srgb, ${lc} 28%, transparent); } .dot { width: 5px; height: 5px; border-radius: 50%; animation: pulse 2.4s ease-in-out infinite; } .dot.cluster { background: ${sc}; } .dot.load-dot { background: ${lc}; animation-delay: 0.5s; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } } `; } // ───────────────────────────────────────────── // HTML template // ───────────────────────────────────────────── _html(compact) { const loadAttr = this._load; const hasCluster = !compact && this.hasAttribute('cluster-status'); const hasLoad = !compact && loadAttr !== null; const showRight = hasCluster || hasLoad; const circuit = showRight ? ` ` : ''; const clusterRow = hasCluster ? `
Cluster
${this._statusLabel}
` : ''; const loadRow = hasLoad ? `
Load
${parseInt(loadAttr, 10)} %
` : ''; const rightSide = showRight ? ` ${circuit}
${clusterRow} ${loadRow}
` : ''; return ` `; } // ───────────────────────────────────────────── // SVG logo mark // ───────────────────────────────────────────── _logoMark() { const src = this._logoSrc; if (src) { return `zen`; } return ` `; } } customElements.define('zen-banner', ZenBanner);