/**
* 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 ? `
` : '';
const loadRow = hasLoad ? `
Load
${parseInt(loadAttr, 10)} %
` : '';
const rightSide = showRight ? `
${circuit}
${clusterRow}
${loadRow}
` : '';
return `
${this._logoMark()}
ZEN ${this._subtitle ?? 'COMPUTE'}
${this._tagline ?? (compact ? 'Orchestrator' : 'Orchestrator Overview')}
${rightSide}
`;
}
// ─────────────────────────────────────────────
// SVG logo mark
// ─────────────────────────────────────────────
_logoMark() {
const src = this._logoSrc;
if (src) {
return `
`;
}
return `
`;
}
}
customElements.define('zen-banner', ZenBanner);