"""Dark engineering control-room theme for Murphy Unified dashboard. Provides CSS, JS, colour constants, Plotly template, and helper functions shared by every tab. """ from __future__ import annotations import plotly.graph_objects as go import plotly.io as pio # ── Colour palette ────────────────────────────────────────────────────────── BG = "#1a1e24" SURFACE = "#21262e" SURFACE2 = "#272d37" BORDER = "#333c48" BORDER_BRIGHT = "#445060" ACCENT = "#4a9eca" ACCENT2 = "#4fc98a" WARN = "#d4922a" DANGER = "#c94a4a" TEXT = "#b8c8d8" TEXT_DIM = "#6a7e92" TEXT_BRIGHT = "#d8e8f0" TRACE_COLORS: list[str] = [ACCENT, ACCENT2, WARN, DANGER, "#7a8fca", "#ca7a9e"] # ── Named semantic colors for tab-level use (avoid hardcoded hex in tabs) ── ENERGY_POS = ACCENT2 # positive energy terms in bar charts ENERGY_NEG = DANGER # negative energy terms VALVE_ICV = ACCENT # ICV traces VALVE_DCV = DANGER # DCV traces # ── Google Fonts ──────────────────────────────────────────────────────────── FONTS_HTML = ( '' ) # ── CSS ───────────────────────────────────────────────────────────────────── CSS = """ /* ── Murphy Unified — dark engineering theme ─────────────────────────── */ :root { --bg: #1a1e24; --surface: #21262e; --surface2: #272d37; --border: #333c48; --border-bright: #445060; --accent: #4a9eca; --accent2: #4fc98a; --warn: #d4922a; --danger: #c94a4a; --text: #b8c8d8; --text-dim: #6a7e92; --text-bright: #d8e8f0; --mono: 'JetBrains Mono', monospace; --body: 'DM Sans', sans-serif; } /* ── Plotly modebar — always visible on hover (issue #13) ──────────── */ .js-plotly-plot .modebar { opacity: 0.7 !important; } .js-plotly-plot:hover .modebar { opacity: 1 !important; } .js-plotly-plot .modebar-btn { font-size: 16px; } /* ── body / dark-mode override ──────────────────────────────────────── */ body, .dark { background: var(--bg) !important; color: var(--text) !important; font-family: var(--body) !important; } .gradio-container { background: var(--bg) !important; max-width: 100% !important; } .block, .gr-block, .gr-box, .gr-panel { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: 4px !important; } input, textarea, select, .gr-input, .gr-text-input { background: var(--surface2) !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: 4px !important; font-family: var(--mono) !important; } button, .gr-button { background: var(--surface2) !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: 4px !important; font-family: var(--body) !important; } button.primary, .gr-button.primary { background: var(--accent) !important; color: var(--bg) !important; border-color: var(--accent) !important; } /* ── Tab navigation ─────────────────────────────────────────────────── */ .tabs .tab-nav button, .tab-nav button { font-family: var(--mono) !important; font-size: 10px !important; text-transform: uppercase !important; letter-spacing: 2px !important; color: var(--text-dim) !important; background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: 4px !important; padding: 8px 16px !important; } .tabs .tab-nav button.selected, .tab-nav button.selected { background: var(--surface2) !important; color: var(--accent) !important; border-color: var(--border-bright) !important; border-bottom: 2px solid var(--accent) !important; } /* ── Headings ───────────────────────────────────────────────────────── */ h1, h2, h3, h4, h5, h6 { font-family: var(--body) !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 2px !important; color: var(--text-dim) !important; } /* ── Tables ──────────────────────────────────────────────────────────── */ table { background: var(--surface) !important; border-collapse: collapse !important; } thead, th { background: var(--surface2) !important; font-family: var(--body) !important; font-weight: 600 !important; text-transform: uppercase !important; color: var(--text-dim) !important; border: 1px solid var(--border) !important; } td { background: var(--surface) !important; color: var(--text) !important; border: 1px solid var(--border) !important; font-family: var(--mono) !important; font-size: 12px !important; } /* ── Accordion ──────────────────────────────────────────────────────── */ .gr-accordion .label-wrap, .accordion .label-wrap { font-family: var(--body) !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 2px !important; color: var(--text-dim) !important; } /* ── Sliders ────────────────────────────────────────────────────────── */ .gr-slider label, label span { font-family: var(--body) !important; color: var(--text-dim) !important; } /* ── Dropdown / listbox ─────────────────────────────────────────────── */ .gr-dropdown, .gr-listbox, ul.options { background: var(--surface2) !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: 4px !important; } .gr-dropdown li:hover, .gr-listbox li:hover, ul.options li:hover { background: var(--surface) !important; } /* ── Footer hidden ──────────────────────────────────────────────────── */ footer { display: none !important; } /* ── Metric cards (LEFT accent bar) ─────────────────────────────────── */ .metric-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; } .metric-card { flex: 1; min-width: 140px; background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 4px; padding: 8px 12px; } .metric-card.green { border-left-color: var(--accent2); } .metric-card.amber { border-left-color: var(--warn); } .metric-card.red { border-left-color: var(--danger); } .metric-val { font-family: var(--mono); font-size: 18px; color: var(--text-bright); font-variant-numeric: tabular-nums; } .metric-label { font-family: var(--body); font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 2px; font-weight: 600; } /* ── Status bar ─────────────────────────────────────────────────────── */ .status-bar { font-family: var(--mono); font-size: 10px; color: var(--text-dim); border-top: 1px solid var(--border); margin-top: 8px; padding-top: 6px; } /* ── Checkboxes / radio ─────────────────────────────────────────────── */ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent) !important; width: 16px !important; height: 16px !important; appearance: auto !important; -webkit-appearance: auto !important; opacity: 1 !important; position: relative !important; pointer-events: auto !important; } input[type="checkbox"]:checked, input[type="radio"]:checked { background: var(--accent) !important; border-color: var(--accent) !important; } /* Force native rendering over Gradio's custom styling */ .checkbox-item input, label input[type="checkbox"], label input[type="radio"] { appearance: auto !important; -webkit-appearance: auto !important; opacity: 1 !important; } /* Selected item label highlight */ .checkbox-item.selected, label:has(input:checked) { border-color: var(--accent) !important; background: rgba(74, 158, 202, 0.15) !important; color: var(--text-bright) !important; } /* ── Status pills ───────────────────────────────────────────────────── */ .pill { display: inline-flex; font-family: var(--mono); font-size: 10px; border-radius: 20px; padding: 2px 10px; align-items: center; } .pill.nominal { background: rgba(79, 201, 138, 0.15); color: var(--accent2); } .pill.warn { background: rgba(212, 146, 42, 0.15); color: var(--warn); } .pill.danger { background: rgba(201, 74, 74, 0.15); color: var(--danger); } """ # ── JS — force dark mode ─────────────────────────────────────────────────── FORCE_DARK = """ function FORCE_DARK() { document.body.classList.add('dark'); document.documentElement.style.colorScheme = 'dark'; /* Issue #13: ensure Plotly modebar (download/copy buttons) is visible */ const observer = new MutationObserver(() => { document.querySelectorAll('.js-plotly-plot').forEach(plot => { if (plot._fullLayout && !plot._modebarPatched) { Plotly.relayout(plot, {'modebar.orientation': 'v'}); plot._modebarPatched = true; } }); }); observer.observe(document.body, {childList: true, subtree: true}); } """ # ── Metric card helper ────────────────────────────────────────────────────── def metric_html(label: str, value: str, unit: str = "", accent: str = "") -> str: """Render a single metric card div. Parameters ---------- label : str Short uppercase label displayed beneath the value. value : str Primary numeric / text value. unit : str, optional Unit suffix appended after *value*. accent : str, optional Extra CSS class for the card, e.g. ``"green"``, ``"amber"``, ``"red"``. Returns ------- str HTML string for one ``.metric-card``. """ cls = f"metric-card {accent}" if accent else "metric-card" unit_span = f" {unit}" if unit else "" return ( f'
' f'
{value}{unit_span}
' f'
{label}
' f"
" ) # ── Font family strings (for inline styles & Plotly) ─────────────────────── FONT_MONO = "JetBrains Mono, monospace" FONT_BODY = "DM Sans, sans-serif" def engine_banner(label: str, color: str = ACCENT) -> str: """Render a small engine-label banner for the left panel.""" return ( f'
' f'{label}
' ) # ── Plotly template ───────────────────────────────────────────────────────── _axis = dict( gridcolor="rgba(40,55,75,0.9)", linecolor=BORDER_BRIGHT, zeroline=False, tickfont=dict(family=FONT_MONO, size=10, color=TEXT_DIM), ) _template = go.layout.Template( layout=go.Layout( paper_bgcolor=SURFACE, plot_bgcolor=SURFACE, font=dict(family=FONT_BODY, color=TEXT), title=dict(font=dict(family=FONT_BODY, color=TEXT_DIM)), xaxis=_axis, yaxis=_axis, colorway=TRACE_COLORS, margin=dict(l=56, r=16, t=44, b=44), modebar=dict( bgcolor="rgba(0,0,0,0)", color=TEXT_DIM, activecolor=ACCENT, orientation="v", ), ) ) # Plotly config applied to all figures — ensures modebar with download/copy # button is visible (issue #13). PLOTLY_CONFIG = { "displayModeBar": True, "modeBarButtonsToAdd": ["toImage"], "toImageButtonOptions": { "format": "png", "filename": "murphy_plot", "scale": 2, }, } pio.templates["murphy"] = _template pio.templates.default = "murphy"