Spaces:
Sleeping
Sleeping
| """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 = ( | |
| '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?' | |
| "family=JetBrains+Mono:wght@400;500;600&" | |
| 'family=DM+Sans:wght@400;500;600;700&display=swap">' | |
| ) | |
| # ββ 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" <span style='font-size:12px;color:{TEXT_DIM}'>{unit}</span>" if unit else "" | |
| return ( | |
| f'<div class="{cls}">' | |
| f' <div class="metric-val">{value}{unit_span}</div>' | |
| f' <div class="metric-label">{label}</div>' | |
| f"</div>" | |
| ) | |
| # ββ 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'<div style="font-family:{FONT_MONO};font-size:11px;' | |
| f'color:{color};letter-spacing:3px;text-transform:uppercase;' | |
| f'padding:6px 0;border-bottom:1px solid {BORDER};margin-bottom:8px;">' | |
| f'{label}</div>' | |
| ) | |
| # ββ 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" | |